当前位置: 首页 > news >正文

【AI】SourceInsight v4.0.0.150分析文档

【AI】SourceInsight v4.0.0.150分析文档

分析对象:sourceinsight4.exe (v4.0.0.150)
分析工具:IDA Pro + MCP
AI模型:GLM5.1、deepseek V4

目录
  • 【AI】SourceInsight v4.0.0.150分析文档
    • 快速查找表
      • 核心函数速查
      • 关键数据表
      • 简单补丁速查
      • 序列号格式速查
    • 1. 总体架构
      • 许可状态机 (ConnectedState_0)
    • 2. License 对象与结构体
    • 3. 序列号验证
      • 3.1 序列号格式
      • 3.2 校验和算法 (compute_serial_checksum_51B7A0)
      • 3.3 序列号格式验证 (validate_serial_51DA20)
      • 3.4 详细序列号解析 (parse_serial_with_details_51C070)
      • 3.5 试用序列号生成 (generate_trial_serial_51ED70)
    • 4. si4.lic 文件格式与解析
      • 4.1 文件格式
      • 4.2 XML 解析 (parse_si4_lic_51C4D0)
      • 4.3 签名数据收集 (collect_signature_data_51B9C0)
      • 4.4 加载 si4.lic (load_si4_lic_51DE60)
    • 5. si4.lic 文件创建与本地签名
      • 5.1 创建 si4.lic (x_create_lic_51D400)
      • 5.2 本地签名算法 (sub_403260substitution_hash_402F00)
      • 5.3 Base64 编码 (sub_402D50)
      • 5.4 本地签名的验证 (sub_51D640)
      • 5.5 本地签名的关键发现
    • 6. RSA 签名验证
      • 6.1 签名验证流程 (verify_license_signature_51E630)
      • 6.2 RSA 验证 (rsa_verify_51BB30)
      • 6.3 RSA签名字节序(关键发现)
      • 6.4 在线签名验证 vs 本地签名验证对比
      • 6.5 本地签名验证详细流程 (sub_51D640)
    • 7. 本地激活流程
      • 7.1 在线激活 (activate_license_local_51F8E0)
      • 7.2 URL 解码 (url_decode_427620)
      • 7.3 延迟激活 (deferred_activation_51ED20)
      • 7.4 本地临时激活 (local_activation_51FEA0)
      • 7.5 ActId 生成 (sub_403290)
    • 8. 网络验证流程
      • 8.1 HTTP POST 通信 (x_net_post_427200)
      • 8.2 在线激活请求 (request_activation_51C5D0)
      • 8.3 在线验证 (validate_license_online_51F840)
      • 8.4 互联网连接检查 (check_internet_connected_4271E0)
      • 8.5 连接检查与重试 (check_internet_connection_51D000)
      • 8.6 服务器错误码含义
    • 9. 许可证停用流程
      • 9.1 停用入口 (cmd_deactivate_license_491670)
      • 9.2 停用实现 (deactivate_license_51EC00)
      • 9.3 停用网络请求 (request_deactivation_51C9A0)
    • 10. 黑名单机制
      • 10.1 随机黑名单检查 (check_blacklist_random_51D7D0)
      • 10.2 黑名单数据下载 (sub_43EDA0)
      • 10.3 黑名单检查触发点
      • 10.4 黑名单条目检查 (check_blacklist_entry_462080)
    • 11. 周期性验证与后台线程
      • 11.1 后台验证线程 (license_bg_thread_51FBE0)
      • 11.2 周期性验证 (validate_license_periodic_51DD50)
      • 11.3 触发频率
    • 12. 许可证过期检查
      • 12.1 过期判断 (check_license_expiry_51CC40)
    • 13. DPAPI 加密存储
      • 13.1 保存许可数据 (save_license_data_51E880)
      • 13.2 验证存储数据 (verify_stored_license_51E950)
      • 13.3 内容哈希计算 (compute_content_hash_51CBA0)
    • 14. 升级许可证验证
      • 14.1 升级序列号判断 (is_upgrade_serial_51C1A0)
      • 14.2 查找SI3旧许可证 (find_si3_upgrade_license_51C1D0)
      • 14.3 SI3序列号验证 (validate_si3_serial_570630)
      • 14.4 SI3前缀判断 (is_si3_serial_prefix_5706B0)
      • 14.5 升级许可验证流程 (validate_upgrade_license_51DAE0)
    • 15. 硬件ID比较
      • 15.1 HWID格式 (parse_hwid_51BDB0)
      • 15.2 HWID哈希计算 (compute_hwid_hash_51B810)
      • 15.3 HWID比较 (compare_hwid_to_license_51CDA0)
    • 16. Nag 屏幕与UI交互
      • 16.1 Nag屏幕 (show_nag_screen_5209B0)
      • 16.2 命令入口 (cmd_show_nag_491630)
      • 16.3 关于对话框 (paint_about_dialog_4F7C20)
    • 17. 命令行参数
    • 18. 辅助函数(日志/对话框)
      • 18.1 日志函数
      • 18.2 对话框函数
    • 19. 注册机实现
      • 19.1 序列号生成
      • 19.2 si4.lic 生成(在线激活方式)
      • 19.3 si4.lic 生成(本地激活方式)
      • 19.4 公钥替换
      • 19.5 签名绕过补丁(推荐方法)
      • 19.6 DLL 代理方法
    • 20. 函数索引表
      • 许可核心函数
      • 序列号相关
      • si4.lic 文件操作
      • 签名验证
      • 激活与验证
      • 停用
      • 黑名单
      • 过期与时间
      • DPAPI 加密
      • 网络通信
      • 升级许可
      • 硬件ID
      • UI 与交互
      • 后台线程与周期触发
      • 日志与对话框
      • 辅助函数
    • 21. 关键数据与常量
      • 21.1 替换表与常量
      • 21.2 全局变量
    • 22. 总结与技术要点
      • 22.1 核心技术发现
      • 22.2 注册机实现要点
      • 22.3 攻击方案对比
      • 22.4 安全建议(针对 SourceDynamics)
    • 23. 签名验证测试结果与RSA字节序发现(2026-04-29)
      • 23.1 RSA签名字节序问题(关键发现)
      • 23.2 本地许可证验证结果
      • 23.3 init_license_520000 完整验证链
      • 23.4 ActId验证详解 (sub_51EE40)
      • 23.5 Version String计算 (sub_51BCB0 → get_version_string_51E770)
      • 23.6 在线 vs 本地许可证格式差异
      • 23.7 序列号校验和验证规则
      • 23.8 ActId前缀表(50项,XOR编码)
      • 23.9 本地签名 vs 在线签名的关键差异
      • 23.10 ActId/HWID 算法深度验证 (2026-05-05)
      • 23.11 简单二进制补丁方案 (Simple Binary Patches)
        • 方案一:两字节补丁(推荐)
        • 方案二:完整补丁(同时支持本地和在线许可)
        • VA → 文件偏移 计算方法
      • 23.12 30天试用许可生成流程
  • kg
  • ps


快速查找表

核心函数速查

函数 地址 功能 Keygen 对应
license_obj 0x68BC90 全局 License 对象
substitution_hash_402F00 0x402F00 替换表哈希算法 substitution_hash()
parse_commandline_46A420 0x46A420 命令行解析
init_license_520000 0x520000 许可系统初始化入口
load_si4_lic_51DE60 0x51DE60 加载 si4.lic 文件 parse_lic_file()
x_create_lic_51D400 0x51D400 生成 si4.lic 文件 generate_lic_local() / generate_lic_online()
compute_serial_checksum_51B7A0 0x51B7A0 计算序列号校验和 generate_serial()
validate_serial_51DA20 0x51DA20 验证序列号 validate_serial()
verify_license_signature_51E630 0x51E630 验证签名(在线RSA) verify_license_file()
rsa_verify_51BB30 0x51BB30 RSA-2048+SHA1 验证 _sign_online()
sub_51D640 0x51D640 本地签名验证 _sign_local()
init_hostid_51C560 0x51C560 收集系统信息 compute_hwid()
get_version_string_51E770 0x51E770 计算版本字符串 compute_version_string()
sub_403290 0x403290 XOR 解码 ActId 前缀 VALID_ACTID_PREFIXES
sub_4035C0 0x4035C0 匹配 ActId 前缀 is_local_actid()
license_bg_thread_51FBE0 0x51FBE0 后台验证线程

关键数据表

名称 地址 大小 说明
ALPHABET_TABLE 0x612178 26B 序列号/HWID 字符表
SUBSTITUTION_TABLE 0x612298 256B 通用替换表
LOCAL_SIGNATURE_SALT 0x5E2C10 256B 本地签名专有盐表
XOR Key Table 0x62E5D0 10000B ActId 前缀 XOR 密钥
Encoded ActId Prefixes 0x6616D0 200B 50 个 XOR 编码的前缀
Original RSA Public Key 0x661200 ~451B 嵌入的 PEM 公钥

简单补丁速查

补丁 VA 文件偏移 原始 修改 效果
签名绕过 0x5200DC 0x11F4DC 74 0E EB 0E jz→jmp
线程杀死 0x51FBE0 0x11EFE0 83 EC 18 C3 sub esp→ret
本地签名绕过 0x5200B1 0x11F4B1 75 2B 90 90 jnz→nop

序列号格式速查

位置:  0123-4567-8901-2345-6789
内容:  S4XY-????-????-CCCCX = T(试用)/B(Beta)/S(Standard)/U(升级)
Y = G(新购)/R(续订)  注: v4.0.0.150 不支持 'V'
? = 随机字母数字 (位置6 仅限 RGDF)
C = 校验和 (4 chars, 由 ALPHABET_TABLE 编码)

1. 总体架构

SourceInsight 4 的许可系统围绕全局 License 对象 (license_obj @ 0x68BC90) 构建,涵盖以下核心流程:

程序启动└─ WinMain @ 0x460930├─ get_license_51F7D0() → 创建 License 单例└─ init_license_520000()├─ clear_license_data_51C450()├─ set_lic_filepath_51E5C0()├─ init_hostid_51C560()├─ _beginthread(license_bg_thread_51FBE0)  ← 后台验证线程├─ load_si4_lic_51DE60()  ← 加载 si4.lic│    └─ parse_si4_lic_51C4D0()  ← XML 解析└─ set_default_registry_51BE70()命令行参数└─ parse_commandline_46A420├─ --license-file <path>   → args_license_file_C7C├─ --license-serial <sn>   → args_license_serial_B7C└─ --reset-license         → reset_license_69AF38运行时周期性验证├─ keyboard_handler_4FCC60   → 每100次按键触发 validate_license_periodic_51DD50├─ main_command_dispatch_466F50 → 每15次命令触发 validate_license_periodic_51DD50└─ idle_timer_proc_582900   → 每200次空闲触发 validate_license_periodic_51DD50黑名单随机检查├─ on_project_open_blacklist_4B85C0├─ on_file_open_blacklist_4E69D0└─ on_idle_blacklist_5B1DD0

许可状态机 (ConnectedState_0)

状态 含义
0 未激活 无有效许可
1 已激活 正式许可(在线或离线激活)
2 延迟激活 网络错误时临时激活(deferred_activation_51ED20)
3 本地激活 网络不可用时本地临时激活(local_activation_51FEA0)

2. License 对象与结构体

全局对象位于 .data:0068BC90,大小 3540 字节,38 个成员:

偏移 名称 类型 说明
0x000 ConnectedState_0 DWORD 许可状态:0=未激活, 1=已激活, 2=延迟, 3=本地
0x004 Serial_4 char[256] 序列号
0x104 LicensedUser_104 char[256] 授权用户名
0x204 Organization_204 char[256] 组织名
0x304 Email_304 char[256] 邮箱
0x404 copy_Serial_404 char[256] 序列号副本(prevserial)
0x504 UpgradeLicense_504 char[256] 升级序列号(SI3旧序列号)
0x604 versionMajor_604 int 主版本号
0x608 MinorVersion_608 int 次版本号
0x60C type_60C char 许可类型:0=无, 1=试用, 3=永久(Standard)
0x610 activated_time_610 DWORD 激活时间
0x614 activated_time_hi_614 DWORD 激活时间高位
0x618 activated_time_extra_618 DWORD 激活时间额外部分
0x61C expired_time_61C DWORD 过期时间
0x620 expired_time_hi_620 DWORD 过期时间高位
0x624 expired_time_extra_624 DWORD 过期时间额外部分
0x628 HWID_628 char[18] 硬件ID(格式:XXXXXXXX-YYYYYYYY)
0x63A ActId_63A char[256] 激活ID
0x73A signature_flag_73A char 签名标志
0x73B signature_data_73B BYTE[33] 签名数据
0x75C lic_name_75C char[256] si4.lic 文件路径
0x85C sls_sourceinsight_com_85C DWORD 服务器URL指针
0x860 request_activation_860 DWORD 激活URL路径指针 ("/request_activation/")
0x864 create_trial_license_864 DWORD 试用URL路径指针 ("/create_trial_license/")
0x868 request_deactivation_868 DWORD 停用URL路径指针 ("/request_deactivation/")
0x86C HostId_86C BYTE[784] 主机ID信息
0xB7C args_license_serial_B7C BYTE[256] 命令行指定的序列号
0xC7C args_license_file_C7C BYTE[264] 命令行指定的许可文件
0xD84 rtl_critical_sectionD84 RTL_CRITICAL_SECTION 线程同步锁1
0xD9C critsec1_inited_D9C DWORD 临界区1初始化标志
0xDA0 thread_count1_DA0 int 线程计数1
0xDA4 rtl_critical_sectionDA4 RTL_CRITICAL_SECTION 线程同步锁2
0xDBC critsec2_inited_DBC DWORD 临界区2初始化标志
0xDC0 thread_count2_DC0 int 线程计数2
0xDC4 blacklist_check_result_DC4 DWORD 黑名单链表头指针
0xDC8 blacklist_flag_DC8 DWORD 黑名单标志(非0则清空许可)
0xDCC unknown_DCC DWORD 未知
0xDD0 validate_interval_days_DD0 DWORD 验证间隔天数(默认6)

3. 序列号验证

3.1 序列号格式

格式:S4SG-XYYZZ-AAAA-BBBB(19字符)

位置 字符 含义
0 S 固定前缀
1 4 主版本号(数字0-9)
2 S 产品类型:T=试用, B=Beta, S=标准, U=升级
3 G 许可类型:G=新购, R=续订
4 - 分隔符
5 随机 字母数字
6 R/G/D/F 固定选项
7-8 随机 字母数字
9 - 分隔符
10-13 随机 字母数字
14 - 分隔符
15-18 校验 4位校验字符

3.2 校验和算法 (compute_serial_checksum_51B7A0)

// 反编译代码 @ 0x51B7A0
int __cdecl compute_serial_checksum_51B7A0(_BYTE *a1, unsigned int a2, int a3, int a4)
{unsigned int i;unsigned __int8 v5;unsigned int j;for ( i = 0; i < 4; *(_BYTE *)(i + a4 - 1) = byte_612178[v5 % 26] ){v5 = *(_BYTE *)((unsigned __int8)(i + *a1) + a3);  // hash = substitutionTable[serial[0] + i]for ( j = 1; j < a2; ++j )                          // for j=1 to 14 (a2=15)v5 = *(_BYTE *)((v5 ^ (char)a1[j]) + a3);         // hash = substitutionTable[serial[j] ^ hash]++i;}
}

关键点:内层循环 j=1..14 不跳过破折号位置(4和9),破折号字符 '-' (0x2D) 参与哈希计算。

数据表

  • substitutionTable @ 0x612298(256字节)
  • alphabetTable @ 0x612178(26字节 = "KV96GMJYH7QF5TCW4U3XZPRSDN")

3.3 序列号格式验证 (validate_serial_51DA20)

// 反编译代码 @ 0x51DA20
BOOL __cdecl validate_serial_51DA20(char *Str)
{_strupr(Str);if ( strlen(Str) != 19 ) return 0;if ( Str[4] != 45 )  return 0;  // '-'if ( Str[9] != 45 )  return 0;  // '-'if ( Str[14] != 45 ) return 0;  // '-'if ( *Str != 83 )    return 0;  // 'S'if ( Str[6] != 82 && Str[6] != 71 && Str[6] != 68 && Str[6] != 70 ) return 0;  // R/G/D/Fif ( Str[1] < 48 || Str[1] > 57 ) return 0;  // 数字if ( Str[2] != 84 && Str[2] != 66 && Str[2] != 83 && Str[2] != 85 ) return 0;  // T/B/S/Uif ( Str[3] != 71 && Str[3] != 82 ) return 0;  // G/Rstrcpy(Destination, Str);Destination[15] = 0;  // 截断到前15个字符compute_serial_checksum_51B7A0(Destination, 0xFu, (int)byte_612298, (int)&v6);return *(_DWORD *)(Str + 15) == v6;  // 比较后4个字符
}

3.4 详细序列号解析 (parse_serial_with_details_51C070)

// 反编译代码 @ 0x51C070 (关键部分)
v6 = Str[1];  // 版本号
*(_DWORD *)a4 = v6 - 48;  // version_major = digit valuev7 = Str[2];  // 类型
switch ( v7 ) {case 'T': *(_DWORD *)a3 = 1; break;  // Trialcase 'B': *(_DWORD *)a3 = 3; break;  // Betacase 'S': *(_DWORD *)a3 = 0; break;  // Standardcase 'U': *(_DWORD *)a3 = 0; break;  // Upgrade
}v8 = Str[3];  // 许可类型
if ( v8 == 71 ) *(_DWORD *)a2 = 1;  // 'G' = 新购
else *(_DWORD *)a2 = 0;              // 'R' = 续订

3.5 试用序列号生成 (generate_trial_serial_51ED70)

当用户选择试用但序列号不是Trial类型时,程序自动生成一个试用序列号:

// 反编译代码 @ 0x51ED70
char *__cdecl generate_trial_serial_51ED70(char *Buffer)
{// 获取当前日期sub_454500(&v5);  // 获取年月日get_version_string_51E770(Str, 0);  // 获取版本字符串if ( strlen(Str) < 8 )sub_448DD0(Str, 0, 48, 8 - strlen(Str), 8);  // 左填充'0'到8位sprintf(Buffer, "S4TR-%02d%02d-", v5 - 2000, v6);  // S4TR-YYMM- 前缀v2 = &Str[strlen(Str) - 4];  // 取版本字符串后4位v3 = *v2;*v2 = 0;strcat(Buffer, v2 - 4);  // 拼接版本字符串中间4位*v2 = v3;strcat(Buffer, "-");strcat(Buffer, v2);      // 拼接版本字符串后4位return Buffer;
}

4. si4.lic 文件格式与解析

4.1 文件格式

<!--Source Insight 4.x License FileDO NOT EDIT THIS FILE. Doing so will render it unusable.This license was created for:用户名组织邮箱-->
<SourceInsightLicense><LicensePropertiesActId="Deferred"Serial="S4SG-XYYZZ-AAAA-BBBB"LicensedUser="用户名"Organization="组织"Email="邮箱"Type="Standard"Version="4"MinorVersion="0"Date="2026-04-26"Expiration="2026-07-25"    ← 仅Trial类型有此行/><SignatureValue="Base64编码的签名数据"/>
</SourceInsightLicense>

4.2 XML 解析 (parse_si4_lic_51C4D0)

// 反编译代码 @ 0x51C4D0
void __cdecl __noreturn parse_si4_lic_51C4D0(int Code, int a2)
{File_45FC80 = XML_ReadFile_45FC80(Code);  // 读取XML文件if ( File_45FC80 ){v4 = sub_45CFD0(File_45FC80, "LicenseProperties");  // 查找LicenseProperties元素if ( v4 ){v5 = sub_45CFD0(v3, "Signature");  // 查找Signature元素if ( v5 ){// 遍历LicenseProperties的所有属性for ( i = *(_DWORD *)(v6 + 4); i; i = *(_DWORD *)(i + 8) )collect_signature_data_51B9C0(*(char **)(i + 20), *(char **)(i + 24));// 获取Signature的Value属性v8 = sub_45D7F0(v5, "Value");collect_signature_data_51B9C0("Signature", *(char **)(v8 + 24));}}}
}

4.3 签名数据收集 (collect_signature_data_51B9C0)

// 反编译代码 @ 0x51B9C0
int __stdcall collect_signature_data_51B9C0(char *Source, char *Str)
{v5 = strlen(Str);Stra = (char *)malloc(v5 + 1);  // 分配value副本v6 = strlen(Source);v7 = (char *)malloc(v6 + 1);    // 分配key副本strcpy(v7, Source);              // 复制属性名strcpy(Stra, Str);              // 复制属性值// 在数组中找到空槽位v9 = 0;while ( *(_DWORD *)(v4 + 4 * v9) ){if ( ++v9 >= 0x80 )  // 最多128个条目return 1;}*(_DWORD *)(v4 + 4 * v9 + 512) = Stra;  // 存储value指针*(_DWORD *)(v4 + 4 * v9) = v8;           // 存储key指针return 1;
}

4.4 加载 si4.lic (load_si4_lic_51DE60)

完整加载流程(反编译已修复):

// 反编译代码 @ 0x51DE60
int __thiscall load_si4_lic_51DE60(char *this)
{unsigned int i; // eaxchar *v3; // ediBOOL v4; // ebxint v6; // edichar v7; // alint v8; // eaxchar *String1; // [esp+Ch] [ebp-41Ch] BYREFint v10; // [esp+10h] [ebp-418h] BYREFint v11; // [esp+14h] [ebp-414h] BYREFint v12; // [esp+18h] [ebp-410h] BYREFint v13[256]; // [esp+1Ch] [ebp-40Ch] BYREFunsigned int v14; // [esp+424h] [ebp-4h]for ( i = 0; i < 0x80; ++i ) /*0x51de82*/{v13[i] = 0; /*0x51de84*/v13[i + 0x80] = 0; /*0x51de88*/}v14 = 0; /*0x51de97*/parse_si4_lic_51C4D0((int)(this + 0x75C), (int)v13); /*0x51deaa*/*((_DWORD *)this + 0x183) = 0; /*0x51dec0*/if ( sub_51BA60("Type", (int)&String1) ) /*0x51dec6*/ // 在key-value数组中查找"Type"属性{v3 = String1; /*0x51decf*/if ( !_stricmp(String1, "Trial") ) /*0x51ded9*/{*((_DWORD *)this + 0x183) = 1; /*0x51dee5*/}else if ( !_stricmp(v3, "Beta") ) /*0x51def7*/{*((_DWORD *)this + 0x183) = 3; /*0x51df03*/}else if ( !_stricmp(v3, "Standard") ) /*0x51df15*/{*((_DWORD *)this + 0x183) = 0; /*0x51df21*/}}if ( !sub_51BA60("LicensedUser", (int)&String1) ) /*0x51df35*/goto LABEL_41; /*0x51df35*/strcpy(this + 0x104, String1); /*0x51df4e*/if ( sub_51BA60("Organization", (int)&String1) ) /*0x51df64*/strcpy(this + 0x204, String1); /*0x51df79*/if ( sub_51BA60("Email", (int)&String1) ) /*0x51df8f*/strcpy(this + 0x304, String1); /*0x51dfa4*/if ( !sub_51BA60("Serial", (int)&String1) ) /*0x51dfba*/goto LABEL_41; /*0x51dfba*/strcpy(this + 4, String1); /*0x51dfd1*/if ( !sub_51BA60("ActId", (int)&String1) ) /*0x51dfe7*/goto LABEL_41; /*0x51dfee*/strcpy(this + 0x63A, String1); /*0x51e000*/v4 = sub_4035C0((int)(this + 0x63A), (int)&unk_6616D0, 4u, 0x32, 0x1B7F) > 0; /*0x51e026*/ // 检查ActId是否为本地激活格式(匹配unk_6616D0处的50个XOR加密字符串)if ( !_stricmp(this + 0x63A, "Deferred") ) /*0x51e028*/{*(_DWORD *)this = 2; /*0x51e034*/
LABEL_19:v14 = 0xFFFFFFFF; /*0x51e03a*/sub_51B980((void **)v13); /*0x51e049*/ // 释放key-value数组return 0xC8; /*0x51e06b*/}if ( !parse_serial_with_details_51C070(this + 4, (int)&v12, (int)&v11, (int)&v10, !v4) /*0x51e09a*/|| v11 != *((_DWORD *)this + 0x183) ){v14 = 0xFFFFFFFF; /*0x51e09c*/sub_51B980((void **)v13); /*0x51e0ab*/return 0x1EF; /*0x51e0b5*/}v6 = v10; /*0x51e0be*/if ( v10 != (unsigned __int8)versionMajor_6696FB ) /*0x51e0c4*/goto LABEL_35; /*0x51e0c4*/if ( check_blacklist_entry_462080(&stru_678750, this + 4) ) /*0x51e0d0*/{v14 = 0xFFFFFFFF; /*0x51e0d9*/sub_51B980((void **)v13); /*0x51e0e8*/return 0x1CC; /*0x51e0f2*/}if ( v4 ) /*0x51e0f9*/{*(_DWORD *)this = 3; /*0x51e132*/}else{*(_DWORD *)this = 1; /*0x51e109*/if ( !sub_51BA60("HWID", (int)&String1) ) /*0x51e116*/{
LABEL_41:v14 = 0xFFFFFFFF; /*0x51e278*/sub_51B980((void **)v13); /*0x51e287*/return 0x1D5; /*0x51e295*/}strcpy(this + 0x628, String1); /*0x51e128*/ // HWID_628}if ( !sub_51BA60("Version", (int)&String1) ) /*0x51e146*/goto LABEL_41; /*0x51e146*/v7 = *String1; /*0x51e157*/if ( *String1 < 0x30 || v7 > 0x39 ) /*0x51e163*/goto LABEL_41; /*0x51e163*/v8 = v7 - 0x30; /*0x51e16c*/*((_DWORD *)this + 0x181) = v8; /*0x51e16f*/if ( v8 != (unsigned __int8)versionMajor_6696FB || v8 != v6 ) /*0x51e182*/{
LABEL_35:v14 = 0xFFFFFFFF; /*0x51e184*/sub_51B980((void **)v13); /*0x51e193*/return 0x1EA; /*0x51e19d*/}*((_DWORD *)this + 0x186) = 0; /*0x51e1b2*/*((_DWORD *)this + 0x185) = 0; /*0x51e1b8*/*((_DWORD *)this + 0x184) = 0; /*0x51e1be*/if ( !sub_51BA60("Expiration", (int)&String1) || (sub_4543C0(String1), sub_452D60((int *)this + 0x184)) ) /*0x51e1e3*/ // 解析Expiration日期,验证有效性{*((_DWORD *)this + 0x189) = 0; /*0x51e1fa*/*((_DWORD *)this + 0x188) = 0; /*0x51e200*/*((_DWORD *)this + 0x187) = 0; /*0x51e206*/if ( !sub_51BA60("Date", (int)&String1) ) /*0x51e20c*/goto LABEL_19; /*0x51e20c*/sub_4543C0(String1); /*0x51e224*/ // 解析Date日期字符串if ( sub_452D60((int *)this + 0x187) ) /*0x51e22f*/ // 验证Date日期有效性goto LABEL_19; /*0x51e236*/}v14 = 0xFFFFFFFF; /*0x51e23c*/sub_51B980((void **)v13); /*0x51e24b*/return 0x1E3; /*0x51e054*/
}

返回值

  • 0xC8 (200) = 成功(Deferred激活,或Date有效)
  • 0x1CC (460) = 序列号在黑名单中 / Signature元素缺失
  • 0x1CE (462) = 本地签名验证失败
  • 0x1D5 (469) = 缺少必需属性(LicensedUser/Serial/ActId/HWID/Version)
  • 0x1E3 (483) = 日期解析失败(Expiration无效且Date无效,或Expiration有效但Date无效)
  • 0x1EA (490) = 版本不匹配
  • 0x1EF (495) = 序列号无效或类型不匹配

5. si4.lic 文件创建与本地签名

5.1 创建 si4.lic (x_create_lic_51D400)

这是本地创建 si4.lic 文件的核心函数。关键发现本地激活时签名不是RSA签名,而是使用替换表哈希算法经过Base64编码

// 反编译代码 @ 0x51D400
int __thiscall x_create_lic_51D400(License *this)
{char *v2; // ebpint type_60C; // eaxint v5; // eaxbool v6; // zfint v7; // edxint v8; // eaxint v9; // edxint v10; // eaxconst char *v11; // eaxconst char *v12; // edisize_t v13; // eaxFILE *v14; // esiconst char *Type; // [esp+Ch] [ebp-514h]int v16; // [esp+10h] [ebp-510h] BYREFint v17; // [esp+14h] [ebp-50Ch]int v18; // [esp+18h] [ebp-508h]int v19[32]; // [esp+1Ch] [ebp-504h] BYREFchar Expiration_or_null[256]; // [esp+9Ch] [ebp-484h] BYREFchar Date[256]; // [esp+19Ch] [ebp-384h] BYREFchar Buffer[256]; // [esp+29Ch] [ebp-284h] BYREFchar v23[388]; // [esp+39Ch] [ebp-184h] BYREFv2 = sub_448540(0x2000); /*0x51d415*/if ( !v2 ) /*0x51d41e*/return 0; /*0x51d422*/type_60C = this->type_60C; /*0x51d42c*/Type = 0; /*0x51d434*/if ( type_60C ) /*0x51d438*/{v5 = type_60C - 1; /*0x51d43a*/if ( v5 ) /*0x51d43b*/{if ( v5 == 2 ) /*0x51d440*/Type = "Beta"; /*0x51d442*/}else{Type = "Trial"; /*0x51d44c*/}}else{Type = "Standard"; /*0x51d456*/}v18 = 0; /*0x51d462*/v17 = 0; /*0x51d466*/v16 = 0; /*0x51d46a*/sub_4541F0(&v16); /*0x51d46e*/sub_452CF0(&v16, Date); /*0x51d47f*/v6 = this->type_60C == 1; /*0x51d484*/v7 = v17; /*0x51d48f*/v8 = v18; /*0x51d493*/this->expired_time_61C = v16; /*0x51d497*/this->expired_time_hi_620 = v7; /*0x51d49d*/this->expired_time_extra_624 = v8; /*0x51d4a3*/if ( v6 ) /*0x51d4a9*/{sub_4542D0(0x1F); /*0x51d4b1*/sub_452CF0(&v16, Buffer); /*0x51d4c2*/sprintf(Expiration_or_null, "\t\tExpiration=\"%s\"\n", Buffer); /*0x51d4dc*/v9 = v17; /*0x51d4e5*/v10 = v18; /*0x51d4e9*/this->activated_time_610 = v16; /*0x51d4ed*/this->activated_time_hi_614 = v9; /*0x51d4f3*/this->activated_time_extra_618 = v10; /*0x51d4fc*/}else{Expiration_or_null[0] = 0; /*0x51d504*/this->activated_time_extra_618 = 0; /*0x51d50b*/this->activated_time_hi_614 = 0; /*0x51d511*/this->activated_time_610 = 0; /*0x51d517*/}sprintf( /*0x51d56b*/v2,"<!--\n""\tSource Insight 4.x License File\n""\n""\tDO NOT EDIT THIS FILE. Doing so will render it unusable.\n""\n""\tThis license was created for:\n""\n""\t\t%s\n""\t\t%s\n""\t\t%s\n""\n""-->\n""<SourceInsightLicense>\n""\t<LicenseProperties\n""\t\tActId=\"%s\"\n""\t\tSerial=\"%s\"\n""\t\tLicensedUser=\"%s\"\n""\t\tOrganization=\"%s\"\n""\t\tEmail=\"%s\"\n""\t\tType=\"%s\"\n""\t\tVersion=\"%d\"\n""\t\tMinorVersion=\"%d\"\n""\t\tDate=\"%s\"\n""%s\t/>\n""\t",this->LicensedUser_104,this->Organization_204,this->Email_304,this->ActId_63A,this->Serial_4,this->LicensedUser_104,this->Organization_204,this->Email_304,Type,(unsigned __int8)versionMajor_6696FB,(unsigned __int8)MinorVersion_6696FA,Date,Expiration_or_null);v11 = sub_448540(0x2000); /*0x51d575*/v12 = v11; /*0x51d57a*/if ( v11 /*0x51d5e9*/&& (sub_44A500((int)v2, "\n\r\t ", (int)v11),v13 = strlen(v12),sub_403260((int)v12, v13 + 1, 0x7B0, 0x80, (int)v19),v23[sub_402D50((int)v23, (int)v19, 0x80u)] = 0,(v14 = sub_45BF70(this->lic_name_75C, "w")) != 0) ){fprintf(v14, "%s<Signature\n\t\tValue=\"%s\"\n\t/>\n</SourceInsightLicense>\n", v2, v23); /*0x51d610*/fclose(v14); /*0x51d616*/sub_426800(v2); /*0x51d61c*/return 1; /*0x51d627*/}else{sub_426800(v2); /*0x51d5ec*/return 0; /*0x51d5f7*/}
}

关键步骤解析

  1. 分配缓冲区sub_448540(0x2000) 分配8KB缓冲区
  2. 确定Type字符串:根据type_60C字段(0=Standard, 1=Trial, 3=Beta)
  3. 获取当前日期sub_4541F0获取当前时间,sub_452CF0格式化为日期字符串
  4. Trial过期日期:如果是Trial类型,sub_4542D0(0x1F)加31天计算过期日期
  5. 格式化XML内容:sprintf生成LicenseProperties部分
  6. 去除空白字符sub_44A500(v2, "\n\r\t ", v11) 从XML内容中去除换行、回车、制表符和空格
  7. 计算替换表哈希sub_403260(v12, v13+1, 0x7B0, 0x80, v19) 计算本地签名哈希
    • iterations=0x7B0 (1968), keylen=0x80 (128)
  8. Base64编码sub_402D50(v23, v19, 0x80) 将128字节哈希编码为Base64字符串
  9. 写入文件:fprintf将完整si4.lic内容写入文件

5.2 本地签名算法 (sub_403260substitution_hash_402F00)

关键发现substitution_hash_402F00(IDA中原名pbkdf2_derive_key_402F00,该命名有误导性)不是 PBKDF2算法,而是替换表哈希算法

// 反编译代码 @ 0x403260
// 注意: IDA中原名 pbkdf2_derive_key_402F00,实际是替换表哈希,不是PBKDF2
void __cdecl sub_403260(int a1, int a2, int a3, int a4, int a5)
{substitution_hash_402F00((unsigned __int8 *)a1, a2, a3, a4, a5, (int)&unk_5E2C10);// 参数: data=a1, datalen=a2, iterations=a3(0x7B0=1968), keylen=a4(0x80=128), output=a5, salt_table=&unk_5E2C10
}// 反编译代码 @ 0x402F00
// 替换表哈希算法(不是PBKDF2!)
void __cdecl substitution_hash_402F00(unsigned __int8 *a1, int a2, int a3, int a4, int a5, int a6)
{int i;unsigned __int8 v7;int j;for ( i = 0; i < a4; ++i ){v7 = *(_BYTE *)((a3 + i + *a1) % 256 + a6);  // 初始哈希值for ( j = 1; j < a2; ++j )v7 = *(_BYTE *)(a6 + (v7 ^ a1[j]));         // 迭代哈希*(_BYTE *)(i + a5) = v7;}
}

算法解析

  1. 对输出的每个字节进行迭代
  2. 初始哈希值:(iterations + i + first_byte) % 256 作为salt表索引
  3. 对输入数据的每个字节进行异或运算,再查salt表
  4. 最终结果为128字节哈希值

5.3 Base64 编码 (sub_402D50)

// 反编译代码 @ 0x402D50
unsigned int __cdecl sub_402D50(int a1, int a2, unsigned int a3)
{// 标准 Base64 编码// 字母表: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="// a1=输出缓冲区, a2=输入数据, a3=输入长度// 如果 a1==NULL: 返回所需输出长度 = 4 * ((a3 + 2) / 3)// 每3字节输入 → 4字节Base64输出// 不足部分用 '=' (0x3D) 填充
}

5.4 本地签名的验证 (sub_51D640)

本地激活时的签名验证

// 反编译代码 @ 0x51D640
int __cdecl sub_51D640(int Code, void *a2)
{int result;_DWORD *File_45FC80;_DWORD *v4;_DWORD *v5;int v6;unsigned __int8 *v7;int v8;char *v9;size_t v10;unsigned int v11;int *v12;_BYTE *v13;int v14;int v15; // [esp+4h] [ebp-2124h] BYREFint v16[32]; // [esp+8h] [ebp-2120h] BYREF_BYTE v17[256]; // [esp+88h] [ebp-20A0h] BYREFchar Src[8096]; // [esp+188h] [ebp-1FA0h] BYREFresult = read_lic_file_51BAB0((LPCCH)Code, (int)Src, 0x1FA0);if ( result == 0xC8 ){compute_content_hash_51CBA0(Src, a2);File_45FC80 = XML_ReadFile_45FC80((void *)Code);if ( File_45FC80&& (v4 = (_DWORD *)sub_45CFD0(File_45FC80, "Signature"), (v5 = v4) != 0)&& (v6 = sub_45D7F0(v4, "Value")) != 0 ){v7 = *(unsigned __int8 **)(v6 + 0x18);  // Signature Value字符串指针Src[v5[8]] = 0;  // 截断到LicenseProperties结束位置v8 = sub_4484E0(Src);  // strdup: 复制内容字符串v9 = (char *)v8;if ( v8 ){// 1. 去除空白字符sub_44A500((int)Src, "\n\r\t ", v8);v10 = strlen(v9);// 2. 计算哈希sub_403260((int)v9, v10 + 1, 0x7B0, 0x80, (int)v16);// 3. Base64解码签名base64_decode_402E10(v7, v17, &v15);// 4. 比较哈希值if ( v15 == 0x80 )  // 128字节{v11 = 0x80;v12 = v16;v13 = v17;while ( *(_DWORD *)v13 == *v12 ){v11 -= 4;++v12;v13 += 4;if ( v11 < 4 ){if ( !v11|| *(_BYTE *)v12 == *v13&& (v11 <= 1 || *((_BYTE *)v12 + 1) == v13[1] && (v11 <= 2 || *((_BYTE *)v12 + 2) == v13[2])) ){v14 = 1;goto LABEL_20;}break;}}}v14 = 0;
LABEL_20:sub_426800(v9);return v14 != 0 ? 0xC8 : 0x1CE;}else{return 0x1EC;}}else{return 0x1CC;}}return result;
}

sub_51D640 返回值

  • 0xC8 (200) = 本地签名验证成功
  • 0x1CC (460) = Signature元素缺失
  • 0x1CE (462) = 本地签名不匹配
  • 0x1EC (492) = 内存分配失败

5.5 本地签名的关键发现

本地激活时生成的 si4.lic 文件中的 Signature 不是 RSA 签名!

本地签名流程:

  1. 去除 XML 内容中的所有空白字符(\n, \r, \t,
  2. 使用替换表哈希算法从去除空白后的内容派生 128 字节哈希值(iterations=1968)
  3. Base64 编码这 128 字节作为 Signature Value

在线激活时,服务器返回的 si4.lic 中的 Signature 是 RSA-2048 + SHA-1 签名。

验证签名时

  • 如果 ConnectedState==3(本地激活):调用 sub_51D640 使用替换表哈希验证
  • 如果 ConnectedState==1(在线激活):调用 verify_license_signature_51E630 使用 RSA 验证

6. RSA 签名验证

6.1 签名验证流程 (verify_license_signature_51E630)

// 反编译代码 @ 0x51E630
int __cdecl verify_license_signature_51E630(int arg_0, void *a2)
{int result;_DWORD *File_45FC80;_DWORD *v4;_DWORD *v5;int v6;unsigned __int8 *v7;size_t v8;char *v9;int v10;char *i;char v12;size_t v13;int v14;DWORD dwSigLen; // [esp+4h] [ebp-23A4h] BYREFBYTE pbSignature[1024]; // [esp+8h] [ebp-23A0h] BYREFchar Str[8096]; // [esp+408h] [ebp-1FA0h] BYREFresult = read_lic_file_51BAB0((LPCCH)arg_0, (int)Str, 8096);if ( result == 200 ){compute_content_hash_51CBA0(Str, a2);File_45FC80 = XML_ReadFile_45FC80(arg_0);if ( File_45FC80&& (v4 = (_DWORD *)sub_45CFD0(File_45FC80, "Signature"), (v5 = v4) != 0)&& (v6 = sub_45D7F0(v4, "Value")) != 0 ){v7 = *(unsigned __int8 **)(v6 + 24);Str[v5[8]] = 0;v8 = strlen(Str);v9 = (char *)malloc(v8 + 1);if ( v9 ){v10 = 0;for ( i = Str; *i; ++i ){v12 = *i;if ( *i != 10 && v12 != 13 && v12 != 9 && v12 != 32 )v9[v10++] = v12;}v9[v10] = 0;base64_decode_402E10(v7, pbSignature, &dwSigLen);v13 = strlen(v9);v14 = rsa_verify_51BB30((BYTE *)v9, v13, pbSignature, dwSigLen);free(v9);return v14;}else{return 492;}}else{return 460;}}return result;
}

verify_license_signature_51E630 返回值

  • 200 (0xC8) = RSA签名验证成功(由rsa_verify_51BB30返回)
  • 460 (0x1CC) = Signature元素缺失
  • 492 (0x1EC) = 内存分配失败
  • 其他 = rsa_verify_51BB30的错误码

6.2 RSA 验证 (rsa_verify_51BB30)

使用 Windows CryptoAPI 进行 RSA-2048 + SHA-1 签名验证:

// 反编译代码 @ 0x51BB30
int __cdecl rsa_verify_51BB30(BYTE *pbData, DWORD dwDataLen, BYTE *pbSignature, DWORD dwSigLen)
{BOOL v5;HCRYPTHASH phHash;HCRYPTPROV phProv;struct _CERT_PUBLIC_KEY_INFO *pvStructInfo;HCRYPTKEY phKey;DWORD pcbBinary;DWORD pcbStructInfo;BYTE pbBinary[2048];phHash = 0;phKey = 0;pcbBinary = 2048;phProv = 0;if ( !CryptStringToBinaryA(pszPublicKeyPEM, 0, 0, pbBinary, &pcbBinary, 0, 0)|| !CryptDecodeObjectEx(1u, (LPCSTR)8, pbBinary, pcbBinary, 0x8000u, 0, &pvStructInfo, &pcbStructInfo) ){return 472;}if ( !CryptAcquireContextW(&phProv, 0, 0, 1u, 0xF0000000) )return 473;if ( !CryptImportPublicKeyInfo(phProv, 1u, pvStructInfo, &phKey) )return 474;LocalFree(pvStructInfo);if ( !CryptCreateHash(phProv, 0x8004u, 0, 0, &phHash) )return 474;if ( !CryptHashData(phHash, pbData, dwDataLen, 0) )return 475;v5 = CryptVerifySignatureW(phHash, pbSignature, dwSigLen, phKey, 0, 0);CryptDestroyHash(phHash);CryptReleaseContext(phProv, 0);return v5 ? 200 : 462;
}

公钥 位于 0x661200(PEM格式,450字节):

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsi1qJjMKu2BtB6AXnupP
LRclMtfYzHio6MfhlvSaEWruubIJUZz8PzBlsKhPLJMJlnuijPkksuj4FIMsOcue
zMEjtoPlujvHMfnSQU6TfXBiNhfQigIHlNDFyKIeNNduD3UHJJGSwZjXz2KoUOa2
43B02ZuWjMD41OpwhADVqLLcyNdUQYirX9TBsaszTt+jQMla1mLHp3dlVdX1H7Kn
eLLbOgjuDm1n9P6pjzdBBZaM5zbeWj0lwVKEsNFSBceRe+hLTJZ5fTwCh298LN+q
AW/VTnZBI4N9QsTqUAAD9g7QG8CBDgQhS4pRwCE2mOAMEtSlLt1qg9m0JTo5dgck
MwIDAQAB
-----END PUBLIC KEY-----

返回值:200=成功, 462=签名无效, 472/473/474=其他错误

6.3 RSA签名字节序(关键发现)

问题:使用Python的cryptography库验证官方si4.lic的RSA签名时始终失败。

根本原因:Windows CryptoAPI的CryptVerifySignatureW使用小端序(little-endian)存储RSA签名,而Python的cryptography库使用大端序(big-endian)。

验证证据

  1. 原始签名字节直接RSA解密(大端序)→ 结果以281fd08e开头(非PKCS#1 v1.5格式)
  2. 反转签名字节序后RSA解密(小端序→大端序)→ 结果以0001ffff...开头(标准PKCS#1 v1.5 ✅)
  3. 从签名提取的SHA-1: 15ae1f111fcf23eef5b6333a42cad36b1310fc10
  4. 计算的SHA-1: 15ae1f111fcf23eef5b6333a42cad36b1310fc10完全匹配

修复方法

  • 验证时: sig_bytes_reversed = file_sig_bytes[::-1] → 传给Python的verify
  • 签名时: signature = private_key.sign(...)[::-1] → 反转后base64编码存入文件

6.4 在线签名验证 vs 本地签名验证对比

特性 在线签名验证 (verify_license_signature_51E630) 本地签名验证 (sub_51D640)
ConnectedState 1 (在线) 3 (本地)
签名算法 RSA-2048 + SHA-1 替换表哈希
签名长度 256字节 128字节
公钥/盐 内嵌PEM公钥 (0x661200) 替换表 (0x5E2C10)
null终止符 不含 (使用strlen) 包含 (使用strlen+1)
迭代次数 N/A 0x7B0 (1968)
签名字节序 小端序 (Windows CryptoAPI) N/A (直接比较字节)
去除空白字符 \n\r\t \n\r\t (相同)
XML解析器 XML_ReadFile_45FC80 XML_ReadFile_45FC80
截断位置 v5[8] (Signature元素偏移) v5[8] (Signature元素偏移)

关键差异:在线签名验证使用strlen(不含null),本地签名验证使用strlen+1(含null)。

6.5 本地签名验证详细流程 (sub_51D640)

1. read_lic_file_51BAB0(path, Str, 0x1FA0) → 读取文件到Str
2. compute_content_hash_51CBA0(Str, a2) → DPAPI加密(不修改Str)
3. XML_ReadFile_45FC80(path) → 解析XML
4. sub_45CFD0(xml, "Signature") → 查找Signature元素
5. sub_45D7F0(sig_elem, "Value") → 查找Value属性
6. v7 = *(v6 + 0x18) → 获取签名值的字符串指针
7. Str[v5[8]] = 0 → 在Signature偏移处截断(null终止)
8. sub_4484E0(Str) → 分配新缓冲区并复制Str
9. sub_44A500(Str, "\n\r\t ", new_buf) → 去除空白字符
10. strlen(new_buf) + 1 → 计算长度(含null终止符)
11. sub_403260(new_buf, len+1, 0x7B0, 0x80, hash_out) → 计算hash→ 内部调用 substitution_hash_402F00(data, datalen, 0x7B0, 0x80, hash_out, &unk_5E2C10)
12. base64_decode_402E10(sig_value, sig_bytes, &sig_len) → 解码签名
13. 比较hash_out和sig_bytes(128字节)
14. 返回 0xC8 (200) 或 0x1CE (462)

7. 本地激活流程

7.1 在线激活 (activate_license_local_51F8E0)

// 反编译代码 @ 0x51F8E0
int __thiscall activate_license_local_51F8E0(int this)
{memset(Str, 0, sizeof(Str));// 1. 初始化HostIdif ( !init_hostid_51C560(this + 2156) ){log_warn("InitHostId: failed");// ...}// 2. 发送激活请求到服务器result = request_activation_51C5D0(v3, 8096, 0);  // 0=首次激活if ( result == 200 ){log_info("License Activated");// 3. URL解码服务器响应if ( url_decode_427620(v3, Str, 0x2000) && strlen(Str) >= 8 ){// 4. 保存许可数据到DPAPI(跳过前8字节)save_license_data_51E880(&Str[8]);// 5. 保存下次验证时间save_next_validate_time_51D1D0(*(this + 3536));// 6. 写入si4.lic文件(服务器返回的内容,跳过前8字节)return write_lic_file_51BD60(this + 1884, &Str[8]);}else{return 464;  // 响应格式错误}}return result;
}

7.2 URL 解码 (url_decode_427620)

// 反编译代码 @ 0x427620
int __cdecl url_decode_427620(_BYTE *a1, int a2, unsigned int a3)
{// 标准 URL 解码// %XX → 对应字节(十六进制解码)// + → 空格// 其他字符保持不变
}

7.3 延迟激活 (deferred_activation_51ED20)

当网络不可达但用户有序列号时,设置 ConnectedState=2 并创建本地 si4.lic:

// 反编译代码 @ 0x51ED20
int __thiscall deferred_activation_51ED20(License *this)
{this->ConnectedState_0 = 2;  // 延迟激活状态strcpy(this->ActId_63A, "Deferred");x_create_lic_51D400(this);  // 创建本地签名的si4.liclog_warn("License Activation Deferred");show_info_msg("Your license has been temporarily activated...");return 1;
}

7.4 本地临时激活 (local_activation_51FEA0)

当网络不可达且用户需要使用时,创建本地临时激活:

// 反编译代码 @ 0x51FEA0
int __thiscall local_activation_51FEA0(License *this)
{// 1. 如果是Trial类型但序列号不是S4T开头,生成试用序列号if ( this->type_60C == 1 ){if ( strlen(this->Serial_4) < 4 || this->Serial_4[2] != 'T' )generate_trial_serial_51ED70(this->Serial_4);}else if ( this->type_60C == 3 )  // Beta类型不支持本地激活{return 0;}// 2. 设置本地激活状态this->ConnectedState_0 = 3;// 3. 生成随机的4字符ActIdv3 = sub_403290(&unk_6616D0, 4, 50, 7039);  // 解码50个预置字符串v4 = rand();memcpy(this->ActId_63A, v3[v4 % 50], 4);  // 随机选一个this->ActId_63A[4] = 0;get_version_string_51E770(&this->ActId_63A[strlen(this->ActId_63A)], 0);  // 追加版本号// 4. 创建本地签名的si4.licif ( x_create_lic_51D400(this) ){log_warn("Local License Activation");show_info_msg("A local license has been activated temporarily...");return 1;}return 0;
}

7.5 ActId 生成 (sub_403290)

// 反编译代码 @ 0x403290
// 从预置数据(unk_6616D0)解码50个4字节字符串
// 使用XOR解密: *v10++ ^= byte_62E5D0[(addr_offset) % 0x2710]
// 参数: a1=数据源, Size=4(每项字节数), a3=50(项数), a4=7039(解密偏移)

8. 网络验证流程

8.1 HTTP POST 通信 (x_net_post_427200)

// 反编译代码 @ 0x427200
int __cdecl x_net_post_427200(char *Str, int a2, const CHAR *lpMultiByteStr,char *lpOptional, _BYTE *lpBuffer, int a6)
{v8 = a2 ? 443 : 80;           // HTTPS或HTTP端口v7 = a2 ? 0x484E300 : 0x404C300;  // 标志hInternet = InternetOpenA("Source Insight", ...);  // 打开Internet会话v12 = InternetConnectA(hInternet, Str, v8, ...);   // 连接服务器v14 = HttpOpenRequestA(v12, "POST", lpMultiByteStr, ...);  // 构造POST请求HttpAddRequestHeadersA(v14, "Content-Type: application/x-www-form-urlencoded", ...);HttpAddRequestHeadersA(v14, "Accept: text/plain", ...);sprintf(v19, "Content-length: %d\n", strlen(lpOptional));HttpAddRequestHeadersA(v14, v19, ...);if ( HttpSendRequestW(v14, 0, 0, lpOptional, strlen(lpOptional)) ){HttpQueryInfoW(v14, 0x20000013, &Buffer, &dwBufferLength, 0);  // 查询HTTP状态码if ( Buffer == 200 ){InternetReadFile(v14, lpBuffer, a6 - 1, &dwNumberOfBytesRead);lpBuffer[dwNumberOfBytesRead] = 0;return 200;}}else{return (GetLastError() == 12045) + 1004;  // 12045=证书错误→1005}
}

错误码映射

网络错误码 含义
1001 InternetOpen 失败
1002 InternetConnect 失败
1003/1006 HttpOpenRequest 失败
1004 HttpSendRequest 失败
1005 SSL证书错误
1007 InternetReadFile 失败

8.2 在线激活请求 (request_activation_51C5D0)

POST 参数构造(发送到 /request_activation//create_trial_license/):

version_maj=4&version_min=0&version_build=150&serial=S4SG-XXXX-XXXX-XXXX
&user_name=用户名&org=组织&email=邮箱
&prevserial=旧序列号&upgradeserial=升级序列号
&cpu_count=N&localactivation=0或1
&usersid=用户SID哈希&volumeid=卷ID哈希&compname=计算机名
&os_version_maj=10&os_version_min=0&os_server=0
&app_type=release&requestid=随机ID&local_time=2026-04-26T12:00:00

8.3 在线验证 (validate_license_online_51F840)

// 反编译代码 @ 0x51F840
int __thiscall validate_license_online_51F840(int this)
{// 1. 初始化HostIdif ( !init_hostid_51C560(this + 2156) )log_warn("InitHostId: failed");// 2. 发送验证请求(a3=1表示验证模式)result = request_activation_51C5D0(v3, 8096, 1);if ( result == 200 ){// 3. 读取本地si4.lic文件并比较if ( read_lic_file_51BAB0(this + 1884, v3, 8096) == 200 )save_license_data_51E880(v3);  // 保存到DPAPI// 4. 保存下次验证时间save_next_validate_time_51D1D0(*(this + 3536));return 200;}return result;
}

8.4 互联网连接检查 (check_internet_connected_4271E0)

// 反编译代码 @ 0x4271E0
BOOL __thiscall check_internet_connected_4271E0(License *this)
{return InternetGetConnectedState((LPDWORD)&dwFlags, 0);
}

8.5 连接检查与重试 (check_internet_connection_51D000)

// 反编译代码 @ 0x51D000
int __thiscall check_internet_connection_51D000(License *this)
{if ( !check_internet_connected_4271E0(this) ){while ( x_net_post_427200(this->sls_sourceinsight_com_85C, 1,"/lsstatus/", Optional, Buffer, 4096) - 200 > 99 ){if ( !show_yesno_dialog("There is no Internet connection...") )return 0;  // 用户取消if ( check_internet_connected_4271E0(this) )return 1;}}return 1;
}

8.6 服务器错误码含义

根据字符串分析,以下错误码有特定含义:

错误码 含义 来源字符串
460 许可文件无效 "There was an error processing the license..."
462 签名无效 "The license file is not correct..."
464 响应格式错误 (内部错误码)
480 代理认证 "A proxy server returned 'Proxy Authentication Required'..."
484 读取失败 (网络错误)
485 版本不匹配 "The license file does not work with this version..."
489 机器不匹配 "The license file is not valid for this machine..."
495 序列号被禁用/撤销 "The serial number you provided has been disabled or revoked..."
496 SSL证书错误 "The certificate is not valid or missing..."
497 序列号无效 "The serial number you provided is not valid..."
498 激活数量过多 "There are too many machines activated..."
499 试用已存在 "A trial license was already activated on this machine..."

9. 许可证停用流程

9.1 停用入口 (cmd_deactivate_license_491670)

// 反编译代码 @ 0x491670
int sub_491670()
{if ( license_obj->ConnectedState_0 == 1 || license_obj->ConnectedState_0 == 3 ){if ( type == 1 )show_error_msg("You cannot deactivate a trial license.");else if ( type == 3 )show_error_msg("You cannot deactivate a beta license.");else if ( deactivate_license_locked_51FBA0(license_obj) ){if ( !show_nag_screen_5209B0(license_obj) ){dword_699C18 |= 0x200;PostMessageW(hwnd, WM_CLOSE, 0, 0);  // 关闭程序}}}elseshow_error_msg("Your Source Insight license has not been activated yet.");
}

9.2 停用实现 (deactivate_license_51EC00)

// 反编译代码 @ 0x51EC00 (关键逻辑)
int __thiscall deactivate_license_51EC00(License *this)
{log_info("License Deactivation Requested");if ( this->type_60C ) return 0;        // type=0才允许停用if ( !this->Serial_4[0] ) return 0;    // 需要有序列号if ( !show_yesnocancel_dialog("You have asked to deactivate...") )return 0;  // 用户取消if ( this->ConnectedState_0 == 2 || this->ConnectedState_0 == 3 ){delete_lic_and_clear_51D0C0(this);  // 本地激活直接删除return 1;}// 在线停用init_hostid_or_fail_51CF20(this);show_status_msg("Deactivating license %s... Please wait...", this->Serial_4);v3 = request_deactivation_51C9A0(this);if ( is_permanent_error_51C310(v3) ){// 提供重试/强制删除/取消选项v4 = show_options_dialog(3, "Source Insight cannot connect to the licensing server...");if ( v4 == 2 )  // 取消return 0;if ( v4 != 4 )  // 重试{v3 = request_deactivation_51C9A0(this);if ( is_permanent_error_51C310(v3) )continue;}// v4==4: 强制删除}delete_lic_and_clear_51D0C0(this);show_info_msg("Your license has been deactivated on this machine.");return 1;
}

9.3 停用网络请求 (request_deactivation_51C9A0)

POST 到 /request_deactivation/,参数:

version_maj=4&version_min=0&version_build=150&serial=S4SG-XXXX-XXXX-XXXX
&actid=Deferred&usersid=哈希&volumeid=哈希&compname=计算机名
&app_type=release&requestid=随机ID&local_time=2026-04-26T12:00:00

10. 黑名单机制

10.1 随机黑名单检查 (check_blacklist_random_51D7D0)

// 反编译代码 @ 0x51D7D0
void __thiscall check_blacklist_random_51D7D0(License *this, void *Src, int a3, int a4)
{if ( TryEnterCriticalSection(&this->rtl_critical_sectionDA4) ){++this->thread_count2_DC0;if ( !this->blacklist_check_result_DC4 )  // 尚未检查过{// 条件1: 程序运行超过6秒 (0x1770 ms)if ( !dword_68BD20 )dword_68BD20 = GetTickCount();if ( GetTickCount() - dword_68BD20 > 0x1770&& rand() % 100 > 90 )  // 条件2: 10%概率触发{if ( sub_43ECE0(4) )  // 条件3: 检查下载状态{// 下载完成,解析黑名单v6 = sub_446BF0(dword_68BCA4);  // 获取黑名单链表this->blacklist_check_result_DC4 = v6;this->blacklist_flag_DC8 = dword_68BCA0;sub_446210(v6);  // 处理黑名单数据}else{// 尚未下载完成,触发下载sub_43EDA0(Src, a3, a4);  // 下载版本信息}}}--this->thread_count2_DC0;LeaveCriticalSection(v5);}
}

10.2 黑名单数据下载 (sub_43EDA0)

// 反编译代码 @ 0x43EDA0
// 功能:存储从服务器下载的数据块,并进行XOR解密和校验和验证
// this: 数据块存储结构体
// Src: 下载的原始数据(前8字节为数据,后4字节为校验和,再后4字节为密钥索引)
// a3: 数据总长度(含校验和和密钥索引)
// a4: 数据块索引(奇偶使用不同的解密密钥)
int __thiscall sub_43EDA0(void *this, void *Src, int a3, int a4)
{int v5; // eaxchar *v6; // ebxint *v7; // esisize_t v8; // ediint result; // eaxunsigned __int8 *v10; // ecxint v11; // eaxint v12; // edxint v13; // edxint v14; // eax_BYTE *v15; // ecx// 1. 扩展存储空间v5 = a4 + 1;if ( *(_DWORD *)this > a4 + 1 )v5 = *(_DWORD *)this;v6 = (char *)Src;*(_DWORD *)this = v5;// 2. 计算数据偏移和大小v7 = (int *)((char *)this + 0xC * a4 + 0x10);v8 = a3 - 8;  // 实际数据长度 = 总长度 - 8(校验和4字节 + 密钥索引4字节)// 3. 释放旧数据if ( *((_DWORD *)this + 3 * a4 + 6) ){sub_426800(*((void **)this + 3 * a4 + 6));*((_DWORD *)this + 3 * a4 + 6) = 0;*((_DWORD *)this + 1) -= v8;}// 4. 更新统计信息*((_DWORD *)this + 1) += v8;  // 总数据大小*((_DWORD *)this + 3 * a4 + 5) = *(_DWORD *)((char *)Src + v8);  // 存储密钥索引*v7 = v8;  // 存储数据长度// 5. 分配并复制数据result = (int)sub_426CA0(v8, 0);*((_DWORD *)this + 3 * a4 + 6) = result;if ( result ){memcpy((void *)result, Src, v8);// 6. 计算校验和(FNV-1a变体,乘数0x1003F)v10 = (unsigned __int8 *)*((_DWORD *)this + 3 * a4 + 6);v11 = 0;if ( *v7 > 0 ){v12 = *v7;do{v11 = *v10++ + 0x1003F * v11;--v12;}while ( v12 );v6 = (char *)Src;}// 7. 校验和验证if ( v11 != *(_DWORD *)&v6[v8 + 4] )  // 与数据后的4字节校验和比较*((_DWORD *)this + 2) = 1;  // 校验失败标志// 8. XOR解密v13 = *((_DWORD *)this + 3 * a4 + 5);  // 密钥索引v14 = *v7;  // 数据长度v15 = (_BYTE *)*((_DWORD *)this + 3 * a4 + 6);  // 数据指针if ( (a4 & 1) != 0 )  // 奇数索引sub_402EC0(v15, v14, v13, (int)byte_62E5D0, 0x2710);  // 使用密钥表1,长度0x2710else  // 偶数索引sub_402EC0(v15, v14, v13, (int)&byte_62D530, 0x10A0);  // 使用密钥表2,长度0x10A0return 1;}return result;
}

XOR解密函数 (sub_402EC0):

// 反编译代码 @ 0x402EC0
// 功能:使用循环XOR对数据进行解密
// a1=数据, a2=数据长度, a3=密钥偏移, a4=密钥表地址, a5=密钥表长度
_BYTE *__cdecl sub_402EC0(_BYTE *a1, int a2, int a3, int a4, int a5)
{_BYTE *result;int v6;_BYTE *v7;int v8;result = a1;v6 = 0;v7 = a1;if ( a2 > 0 ){do{v8 = (v6 + a3) % a5;  // 循环密钥索引++v6;*v7++ ^= *(_BYTE *)(v8 + a4);  // XOR解密}while ( v6 < a2 );return a1;}return result;
}

解密密钥表

  • byte_62E5D0:奇数索引使用的密钥表,长度 0x2710 (10000) 字节
  • byte_62D530:偶数索引使用的密钥表,长度 0x10A0 (4256) 字节

10.3 黑名单检查触发点

函数 触发时机 上下文参数
on_project_open_blacklist_4B85C0 打开项目时 unk_650610
on_file_open_blacklist_4E69D0 打开文件时 unk_653520
on_idle_blacklist_5B1DD0 空闲时 unk_668890

10.4 黑名单条目检查 (check_blacklist_entry_462080)

// 反编译代码 @ 0x462080
int __thiscall check_blacklist_entry_462080(LPCRITICAL_SECTION lpCriticalSection, void *Src)
{if ( !TryEnterCriticalSection(lpCriticalSection) )return 0;v3 = *((_DWORD *)lpCriticalSection + 11);  // 黑名单链表头if ( v3 ){v5 = sub_447260(v3, Src, &Src);  // 在链表中搜索序列号LeaveCriticalSection(lpCriticalSection);return v5;  // 1=在黑名单中, 0=不在}LeaveCriticalSection(lpCriticalSection);return 0;
}

11. 周期性验证与后台线程

11.1 后台验证线程 (license_bg_thread_51FBE0)

// 反编译代码 @ 0x51FBE0
void __cdecl license_bg_thread_51FBE0(void *a1)
{Sleep(0x1D4C0);  // 初始等待12秒while ( 1 ){// 等待互联网连接while ( dword_69BC90 || !check_internet_connected_4271E0() )Sleep(0xEA60);  // 等待60秒EnterCriticalSection(&a1->rtl_critical_sectionDA4);++a1->thread_count2_DC0;if ( a1->ConnectedState_0 == 3 && !a1->blacklist_flag_DC8 && a1->Serial_4[0] ){// === 本地激活状态 (ConnectedState=3) ===// 检查"LocConv"注册表标志if ( sub_43AB10(dword_698F9C, "LocConv", 1) ){// 检查时间条件sub_4541F0(v7);  // 当前时间sub_51D380(v8);  // 上次本地激活时间if ( days_diff(v7, v8) <= 90 || is_future(v7) ){// 尝试在线激活v3 = activate_license_local_51F8E0(a1);if ( v3 == 200 ){a1->ConnectedState_0 = 1;  // 升级为已激活set_default_registry_51BE70(a1);}else if ( !is_permanent_error_51C310(v3) ){switch ( v3 ){case 460: case 462: case 485: case 489: case 495:delete_lic_and_clear_51D0C0(a1);  // 永久性错误→删除许可break;default:sub_43AA00(dword_698F9C, "LocConv", 0);  // 标记需要重试break;}}}}}else if ( a1->ConnectedState_0 == 1&& (!verify_stored_license_51E950(a1) || check_not_valid_after_51D0F0(a1)) ){// === 已激活状态 (ConnectedState=1) ===// 验证存储数据失败或已过验证时间v5 = validate_license_online_51F840(a1);if ( v5 != 200 ){switch ( v5 ){case 460: case 462: case 485: case 489: case 495:clear_license_data_51C450(a1);  // 永久性错误→清空许可break;default:v6 = sub_4536A0(2);  // 获取随机天数(1-2)save_next_validate_time_51D1D0(v6 + 1);  // 推迟1-3天再验证break;}}}--a1->thread_count2_DC0;LeaveCriticalSection(&a1->rtl_critical_sectionDA4);Sleep(0x1B7740);  // 等待约30分钟 (1800000ms)}
}

11.2 周期性验证 (validate_license_periodic_51DD50)

// 反编译代码 @ 0x51DD50
void __thiscall validate_license_periodic_51DD50(License *this)
{if ( TryEnterCriticalSection(&this->rtl_critical_sectionDA4) ){++this->thread_count2_DC0;// 检查1: 序列号格式验证if ( !validate_serial_51DA20(this->Serial_4) )clear_license_data_51C450(this);// 检查2: 黑名单链表搜索dwordDC4 = this->blacklist_check_result_DC4;if ( dwordDC4 && sub_447260(dwordDC4, this->Serial_4, &v4) )clear_license_data_51C450(this);// 检查3: 黑名单标志if ( this->blacklist_flag_DC8 )clear_license_data_51C450(this);// 检查4: 全局黑名单链表if ( check_blacklist_entry_462080(&stru_678750, this->Serial_4) )clear_license_data_51C450(this);--this->thread_count2_DC0;LeaveCriticalSection(v2);}
}

11.3 触发频率

触发点 函数 频率
键盘输入 keyboard_handler_4FCC60 每100次按键
命令执行 main_command_dispatch_466F50 每15次命令
空闲定时器 idle_timer_proc_582900 每200次空闲回调

12. 许可证过期检查

12.1 过期判断 (check_license_expiry_51CC40)

// 反编译代码 @ 0x51CC40
BOOL __thiscall check_license_expiry_51CC40(License *this)
{if ( this->ConnectedState_0 == 2 ) return 0;  // 延迟激活不过期v3 = this->type_60C;if ( v3 == 3 ) return 1;  // Beta永不过期(直接返回有效)if ( v3 == 1 )  // Trial{if ( has_activated_time(&this->activated_time_610) ){if ( !has_expired_time(&this->expired_time_61C) )return 1;  // 无过期时间→有效// 检查当前时间是否在过期时间之前sub_4541F0(&v7);  // 当前时间if ( is_future(&this->activated_time_610) )  // 激活时间在未来return 1;if ( is_past(&this->expired_time_61C) )  // 已过期return 1;// Trial: 本地激活30天宽限期,在线激活90天宽限期if ( this->ConnectedState_0 == 3 )sub_4542D0(30);  // +30天elsesub_4542D0(90);  // +90天return is_future(result) != 0;}else{return 1;  // 无激活时间→有效}}else  // type=0 (Standard){if ( !v3 && !this->activated_time_610 )return 0;  // 无许可// 检查当前时间是否在激活时间之后return is_past(v12) != 0;}
}

13. DPAPI 加密存储

13.1 保存许可数据 (save_license_data_51E880)

  1. 计算内容哈希 (compute_content_hash_51CBA0) — 机器特定
  2. 使用 DPAPI CryptProtectData 加密
  3. 存储加密数据到注册表 RefXXX

13.2 验证存储数据 (verify_stored_license_51E950)

  1. 从注册表读取加密数据
  2. 使用 DPAPI CryptUnprotectData 解密
  3. 比较32字节哈希与当前机器哈希
  4. 匹配返回1,不匹配返回0
  5. DPAPI 失败且错误码为 NTE_BAD_KEYSET 时返回1(允许)

13.3 内容哈希计算 (compute_content_hash_51CBA0)

// 反编译代码 @ 0x51CBA0
int __cdecl sub_51CBA0(char *Str, void *a2)
{size_t v2; // ediint v3; // edichar *v4; // eaxchar *v5; // esichar Source[256]; // [esp+10h] [ebp-100h] BYREFmemset(a2, 0, 0x20u);sub_453270(Source); // 获取机器GUIDv2 = strlen(Str);v3 = strlen(Source) + v2;v4 = (char *)sub_426CA0(v3 + 1, 0);v5 = v4;if ( !v4 )return 0;strcpy(v4, Str);strcat(v5, Source);sub_402F00((unsigned __int8 *)v5, v3, 23, 32, (int)a2, (int)byte_612198);sub_426800(v5);return 1;
}

14. 升级许可证验证

14.1 升级序列号判断 (is_upgrade_serial_51C1A0)

// 反编译代码 @ 0x51C1A0
int __cdecl is_upgrade_serial_51C1A0(char *Str)
{return strlen(Str) == 19 && Str[2] == 'U';  // 位置2='U'表示升级
}

14.2 查找SI3旧许可证 (find_si3_upgrade_license_51C1D0)

// 反编译代码 @ 0x51C1D0
int __cdecl find_si3_upgrade_license_51C1D0(char *a1)
{v1 = RegOpenKeyEx(..., "Source Insight\3.0", ...);  // 查找SI3注册表v3 = RegOpenKeyEx(v1, "Install", ...);RegQueryValueEx(v3, "SerialNumber", a1, 256);  // 读取SI3序列号if ( !validate_si3_serial_570630(a1) ){log_error("Invalid Upgrade License found in installation: %s", a1);return 0;}return 1;
}

14.3 SI3序列号验证 (validate_si3_serial_570630)

// 反编译代码 @ 0x570630
int __cdecl validate_si3_serial_570630(char *a1)
{if ( !strstr(a1, "SI3US") ) return 0;  // 必须包含SI3US前缀if ( !parse_si3_serial(a1, Str, &v4) ) return 0;if ( is_empty(Str) ) return 0;v3 = compute_si3_checksum(Str);return v4 == v3;  // 校验和匹配
}

14.4 SI3前缀判断 (is_si3_serial_prefix_5706B0)

// 反编译代码 @ 0x5706B0
int __cdecl is_si3_serial_prefix_5706B0(int a1)
{if ( strstr(a1, "SI3US") ) return 1;if ( strstr(a1, "S13US") ) return 1;return 0;
}

14.5 升级许可验证流程 (validate_upgrade_license_51DAE0)

// 反编译代码 @ 0x51DAE0
int __cdecl validate_upgrade_license_51DAE0(char *Destination)
{if ( !find_si3_upgrade_license_51C1D0(Destination) )  // 尝试从注册表找{show_yesno_dialog("You have entered a serial number for an Upgrade license...");while (1) {input_dialog("Please enter your old Source Insight version 3.x serial number...", String);_strupr(String);if ( validate_si3_serial_570630(String) )break;if ( !show_yesno_dialog("The old Source Insight version 3.x serial number you entered is not valid...") )return 0;}strcpy(Destination, String);  // 保存验证通过的SI3序列号}return 1;
}

15. 硬件ID比较

15.1 HWID格式 (parse_hwid_51BDB0)

// 反编译代码 @ 0x51BDB0
int __cdecl parse_hwid_51BDB0(int Str, _BYTE *a2, _BYTE *a3)
{if ( strlen(Str) != 0x11 )  // HWID必须是17个字符goto LABEL_3;// 前8字节a2[0..7] = Str[0..7];// 第9个字符必须是 '-'if ( Str[8] != 0x2D ){log_error("ParseHardwareId: bad format: '%s'", Str);return 0;}// 后8字节a3[0..7] = Str[9..16];return 1;
}

HWID格式: XXXXXXXX-YYYYYYYY(17字符,8+1+8)

15.2 HWID哈希计算 (compute_hwid_hash_51B810)

// 反编译代码 @ 0x51B810
int __cdecl compute_hwid_hash_51B810(_BYTE *a1, unsigned int a2, int a3, int a4)
{// 与序列号校验和算法类似,但输出8个字符for ( i = 0; i < 8; *(_BYTE *)(i + a4 - 1) = alphabetTable[v5 % 0x1A] ){v5 = *(_BYTE *)((unsigned __int8)(i + *a1) + a3);  // hash = substitutionTable[data[0] + i]for ( j = 1; j < a2; ++j )v5 = *(_BYTE *)((v5 ^ (char)a1[j]) + a3);         // hash = substitutionTable[data[j] ^ hash]++i;}
}

15.3 HWID比较 (compare_hwid_to_license_51CDA0)

// 反编译代码 @ 0x51CDA0 (关键逻辑)
int __thiscall compare_hwid_to_license_51CDA0(int this, _DWORD *a2, BOOL *a3)
{// 解析存储的HWIDparse_hwid_51BDB0(this + 1576, v19, v17);  // v19=前8字节(Part1), v17=后8字节(Part2)// 计算当前硬件的哈希compute_hwid_hash_51B810(this + 2156, strlen(this+2156), substitutionTable, v20);  // volumeid哈希compute_hwid_hash_51B810(this + 2412, strlen(this+2412), substitutionTable, v18);  // usersid哈希if ( *(_DWORD *)(this + 2936) )  // type != 0 (Trial/Beta){// Trial/Beta: 比较license HWID后半部分(v17)与usersid哈希(v18)*a2 = (compare_result == 0);  // 1=匹配*a3 = direction;}else  // type == 0 (Standard){// Standard: 比较volumeid哈希(v20)与license HWID前半部分(v19)*a2 = (compare_result == 0);*a3 = 1;}
}

重要发现:HWID比较仅在停用许可证时调用deactivate_license_51EC00),不在正常验证流程中。本地激活(ConnectedState=3)和在线激活(ConnectedState=1)的验证流程均不检查HWID。

许可类型 比较部分 含义
Standard (type=0) Part1 (前8字符) vs volumeid哈希 卷GUID哈希
Trial/Beta (type≠0) Part2 (后8字符) vs usersid哈希 用户SID哈希

16. Nag 屏幕与UI交互

16.1 Nag屏幕 (show_nag_screen_5209B0)

显示未注册/试用提醒对话框。

16.2 命令入口 (cmd_show_nag_491630)

// 反编译代码 @ 0x491630
int sub_491630()
{return show_nag_screen_5209B0(license_obj) != 0 ? 0 : 2;
}

16.3 关于对话框 (paint_about_dialog_4F7C20)

显示许可信息:

  • ConnectedState=1或3: "This software is registered to %s at %s"
  • 序列号: "Serial Number: %s"
  • 激活日期: "Activated on %s"
  • 过期日期: "Activated on: %s Expires on: %s"
  • 试用许可: "Trial License expires in %d days."
  • 试用过期: "Trial License has expired!"
  • 未激活: "License is not activated"
  • %s许可过期: "%s License has expired!"(%s="Trial")

17. 命令行参数

通过字符串分析,parse_commandline_46A420 支持以下许可相关参数:

参数 说明
--license-file <path> 指定 si4.lic 文件路径
--license-serial <sn> 指定序列号
--reset-license 重置许可

日志字符串:

  • "command line license file specified: %s" — 指定了许可文件
  • "command line serial specified: %s" — 指定了序列号

18. 辅助函数(日志/对话框)

18.1 日志函数

地址 函数名 类型 说明
0x4140A0 log_info_4140A0 int __cdecl (const char *Format, ...) 记录INFO级别日志
0x413F90 log_warn_413F90 int __cdecl (const char *Format, ...) 记录WARNING级别日志
0x413E70 log_error_413E70 int __cdecl (const char *Format, ...) 记录ERROR级别日志

18.2 对话框函数

地址 函数名 类型 说明
0x40B560 show_error_msg_40B560 int __cdecl (const char *Format, ...) 显示错误消息框
0x40B700 show_info_msg_40B700 int __cdecl (const char *Format, ...) 显示信息消息框
0x40B270 show_yesno_dialog_40B270 int __cdecl (const char *Format, ...) 显示Yes/No对话框
0x40B4B0 show_yesnocancel_dialog_40B4B0 int __cdecl (const char *Format, ...) 显示Yes/No/Cancel对话框
0x40BDA0 show_options_dialog_40BDA0 int __cdecl (int a1, const char *Format, ...) 显示多选项对话框
0x40C080 show_status_msg_40C080 int __cdecl (const char *Format, ...) 显示状态消息

注意sub_40B4B0 原被IDA识别为 __stdcall,实际应为 __cdecl(可变参数),需在IDA中修正。


19. 注册机实现

19.1 序列号生成

基于 substitutionTable(0x612298, 256字节)和 alphabetTable(0x612178, 26字节 = "KV96GMJYH7QF5TCW4U3XZPRSDN"):

def generate_serial():serial = list("S4SG-")serial.append(random.choice(string.ascii_uppercase + string.digits))serial.append(random.choice("RGDF"))serial.append(random.choice(string.ascii_uppercase + string.digits))serial.append(random.choice(string.ascii_uppercase + string.digits))serial.append("-")for _ in range(4):serial.append(random.choice(string.ascii_uppercase + string.digits))serial.append("-")serial_str = "".join(serial)check_chars = []for i in range(4):hash_val = SUBSTITUTION_TABLE[ord(serial_str[0]) + i]for j in range(1, 15):  # 包含破折号位置4和9hash_val = SUBSTITUTION_TABLE[ord(serial_str[j]) ^ hash_val]check_chars.append(ALPHABET_TABLE[hash_val % 26])serial_str += "".join(check_chars)return serial_str

19.2 si4.lic 生成(在线激活方式)

签名覆盖 <Signature> 标签之前的所有内容,去除空白字符后进行 RSA-SHA1 签名:

def generate_si4_lic(serial, user, org, email, license_type, private_key):content_before_sig = ('<SourceInsightLicense>\n''    <LicenseProperties ActId=""\n''                       HWID=""\n'f'                       Serial="{serial}"\n'f'                       LicensedUser="{user}"\n'f'                       Organization="{org}"\n'f'                       Email="{email}"\n'f'                       Type="{license_type}"\n''                       Version="4"\n''                       MinorVersion="0"\n'f'                       Date="{today}" /\n''    ')stripped = content_before_sig.replace(' ','').replace('\t','').replace('\n','').replace('\r','')signature = private_key.sign(stripped.encode('ascii'), padding.PKCS1v15(), hashes.SHA1())sig_value = base64.b64encode(signature).decode('ascii')return content_before_sig + f'<Signature Value="{sig_value}" /\n</SourceInsightLicense>'

19.3 si4.lic 生成(本地激活方式)

本地签名使用替换表哈希算法而非 RSA:

def generate_local_si4_lic(serial, user, org, email, license_type, actid="Deferred"):content = format_lic_content(serial, user, org, email, license_type, actid)stripped = content.replace(' ','').replace('\t','').replace('\n','').replace('\r','')# 替换表哈希算法 (iterations=1968, keylen=128)def substitution_hash(data, salt, iterations, keylen):result = bytearray(keylen)for i in range(keylen):hash_val = salt[(iterations + i + data[0]) % 256]for j in range(1, len(data)):hash_val = salt[hash_val ^ data[j]]result[i] = hash_valreturn result# 从二进制中提取 salt (unk_5E2C10)salt = bytearray([0x32, 0xdf, 0x71, 0xb7, 0x61, 0x3d, 0x6b, 0x57, 0xd7, 0xa1, 0x34, 0x38, 0xf2, 0xe1, 0xf3, 0xb8,0x23, 0xdd, 0x78, 0xb5, 0x33, 0x6f, 0xd4, 0xf9, 0xa6, 0xe8, 0xcc, 0x7c, 0x9f, 0xb3, 0x22, 0xda,0x37, 0xf1, 0x2f, 0x4e, 0xe7, 0x6a, 0x75, 0xa8, 0x26, 0xeb, 0x3f, 0x6c, 0x69, 0x20, 0x87, 0x62,0xa7, 0x41, 0x96, 0x90, 0xb4, 0x42, 0x63, 0x99, 0xd0, 0x4d, 0x97, 0xbe, 0x40, 0xcf, 0x84, 0xe5,0x1d, 0x5a, 0x0c, 0x7f, 0xc7, 0xea, 0xee, 0xec, 0x00, 0xd5, 0x49, 0x2d, 0x51, 0xad, 0xb9, 0x89,0x1a, 0x80, 0xf5, 0xfe, 0x91, 0x01, 0x3c, 0x73, 0x93, 0x48, 0xa0, 0xe0, 0x94, 0xaa, 0x39, 0x8f,0x58, 0xe2, 0x31, 0x0b, 0xbb, 0xce, 0x4c, 0xd2, 0x56, 0xc2, 0x5e, 0x27, 0xb6, 0xfb, 0x65, 0xae,0x9a, 0xb0, 0xef, 0x36, 0xc5, 0x72, 0x5b, 0x7e, 0x54, 0x2c, 0x0f, 0xf6, 0xa9, 0x85, 0x2a, 0xb1,0x55, 0x60, 0xbd, 0x10, 0x86, 0xf7, 0xc1, 0x88, 0x12, 0xed, 0x67, 0xc4, 0x74, 0x30, 0x1b, 0xbc,0x77, 0x52, 0x3e, 0x8c, 0xe6, 0xff, 0x15, 0xde, 0x6d, 0x14, 0xa2, 0xcd, 0xa3, 0xd6, 0x17, 0x81,0x8d, 0x68, 0xa5, 0xfa, 0x3a, 0x04, 0x21, 0x1f, 0xac, 0x05, 0xa4, 0x76, 0x11, 0x70, 0x9e, 0x46,0x24, 0x5d, 0xc6, 0xe4, 0x95, 0x82, 0x1c, 0xba, 0x59, 0x09, 0xd9, 0x44, 0x98, 0x92, 0x07, 0xaf,0xc8, 0x45, 0x4b, 0x35, 0x0a, 0x0d, 0xfc, 0x9d, 0x16, 0x3b, 0xd3, 0x7d, 0xd1, 0xf4, 0xfd, 0xca,0x8e, 0x4f, 0xe3, 0xc9, 0x8b, 0xdc, 0x5c, 0xc0, 0x1e, 0x9b, 0x18, 0x02, 0x47, 0x03, 0x2b, 0x0e,0x25, 0x06, 0x6e, 0xf8, 0x5f, 0xbf, 0x8a, 0x7b, 0x50, 0xd8, 0x79, 0x9c, 0xab, 0x43, 0x53, 0xcb,0x66, 0x4a, 0xb2, 0xf0, 0xe9, 0x19, 0x29, 0x7a, 0xc3, 0x08, 0x83, 0xdb, 0x64, 0x13, 0x2e, 0x28])hash_data = substitution_hash(stripped.encode('ascii'), salt, 1968, 128)sig_value = base64.b64encode(hash_data).decode('ascii')return content + f'<Signature Value="{sig_value}" /\n</SourceInsightLicense>'

19.4 公钥替换

  • 位置:VA 0x661200, 文件偏移 0x25F800
  • 原始大小:450字节(PEM格式)+ 1字节null终止符
  • 方法:用自生成密钥对的公钥替换原始公钥

19.5 签名绕过补丁(推荐方法)

  • 位置:VA 0x51BC8B, 文件偏移 0x11B08B
  • 原始字节(14字节):F7 DE 1B C0 25 FA FE FF FF 05 CE 01 00 00
  • 补丁字节B8 C8 00 00 00 90 90 90 90 90 90 90 90 90(mov eax, 0xC8; nop*9)
  • 效果rsa_verify_51BB30 始终返回 200(成功)

19.6 DLL 代理方法

创建 msimg32.dll 代理,hook CryptVerifySignatureW 使其始终返回 TRUE。不修改原始 exe。


20. 函数索引表

许可核心函数

地址 函数名 说明 Keygen
0x51E4C0 license_51E4C0 License 构造函数
0x51F7D0 get_license_51F7D0 获取 License 单例
0x520000 init_license_520000 初始化许可系统(见 §1 架构图)
0x51C450 clear_license_data_51C450 清空许可数据
0x51E5C0 set_lic_filepath_51E5C0 设置 si4.lic 文件路径 get_lic_filepath()

序列号相关

地址 函数名 说明 Keygen
0x51DA20 validate_serial_51DA20 序列号格式与校验验证 validate_serial()
0x51B7A0 compute_serial_checksum_51B7A0 计算序列号校验位(见 §3) generate_serial()
0x51C070 parse_serial_with_details_51C070 解析序列号详细信息 validate_serial()
0x51C1A0 is_upgrade_serial_51C1A0 判断是否升级序列号
0x51ED70 generate_trial_serial_51ED70 生成试用序列号 generate_serial('T')

si4.lic 文件操作

地址 函数名 说明 Keygen
0x51DE60 load_si4_lic_51DE60 加载 si4.lic 文件(见 §4.4) parse_lic_file()
0x51C4D0 parse_si4_lic_51C4D0 解析 si4.lic XML(见 §4.2) parse_lic_file()
0x51D400 x_create_lic_51D400 创建 si4.lic(见 §5,在线+本地签名) generate_lic_local() / generate_lic_online()
0x51BAB0 read_lic_file_51BAB0 读取许可文件内容
0x51BD60 write_lic_file_51BD60 写入许可文件
0x51B9C0 collect_signature_data_51B9C0 收集签名验证数据(见 §4.3)
0x51D0C0 delete_lic_and_clear_51D0C0 删除 lic 文件并清空许可

签名验证

地址 函数名 说明 Keygen
0x51BB30 rsa_verify_51BB30 RSA 签名验证(CryptVerifySignatureW) _sign_online() / verify_license_file()
0x51E630 verify_license_signature_51E630 许可签名验证流程(见 §6) verify_license_file()
0x51D640 verify_local_signature_51D640 本地签名验证(见 §5.3) _sign_local()
0x402E10 base64_decode_402E10 Base64 解码 base64.b64decode()
0x402D50 base64_encode_402D50 Base64 编码 base64.b64encode()
0x403260 substitution_hash_wrapper_403260 替换表哈希包装 substitution_hash()
0x402F00 substitution_hash_402F00 替换表哈希实现(见 §5.1) substitution_hash()

激活与验证

地址 函数名 说明 Keygen
0x51F8E0 activate_license_local_51F8E0 在线激活(见 §7.1)
0x51F840 validate_license_online_51F840 在线许可验证(见 §8.4)
0x51DD50 validate_license_periodic_51DD50 周期性许可验证(见 §11)
0x51DDE0 validate_license_simple_51DDE0 简化许可验证(见 §11.2)
0x51C5D0 request_activation_51C5D0 请求在线激活
0x51C310 is_permanent_error_51C310 判断是否永久性错误
0x51ED20 deferred_activation_51ED20 延迟激活(ConnectedState=2,见 §8.3) generate_lic_local(..., deferred=True)
0x51FEA0 local_activation_51FEA0 本地临时激活(ConnectedState=3,见 §7.2) generate_lic_local()

停用

地址 函数名 说明 Keygen
0x51EC00 deactivate_license_51EC00 停用许可(主逻辑,见 §9)
0x51FBA0 deactivate_license_locked_51FBA0 停用许可(加锁版)
0x51C9A0 request_deactivation_51C9A0 请求网络停用
0x51D000 check_internet_connection_51D000 检查互联网连接
0x491670 cmd_deactivate_license_491670 停用命令处理器

黑名单

地址 函数名 说明 Keygen
0x51D7D0 check_blacklist_random_51D7D0 随机黑名单检查(见 §10)
0x462080 check_blacklist_entry_462080 检查黑名单条目
0x462310 parse_version_info_462310 解析版本信息(含黑名单)
0x43EDA0 download_version_info_43EDA0 下载版本信息(含黑名单数据)
0x43ECE0 check_download_status_43ECE0 检查下载状态
0x4B85C0 on_project_open_blacklist_4B85C0 打开项目时黑名单检查
0x4E69D0 on_file_open_blacklist_4E69D0 打开文件时黑名单检查
0x5B1DD0 on_idle_blacklist_5B1DD0 空闲时黑名单检查

过期与时间

地址 函数名 说明
0x51CC40 check_license_expiry_51CC40 检查许可有效期
0x51EAD0 is_license_expired_51EAD0 检查许可是否过期
0x51EA10 get_trial_remaining_days_51EA10 获取试用剩余天数
0x51D0F0 check_not_valid_after_51D0F0 检查许可是否在有效期内
0x51D1D0 save_next_validate_time_51D1D0 保存下次验证时间
0x51BE60 set_validation_interval_51BE60 设置验证间隔天数
0x51D380 get_last_local_activation_time_51D380 获取上次本地激活时间

DPAPI 加密

地址 函数名 说明
0x51E880 save_license_data_51E880 保存许可数据(DPAPI加密)
0x51E950 verify_stored_license_51E950 验证存储的许可数据
0x51CBA0 compute_content_hash_51CBA0 计算内容哈希

网络通信

地址 函数名 说明
0x427200 x_net_post_427200 HTTP POST 请求
0x4271E0 check_internet_connected_4271E0 检查互联网连接状态
0x427810 url_encode_append_param_427810 URL编码追加参数
0x4278A0 url_encode_append_int_param_4278A0 URL编码追加整数参数
0x4276F0 url_encode_value_4276F0 URL编码值
0x427620 url_decode_427620 URL解码

升级许可

地址 函数名 说明
0x51C1D0 find_si3_upgrade_license_51C1D0 查找SI3升级许可证
0x51DAE0 validate_upgrade_license_51DAE0 验证升级许可证
0x570630 validate_si3_serial_570630 验证SI3序列号
0x5706B0 is_si3_serial_prefix_5706B0 判断SI3序列号前缀

硬件ID

地址 函数名 说明
0x51C560 init_hostid_51C560 初始化主机ID
0x51CDA0 compare_hwid_to_license_51CDA0 比较HWID与许可证
0x51D080 check_hwid_match_51D080 检查HWID匹配
0x51CF20 init_hostid_or_fail_51CF20 初始化HostId或失败
0x51B810 compute_hwid_hash_51B810 计算HWID哈希
0x51BDB0 parse_hwid_51BDB0 解析HWID字符串
0x453270 get_machine_guid_453270 获取机器GUID

UI 与交互

地址 函数名 说明
0x51EF40 serial_input_dialog_proc_51EF40 序列号输入对话框
0x4F7C20 paint_about_dialog_4F7C20 绘制关于对话框
0x5209B0 show_nag_screen_5209B0 显示Nag屏幕
0x520BD0 check_and_show_nag_520BD0 检查并显示Nag
0x464F60 refresh_license_display_464F60 刷新许可显示
0x491630 cmd_show_nag_491630 Nag命令入口

后台线程与周期触发

地址 函数名 说明
0x51FBE0 license_bg_thread_51FBE0 许可后台验证线程
0x466F50 main_command_dispatch_466F50 主命令分发(每15次触发验证)
0x4FCC60 keyboard_handler_4FCC60 键盘处理(每100次触发验证)
0x582900 idle_timer_proc_582900 空闲定时器(每200次触发验证)

日志与对话框

地址 函数名 类型 说明
0x4140A0 log_info_4140A0 __cdecl (fmt, ...) INFO日志
0x413F90 log_warn_413F90 __cdecl (fmt, ...) WARNING日志
0x413E70 log_error_413E70 __cdecl (fmt, ...) ERROR日志
0x40B560 show_error_msg_40B560 __cdecl (fmt, ...) 错误消息框
0x40B700 show_info_msg_40B700 __cdecl (fmt, ...) 信息消息框
0x40B270 show_yesno_dialog_40B270 __cdecl (fmt, ...) Yes/No对话框
0x40B4B0 show_yesnocancel_dialog_40B4B0 __cdecl (fmt, ...) Yes/No/Cancel对话框
0x40BDA0 show_options_dialog_40BDA0 __cdecl (int, fmt, ...) 多选项对话框
0x40C080 show_status_msg_40C080 __cdecl (fmt, ...) 状态消息

辅助函数

地址 函数名 说明
0x51B880 format_local_time_51B880 格式化本地时间
0x51BD20 generate_request_id_51BD20 生成请求ID
0x51E770 get_version_string_51E770 获取版本字符串
0x51E7F0 get_registry_ref_51E7F0 获取注册表引用名
0x51BE70 set_default_registry_51BE70 设置注册表默认值
0x45FC80 XML_ReadFile_45FC80 读取XML文件
0x403290 decode_preset_strings_403290 解码预置字符串(XOR解密)

21. 关键数据与常量

21.1 替换表与常量

名称 地址 大小 说明
substitutionTable 0x612298 256字节 哈希算法替换表
alphabetTable 0x612178 26字节 校验字符表: "KV96GMJYH7QF5TCW4U3XZPRSDN"
pbkdf2_salt 0x612198 256字节 DPAPI内容哈希盐
local_signature_salt 0x5E2C10 256字节 本地签名盐
pszPublicKeyPEM 0x661200 451字节 RSA公钥(PEM格式)
unk_6616D0 0x6616D0 200字节 50个4字节预置字符串(XOR加密)

21.2 全局变量

名称 地址 类型 说明
license_obj 0x68BC90 License* 全局许可对象
ConnectedState_0 0x68BC90 DWORD 许可状态
Serial_4 0x68BC94 char[256] 序列号
type_60C 0x68C29C char 许可类型
ActId_63A 0x68C2DA char[256] 激活ID
lic_name_75C 0x68C3FC char[256] si4.lic 文件路径

22. 总结与技术要点

22.1 核心技术发现

  1. 双重签名机制
    • 在线激活:RSA-2048 + SHA-1 签名(服务器生成)
    • 本地激活:替换表哈希算法 + Base64(本地生成)
  2. 许可状态机
    • 4种状态:0=未激活, 1=已激活, 2=延迟, 3=本地
    • 状态转换通过不同的激活函数实现
  3. 安全机制
    • 序列号校验和算法(包含破折号)
    • 机器特定哈希(DPAPI + 机器GUID)
    • 黑名单随机检查(10%概率)
    • 周期性在线验证(30分钟)
    • 硬件ID匹配(volumeid/usersid)
  4. 关键流程
    • 启动:init_license_520000load_si4_lic_51DE60
    • 激活:activate_license_local_51F8E0 / local_activation_51FEA0
    • 验证:validate_license_online_51F840 / validate_license_periodic_51DD50
    • 停用:deactivate_license_51EC00

22.2 注册机实现要点

本项目的 Python 注册机 (si4_keygen.py) 完整实现了以下功能,详见 快速查找表:

功能 Keygen 函数 对应 IDA 函数 模式
生成序列号 generate_serial() compute_serial_checksum_51B7A0 --serial-only
验证序列号 validate_serial() validate_serial_51DA20 内置
本地许可 generate_lic_local() x_create_lic_51D400 + sub_51D640 --local
在线许可 generate_lic_online() x_create_lic_51D400 + rsa_verify_51BB30 --online
延迟许可 generate_lic_local(deferred=True) deferred_activation_51ED20 --deferred
试用许可 generate_serial('T') + Deferred generate_trial_serial_51ED70 --trial
验证许可 verify_license_file() verify_license_signature_51E630 --verify
替换公钥 patch_exe_with_public_key() --patch-exe
适配公钥 _load_or_gen_key(fit=True) --fit-key

22.3 攻击方案对比

方案 复杂度 修改 EXE 需要私钥 适用场景
简单补丁 ★☆☆☆☆ 是(2字节) 快速破解,最推荐
公钥替换 ★★☆☆☆ 是(~450字节) 干净方案,配合 --patch-exe
DLL 代理 ★★★☆☆ 不修改原始 EXE
本地激活 ★☆☆☆☆ 仅需 --local,但30天后可能被后台线程升级

推荐方案:使用 --local --deferred 或简单补丁方案。本地激活(ConnectedState=3)无需私钥,
但后台线程可能在30分钟后尝试将其升级为在线许可。配合补丁2(杀死后台线程)可完美解决。

22.4 安全建议(针对 SourceDynamics)

  1. 保护替换表:这些表是签名算法的核心
  2. 网络验证:定期检查可发现被破解的许可
  3. 黑名单:及时更新黑名单以阻止被禁用的序列号
  4. 硬件绑定:HWID机制增加了许可证移植难度

23. 签名验证测试结果与RSA字节序发现(2026-04-29)

23.1 RSA签名字节序问题(关键发现)

问题: 对官方试用si4.lic进行RSA验证时,使用Python的cryptography库始终失败。

根本原因: Windows CryptoAPI的CryptVerifySignatureW使用小端序(little-endian)存储RSA签名,而Python的cryptography库使用大端序(big-endian)。

验证过程:

  1. 原始RSA解密(大端序)结果以281fd08e开头 → 不是PKCS#1 v1.5格式
  2. 反转签名字节序后,RSA解密结果以0001ffff...开头 → 标准PKCS#1 v1.5格式 ✅
  3. 从签名中提取的SHA-1哈希: 15ae1f111fcf23eef5b6333a42cad36b1310fc10
  4. 我们计算的SHA-1哈希: 15ae1f111fcf23eef5b6333a42cad36b1310fc10完全匹配

修复方法:

  • 验证时: sig_bytes_reversed = file_sig_bytes[::-1] → 传给Python的verify
  • 签名时: signature = private_key.sign(...)[::-1] → 反转后base64编码存入文件

si4.lic验证结果(修复后):

检查项 结果 说明
激活类型 Online (ConnectedState=1) ActId="9998765740" 是纯数字
Serial格式 ✅ Valid S4TR-5D5Z-ZRNX-HZP7 校验和正确
RSA签名 ✅ VALID 反转字节序后验证通过
SHA-1哈希 ✅ MATCH 签名中哈希与计算哈希完全一致
exe公钥 原始未修改 与分析记录的公钥完全一致

签名验证的数据内容(通过调试确认):

<!--SourceInsight4.xLicenseFileDONOTEDITTHISFILE.Doingsowillrenderitunusable.Thislicensewascreatedfor:testcomikun@kk.com--><SourceInsightLicense><HeaderValue="1"/><LicensePropertiesLicensedUser="test"ActId="9998765740"HWID="WMF4UMY6-JXNSU993"Serial="S4TR-5D5Z-ZRNX-HZP7"Organization="com"Email="ikun@kk.com"Type="Trial"Version="4"MinorVersion="0"Date="2026-04-29"Expiration="2026-05-29"/>

关键要点:

  • 签名数据包含XML注释
  • 签名数据包含<Header Value="1"/>元素
  • 签名数据在<Signature元素处截断
  • 去除所有空白字符(空格、制表符、CR、LF)
  • 不含null终止符(在线签名用strlen,本地签名用strlen+1)
  • 签名字节序为小端序(Windows CryptoAPI标准)

23.2 本地许可证验证结果

使用keygen --local 模式生成的许可证通过所有验证步骤:

检查项 结果 说明
激活类型 Local (ConnectedState=3) ActId前缀在50项前缀表中
本地签名 ✅ VALID 128字节substitution hash完全匹配
ActId前缀 ✅ VALID 在有效前缀列表中
ActId后缀 ✅ MATCH 与当前机器version string匹配

23.3 init_license_520000 完整验证链

init_license_520000()├─ load_si4_lic_51DE60() → 返回 0xC8 (200)?│    ├─ 解析XML,提取属性│    ├─ 检查ActId → 确定ConnectedState│    │    ├─ "Deferred" → ConnectedState=2│    │    ├─ 前缀匹配sub_4035C0 → ConnectedState=3 (本地)│    │    └─ 其他 → ConnectedState=1 (在线)│    ├─ parse_serial_with_details_51C070(serial, &v12, &v11, &v10, !v4)│    │    └─ 本地激活时(!v4=false): 校验和NOT验证│    ├─ 检查v11(序列号类型) == license Type│    ├─ 检查v10(版本) == versionMajor_6696FB (4)│    ├─ check_blacklist_entry_462080│    └─ 检查Version属性 == versionMajor_6696FB│├─ ConnectedState==2 → ConnectedState=0, done│├─ ConnectedState==3 (本地):│    ├─ sub_51D640() → 本地签名验证│    │    ├─ 截断到Signature偏移│    │    ├─ 去除空白字符│    │    ├─ sub_403260(data, strlen+1, 0x7B0, 0x80, output) ← 含null终止符│    │    └─ 比较base64解码的128字节签名│    ││    └─ sub_51EE40() → ActId验证│         ├─ sub_4035C0() → 检查前缀(4字符)在50项前缀表中│         └─ strcmp(ActId+4, version_string) → 检查后缀匹配机器version string│├─ ConnectedState==1 (在线):│    └─ verify_license_signature_51E630() → RSA签名验证│         ├─ 截断到Signature偏移│         ├─ 去除空白字符│         ├─ rsa_verify_51BB30(data, strlen, sig, siglen) ← 不含null终止符│         └─ RSA-2048 + SHA-1 via CryptVerifySignatureW│└─ 任何验证失败 → clear_license_data_51C450 → ConnectedState=0→ "License is not activated"

23.4 ActId验证详解 (sub_51EE40)

BOOL __thiscall sub_51EE40(const char *this)
{// 1. 检查ActId前缀是否在50项前缀表中v2 = sub_4035C0((int)(this + 0x63A), (int)&unk_6616D0, 4u, 0x32, 0x1B7F);//    返回值: 前缀长度(4)如果匹配, 0如果不匹配// 2. 计算当前机器的version string// 3. 比较ActId后缀与version stringreturn v2 && get_version_string_51E770(Str2, 0) && strcmp(this + v2 + 0x63A, Str2) == 0;
}

ActId格式: [4字符前缀][version string]

例如: ActId = "C7621146394088"

  • 前缀 = "C762" (在50项前缀表中)
  • 后缀 = "1146394088" (必须匹配当前机器的version string)

23.5 Version String计算 (sub_51BCB0 → get_version_string_51E770)

// sub_51BCB0: 版本字符串计算
void __cdecl sub_51BCB0(const char *a1, int a2, int a3, int a4)
{sprintf(Str, "%s%s%s", a1, a1 + 0x100, a1 + 0x200);  // vol_guid + user_sid + computer_namev4 = strlen(Str);                                       // 不含null终止符sub_403260((int)Str, v4, a4 != 0 ? 0x42A : 0x7A9, a2, a3);// iterations: a4!=0 → 0x42A(1066), a4==0 → 0x7A9(1961)// keylen: a2=4
}// get_version_string_51E770: 获取版本字符串
int __cdecl get_version_string_51E770(char *Buffer, int a2)
{sub_51C390(v4);                          // 初始化结构体init_hostid_51C560(v4);                  // 填充HostId数据sub_51BCB0(v4, 4, &v3, a2);             // 计算hash (a2=0 → iterations=0x7A9)sprintf(Buffer, "%u", v3);              // 格式化为无符号十进制整数return 1;
}

Version String计算流程:

  1. 拼接: vol_guid + user_sid + computer_name
  2. 哈希: sub_403260(data, strlen(data), 0x7A9, 4, &result)不含null终止符
  3. 格式化: sprintf(buffer, "%u", result) — 无符号十进制整数

HostId数据来源 (init_hostid_51C560):

  • get_machine_guid_453270(a1) → Volume GUID (offset 0x000)
  • sub_453460(a1 + 0x100) → User SID (offset 0x100)
  • sub_4516A0(a1 + 0x200) → Computer Name (offset 0x200)
  • sub_4486F0(a1 + 0x200) → 替换chars < 0x20 为 '?'

编码: sub_44B140 使用 WideCharToMultiByte 编码页 0xFDE9 (65001 = UTF-8)

23.6 在线 vs 本地许可证格式差异

在线许可证格式 (ConnectedState=1):

<SourceInsightLicense><HeaderValue="1"/>	<LicensePropertiesLicensedUser="..."ActId="9998765740"HWID="WMF4UMY6-JXNSU993"Serial="S4TR-5D5Z-ZRNX-HZP7"Organization="..."Email="..."Type="Trial"Version="4"MinorVersion="0"Date="2026-04-29"Expiration="2026-05-29"/><SignatureValue="...RSA签名..."/>
</SourceInsightLicense>

本地许可证格式 (ConnectedState=3):

<SourceInsightLicense><LicensePropertiesActId="C7621146394088"Serial="S4SG-..."LicensedUser="..."Organization="..."Email="..."Type="Standard"Version="4"MinorVersion="0"Date="2026-04-29"/><SignatureValue="...替换表哈希签名..."/>
</SourceInsightLicense>

关键差异:

项目 在线许可证 本地许可证
Header元素 <Header Value="1"/>
HWID属性
ActId 纯数字 4字符前缀 + version string
Expiration Trial有 Trial有
签名类型 RSA-2048 + SHA-1 (256字节) 替换表哈希 (128字节)
属性顺序 LicensedUser在前 ActId在前

23.7 序列号校验和验证规则

关键发现: parse_serial_with_details_51C070 的第5个参数 a5 控制校验和验证:

  • 本地激活 (v4=true): a5 = !v4 = false校验和NOT验证
  • 在线激活 (v4=false): a5 = !v4 = true → 校验和IS验证

位置6的R/G/D/F检查也仅在 a5=true 时执行。

23.8 ActId前缀表(50项,XOR编码)

前缀表存储在 unk_6616D0,使用XOR编码,密钥表在 byte_62E5D0(10000字节)。

解码算法: decoded_byte = encoded_byte ^ key_table[(byte_offset + 7039) % 10000]

解码后的50个有效前缀:

# 前缀 # 前缀 # 前缀 # 前缀 # 前缀
0 673A 10 4B67 20 047E 30 557A 40 FE7E
1 44D3 11 18E3 21 08F1 31 F661 41 83A6
2 5B36 12 3E93 22 0A51 32 F148 42 F509
3 08E5 13 F358 23 B755 33 49F9 43 0AD6
4 C603 14 87A8 24 61B5 34 4F26 44 1F3F
5 D775 15 A360 25 L555 35 52A9 45 365D
6 C762 16 D2F4 26 76B6 36 03A1 46 1C67
7 16F5 17 68E3 27 3BF2 37 0E90 47 DA22
8 55E0 18 13DC 28 D775 38 74E6 48 A478
9 00D0 19 7B3E 29 0F09 39 1EA4 49 FA17

注意: 第25项 "L555" 包含非十六进制字符 'L',这是原始数据中的异常值。

23.9 本地签名 vs 在线签名的关键差异

项目 本地签名 (sub_51D640) 在线签名 (verify_license_signature_51E630)
算法 替换表哈希 RSA-2048 + SHA-1
签名长度 128字节 256字节
哈希数据 null终止符 (strlen+1) 不含null终止符 (strlen)
迭代参数 iterations=0x7B0 (1968) N/A
输出长度 keylen=0x80 (128) N/A
验证方式 逐字节比较 CryptVerifySignatureW
需要密钥 否(使用固定salt表) 是(需要嵌入公钥)


23.10 ActId/HWID 算法深度验证 (2026-05-05)

通过 IDA Pro MCP 重新读取 exe 数据表并反编译关键函数,逐项验证算法正确性:

数据表验证(通过 get_bytes 从 exe 读取):

表名 地址 大小 验证结果
ALPHABET_TABLE 0x612178 26B ✓ "KV96GMJYH7QF5TCW4U3XZPRSDN"
SUBSTITUTION_TABLE 0x612298 256B ✓ 256字节完全匹配
LOCAL_SIGNATURE_SALT 0x5E2C10 256B ✓ 256字节完全匹配
Encoded ActId prefixes 0x6616D0 200B ✓ 50×4字节XOR编码
XOR key table 0x62E5D0 10000B ✓ 偏移7039起200字节已读取验证

ActId 前缀解码验证:通过反编译 sub_403290 (0x403290) 和读取 XOR 密钥表,手工计算解码:

prefix[i][j] = encoded[4*i+j] ^ key_table[(7039+4*i+j) % 10000]
  • 前缀 #0: encoded 8D 8D 5B 1F ^ key BB BA 68 5E = "673A"
  • 前缀 #6: encoded 62 3B 8C FE ^ key 21 0C BA CC = "C762"
  • 前缀 #25: encoded CF DB E7 E7 ^ key 83 EE D2 D2 = "L555" ✓ (非 "1555")
  • 前缀 #28: encoded F3 59 C8 3D ^ key 80 CC 2A E3 = "D775"
  • 前缀 #47: encoded 0E 73 E6 23 ^ key FE 0A A7 31 = "DA22"

函数反编译验证(通过 IDA Pro MCP decompile):

函数 地址 验证结果
substitution_hash_402F00 0x402F00 ✓ 算法: salt[(iter+i+data[0])%256] → for j=1..len: salt[v^data[j]]
compute_hwid_hash_51B810 0x51B810 ✓ 使用 SUBSTITUTION_TABLE 进行8轮迭代,输出映射到 ALPHABET_TABLE
sub_51BCB0 0x51BCB0 sprintf(Str,"%s%s%s",a1,a1+0x100,a1+0x200);iters=0x7A9(keylen)
get_version_string_51E770 0x51E770 sub_51BCB0(v4,4,&v3,a2)sprintf(Buffer,"%u",v3)
x_create_lic_51D400 0x51D400 ✓ strip "\n\r\t "sub_403260(v12,strlen+1,0x7B0,0x80,v19)含null终止符
init_hostid_51C560 0x51C560 ✓ vol_guid@0x000 + user_sid@0x100 + comp_name@0x200 + clean@0x200
local_activation_51FEA0 0x51FEA0 sub_403290(&unk_6616D0,4,50,7039);随机前缀+版本字符串
sub_51EE40 0x51EE40 sub_4035C0 检查前缀;strcmp(ActId+v2, version_string)
parse_hwid_51BDB0 0x51BDB0 ✓ 长度0x11(17);格式8+'-'+8
compare_hwid_to_license_51CDA0 0x51CDA0 ✓ Standard: 比较part1(volguid hash);非Standard: 比较part2(usersid hash)
sub_403290 0x403290 size=4, count=50, xor_offset=7039, key_table=byte_62E5D0, mod=0x2710

23.11 简单二进制补丁方案 (Simple Binary Patches)

v4.0.0.150 可以通过修改极少字节来绕过许可验证。

方案一:两字节补丁(推荐)

补丁 1:绕过签名验证 — 修改 init_license_520000 中的 RSA 签名检查

文件偏移: 需要根据 PE 节映射计算(IDA VA → 文件偏移)
VA:  0x5200DC
原始: 74 0E          (jz  +0x0E → 签名有效时跳转到成功路径)
修改: EB 0E          (jmp +0x0E → 无论签名是否有效,始终跳转到成功路径)

反汇编上下文:

5200CF:  call verify_license_signature_51E630   ; 验证RSA签名
5200D4:  add esp, 8
5200D7:  cmp eax, 0C8h                         ; 结果 == 200(成功)?
5200DC:  jz  loc_5200EC        ; ← 将此 jz 改为 jmp (74→EB)
5200DE:  call clear_license_data_51C450         ; 失败处理
5200E5:  jmp loc_5200EC

补丁 2:杀死后台验证线程 — 修改 license_bg_thread_51FBE0 入口

VA:  0x51FBE0
原始: 83 EC 18       (sub esp, 0x18 → 函数序言)
修改: C3             (ret → 立即返回,线程不执行任何操作)

反汇编上下文:

51FBE0:  sub esp, 18h        ; ← 将第一字节 83 改为 C3
51FBE3:  push ebx
51FBE4:  mov ebx, ds:Sleep
...

补丁 1 使程序在启动时不检查 si4.lic 的 RSA 签名,补丁 2 阻止后台线程在30分钟后重新验证或删除许可文件。两者结合即可实现永久破解。

方案二:完整补丁(同时支持本地和在线许可)

如果需要同时绕过本地签名验证(ConnectedState=3),增加:

补丁 1b:绕过本地签名验证

VA:  0x5200B1
原始: 75 2B          (jnz loc_5200DE → 本地签名失败时跳转到错误处理)
修改: 90 90          (nop; nop → 忽略本地签名失败,继续执行)

反汇编上下文:

5200A4:  call sub_51D640                          ; 验证本地替换表签名
5200A9:  add esp, 8
5200AC:  cmp eax, 0C8h                            ; 结果 == 200?
5200B1:  jnz loc_5200DE          ; ← 将此 jnz 改为 nop; nop
5200B3:  mov ecx, esi
5200B5:  call sub_51EE40                          ; ActId 验证

VA → 文件偏移 计算方法

Source Insight 4 使用典型的 PE 结构。IDA 中的 VA(虚拟地址)需要转换为文件偏移才能用十六进制编辑器修改:

文件偏移 = VA - ImageBase - 节RVA + 节文件偏移

对于 sourceinsight4.exe v4.0.0.150:

  • ImageBase: 0x400000
  • .text 节 RVA 约 0x1000,文件偏移约 0x400
.text 节内 VA 的文件偏移 ≈ VA - 0x400000 + 0x400 - 0x1000 = VA - 0x400C00
  • VA 0x5200DC → 文件偏移 ≈ 0x11F4DC
  • VA 0x51FBE0 → 文件偏移 ≈ 0x11EFE0
  • VA 0x5200B1 → 文件偏移 ≈ 0x11F4B1

注意:以上文件偏移已验证精确。也可在十六进制编辑器中搜索对应的字节序列来定位:

  • 补丁1:搜索 74 0E 附近有 83 F8 C8(cmp eax, 0C8h)的 FF 15 ?? ?? ?? ??(call 间接)
  • 补丁2:搜索函数开头的 83 EC 18 53 8B 1D(sub esp, 0x18; push ebx; mov ebx, ...)

23.12 30天试用许可生成流程

完整流程图:

用户点击 "Begin 30-day Free Trial"│├─ 1. 初始化License对象 (get_license_51F7D0 → license_51E4C0)│     ├─ 设置服务器: sls.sourceinsight.com│     ├─ 设置端点:│     │    ├─ /request_activation/│     │    ├─ /create_trial_license/    ← 试用许可专用端点│     │    └─ /request_deactivation/│     └─ validate_interval_days = 6│├─ 2. 初始化HostId (init_hostid_51C560)│     ├─ Volume GUID (get_machine_guid_453270)│     ├─ User SID (sub_453460)│     └─ Computer Name (sub_4516A0, UTF-8编码)│├─ 3. 构建HTTP POST请求 (request_activation_51C5D0)│     ├─ 参数列表:│     │    ├─ serial=S4TR-5D5Z-ZRNX-HZP7│     │    ├─ product_version_major=4│     │    ├─ product_version_minor=0│     │    ├─ product_version_build=150│     │    ├─ user_name=...│     │    ├─ user_org=...│     │    ├─ user_email=...│     │    ├─ prevserial=...│     │    ├─ upgradeserial=...│     │    ├─ cpu_count=...│     │    ├─ localactivation=false (试用为在线激活)│     │    ├─ usersid=S-1-5-21-...│     │    ├─ volumeid=7245bfa7-...│     │    ├─ compname=DESKTOP-...│     │    ├─ os_version_major/minor/build=...│     │    ├─ os_server=true/false│     │    ├─ app_type=release│     │    ├─ requestid=... (随机生成)│     │    └─ local_time=... (格式化时间)│     ││     └─ POST到: https://sls.sourceinsight.com/create_trial_license/│├─ 4. 处理服务器响应 (activate_license_local_51F8E0)│     ├─ 服务器返回: URL编码的字符串│     ├─ URL解码 (sub_427620): %XX→字节, +→空格│     ├─ 前8字节: 头部/元数据(跳过)│     └─ 从偏移8开始: si4.lic文件内容│├─ 5. 保存许可数据│     ├─ save_license_data_51E880(&Str[8])│     │    └─ DPAPI加密 → 写入注册表│     ├─ save_next_validate_time_51D1D0(interval)│     └─ write_lic_file_51BD60(lic_path, &Str[8])│          └─ "wb"模式写入si4.lic文件│└─ 6. 后续验证 (license_bg_thread_51FBE0)├─ 每30分钟循环 (Sleep 0x1B7740 = 1,800,000ms)├─ 检查互联网连接├─ ConnectedState=1 → validate_license_online_51F840│    └─ POST到 /request_activation/ (a4=1, 在线验证模式)└─ 错误处理:├─ 460/462/485/489/495 → 删除lic + 清除数据└─ 其他错误 → 延后验证时间

服务器响应格式:

[8字节头部][si4.lic完整内容]
  • 头部8字节: 可能是状态码或长度信息
  • si4.lic内容: 从偏移8开始,包含完整的XML许可证文件
  • 整个响应经过URL编码

试用许可的si4.lic特征:

  • Type="Trial"
  • Expiration="YYYY-MM-DD" (创建日期+30天)
  • ActId为纯数字 (如"9998765740")
  • HWID存在 (如"WMF4UMY6-JXNSU993")
  • <Header Value="1"/>元素
  • 签名为RSA-2048 + SHA-1 (256字节, 小端序)

本地激活到在线激活的转换 (license_bg_thread_51FBE0):

ConnectedState=3 (本地) + 有互联网→ activate_license_local_51F8E0()→ POST到 /request_activation/ (localactivation=true)→ 成功 → ConnectedState=1 (在线)→ 失败460/462/485/489/495 → 删除lic + 清除数据

服务器错误码映射 (request_activation_51C5D0):

服务器码 本地码 含义
1001 465 序列号无效
1002 466 序列号已被使用
1003/1006 467 激活失败
1004 480 许可证过期
1005 496 服务器错误
1007 484 黑名单
  • SourceInsight4Patch 项目(生成 LicenseSerial 和 si4.lic)
  • Windows CryptoAPI 文档
  • XML 解析库文档
  • Base64 编码标准
  • PBKDF2 算法文档(参考)

kg

#!/usr/bin/env python3
"""
SourceInsight 4 License Generator  (based on RE of sourceinsight4.exe v4.0.0.150)USAGE EXAMPLES
==============# 1. Local activation (no patch needed, run on target machine)python si4_keygen.py# 2. 30-day trial (local activation, Trial type)python si4_keygen.py --trial# 3. Deferred activation (ActId=Deferred, no signature check)python si4_keygen.py --deferred# 4. Online activation (RSA-2048 signature, requires patching exe or DLL proxy)python si4_keygen.py --online# 5. Online activation with auto-generated RSA key that fits in exepython si4_keygen.py --online --fit-key# 6. Online activation + patch exe with new public keypython si4_keygen.py --online --fit-key --patch-exe# 7. Verify a license filepython si4_keygen.py --verify si4.lic# 8. Generate serial number onlypython si4_keygen.py --serial-onlyACTIVATION MODES
================--local     Local activation (default). Uses substitution table hash.ConnectedState=3. Must run on target machine (ActId containsmachine-specific version string). No exe patch needed.--trial     Shortcut for --local --type Trial. Generates 30-day triallicense with Expiration field.--online    Online activation. Uses RSA-2048 + SHA-1 signature.ConnectedState=1. Requires replacing exe's public key orusing a DLL proxy to hook CryptVerifySignatureW.--deferred  Deferred activation. ActId="Deferred". No signatureverification. ConnectedState=2.SIGNATURE TYPES
===============Local:  substitution_hash(data_with_null, salt_table, iter=0x7B0, keylen=128)data = strip_whitespace(content_before_Signature) + '\\0'salt_table at VA 0x5E2C10 in exeOnline: RSA-2048 + SHA-1, PKCS#1 v1.5 paddingdata = strip_whitespace(content_before_Signature)   (NO null terminator)IMPORTANT: Windows CryptoAPI uses LITTLE-ENDIAN byte order for signaturesPython cryptography uses BIG-ENDIAN, so we reverse bytes when signing/verifyingPublic key PEM at VA 0x661200 (file offset 0x25F800) in exeSERIAL FORMAT
=============S4TG-XYYZZ-AAAA-BBBBPos 0:   'S' (fixed)Pos 1:   digit (must match versionMajor=4)Pos 2:   license type: T=Trial, B=Beta, S=Standard, U=UpgradePos 3:   category: G=General, R=RetailPos 4:   '-' (separator)Pos 5:   random alphanumericPos 6:   R/G/D/FPos 7-8: random alphanumericPos 9:   '-' (separator)Pos 10-13: random alphanumericPos 14:   '-' (separator)Pos 15-18: checksum (4 chars from ALPHABET_TABLE)FILE STRUCTURE
==============This script is organized into the following sections:1. IMPORTS2. DATA TABLES     – tables extracted from sourceinsight4.exe3. CONSTANTS       – global defaults and layout constants4. CORE ALGORITHMS – substitution_hash, serial gen/validation5. SYSTEM INFO     – volume GUID, user SID, computer name, HWID, version string6. LICENSE HELPERS – ActId prefix list, build helpers, signing functions7. LICENSE GEN     – generate_lic_local, generate_lic_online8. LICENSE PARSE   – parse_lic_file, is_local_actid, verify_license_file9. EXE PATCHING    – find_exe_path, patch_exe_with_public_key, show_patch_info10. KEY MANAGEMENT – _ensure_crypto, _load_or_gen_key11. CLI / main()   – argument parsing and dispatch
"""# =============================================================================
# SECTION 1: IMPORTS
# =============================================================================import os
import sys
import base64
import random
import string
import struct
import xml.etree.ElementTree as ET
from datetime import date, timedeltatry:from cryptography.hazmat.primitives import hashes, serializationfrom cryptography.hazmat.primitives.asymmetric import rsa, paddingfrom cryptography.hazmat.backends import default_backendHAS_CRYPTO = True
except ImportError:HAS_CRYPTO = False# =============================================================================
# SECTION 2: DATA TABLES (extracted byte-exact from sourceinsight4.exe)
#
#   Each table below was verified against the EXE via IDA Pro `get_bytes`.
#   Byte-for-byte identical to the original binary.
# =============================================================================# --- SUBSTITUTION_TABLE (256 bytes) @ VA 0x612298 ---
SUBSTITUTION_TABLE = [0x23, 0xDD, 0x78, 0xB5, 0x33, 0x6F, 0xD4, 0xF9, 0xA6, 0xE8, 0xCC, 0x7C, 0x9F, 0xB3, 0x22, 0xDA,0x32, 0xDF, 0x71, 0xB7, 0x61, 0x3D, 0x6B, 0x57, 0xD7, 0xA1, 0x34, 0x38, 0xF2, 0xE1, 0xF3, 0xB8,0x1A, 0x80, 0xF5, 0xFE, 0x91, 0x01, 0x3C, 0x73, 0x93, 0x48, 0xA0, 0xE0, 0x94, 0xAA, 0x39, 0x8F,0x58, 0xE2, 0x31, 0x0B, 0xBB, 0xCE, 0x4C, 0xD2, 0x56, 0xC2, 0x5E, 0x27, 0xB6, 0xFB, 0x65, 0xAE,0x55, 0x60, 0xBD, 0x10, 0x86, 0xF7, 0xC1, 0x88, 0x12, 0xED, 0x67, 0xC4, 0x74, 0x30, 0x1B, 0xBC,0x9A, 0xB0, 0xEF, 0x36, 0xC5, 0x72, 0x5B, 0x7E, 0x54, 0x2C, 0x0F, 0xF6, 0xA9, 0x85, 0x2A, 0xB1,0x37, 0xF1, 0x2F, 0x4E, 0xE7, 0x6A, 0x75, 0xA8, 0x26, 0xEB, 0x3F, 0x6C, 0x69, 0x20, 0x87, 0x62,0x8D, 0x68, 0xA5, 0xFA, 0x3A, 0x04, 0x21, 0x1F, 0xAC, 0x05, 0xA4, 0x76, 0x11, 0x70, 0x9E, 0x46,0x24, 0x5D, 0xC6, 0xE4, 0x95, 0x82, 0x1C, 0xBA, 0x59, 0x09, 0xD9, 0x44, 0x98, 0x92, 0x07, 0xAF,0xA7, 0x41, 0x96, 0x90, 0xB4, 0x42, 0x63, 0x99, 0xD0, 0x4D, 0x97, 0xBE, 0x40, 0xCF, 0x84, 0xE5,0x1D, 0x5A, 0x0C, 0x7F, 0xC7, 0xEA, 0xEE, 0xEC, 0x00, 0xD5, 0x49, 0x2D, 0x51, 0xAD, 0xB9, 0x89,0x77, 0x52, 0x3E, 0x8C, 0xE6, 0xFF, 0x15, 0xDE, 0x6D, 0x14, 0xA2, 0xCD, 0xA3, 0xD6, 0x17, 0x81,0xC8, 0x45, 0x4B, 0x35, 0x0A, 0x0D, 0xFC, 0x9D, 0x16, 0x3B, 0xD3, 0x7D, 0xD1, 0xF4, 0xFD, 0xCA,0x25, 0x06, 0x6E, 0xF8, 0x5F, 0xBF, 0x8A, 0x7B, 0x50, 0xD8, 0x79, 0x9C, 0xAB, 0x43, 0x53, 0xCB,0x8E, 0x4F, 0xE3, 0xC9, 0x8B, 0xDC, 0x5C, 0xC0, 0x1E, 0x9B, 0x18, 0x02, 0x47, 0x03, 0x2B, 0x0E,0x66, 0x4A, 0xB2, 0xF0, 0xE9, 0x19, 0x29, 0x7A, 0xC3, 0x08, 0x83, 0xDB, 0x64, 0x13, 0x2E, 0x28,
]# --- ALPHABET_TABLE (26 bytes) @ VA 0x612178 ---
# Used for serial checksum encoding and HWID character mapping
ALPHABET_TABLE = "KV96GMJYH7QF5TCW4U3XZPRSDN"# --- LOCAL_SIGNATURE_SALT (256 bytes) @ VA 0x5E2C10 ---
# Used as the salt/table for local (substitution_hash) signatures.
# This is a permutation of SUBSTITUTION_TABLE, not identical to it.
LOCAL_SIGNATURE_SALT = [0x32, 0xDF, 0x71, 0xB7, 0x61, 0x3D, 0x6B, 0x57, 0xD7, 0xA1, 0x34, 0x38, 0xF2, 0xE1, 0xF3, 0xB8,0x23, 0xDD, 0x78, 0xB5, 0x33, 0x6F, 0xD4, 0xF9, 0xA6, 0xE8, 0xCC, 0x7C, 0x9F, 0xB3, 0x22, 0xDA,0x37, 0xF1, 0x2F, 0x4E, 0xE7, 0x6A, 0x75, 0xA8, 0x26, 0xEB, 0x3F, 0x6C, 0x69, 0x20, 0x87, 0x62,0xA7, 0x41, 0x96, 0x90, 0xB4, 0x42, 0x63, 0x99, 0xD0, 0x4D, 0x97, 0xBE, 0x40, 0xCF, 0x84, 0xE5,0x1D, 0x5A, 0x0C, 0x7F, 0xC7, 0xEA, 0xEE, 0xEC, 0x00, 0xD5, 0x49, 0x2D, 0x51, 0xAD, 0xB9, 0x89,0x1A, 0x80, 0xF5, 0xFE, 0x91, 0x01, 0x3C, 0x73, 0x93, 0x48, 0xA0, 0xE0, 0x94, 0xAA, 0x39, 0x8F,0x58, 0xE2, 0x31, 0x0B, 0xBB, 0xCE, 0x4C, 0xD2, 0x56, 0xC2, 0x5E, 0x27, 0xB6, 0xFB, 0x65, 0xAE,0x9A, 0xB0, 0xEF, 0x36, 0xC5, 0x72, 0x5B, 0x7E, 0x54, 0x2C, 0x0F, 0xF6, 0xA9, 0x85, 0x2A, 0xB1,0x55, 0x60, 0xBD, 0x10, 0x86, 0xF7, 0xC1, 0x88, 0x12, 0xED, 0x67, 0xC4, 0x74, 0x30, 0x1B, 0xBC,0x77, 0x52, 0x3E, 0x8C, 0xE6, 0xFF, 0x15, 0xDE, 0x6D, 0x14, 0xA2, 0xCD, 0xA3, 0xD6, 0x17, 0x81,0x8D, 0x68, 0xA5, 0xFA, 0x3A, 0x04, 0x21, 0x1F, 0xAC, 0x05, 0xA4, 0x76, 0x11, 0x70, 0x9E, 0x46,0x24, 0x5D, 0xC6, 0xE4, 0x95, 0x82, 0x1C, 0xBA, 0x59, 0x09, 0xD9, 0x44, 0x98, 0x92, 0x07, 0xAF,0xC8, 0x45, 0x4B, 0x35, 0x0A, 0x0D, 0xFC, 0x9D, 0x16, 0x3B, 0xD3, 0x7D, 0xD1, 0xF4, 0xFD, 0xCA,0x8E, 0x4F, 0xE3, 0xC9, 0x8B, 0xDC, 0x5C, 0xC0, 0x1E, 0x9B, 0x18, 0x02, 0x47, 0x03, 0x2B, 0x0E,0x25, 0x06, 0x6E, 0xF8, 0x5F, 0xBF, 0x8A, 0x7B, 0x50, 0xD8, 0x79, 0x9C, 0xAB, 0x43, 0x53, 0xCB,0x66, 0x4A, 0xB2, 0xF0, 0xE9, 0x19, 0x29, 0x7A, 0xC3, 0x08, 0x83, 0xDB, 0x64, 0x13, 0x2E, 0x28,
]# --- VALID_ACTID_PREFIXES (50 entries) ---
# XOR-decoded via sub_403290 with key_table @ VA 0x62E5D0.
# When ActId starts with any of these 4-char prefixes, it's treated
# as a local (ConnectedState=3) activation.
VALID_ACTID_PREFIXES = ["673A", "44D3", "5B36", "08E5", "C603", "D775", "C762", "16F5", "55E0", "00D0","4B67", "18E3", "3E93", "F358", "87A8", "A360", "D2F4", "68E3", "13DC", "7B3E","047E", "08F1", "0A51", "B755", "61B5", "L555", "76B6", "3BF2", "D775", "0F09","557A", "F661", "F148", "49F9", "4F26", "52A9", "03A1", "0E90", "74E6", "1EA4","FE7E", "83A6", "F509", "0AD6", "1F3F", "365D", "1C67", "DA22", "A478", "FA17",
]# --- ORIGINAL PUBLIC KEY (RSA-2048) @ VA 0x661200 ---
# Extracted byte-exact from sourceinsight4.exe .rdata section.
# Windows CryptoAPI uses this embedded PEM for CryptVerifySignatureW.
ORIGINAL_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsi1qJjMKu2BtB6AXnupP
LRclMtfYzHio6MfhlvSaEWruubIJUZz8PzBlsKhPLJMJlnuijPkksuj4FIMsOcue
zMEjtoPlujvHMfnSQU6TfXBiNhfQigIHlNDFyKIeNNduD3UHJJGSwZjXz2KoUOa2
43B02ZuWjMD41OpwhADVqLLcyNdUQYirX9TBsaszTt+jQMla1mLHp3dlVdX1H7Kn
eLLbOgjuDm1n9P6pjzdBBZaM5zbeWj0lwVKEsNFSBceRe+hLTJZ5fTwCh298LN+q
AW/VTnZBI4N9QsTqUAAD9g7QG8CBDgQhS4pRwCE2mOAMEtSlLt1qg9m0JTo5dgck
MwIDAQAB
-----END PUBLIC KEY-----
"""PUBLIC_KEY_VA = 0x661200
PUBLIC_KEY_RVA = 0x261200
PUBLIC_KEY_FILE_OFFSET = 0x25F800
PUBLIC_KEY_MAX_SIZE = len(ORIGINAL_PUBLIC_KEY_PEM.encode('ascii'))# =============================================================================
# SECTION 3: CONSTANTS
# =============================================================================# XML formatting: CRLF line endings (\r\n) and tab indentation (\t)
# These match the exact format produced by x_create_lic_51D400 in the EXE.
_R = '\r\n'
_T = '\t'# Default license properties – can be overridden via CLI arguments
DEFAULT_USER = "ikun"
DEFAULT_ORG = "kunkun"
DEFAULT_EMAIL = "ikun@kunkun.com"
DEFAULT_LIC_TYPE = "Standard"# =============================================================================
# SECTION 4: CORE ALGORITHMS
#
#   These implement the same algorithms found in sourceinsight4.exe.
#   Each function docstring references the corresponding IDA function.
# =============================================================================def substitution_hash(data, salt, iterations=1968, keylen=128):"""Core substitution-table hash algorithm.This is the fundamental building block used by serial checksums,HWID computation, version strings, and local signatures.IDA equivalent: sub_402F00 @ 0x402F00Algorithm:for i in 0..keylen-1:v = salt[(iterations + i + data[0]) % 256]for j in 1..len(data)-1:v = salt[v ^ data[j]]result[i] = vArgs:data:    Input bytes to hashsalt:    256-byte substitution tableiterations: Bootstrap offset (different values produce different hashes)keylen:  Output length in bytesReturns:bytes of length `keylen`"""result = bytearray(keylen)for i in range(keylen):v = salt[(iterations + i + data[0]) % 256]  # seed = salt[iter+i+first_byte]for j in range(1, len(data)):v = salt[v ^ data[j]]                   # chain: v = salt[v xor data[j]]result[i] = vreturn bytes(result)def generate_serial(license_type='S', license_category='G'):"""Generate a valid SourceInsight 4 serial number.Format: S4XX-XXXX-XXXX-CCCC (19 chars, 4-char checksum)The checksum uses substitution_hash with SUBSTITUTION_TABLE as salt,then maps each byte to ALPHABET_TABLE[v % 26].IDA equivalent (checksum): compute_serial_checksum_51B7A0 @ 0x51B7A0Args:license_type:     'T'=Trial, 'B'=Beta, 'S'=Standard, 'U'=Upgradelicense_category: 'G'=General(新购), 'R'=Retail(续订)Returns:19-character serial string"""s = list("S4")s.append(license_type)s.append(license_category)s.append("-")s.append(random.choice(string.ascii_uppercase + string.digits))s.append(random.choice("RGDF"))s.append(random.choice(string.ascii_uppercase + string.digits))s.append(random.choice(string.ascii_uppercase + string.digits))s.append("-")for _ in range(4):s.append(random.choice(string.ascii_uppercase + string.digits))s.append("-")serial_str = "".join(s)check = []for i in range(4):v = SUBSTITUTION_TABLE[ord(serial_str[0]) + i]  # seed = table[serial[0] + i]for j in range(1, 15):v = SUBSTITUTION_TABLE[ord(serial_str[j]) ^ v]  # v = table[serial[j] ^ v]check.append(ALPHABET_TABLE[v % 26])return serial_str + "".join(check)def validate_serial(serial_str):"""Validate a SourceInsight 4 serial number.Checks format (length, separators, character constraints) andverifies the 4-character checksum.IDA equivalent: validate_serial_51DA20 @ 0x51DA20Args:serial_str: The serial number to validateReturns:(bool, str): (is_valid, message)"""serial_str = serial_str.upper()if len(serial_str) != 19:return False, "Length must be 19"if serial_str[4] != '-' or serial_str[9] != '-' or serial_str[14] != '-':return False, "Separators at positions 4,9,14 must be '-'"if serial_str[0] != 'S':return False, "Position 0 must be 'S'"if not serial_str[1].isdigit():return False, "Position 1 must be a digit"if serial_str[2] not in 'TBSU':return False, "Position 2 must be T/B/S/U"if serial_str[3] not in 'GR':return False, "Position 3 must be G/R"if serial_str[6] not in 'RGDF':return False, "Position 6 must be R/G/D/F"check = []for i in range(4):v = SUBSTITUTION_TABLE[ord(serial_str[0]) + i]for j in range(1, 15):v = SUBSTITUTION_TABLE[ord(serial_str[j]) ^ v]check.append(ALPHABET_TABLE[v % 26])expected = "".join(check)actual = serial_str[15:19]if expected != actual:return False, f"Checksum mismatch: expected {expected}, got {actual}"return True, "Valid"# =============================================================================
# SECTION 5: SYSTEM INFORMATION
#
#   Gathers machine-specific identifiers used for HWID and version string.
#   These match the data collected by init_hostid_51C560 @ 0x51C560.
# =============================================================================def get_volume_guid():"""Get the Volume GUID of the system drive (C:).Uses GetVolumeNameForVolumeMountPointW to obtain the GUID path,then extracts the GUID portion between '{' and '}'.IDA equivalent: get_machine_guid_453270 @ 0x453270 (used in init_hostid_51C560)Returns:Volume GUID string (e.g. "7245bfa7-ac2f-4433-b5ac-060ab8e861d6") or "" on failure"""if sys.platform != 'win32':return ""try:import ctypesk = ctypes.windll.kernel32buf = ctypes.create_unicode_buffer(260)if k.GetEnvironmentVariableW("SystemDrive", buf, 260) == 0:d = "C:\\"else:d = buf.valueif not d.endswith('\\'):d += '\\'vn = ctypes.create_unicode_buffer(260)if not k.GetVolumeNameForVolumeMountPointW(d, vn, 255):return ""vp = vn.valuepfx = "\\\\?\\Volume{"if not vp.startswith(pfx):return ""end = vp.find("}", len(pfx))return vp[len(pfx):end] if end >= 0 else ""except Exception:return ""def get_user_sid():"""Get the current user's SID (Security Identifier) as a string.Uses OpenProcessToken + GetTokenInformation (TokenUser) +ConvertSidToStringSidW to obtain the SID, then converts toUTF-8 via WideCharToMultiByte (code page 65001).IDA equivalent: sub_453460 @ 0x453460 (used in init_hostid_51C560)Returns:SID string (e.g. "S-1-5-21-...") or "" on failure"""if sys.platform != 'win32':return ""try:import ctypesimport ctypes.wintypesadv = ctypes.windll.advapi32ker = ctypes.windll.kernel32adv.OpenProcessToken.restype = ctypes.wintypes.BOOLadv.OpenProcessToken.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.HANDLE)]adv.GetTokenInformation.restype = ctypes.wintypes.BOOLadv.GetTokenInformation.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD)]adv.ConvertSidToStringSidW.restype = ctypes.wintypes.BOOLadv.ConvertSidToStringSidW.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)]ker.GetCurrentProcess.restype = ctypes.wintypes.HANDLEker.GetCurrentProcess.argtypes = []ker.LocalFree.restype = ctypes.c_void_pker.LocalFree.argtypes = [ctypes.c_void_p]ker.CloseHandle.restype = ctypes.wintypes.BOOLker.CloseHandle.argtypes = [ctypes.wintypes.HANDLE]ker.WideCharToMultiByte.restype = ctypes.c_intker.WideCharToMultiByte.argtypes = [ctypes.wintypes.UINT, ctypes.wintypes.DWORD, ctypes.c_wchar_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.wintypes.BOOL)]ph = ker.GetCurrentProcess()th = ctypes.wintypes.HANDLE()if not adv.OpenProcessToken(ph, 8, ctypes.byref(th)):return ""rl = ctypes.wintypes.DWORD()adv.GetTokenInformation(th, 1, None, 0, ctypes.byref(rl))buf = ctypes.create_string_buffer(rl.value)if not adv.GetTokenInformation(th, 1, buf, rl, ctypes.byref(rl)):ker.CloseHandle(th)return ""sp = ctypes.cast(buf, ctypes.POINTER(ctypes.c_void_p))[0]ss_ptr = ctypes.c_void_p()if not adv.ConvertSidToStringSidW(sp, ctypes.byref(ss_ptr)):ker.CloseHandle(th)return ""result = ctypes.wstring_at(ss_ptr.value)ker.LocalFree(ss_ptr)ker.CloseHandle(th)b2 = ctypes.create_string_buffer(512)ker.WideCharToMultiByte(65001, 0, result, -1, b2, 512, None, None)return b2.value.decode('utf-8', errors='replace')except Exception:return ""def get_computer_name():"""Get the computer's NetBIOS name in UTF-8 encoding.Uses GetComputerNameW then WideCharToMultiByte (code page 65001).IDA equivalent: sub_4516A0 @ 0x4516A0 (used in init_hostid_51C560)Returns:Computer name string or "" on failure"""if sys.platform != 'win32':return ""try:import ctypesimport ctypes.wintypesk = ctypes.windll.kernel32buf = ctypes.create_unicode_buffer(256)sz = ctypes.wintypes.DWORD(256)k.GetComputerNameW(buf, ctypes.byref(sz))b2 = ctypes.create_string_buffer(512)k.WideCharToMultiByte(65001, 0, buf.value, -1, b2, 512, None, None)name = b2.value.decode('utf-8', errors='replace')return ''.join(c if ord(c) >= 0x20 else '?' for c in name)except Exception:return ""def compute_version_string():"""Compute the machine-specific version string.Concatenates volume_guid + user_sid + computer_name, then hasheswith substitution_hash(iterations=0x7A9, keylen=4) usingLOCAL_SIGNATURE_SALT as the table. The 4-byte result is interpretedas a little-endian uint32 and returned as a decimal string.IDA equivalent: get_version_string_51E770 @ 0x51E770Returns:Decimal version string (e.g. "1146394088") or None on failure"""host_id = get_volume_guid() + get_user_sid() + get_computer_name()if not host_id:return Noneh = substitution_hash(host_id.encode('ascii'), LOCAL_SIGNATURE_SALT, iterations=0x7A9, keylen=4)return str(struct.unpack('<I', h)[0])def _hwid_part(data_bytes):"""Compute an 8-character HWID component from binary data.Uses substitution_hash (iterations=0, keylen=8) withSUBSTITUTION_TABLE as the table, then maps each byte to ALPHABET_TABLE.IDA equivalent: sub_51B810 @ 0x51B810 (compute_hwid_hash_51B810)Args:data_bytes: Raw bytes (volume GUID or user SID encoded as ASCII)Returns:8-character string from ALPHABET_TABLE"""r = []for i in range(8):v = SUBSTITUTION_TABLE[(i + data_bytes[0]) & 0xFF]for j in range(1, len(data_bytes)):v = SUBSTITUTION_TABLE[(v ^ data_bytes[j]) & 0xFF]r.append(ALPHABET_TABLE[v % 26])return ''.join(r)def compute_hwid():"""Compute the full hardware ID (HWID).Format: XXXXXXXX-YYYYYYYY (17 chars)Part 1: _hwid_part(volume_guid)  – volume GUID hashPart 2: _hwid_part(user_sid)     – user SID hashIf either component is unavailable, "AAAAAAAA" is used as fallback.IDA equivalent: compute_hwid_hash_51B810 (each part) andinit_hostid_51C560 @ 0x51C560 (assembly)Returns:HWID string (e.g. "R7NFNJ3G-4JF7R5PH") or "" if both components fail"""vg = get_volume_guid()us = get_user_sid()if not vg and not us:return ""p1 = _hwid_part(vg.encode('ascii')) if vg else "AAAAAAAA"p2 = _hwid_part(us.encode('ascii')) if us else "AAAAAAAA"return f"{p1}-{p2}"def generate_actid():"""Generate an ActId for local activation.Format: [4-char random prefix] + [version string]The prefix must be one of the 50 VALID_ACTID_PREFIXES decoded fromthe XOR key table at VA 0x62E5D0.IDA equivalent (prefix decode): sub_403290 @ 0x403290IDA equivalent (prefix match):  sub_4035C0 @ 0x4035C0Returns:ActId string (e.g. "0A511146394088")"""prefix = random.choice(VALID_ACTID_PREFIXES)vs = compute_version_string()return prefix + (vs if vs else "0")# =============================================================================
# SECTION 6: LICENSE HELPERS
#
#   Signing functions, XML building utilities, and helper functions
#   used by both local and online license generation.
# =============================================================================def _strip_ws(s):"""Remove whitespace (space, tab, CR, LF) from a string.This replicates the stripping performed by verify_license_signature_51E630before passing data to rsa_verify_51BB30 or sub_51D640."""return s.replace(' ', '').replace('\t', '').replace('\n', '').replace('\r', '')def _sign_local(content_before_sig):"""Create a local (substitution hash) signature.Strips whitespace from the content, appends a null byte, then computessubstitution_hash(iterations=0x7B0, keylen=128) using LOCAL_SIGNATURE_SALT.The resulting 128 bytes are Base64-encoded.IDA equivalent: sub_51D640 @ 0x51D640Args:content_before_sig: The XML content from <!-- ... --> up to (butnot including) the <Signature> element.Returns:Base64-encoded 128-byte signature string"""data = _strip_ws(content_before_sig).encode('latin-1') + b'\x00'h = substitution_hash(data, LOCAL_SIGNATURE_SALT, iterations=0x7B0, keylen=128)return base64.b64encode(h).decode('ascii')def _sign_online(content_before_sig, private_key):"""Create an online (RSA-2048 + SHA-1) signature.Strips whitespace (NO null terminator – unlike local), then signswith RSA-2048 + SHA-1 (PKCS#1 v1.5 padding).IMPORTANT: Windows CryptoAPI outputs signatures in LITTLE-ENDIAN byteorder. The Python cryptography library uses BIG-ENDIAN. Therefore wereverse the signature bytes (sig[::-1]) before Base64 encoding.IDA equivalents:verify_license_signature_51E630 @ 0x51E630 (overall flow)rsa_verify_51BB30 @ 0x51BB30 (CryptVerifySignatureW call)Args:content_before_sig: XML content before <Signature> tagprivate_key:        RSA private key objectReturns:Base64-encoded RSA signature (256 bytes → 344 base64 chars)"""if private_key is None:return "InvalidSignature"data = _strip_ws(content_before_sig).encode('latin-1')sig = private_key.sign(data, padding.PKCS1v15(), hashes.SHA1())return base64.b64encode(sig[::-1]).decode('ascii')  # LE→BE byte reversaldef _build_comment(user, org, email):"""Build the XML comment header for a license file.This replicates the exact comment format produced by x_create_lic_51D400."""return (f'<!--{_R}'f'{_T}Source Insight 4.x License File{_R}'f'{_R}'f'{_T}DO NOT EDIT THIS FILE.  Doing so will render it unusable.{_R}'f'{_R}'f'{_T}This license was created for:{_R}'f'{_T}{_R}'f'{_T}{_T}{user}{_R}'f'{_T}{_T}{org}{_R}'f'{_T}{_T}{email}{_R}'f'{_T}{_R}'f'-->')def _build_sig_element(sig_value):"""Build the <Signature> and closing </SourceInsightLicense> XML."""return (f'<Signature{_R}'f'{_T}{_T}Value="{sig_value}"{_R}'f'{_T}/>{_R}'f'</SourceInsightLicense>')# =============================================================================
# SECTION 7: LICENSE GENERATION
#
#   Top-level functions for generating complete si4.lic files.
#   These mirror x_create_lic_51D400 @ 0x51D400 in the EXE.
# =============================================================================def generate_lic_local(serial, user=DEFAULT_USER, org=DEFAULT_ORG, email=DEFAULT_EMAIL,lic_type=DEFAULT_LIC_TYPE, actid=None, deferred=False):"""Generate a complete si4.lic file with local (substitution hash) signature.Produces a ConnectedState=3 (local activation) license. If deferred=True,produces ConnectedState=2 (ActId="Deferred", no signature check).IDA equivalent: x_create_lic_51D400 @ 0x51D400The XML structure does NOT include a <Header> element (local only).Args:serial:    Valid 19-char serial numberuser:      Licensed user nameorg:       Organization nameemail:     Email addresslic_type:  "Standard" or "Trial"actid:     Custom ActId (auto-generated if None)deferred:  If True, ActId="Deferred" (ConnectedState=2)Returns:Complete si4.lic XML string with signature"""today = date.today().strftime("%Y-%m-%d")actid_str = "Deferred" if deferred else (actid or generate_actid())exp = f'{_T}{_T}Expiration="{(date.today() + timedelta(days=31)).strftime("%Y-%m-%d")}"{_R}' if lic_type == "Trial" else ""before_sig = (f'{_build_comment(user, org, email)}{_R}'f'<SourceInsightLicense>{_R}'f'{_T}<LicenseProperties{_R}'f'{_T}{_T}ActId="{actid_str}"{_R}'f'{_T}{_T}Serial="{serial}"{_R}'f'{_T}{_T}LicensedUser="{user}"{_R}'f'{_T}{_T}Organization="{org}"{_R}'f'{_T}{_T}Email="{email}"{_R}'f'{_T}{_T}Type="{lic_type}"{_R}'f'{_T}{_T}Version="4"{_R}'f'{_T}{_T}MinorVersion="0"{_R}'f'{_T}{_T}Date="{today}"{_R}'f'{exp}'f'{_T}/>{_R}'f'{_T}')return before_sig + _build_sig_element(_sign_local(before_sig))def generate_lic_online(serial, user=DEFAULT_USER, org=DEFAULT_ORG, email=DEFAULT_EMAIL,lic_type=DEFAULT_LIC_TYPE, actid="", hwid="", private_key=None):"""Generate a complete si4.lic file with online (RSA-2048) signature.Produces a ConnectedState=1 (online activation) license.Includes a <Header Value="1"/> element and an HWID attribute.ActId is a random 10-digit number (pure digits).IDA equivalent: x_create_lic_51D400 @ 0x51D400 (same function,but with different signature path)NOTE: The RSA signature will NOT validate against the EXE's originalpublic key unless you patch the EXE or use a DLL proxy. Use--fit-key --patch-exe to automatically replace the key.Args:serial:      Valid 19-char serial numberuser:        Licensed user nameorg:         Organization nameemail:       Email addresslic_type:    "Standard" or "Trial"actid:       Custom ActId (10-digit number, auto-generated if empty)hwid:        Hardware ID (format: XXXXXXXX-YYYYYYYY)private_key: RSA private key for signing (None → no signature)Returns:Complete si4.lic XML string with RSA signature"""today = date.today().strftime("%Y-%m-%d")if not actid:actid = str(random.randint(1000000000, 9999999999))exp = f'{_T}{_T}Expiration="{(date.today() + timedelta(days=31)).strftime("%Y-%m-%d")}"{_R}' if lic_type == "Trial" else ""before_sig = (f'{_build_comment(user, org, email)}{_R}'f'<SourceInsightLicense>{_R}'f'{_T}<Header{_R}'f'{_T}{_T}Value="1"{_R}'f'{_T}/>{_R}'f'{_T}<LicenseProperties{_R}'f'{_T}{_T}LicensedUser="{user}"{_R}'f'{_T}{_T}ActId="{actid}"{_R}'f'{_T}{_T}HWID="{hwid}"{_R}'f'{_T}{_T}Serial="{serial}"{_R}'f'{_T}{_T}Organization="{org}"{_R}'f'{_T}{_T}Email="{email}"{_R}'f'{_T}{_T}Type="{lic_type}"{_R}'f'{_T}{_T}Version="4"{_R}'f'{_T}{_T}MinorVersion="0"{_R}'f'{_T}{_T}Date="{today}"{_R}'f'{exp}'f'{_T}/>{_R}'f'{_R}'f'{_T}')return before_sig + _build_sig_element(_sign_online(before_sig, private_key))# =============================================================================
# SECTION 8: LICENSE PARSING & VERIFICATION
#
#   Functions for reading, parsing, and validating existing si4.lic files.
# =============================================================================def parse_lic_file(lic_path):"""Parse an existing si4.lic file and extract its properties.Returns a dict with:props:             dict of LicenseProperties XML attributessig_value_b64:     Base64-encoded Signature Valuecontent_before_sig: Everything before the <Signature> taghas_crlf:          True if file uses CRLF line endingsfile_size:         Raw file size in bytesArgs:lic_path: Path to si4.lic fileReturns:dict or None on error"""if not os.path.exists(lic_path):print(f"ERROR: File not found: {lic_path}")return Nonewith open(lic_path, 'rb') as f:raw = f.read()content = raw.decode('latin-1')try:root = ET.fromstring(content)except ET.ParseError:print("ERROR: Cannot parse license file as XML.")return Noneif root.tag != 'SourceInsightLicense':print(f"ERROR: Root tag is '{root.tag}', expected 'SourceInsightLicense'.")return Noneprops_elem = root.find('LicenseProperties')if props_elem is None:print("ERROR: No LicenseProperties element found.")return Noneprops = dict(props_elem.attrib)sig_elem = root.find('Signature')sig_value = sig_elem.get('Value', '') if sig_elem is not None else ''sig_pos = content.find('<Signature')content_before_sig = content[:sig_pos] if sig_pos >= 0 else ''has_crlf = b'\r\n' in rawreturn {'props': props,'sig_value_b64': sig_value,'content_before_sig': content_before_sig,'has_crlf': has_crlf,'file_size': len(raw),}def is_local_actid(actid):"""Check if an ActId indicates local activation (ConnectedState=3).Returns True if the ActId starts with one of the 50 VALID_ACTID_PREFIXES.Returns False for Deferred or online (pure numeric) ActIds."""if not actid or actid == 'Deferred':return Falsereturn any(actid.startswith(p) for p in VALID_ACTID_PREFIXES)def verify_license_file(lic_path):"""Verify an existing si4.lic file.Automatically detects activation type (Deferred/Local/Online) andperforms the appropriate verification:- Deferred: No verification needed- Local:    Computes and compares substitution_hash signature- Online:   Verifies RSA signature against original and custom keysAlso validates the serial number checksum and checks HWID/ActIdversion string matching for local licenses.Args:lic_path: Path to si4.lic fileReturns:True if the license is valid, False otherwise"""info = parse_lic_file(lic_path)if info is None:return Falseprops = info['props']sig_b64 = info['sig_value_b64']before_sig = info['content_before_sig']print(f"License file: {lic_path}")print(f"File size: {info['file_size']} bytes")print(f"Line endings: {'CRLF' if info['has_crlf'] else 'LF'}")print()print("License Properties:")for k in ['LicensedUser', 'ActId', 'HWID', 'Serial', 'Organization','Email', 'Type', 'Version', 'MinorVersion', 'Date', 'Expiration']:if k in props:print(f"  {k}: {props[k]}")print()serial = props.get('Serial', '')if serial:ok, msg = validate_serial(serial)print(f"Serial: {msg}")print()actid = props.get('ActId', '')if actid == 'Deferred':print("Activation: Deferred (ConnectedState=2)")print("  No signature verification needed.")return Trueif not sig_b64:print("ERROR: No Signature found.")return Falseif not before_sig:print("ERROR: Cannot determine content before Signature.")return Falselocal = is_local_actid(actid)if local:print("Activation: Local (ConnectedState=3)")print(f"  ActId prefix: {actid[:4]}")vs = compute_version_string()if vs:if actid[4:] == vs:print(f"  ActId version string: MATCH")else:print(f"  ActId version string: MISMATCH (expected '{vs}', got '{actid[4:]}')")print(f"  This license was generated for a DIFFERENT machine!")data = _strip_ws(before_sig).encode('latin-1') + b'\x00'computed = substitution_hash(data, LOCAL_SIGNATURE_SALT, iterations=0x7B0, keylen=128)try:file_sig = base64.b64decode(sig_b64)except Exception as e:print(f"  ERROR: Cannot decode signature: {e}")return Falseprint(f"  Signature: {len(file_sig)} bytes (expected 128)")if len(file_sig) == 128 and file_sig == computed:print(f"  Local signature: VALID")return Trueelse:print(f"  Local signature: INVALID")return Falseelse:print("Activation: Online (ConnectedState=1)")hwid = props.get('HWID', '')if hwid:cur_hwid = compute_hwid()print(f"  HWID: {hwid}")print(f"  Current HWID: {cur_hwid}")print(f"  HWID match: {'YES' if hwid == cur_hwid else 'NO (different machine)'}")try:file_sig = base64.b64decode(sig_b64)except Exception as e:print(f"  ERROR: Cannot decode signature: {e}")return Falseprint(f"  Signature: {len(file_sig)} bytes (expected 256 for RSA-2048)")if len(file_sig) != 256:print(f"  WARNING: Not a valid RSA-2048 signature size.")return Falseif not HAS_CRYPTO:print(f"  Cannot verify: 'cryptography' package not installed.")return Falsetry:pub_key = serialization.load_pem_public_key(ORIGINAL_PUBLIC_KEY_PEM.encode('ascii'), backend=default_backend())except Exception as e:print(f"  ERROR: Cannot load public key: {e}")return Falsedata = _strip_ws(before_sig).encode('latin-1')sig_be = file_sig[::-1]try:pub_key.verify(sig_be, data, padding.PKCS1v15(), hashes.SHA1())print(f"  RSA signature (original key): VALID")return Trueexcept Exception:print(f"  RSA signature (original key): INVALID")priv_path = "si4_private_key.pem"if os.path.exists(priv_path):try:with open(priv_path, 'r') as f:pk = serialization.load_pem_private_key(f.read().encode(), password=None, backend=default_backend())pk.public_key().verify(sig_be, data, padding.PKCS1v15(), hashes.SHA1())print(f"  RSA signature (custom key): VALID")return Trueexcept Exception:print(f"  RSA signature (custom key): INVALID")return False# =============================================================================
# SECTION 9: EXE PATCHING
#
#   Locate sourceinsight4.exe and replace the embedded RSA public key.
# =============================================================================def get_lic_filepath():"""Get the default si4.lic file path for the current platform.Windows: %APPDATA%\Source Insight\4.0\si4.licOthers:  ~/.sourceinsight4/si4.lic"""if sys.platform == 'win32':import ctypesbuf = ctypes.create_unicode_buffer(260)ctypes.windll.shell32.SHGetFolderPathW(None, 35, None, 0, buf)return os.path.join(buf.value, "Source Insight", "4.0", "si4.lic")return os.path.join(os.path.expanduser("~"), ".sourceinsight4", "si4.lic")def find_exe_path():"""Try to locate sourceinsight4.exe in common installation paths."""import globfor p in [r'C:\Program Files\Source Insight 4.0\sourceinsight4.exe',r'C:\Program Files (x86)\Source Insight 4.0\sourceinsight4.exe']:for f in glob.glob(p):if os.path.isfile(f):return freturn Nonedef patch_exe_with_public_key(exe_path, new_pub_pem, backup=True):"""Replace the embedded RSA public key in sourceinsight4.exe.Searches for '-----BEGIN PUBLIC KEY-----' marker in the binary,replaces the PEM data with the new key, and pads with 0x00 bytes.The original key is at VA 0x661200 (file offset 0x25F800) in the.rdata section, with a max capacity of PUBLIC_KEY_MAX_SIZE bytes.Args:exe_path:    Path to sourceinsight4.exenew_pub_pem: New PEM-encoded RSA public keybackup:      Create .bak backup before modifying (default True)Returns:True on success"""if not os.path.isfile(exe_path):print(f"ERROR: Exe not found: {exe_path}")return Falsewith open(exe_path, 'rb') as f:data = f.read()start = data.find(b'-----BEGIN PUBLIC KEY-----')if start < 0:print("ERROR: Cannot find public key PEM in exe.")return Falseend_marker = data.find(b'-----END PUBLIC KEY-----', start)if end_marker < 0:print("ERROR: Cannot find END marker in exe.")return Falseend = end_marker + len(b'-----END PUBLIC KEY-----')if data[end:end+1] == b'\n':end += 1orig_size = end - startnew_bytes = new_pub_pem.encode('ascii')if len(new_bytes) > orig_size:print(f"ERROR: New key ({len(new_bytes)}B) > original ({orig_size}B). Use --fit-key.")return Falseprint(f"PEM at file offset: 0x{start:X}")print(f"Original: {orig_size}B, New: {len(new_bytes)}B")new_data = bytearray(data)new_data[start:start + len(new_bytes)] = new_bytesfor i in range(len(new_bytes), orig_size):new_data[start + i] = 0x00if backup:bak = exe_path + '.bak'if not os.path.exists(bak):with open(bak, 'wb') as f:f.write(data)print(f"Backup: {bak}")with open(exe_path, 'wb') as f:f.write(new_data)print(f"Patched: {exe_path}")return Truedef show_patch_info(new_pub_pem=None):"""Display information about patching the EXE with a new public key."""print()print("=" * 60)print("EXE PATCHING INFO")print("=" * 60)print(f"  Public key VA: 0x{PUBLIC_KEY_VA:X}")print(f"  File offset:   0x{PUBLIC_KEY_FILE_OFFSET:X}")print(f"  Max PEM size:  {PUBLIC_KEY_MAX_SIZE} bytes")print()print("  Original key:")for line in ORIGINAL_PUBLIC_KEY_PEM.strip().split('\n'):print(f"    {line}")if new_pub_pem:new_sz = len(new_pub_pem.encode('ascii'))fits = new_sz <= PUBLIC_KEY_MAX_SIZEprint()print("  New key:")for line in new_pub_pem.strip().split('\n'):print(f"    {line}")print()print(f"  Size: {new_sz} / {PUBLIC_KEY_MAX_SIZE} bytes ({'FITS' if fits else 'DOES NOT FIT'})")if fits:print()print("  Manual patch steps:")print(f"    1. Open sourceinsight4.exe in hex editor")print(f"    2. Go to offset 0x{PUBLIC_KEY_FILE_OFFSET:X}")print(f"    3. Replace PEM with new key, pad rest with 0x00")print(f"    4. Save")print(f"  Or: python si4_keygen.py --online --fit-key --patch-exe")print()print("  Alternative: DLL proxy approach")print("    - Create msimg32.dll that hooks CryptVerifySignatureW")print("    - Hook returns TRUE for all signature verifications")print("    - Place in Source Insight installation directory")print()print("  Simplest: Use --local mode (no patch needed!)")print("    - Local activation uses substitution table hash")print("    - ActId contains machine-specific version string")print("    - ConnectedState=3 (local activation)")# =============================================================================
# SECTION 10: KEY MANAGEMENT
#
#   RSA key generation, loading, and crypto availability checks.
# =============================================================================def _ensure_crypto():"""Check that the cryptography library is installed; exit if not."""if not HAS_CRYPTO:print("ERROR: 'cryptography' package required. Install: pip install cryptography")sys.exit(1)def _load_or_gen_key(key_dir, fit=False):"""Load an existing RSA key pair or generate a new one.If fit=True, generates RSA-2048 keys until the PEM encoding fitswithin PUBLIC_KEY_MAX_SIZE (the space available in the EXE).Tries up to 1000 attempts.Saves the key pair as si4_private_key.pem and si4_public_key.pem.Args:key_dir: Directory for key filesfit:     Ensure the public key fits in the EXE's key slotReturns:RSA private key object"""_ensure_crypto()priv_path = os.path.join(key_dir, "si4_private_key.pem")if os.path.exists(priv_path):print(f"Loading key from: {priv_path}")with open(priv_path, 'r') as f:return serialization.load_pem_private_key(f.read().encode(), password=None, backend=default_backend())if fit:print("Generating RSA-2048 key that fits in exe...")for attempt in range(1000):pk = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend())pem = pk.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo).decode('ascii')if len(pem.encode('ascii')) <= PUBLIC_KEY_MAX_SIZE:print(f"Found fitting key (attempt {attempt+1}, size {len(pem.encode('ascii'))}B)")breakelse:print("Warning: No fitting key found after 1000 attempts.")else:print("Generating RSA-2048 key pair...")pk = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend())pub_path = os.path.join(key_dir, "si4_public_key.pem")with open(pub_path, 'w') as f:f.write(pk.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo).decode('ascii'))with open(priv_path, 'w') as f:f.write(pk.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption()).decode('ascii'))print(f"Keys saved: {pub_path}, {priv_path}")return pk# =============================================================================
# SECTION 11: CLI / main()
#
#   Command-line interface for generating and verifying licenses.
#   Supports --local, --online, --deferred, --trial, --verify modes.
# =============================================================================def main():import argparsep = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,description='SourceInsight 4 License Generator',epilog="""
ACTIVATION MODES (mutually exclusive):--local       Local activation (default). Substitution table hash.ConnectedState=3. Must run on target machine.No exe patch needed.--trial       30-day trial. Same as --local --type Trial.--online      Online activation. RSA-2048 + SHA-1 signature.ConnectedState=1. Requires patching exe or DLL proxy.--deferred    Deferred activation. ActId="Deferred".ConnectedState=2. No signature check.OTHER COMMANDS:--verify FILE   Verify an existing si4.lic file--serial-only   Generate serial number only--patch-info    Show exe patching informationONLINE MODE OPTIONS:--gen-key       Generate new RSA key pair--fit-key       Generate RSA key that fits in original exe space--patch-exe     Patch sourceinsight4.exe with new public key--exe-path PATH Path to sourceinsight4.exe (for --patch-exe)--no-sign       Generate without valid RSA signatureCOMMON OPTIONS:--user NAME     Licensed user name (default: User)--org NAME      Organization name (default: Org)--email ADDR    Email address--type TYPE     License type: Standard or Trial (default: Standard)--output PATH   Output si4.lic file path--key-dir DIR   Directory for RSA key pair (default: current dir)--actid ID      Custom ActId--hwid ID       Custom HWID (format: XXXXXXXX-YYYYYYYY)EXAMPLES:python si4_keygen.py                              # local activationpython si4_keygen.py --trial                      # 30-day trialpython si4_keygen.py --online --fit-key           # online + fitting keypython si4_keygen.py --online --fit-key --patch-exe  # online + patch exepython si4_keygen.py --verify si4.lic             # verify license
""")p.add_argument('--serial-only', action='store_true')p.add_argument('--gen-key', action='store_true')p.add_argument('--fit-key', action='store_true')p.add_argument('--patch-exe', action='store_true')p.add_argument('--exe-path', default=None)p.add_argument('--patch-info', action='store_true')p.add_argument('--verify', default=None, metavar='FILE')p.add_argument('--local', action='store_true', default=False)p.add_argument('--online', action='store_true')p.add_argument('--deferred', action='store_true')p.add_argument('--trial', action='store_true')p.add_argument('--user', default=DEFAULT_USER)p.add_argument('--org', default=DEFAULT_ORG)p.add_argument('--email', default=DEFAULT_EMAIL)p.add_argument('--type', default=DEFAULT_LIC_TYPE, choices=['Standard', 'Trial'])p.add_argument('--output', default=None)p.add_argument('--key-dir', default='.')p.add_argument('--no-sign', action='store_true')p.add_argument('--actid', default=None)p.add_argument('--hwid', default=None)args = p.parse_args()if args.trial:args.type = 'Trial'if args.patch_info:show_patch_info()returnif args.verify:verify_license_file(args.verify)returnif not args.online and not args.local and not args.deferred:args.local = Truelic_type_char = 'T' if args.type == 'Trial' else 'S'serial = generate_serial(license_type=lic_type_char)ok, msg = validate_serial(serial)print(f"Serial: {serial} ({msg})")if args.serial_only:returnprint()vol_guid = get_volume_guid()user_sid = get_user_sid()comp_name = get_computer_name()ver_str = compute_version_string()hwid = args.hwid or compute_hwid()print(f"Volume GUID:  {vol_guid}")print(f"User SID:     {user_sid}")print(f"Computer:     {comp_name}")print(f"Version str:  {ver_str}")print(f"HWID:         {hwid}")if args.online:private_key = Noneif not args.no_sign:if HAS_CRYPTO:private_key = _load_or_gen_key(args.key_dir, fit=args.fit_key)show_patch_info(private_key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo).decode('ascii'))else:print("\nWarning: 'cryptography' not installed. Generating without valid signature.")actid = args.actid or ""lic = generate_lic_online(serial, args.user, args.org, args.email,args.type, actid, hwid, private_key)print(f"\nMode: Online (ConnectedState=1, RSA signature)")else:if not args.deferred and ver_str is None:print("\nERROR: Cannot compute version string. Run on target machine.")sys.exit(1)lic = generate_lic_local(serial, args.user, args.org, args.email,args.type, args.actid, args.deferred)mode = "Deferred (ConnectedState=2)" if args.deferred else "Local (ConnectedState=3)"print(f"\nMode: {mode}")out = args.output or get_lic_filepath()out_dir = os.path.dirname(out)if out_dir and not os.path.exists(out_dir):os.makedirs(out_dir, exist_ok=True)with open(out, 'wb') as f:f.write(lic.encode('latin-1'))print(f"\nSaved: {out}")print(f"\n{lic}")if args.online:if not args.no_sign and HAS_CRYPTO and private_key:print(f"\nIMPORTANT: Replace exe's public key for RSA signature to work.")print(f"Use --patch-exe or see patch info above.")else:print(f"\nNOTE: Invalid RSA signature. Options:")print(f"  1. Use --local mode (no patch needed)")print(f"  2. Use DLL proxy to hook CryptVerifySignatureW")else:if not args.deferred:print(f"\nNOTE: Background thread may convert local to online activation.")print(f"Block internet access or use --deferred to prevent this.")if args.patch_exe and args.online and not args.no_sign and HAS_CRYPTO and private_key:exe = args.exe_path or find_exe_path()if exe:new_pem = private_key.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo).decode('ascii')patch_exe_with_public_key(exe, new_pem)else:print("Cannot find exe. Use --exe-path.")if __name__ == '__main__':main()

ps

本地许可,离线使用,未patch

image-20260505200156658

image-20260505200041830

image-20260505200104401

http://www.jsqmd.com/news/759214/

相关文章:

  • 终极游戏翻译指南:如何用XUnity Auto Translator轻松玩转外语游戏
  • 构建手机号码地理定位系统的技术实现与实践应用
  • LLM任务描述生成与分类技术实践指南
  • Go语言API安全中间件Stark Shield:模块化设计与实战集成指南
  • 2026年4月有实力的环氧粉末涂塑钢管销售厂家口碑推荐,环氧粉末涂塑钢管,环氧粉末涂塑钢管实力厂家口碑推荐 - 品牌推荐师
  • 2026年AI大模型接口中转系统排名揭晓!五大头部服务商各展风采,谁能拔得头筹?
  • 你的IoT设备数据丢过吗?聊聊AT24Cxx这类EEPROM的选型、寿命与数据保护策略
  • 百度网盘Mac版极速下载插件:告别限速,享受高速下载体验
  • 在Linux上用C语言手搓一个五子棋:从终端棋盘到胜负判断的完整实现
  • 2026年5月丨企业选型指南:SD-WAN供应商性价比横向对比 - 品牌企业推荐师(官方)
  • 告别卡顿!在中标麒麟NeoKylin上为你的Qt视频监控软件开启FFmpeg硬解码(QSV/VAAPI)
  • Embedding 模型选型与向量化实战:从 BERT 到多模态
  • 别再写一堆Redis命令了!用Lua脚本实现分布式锁和库存扣减,实战避坑指南
  • Dify上线前必须冻结的6项租户配置,第3项未校验将触发跨租户数据批量导出——立即自查!
  • 初次使用 Taotoken 从注册到发出第一个聊天请求的全流程指南
  • Multisim教育版元件库保姆级使用指南:从虚拟器件到真实元件的快速上手
  • 从乘用车到商用车:搞懂CAN总线,为什么15765和J1939协议硬件一样却用法天差地别?
  • 珠三角高空车防撞车租赁五强出炉!广东战狼凭 “三多” 实力登顶,振邦、老兵紧随其后 - 广州搬家老班长
  • 用Taotoken的OpenAI兼容接口为AE视频片段生成创意文案
  • 2026 嘉兴除甲醛 6 大排名权威发布 - 品牌企业推荐师(官方)
  • SAP PM维修工单实操:从IW31创建到IW32修改,手把手教你搞定设备维修数据归集
  • Dify工业检索响应超时?不是算力问题——而是这6个元数据字段未标准化!(附GB/T 20984-2022合规映射表)
  • 大语言模型上下文优化:CRO方法解析与实践
  • AI代码安全评估框架与SecureCode数据集解析
  • 用Python和Pandas玩转GDELT全球新闻数据库:从数据下载到初步分析的保姆级教程
  • 终极指南:ViGEmBus虚拟手柄驱动 - 3分钟解决Windows游戏手柄兼容性问题
  • 别再手动拖进度条了!用Python+OpenCV实现视频自动摘要,5分钟搞定核心内容提取
  • Dify农业知识库离线版上线倒计时!仅剩72小时——附赠已通过农业农村部备案的NLP微调参数包
  • 2026绍兴除甲醛品牌权威榜单发布!六大实力机构实测测评结果公示 - 品牌企业推荐师(官方)
  • 3步实现Unity游戏自动翻译:XUnity.AutoTranslator新手完全指南