【AI】Helium Hex Editor v2.8.7.1许可机制分析
Helium Hex Editor v2.8.7.1许可机制分析
分析对象:
HeliumHexEditor64.exev2.8.7.1 +HexControl64.dll,均 x64 PE
工具:IDA Pro 9.3(ida-multi-mcp)+ x64dbg(mcp_x64dbg_local_x64)
HexControl64.dll
Operation system: Windows (Server 2003) [AMD64, 64位, DLL]
Linker: Microsoft linker (10.00.40219)
Compiler: Visual C/C++ (16.50.40219) [LTCG/C++]
Language: C/C++
Tool: Microsoft Visual Studio (2010)
MD5: 8f1a9a01d3e72f52f1c960c01a61f42c
SHA1: 5aa4c10f2ca93d1f8c9605bdcf579e9d2f3b507d
SHA256: 18359de18f4b32c9a82139e850e4bb2af00dd3679b4c18549ac3b0b1966d0f02
0. 结论速览
| 事项 | 结论 |
|---|---|
| 许可形态 | 文件签名许可:.lic(UTF-16LE INI)+ 同名 .sig(512B 裸签名);注册框另收"注册码"(同内容经 Blowfish-OFB 加密的 hex) |
| 注册码算法 | Blowfish-OFB;KEY="0x%I64X\0"+"0x%08X\0"+"%u\0"(18B)、IV="kernel32"(8B)(§1.1) |
| 验签 | RSASSA-PKCS1v15-SHA1;消息 = .lic 磁盘全部字节;厂商 RSA-4096(e=17),签名 512B |
| 公钥存放 | .rdata RVA 0x6F35E0,548B XOR 索引加密的 SPKI DER;由 mgr+0xC38/0xC40 指向(§2.2) |
| 公钥解密 | 状态机 case 0xB → sub_18009C3B0:plain[i] = enc[i] ^ i → 结果存 mgr+3112 |
| 防篡改 | 解密后立刻校验:HMAC-MD5(key="Error opening file %s\r\nDo you want to continue ?", msg=解密公钥 DER) 与 mgr+3144 期望串按步长 6 采样比对;失败即 free 公钥(§2.3) |
| 结果位 | mgr+0xCA8(= mgr+3240,同一 MBA 状态字)逐轮随机化 → 不可作验签通过判据;0x88 模式 = 验签链运行且失败 |
| About 注册显示 | 由选择子 *(mgr+0xC58) 决定:==0x1FA68 → 显示 mgr+4686(许可摘要);否则显示"未注册"文案(§4) |
| 功能门控 | 双层:Layer 1 金丝雀(43 处门控块 / 16 函数,§5.2);Layer 2 菜单构建期绑定占位命令 1517/1518(§5.4) |
| 两层关系 | 互相独立:验签通过不会自动解锁金丝雀(§5.1) |
| 持久化 | wininet.dll 代理劫持(tools/wininet_proxy.c):进程内一次性补丁,双击启动即注册态(§7) |
1. 注册码与许可文件
1.1 注册码算法 = Blowfish-OFB
格式(最终结论)
注册码 := HEX_大写( Blowfish-OFB( plain, KEY, IV ) )
plain := "<INI 头部文本>" + "[Signature]\r\nSigData=<签名的小写 hex>\r\n" // 均为 UTF-16LE
KEY := b"0x%I64X\x00" + b"0x%08X\x00" + b"%u\x00" // 18 字节
IV := b"kernel32" // 8 字节 = 块长
KEY 的来源(汇编实证):保存许可 handler sub_18027E230 在栈上构造三个格式串,
其字节序列被直接当作 Blowfish 密钥(编译器内联复用同一缓冲区):
; sub_18027E230 @0x18027E5B8-0x18027E651
mov dword ptr [rbp+80h], 49257830h ; "0x%I" (30 78 25 49)
mov dword ptr [rbp+84h], 00583436h ; "64X\0" (36 34 58 00)
mov dword ptr [rbp+88h], 30257830h ; "0x%0" (30 78 25 30)
mov dword ptr [rbp+8Ch], 25005838h ; "8X\0%" (38 58 00 25)
mov word ptr [rbp+90h], 75h ; "u\0" (75 00)
...
mov qword ptr [rsp+28h], 12h ; KEY 长度 = 18
lea rax, [rbp+80h] ; ★ KEY 指针 = 上述格式串缓冲
mov [rsp+20h], rax
lea rax, [rip+3250A6h] ; IV 源 = "kernel32.dll"(取前 8 字节)
mov [rsp+30h], rax
mov qword ptr [rsp+38h], 8 ; IV 长度 = 8
call sub_1803DCAC0 ; @0x18027E651(Blowfish 包装)
算法与模式判定(运行时实抓 + 数学比对):沿 SetKeyWithIV(sub_1803E2CA0)断点实抓
KEY/IV 实参,对实测密文首块做穷举比对(pycryptodome):
密文块0(输入解码产物) = 1E2EC0EB893483CF
程序输出首 8B = C2 7A 02 B8 3A F5 C4 2E
Blowfish-OFB(KEY18, IV8) 解密 → C2 7A 02 B8 3A F5 C4 2E ★ 逐字节 MATCH
AES-128/192/256、CAST、3DES、DES、RC2、Blowfish-CBC/CFB/ECB → 全部不匹配
OFB 亦与代码内 "FeedbackSize" 查询(sub_1802D91B0 的 NameValuePairs)互证。
key schedule sub_180443470 为标准 Blowfish(π 常量 P-array + S-box 初始化)。
1.2 .lic = UTF-16LE INI(解析器 sub_1800A0DB0)
// 键/节名运行时解密后逐个读取(N=5);[Info] 节 → 许可记录(记录基址 = mgr+3300)
LicenseType → rec+4 (dword)
Name → rec+8 (256 wchar)
ComputerName → rec+520 (256 wchar)
Processor → rec+1032 (49 wchar)
ID → rec+1130 (128 wchar)
// 解析成功后:rec+0 = 1;sub_1800A14E0 生成摘要 → rec+1386(0x1000 字符)
// LicenseType == 11 → 加载期强制置 0(不可用)
- 文件必须带 UTF-16LE BOM(FF FE):无 BOM 会被按 ANSI 读取 → 键值全丢、
LicenseType=0。 - 验签消息 =
.lic磁盘全部字节(含 BOM)(§3)。
1.3 .sig = 512B 裸二进制签名
- 验签器
sub_1802FD4F0以文档对象(磁盘流访问器,vtable +144 =GetFileSizeEx)直接取字节,
无 hex/base64 解码层。 - 异常文案
"Signature size invalid"的实际校验点在0x1802FD73E:
call sub_1803E2090; cmp rax,rbx; jz OK—— 校验的是消息流是否被完整消费,非签名长度;
签名长度由 Crypto++ verifier 按模数长度内部校验。 - ⚠️ 注册框路径的编码损失:注册框写
.sig走WideCharToMultiByte(CP_ACP)
(sub_1800FEBA0),中文系统 CP936 下 0x80–0xFF 无法单字节映射 → 签名失真、验签必失败。
结论:自签名许可必须走"文件直写"(§7.3),注册码 UI 仅作格式验证/对照。
1.4 LicenseType 绑定语义(sub_1800A1780;机器块 = mgr+64)
| LicenseType | 绑定语义(wcsicmp) |
|---|---|
| 1 / 5 / 8 | 恒过(case 1/5/8 共享 wcsicmp(x,x) 自身比较) |
| 2 / 6 / 9 | 绑定用户名:basename(lic.Name) vs basename(机器块+0x000);实测 +0x000 = "COMPUTER\USER" |
| 3 | 绑定路径首段:首段(lic.ComputerName) vs 首段(机器块+0x200);实测 +0x200 = "\COMPUTERNAME"(前导反斜杠 → 首段为空串) |
| 4 / 7 / 10 | 硬件三元组:ComputerName = 机器块+0x200 原值(\DESKTOP-XXX,勿丢反斜杠)、Processor = +0x400(CPU 全名)、ID = +0x462(机器 GUID) |
| 11 | 加载期已被置 0 → 走 default 恒败 |
| 其他 | default 分支恒败 |
附加判据:机器块 +0x964 非 0 且 type∈[5,10] 失败;状态字(sub_180098880,MBA)非 0 且
type∈[8,10] 失败。机器块示例(tools/dump_machine_block.ps1):
+0x000=DESKTOP-AAOCANQ\popk、+0x200=\DESKTOP-AAOCANQ、
+0x400=12th Gen Intel(R) Core(TM) i5-12600KF、+0x462=7245bfa7-…。
售卖映射(站点互证):购买链接 lt=4 = Pro Single Computer(10 US$)、lt=1 = Pro
Portable(49 US$),与 4/7/10、1/5 对应;类型显示名混淆串 Portable(0x5BED70) /
Single Computer(0x5BEE70)(sub_1800A1370)。
2. mgr 字段与公钥链
2.1 mgr 单例与关键字段
mgr 单例 = .data RVA 0x711310(运行期 = 基址 + 0x711310)。
| 偏移 | 含义 |
|---|---|
| +0x9A8 / +0x9B2 | 宿主 HWND(= 引擎命令窗口)/ 内部命令基址 cmdBase(word) |
| +0xC10 | 引擎任务表描述符指针(运行期 = 基址+0x7050A0,见 §5.5) |
| +0xC38 / +0xC40 | 加密公钥 blob 指针 / 长度(初始 0x6F35E0、548)——重定向点 |
| +0xC48 / +0xC50 | 防篡改期望串指针 / 比较步长(=5) |
| +0xC58 | About 状态选择子(==0x1FA68 → About 显示摘要,§4) |
| +0xCA8 | MBA 状态字(= +3240,同一字段)——验签/门控状态 |
| +3112 / +3120 | 解密后的公钥 / 长度(验签输入;9D190 末尾会 free 并清零) |
| +3128 / +3136 | 加密公钥指针 / 长度(= +0xC38/0xC40 的同义读出,防篡改检查输入) |
| +3272 / +3280 | 已装载的 .lic / .sig 文档对象 |
| +3288 | 验签调用第 7 参(实测 =1) |
| +3300 | 许可记录(rec+0 const / +4 LicenseType / +8 Name / +520 ComputerName / +1032 Processor / +1130 ID / +1386 摘要) |
2.2 公钥 blob 存储、解密与长度语义
存储:.rdata RVA 0x6F35E0,548 字节,XOR 索引加密(enc[0..3]=30 83 00 23)。
解密(sub_18009C3B0,状态机 case 0xB):
// 前置:enc = mgr+0xC38(3128)、enclen = mgr+0xC40(3136)、chkstr = mgr+0xC48(3144)
// IDA 反编译(0x18009C49E-0x18009C4D4)逐句对应:
v5 = *(a1 + 3136); // enclen
v6 = *(a1 + 3128); // enc
*(a1 + 3120) = v5; // ★ publen = enclen
v8 = operator new(v5); // ★ 按 enclen 分配
do { v8[i] = i ^ *(unsigned char *)(v6 + i); ++i; } while (i < v5);
*(a1 + 3112) = v8; // → 验签公钥(SPKI DER)
; 对应解密内循环(0x18009C4C0-0x18009C4D2)
18009c4c4 movzx ecx, byte ptr [rsi+rdx] ; enc[i]
18009c4c8 xor cl, al ; ^ i(al = 索引低字节)
18009c4ca mov [rdx], cl
18009c4cc inc rax
18009c4cf cmp rax, rdi
18009c4d2 jb short 18009C4C0
18009c4d4 mov [rbx+0C28h], r11 ; mgr+3112 = 解密缓冲
⚠️ 长度必须是真实值:publen = enclen 且按 enclen 分配,故 mgr+0xC40 必须写 548;
写 50 会把公钥截断成 50 字节 → RSA 初始化必败(历史脚本 mgr_redirect.ps1 把
$blob.Length(路径字符串长度,恰 50)误写为长度,属 bug)。
厂商公钥(tools/extract_pubkey.py):RSA-4096,e=17,模数 512B,SPKI DER 548B。
2.3 防篡改 = HMAC-MD5,绑定"解密后的公钥"
校验代码位置:sub_18009C3B0 解密后立即执行;比较循环见 IDA 反编译
(0x18009C64C-0x18009C6A3,逐句照录):
sub_18002FD80(Dst, *(void **)(a1 + 3144), strlen(*(const char **)(a1 + 3144))); // Dst = 期望串副本
v16 = 0; v17 = Dst; v18 = Memory; // Memory = 派生串(std::string)
for ( i = *(_BYTE *)v18; *(_BYTE *)v18; i = *(_BYTE *)v18 ) { // 逐字节走派生串
if ( !*(_BYTE *)v17 ) break; // 采样位为 0 → 结束
v20 = (char *)v17 + *(unsigned int *)(a1 + 3152); // v17 += 步长(mgr+0xC50 = 5)
v16 += i - *v20; // 累加 char 差:*v20 = 期望串[5+6k]
v18 = (void **)((char *)v18 + 1); // derived[k+1]
v17 = (void **)(v20 + 1); // v17 → 6(k+1)
}
v21 = (v16 == 0);
if ( v16 ) { if (*(void **)(a1 + 3112)) j_j_free(...); *(a1 + 3112) = 0; *(a1 + 3120) = 0; } // 失败即丢公钥
取样位 = 5 + 6k(v17 起始为 Dst[0],每次前进 步长+1 = 6,取样点在 v17+步长);
派生串长 32(HMAC-MD5 的 hex),故 k = 0..31。
派生算法 = HMAC-MD5(运行时 oracle 实证):sub_18031B6A0 的参数为
(out, data, len, key="Error opening file %s\r\nDo you want to continue ?", 48, 0, 0);
在进程内以该函数对厂商 DER 求值,输出与 Python
hmac.new(key, vendor_der, md5).hexdigest().upper() 逐字符一致:
出厂期望串采样(chkstr[5+6k], k=0..31)= 983B20C2BEF31C86EB3DB4063192B7D1
HMAC-MD5(KEY, 厂商DER) = 983B20C2BEF31C86EB3DB4063192B7D1 ★ 完全一致
结论:防篡改与公钥强绑定(派生摘要的输入就是解密后的 SPKI DER)。
换公钥(自签名)的必要条件:令 mgr+0xC48 指向自建期望串
chkstr'[5+6k] = HMAC-MD5(KEY, 我方DER)[k](k=0..31,其余位填非零如 '0',末位 NUL)。
实现上直接调用引擎自身 sub_18031B6A0 作 oracle 计算即可,无需离线复刻算法。
3. 验签(sub_18009D190,状态机 case 0x1D)
void sub_18009D190(mgr, mask /* lParam,必须非 0 */)
{
if (!mgr+3272 || !mgr+3280) return; // .lic/.sig 文档未装载 → 直接返回
pub = mgr+3112; publen = mgr+3120; // ★ 解密后的公钥(case 0xB 填充)
lic_size = lic_doc->vtbl+144(lic_doc); // GetFileSizeEx = .lic 磁盘字节数
r = sub_1802FD4F0(verifier_vtbl /*PKCS1v15-SHA1*/, 0,
lic_doc, 0, lic_size, // 消息 = .lic 全部磁盘字节
sig_doc, mgr+3288 /*=1*/, 0, 0);
/* r 经 MBA 更新到 mgr+3240/3248/3256;末尾 free(mgr+3112)、mgr+3120=0;
然后 PostMessage(hwnd, WM_COMMAND, cmdBase+1, r) */
}
- 验签核心
sub_1802FD4F0:PKCS#1 v1.5 + SHA-1(DigestInfo 前缀sub_1802DD0F0),
verifier 由公钥 DER 构造 → 公钥必须已由 case 0xB 解密就绪,否则 NULL 公钥必失败。 - case 0xB / 0x1D 不在引擎自然任务链中:干净环境实测
mgr+3112恒 0(验签从未真正执行)。
两个命令由分发器sub_180099E80处理,须显式投递或直调(§7.2)。
4. About 状态行链路("已注册/未注册"的真正判据)
链路(断点实证:sub_18009BAA0 / BE10 / BC30 + 用户点击)
About 对话框(资源 0x5E1,类 #32770,标题 About)
init = sub_18017CEE0:
item 1506 ← 版本串 "%s v%s"("HeliumHexEditor v2.8.7")
动态 CreateWindowExW(L"STATIC", …, (HMENU)0x5E8 /*id 1512*/, 280x78) ← 状态行
└ SetWindowSubclass(sub_18017D5C0):收到 WM_SETTEXT 时对文本做 31B6A0(HMAC,key="MS Shell Dlg")
与常量 9EBDCC4AFF7A5275A68159303D4BF59E 比对,不符 → EnableWindow(Register, 0)
PostMessageW(hDlg, WM_COMMAND, 0x5E7, 状态行 HWND)
→ 对话框处理器 sub_18017D310 case 0x5E7:PostMessage(hWnd, WM_COMMAND, cmdBase+3, 该HWND)
→ 分发器 sub_1800A0570 case 3 → PostMessage(cmdBase+4, lParam)
→ case 4 = sub_18009BAA0(决策):
if ((*(u32*)(mgr+0xC58) & 0x596) == 0) {
v6 = (*(u64*)(mgr+0xC58) == 0x1FA68) ? 0 : 1;
*(mgr+0xC20) = mgr ^ hwnd ^ *(mgr+0xC10) ^ (mgr+4686) ^ 0x1FA5A;
PostMessage(cmdBase + qword_1806F9FA0[v6], lParam); // 表 = {7, 6}
}
v6 == 0 → cmdBase+7 = sub_18009BC30(**已注册**文案)
v6 == 1 → cmdBase+6 = sub_18009BE10(未注册文案)
→ sub_18009BC30 取文本指针:
p = *(mgr+0xC20) ^ mgr ^ *(mgr+0x9A8) ^ *(mgr+0xC10) ^ *(mgr+0xC58) ^ 0x32
// 代入 0xC20 定义后 XOR 化简 = (mgr+4686) ^ 0x1FA5A ^ *(mgr+0xC58) ^ 0x32
// 当 *(mgr+0xC58) == 0x1FA68 时恰为 mgr+4686 = 许可摘要字段(rec+1386)
→ sub_18009BE10 走 vtbl[0x50]:`call [*(mgr+0xC10)][0x50]`
= 任务表 0x5FF730[10] = sub_180214C50 → 解出混淆串
"Unregistered : some features are disabled like Save, Export, Join, Split,
Disassembly, Cryptography, Plugins, …"
→ 两者均经 sub_18009BEC0 封装 {std::wstring*, hwnd} → PostMessage(cmdBase+0x10, &st)
→ case 0x10:SendMessageW(hwnd, WM_SETTEXT, 0, text) ← About 状态行最终文本
结论
| 项 | 值 |
|---|---|
| 判据 | 选择子 *(mgr+0xC58):"已注册"取值 = 0x1FA68(本环境引擎自然值 ≠ 该值 → 显示未注册) |
| "已注册"文案 | mgr+4686 = 许可记录摘要(sub_1800A14E0 生成,如 Pro Portable license registered to ) |
| "未注册"文案 | 任务表 0x5FF730[10] = sub_180214C50 解出的混淆串(同函数即 §2.3 的 HMAC-MD5 管线所在族) |
| 与验签的关系 | 无直接联动:About 文案只取决于 mgr+0xC58 与摘要字段 |
5. 功能门控
5.1 两层互相独立
- Layer A(验签)只更新
mgr+3240一族状态字;没有任何路径在验签成功后自动解锁金丝雀。 - Layer B(金丝雀)决定功能是否可用;实测无许可 / 有许可、验签通过与失败,金丝雀状态与
0x4AF自检结果完全一致 → 功能可用性 100% 由 Layer B 决定。 mgr+0xCA8(= +3240)走到-1只表示"状态机这一轮跑完",不是验签通过的判据。
5.2 Layer 1:金丝雀表与 43 处门控块
指针槽 0x714568(CRT 静态初始化器 sub_180504F50):
call operator new(0x68) ; 104B 堆表
mov rcx, rax
call sub_180214A30 ; 表构建器
mov cs:qword_180714568, rax ; 槽 = 堆表地址
lea rcx, sub_180506DF0 ; 析构器 = free(槽值)
jmp atexit
构建器 sub_180214A30 写入 13 个 qword:P[i] = 处理器地址 ^ KEY(i=0..11)、
P[12] = 0xA4EE18F5D7F80873 = MAGIC ^ KEY,KEY = 0x104DCA171F8097F4。
门控块统一形态(magic 立即数 87 9F 78 C8 E2 D2 A3 B4 字节级搜索恰 43 处,分布 16 函数):
mov rax, cs:qword_180714568 ; ★ 0x714568 是"表指针槽"(P = [槽])
cmp qword ptr [rax+60h], MAGIC ; P[12] 是否已解密(明文 MAGIC)
jz/jnz OK / FAIL
; OK :call qword ptr [P + 8k] → 真实处理器
; FAIL:取 vtable 对象 → call sub_180214B10(延迟 1517 发射器)→ 弹窗
43 块分布(成功路径 / 失败路径):
| 函数 | 块数 | 成功路径 | 失败 |
|---|---|---|---|
sub_18022FA10 |
11 | r8d=5..0Eh → 公共块 → call [P+30h] = P[6] |
共享弹窗链 |
sub_18023FEE0 |
13 | blk1→P[1];blk2→P[10];其余 → P[6] | 逐块弹窗链 |
sub_180232980(主分发器) |
4 | case 0x453/0x47C/0x49A/0x4AF→P[7];0x4CF→P[8];0x529→P[10];0xAC9→P[11] | 共享弹窗链 |
sub_1802310B0 |
3 | P[2] / P[3] / P[5] | 弹窗链 |
sub_18019BE70 / 1801A8460 / 1801B5EF0 |
各 1 | P[10] | 弹窗链 |
sub_1801A4420 |
1 | P[4] | 弹窗链 |
sub_1802154F0(任务 T1) |
1 | [obj+0xC78]≠0 且通过 → P[0] |
静默 |
sub_1802187D0 |
1 | sub_180156EE0(不经 P) |
写 [rsi+4ECh] 污染标记 |
sub_180245300 |
1 | P[1] | 弹窗链 |
sub_18024B4E0(case 1513) |
1 | P[0] | 静默返回 |
sub_18024C9D0 / 18024CA30 / 18024CAB0 / 18027B7F0 |
各 1 | P[2] / P[4] / P[9] / P[2] | 弹窗链 |
弹窗链闭环:sub_180214B10 = 延迟 1517 发射器(new(0x30) 构造
{+0x0C=0x111, +0x10=1517, +0x20=17, +0x28=vtable} → sub_180098F40 延迟投递)→
主分发器第二开关 case 1517 → sub_18024B590 → MessageBoxW("Not available for unregistered version");
姊妹 sub_180215200 同构发射 1518 → sub_18024B670。
解锁/加锁 = 对合操作:默认态 P 指向堆上混淆表(P[12] = 密文 0xA4EE18F5D7F80873);
解锁器 sub_1802151B0(任务 T4)对 P[0..12] 原地 XOR KEY(P[12] 变明文 MAGIC);
加锁器 sub_180215410(任务 T0)= memset + 重填混淆内容。二者互为逆操作。
5.3 修复方法与陷阱
- 方法 A(首选):直接把 13 个明文字(12 处理器指针 + MAGIC)写
[槽](等价执行一次 T4);
堆表不在镜像内 → 不触发自毁监控。 - 方法 B(兜底):
VirtualAllocEx新表 → 写字 → 改指针槽base+0x714568。 - ⚠️ 陷阱:不可把表内容直写到
0x714568——那是指针槽;直写会把 P 变成
base+0x22B7C0(桩函数地址),门控读[P+0x60]得到代码字节 → 恒失败
(历史脚本write_canary.ps1"持久生效但点击仍弹"的根因)。
5.4 Layer 2:菜单构建期绑定 1517/1518
菜单构建器 sub_180224D90(~104KB,经 vtable 0x180606BB0 槽调用)在构建期把受限项绑定到
占位命令 1517/1518,而构建早于验签完成;静态定位困难(无 mgr 全局引用、1517 非立即数形态)。
静态闭环落点:主分发器第二开关(sub ecx, 500h,索引表 0x180233758):
case 1517 → sub_18024B590(0x18023338E)、case 1518 → sub_18024B670(0x18023339C)。
0x4AF 属 Layer 1 门控命令(与 0x453/0x47C/0x49A 共享一个金丝雀块 → P[7]),
故直发 0x4AF 可同时验证 Layer A+B。
5.5 引擎任务表(.data 0x5FF730,12 项)
| 槽 | 函数 | 语义(反编译实证) |
|---|---|---|
| T0 | sub_180215410 |
金丝雀表重加锁 |
| T1 | sub_1802154F0 |
lock dec [obj+0xC74];[obj+0xC78]≠0 且金丝雀通过 → call P[0] |
| T2 | sub_180214F80 |
环境探测 → 状态 4 / 状态 2(解码 "HeliumLicenses\") |
| T3 | sub_1802150F0 |
环境探测:GetModuleFileNameW + 厂商 URL |
| T4 | sub_1802151B0 |
金丝雀表解锁(原地 XOR KEY) |
| T5 | sub_180310CB0 |
未析 |
| T6–T8 | nullsub ×3 | 空任务 |
| T9 | sub_180215200 |
延迟 WM_COMMAND(1518) 发射器 |
| T10 | sub_180214C50 |
混淆文案解码(out=(c-step+63)%0x5F+32) |
| T11 | sub_180215270 |
stub:清零出参、返回 1 |
登记链:mgr 初始化 sub_180099410 @0x18009957A →
lea rax, off_1807050A0; mov cs:qword_180711F20, rax(= mgr+0xC10);
任务表描述符 0x7050A0[0] = 0x5FF730;mgr 分发器 case 8 = rand() 随机化任务顺序(反跟踪)。
0x180728Cxx 区为引擎函数的 (start,end) 字节范围记录表(供完整性自校验定位函数区间)。
6. 防护体系
| 机制 | 位置 / 证据 | 对劫持方案的影响 |
|---|---|---|
| 43 处金丝雀门控 | §5.2 | 代理一次性解锁 ✓ |
| 菜单 1517/1518 绑定 | §5.4 | 状态就绪后受限功能实测可用 ✓ |
| 防篡改(HMAC-MD5 绑定公钥) | §2.3 | 代理换血期望串(步长 6)✓ |
| 运行时自毁监控(镜像改写→自杀) | INT3 断点放行后 / 外部改 .text/.rdata → 创建 rip=0 线程群,进程数秒内退出 |
代理只写 .data/堆,不触碰镜像 ✓ |
exe/dll 旁 64B .sig 自完整性 |
磁盘文件自校验(算法未还原,疑 SHA-512/RSA-512) | 磁盘文件未改动 ✓ |
| 反 VM 探测 | 注册表 BIOS/CPU + 进程黑名单(vbox/vmware/qemu/xen/prl) | 物理机无影响 ✓ |
| 行为级反分析 | 调试器在场时整条许可链静默空转(A660/A0DB0/2FD4F0/9D300 断点全套零命中,移除调试器即恢复;非 IAT IsDebuggerPresent) |
观测一律"生产启动 + 外部 RPM/日志",勿依赖调试器 ✓ |
| 网络层 | 仅版本更新检查;购买页走浏览器;无在线许可校验 / 无 kill switch | 无关 |
7. 激活方案
7.1 载体选择
HexControl64.dll 静态导入 WININET 恰 8 函数(InternetOpenW/ConnectW/OpenRequestW/
SendRequestW/QueryInfoW/ReadFile/CrackUrlW/CloseHandle),WININET 非 KnownDLL →
应用目录放置本地 wininet.dll 即被加载器选中(目录绑定:换目录启动不生效)。
实测进程内无一模块引入 version.dll → version.dll 劫持不可行。
代理 DLL 设计两条红线(否则 mshtml/urlmon 链崩溃):
- 真身加载必须用基名不同的副本(
wininet_orig.dll):加载器查重只看基名,
直接LoadLibrary("…\System32\wininet.dll")会命中代理自身 → 转发自递归; - 导出表全量覆盖:
tools/gen_wininet_def.py从真身提取 299 个导出生成
wininet_proxy.def(8 个 C stub + 291 个链接器转发X=wininet_orig.X)。
7.2 进程内补丁流程(tools/wininet_proxy.c 分阶段一次性)
阶段 1 等 HexControl64.dll 映射(本 DLL 是其依赖,先于其 CRT 运行)
阶段 2 准备 wininet_orig.dll(转发真身,基名不同)
阶段 3 取公钥数据:**优先外置 `he_blob.bin`;缺失/无效则回退 DLL 编译期内置数据**
(内置数据 = `tools/he_blob_default.h` 的 `kDefaultBlob[548]`,由
`helium_license.py blobheader` 生成)→ 部署只需 wininet.dll 一个文件
阶段 4 定位 mgr:映像内扫描 SPKI 特征(enc 头 30 83 00 23 / 解密头 30 82 02 20)
→ 扫描指向该 blob 的指针(c40 == 548 双判据)→ mgr 基址
阶段 5 等引擎许可轮完成:rec0(mgr+3300) != 0 ← ★唯一必需的运行时等待(实测 ≈0.4s)
阶段 6 一次性补丁:
6.1 公钥重定向:mgr+0xC38 = ourBlob;mgr+0xC40 = 548(真实长度)
6.2 防篡改换血:mgr+0xC48 → chkstr'(chkstr'[5+6k] = HMAC-MD5(KEY, 我方DER)[k],
派生用引擎自身 sub_18031B6A0 作 oracle 计算)
6.3 直调引擎分发器重跑其自身链(消息通道不可靠:引擎 subclass 被 case 0x21 注销):
sub_180099E80(mgr, cmdBase+0x0B, 1) 解密 + 防篡改
sub_180099E80(mgr, cmdBase+0x1D, 1) 验签编排
sub_18009A370 复刻编排后续命令 0x00/0x0C/0x0F/0x1A/0x24
6.4 About 开关:*(mgr+0xC58) = 0x1FA68(→ About 显示摘要)
6.5 金丝雀解锁:等 T0 完成"重新加锁"之后一次性解锁(早解锁会被反制)
Worker 返回(无驻留循环,稳态零写入)
为什么必须等"许可轮完成":provider 决策(sub_18009A660 头部)会校验
mgr+0xC38/0xC40 == 厂商值;若提前改写 → 决策翻转到注册码路径并静默返回(记录被清、装载/解密/验签跳过)。
故必须等引擎用自己的厂商公钥完成一轮装载(rec0 != 0)之后再改写。
检测"许可轮完成"的方案对比
| 方案 | 可行性 | 结论 |
|---|---|---|
有界条件轮询 rec0 != 0(当前) |
可靠;实测 ≈0.4s 满足(上限 5s) | 采用:启动期一次性、几十次内存读 |
自触发(我方直调 case 2 = sub_18009A660) |
时序冲突:引擎自然轮次可能在其后再次执行并清记录 | ✗ |
事件驱动(给 mgr+0x9A8 窗口装 subclass,观测 WM_COMMAND 后放行补丁) |
可行,但需跨线程唤醒 + 与引擎自身 subclass(case 0x11 装 / case 0x21 卸)共存 | 备选(当前无必要) |
硬件断点(AddVectoredExceptionHandler + Dr0/Dr7) |
技术可行(零轮询、指令级触发),但本程序具行为级反分析,DR 可能触发同样降级 | ✗ 风险大于收益 |
7.3 使用(部署与换签)
:: (1) 生成/刷新“编译期内置公钥头”(修改过公钥时必须重做,否则内置回退是旧公钥)
python helium_license.py blobheader :: ->he_blob_default.h
:: (2) 编译 + 部署(-Itools 用于找到 he_blob_default.h;.def 提供 291 个转发导出,共 299 个)
D:\Program\cygwin64\bin\x86_64-w64-mingw32-gcc.exe -shared -O2 -static-libgcc -Itools ^
tools\wininet_proxy.c tools\wininet_proxy.def -o tools\wininet.dll
copy /Y tools\wininet.dll .\wininet.dll :: 与 exe 同目录(GUI 亦可一键部署)
:: (3) 自检:内置私钥/公钥 ↔ 部署 he_blob.bin ↔ 代理内嵌 blob 三者一致性
python helium_license.py selfcheck
:: (4) 换密钥/重签许可(可选步骤:不放 he_blob.bin 也能工作——DLL 已内置公钥)
python helium_license.py issue --type 1 --name <用户名> --basename he_custom
python helium_license.py blob --public he_public.bin --out .\he_blob.bin
:: 双击启动即注册态;诊断日志:应用目录 he_worker.log
注册方式二选一:
- 文件直写(推荐):
helium_license.py issue生成.lic+.sig落到HeliumLicenses\; - 注册码手动注册:GUI/脚本生成注册码(§1.1 算法),粘贴到程序
Help → Register
(输入框控件 id 1519)。注意:注册框写.sig经WideCharToMultiByte(CP_ACP),
中文系统失真 → UI 注册后需再用文件直写刷新HeliumLicenses\*.sig。
8. 地址索引(RVA,基址 0x180000000)
| RVA | 含义 |
|---|---|
| 0x27E230 / 0x27E651 | 保存许可 handler / Blowfish-OFB 调用点(§1.1 汇编) |
| 0x3DCAC0 → 0x3CA450 → 0x360F60 | Blowfish 分发链 |
| 0x3E2CA0 / 0x3E22A0 / 0x443470 / 0x4431C0 | SetKeyWithIV / key schedule / encipher |
| 0xA0DB0 / 0xA14E0 / 0xA1780 | .lic 解析器 / 摘要生成(→ rec+1386)/ 绑定校验 |
| 0x9C3B0 | 公钥解密 + 防篡改(case 0xB) |
| 0x9D190 | 验签调用者(case 0x1D) |
| 0x2FD4F0 | PKCS1v15-SHA1 验签核心 |
| 0x31B6A0 / 0x31A5D0 | 防篡改派生(HMAC-MD5 管线) |
| 0x5BE860 | 派生用固定 key 字符串(48B) |
| 0x6F35E0 | 厂商公钥 blob(548B XOR 索引加密) |
| 0x5FF668 | 防篡改期望串静态存储(197 hex + NUL;mgr+0xC48 指向) |
| 0x99E80 / 0x9A370 | 命令分发器(0xB/0x1D 与 0/0xC/0xF/0x1A/0x24) |
| 0x9A660 | 许可装载(provider 决策 / 枚举 *.lic;入口 0x9A74F 清记录、0x9B112 绑定失败清记录) |
| 0xA0570 / 0x9BAA0 / 0x9BC30 / 0x9BE10 / 0x9BEC0 | About 状态链:命令分发(case 3/4/6/7/0x10) / 决策 / 已注册文案 / 未注册文案 / 文本投递 |
| 0x1806F9FA0 | 文案分支表 {7, 6}(§4) |
| 0xA14E0 摘要格式 | "%s %s %s\r\n"(类型显示名 + license registered to + Name) |
| 0x17CBF0 / 0x17CEE0 / 0x17D310 / 0x17D5C0 | About 对话框入口 / init / 命令处理 / 状态行 subclass |
| 0x711310 | mgr 单例 |
| 0x714568 | 金丝雀表指针槽(P=[槽]) |
| 0x711F48 / 0x711F50 | 加密公钥 blob 指针 / 长度(= mgr+0xC38/0xC40) |
| 0x711F58 / 0x711F60 | 期望串指针(= mgr+0xC48)/ 步长 5(= mgr+0xC50) |
| 0x2151B0 / 0x215410 | 金丝雀解锁器(T4)/ 加锁器(T0) |
| 0x214B10 / 0x215200 | 延迟 WM_COMMAND(1517) / (1518) 发射器 |
| 0x24B590 / 0x24B670 | 占位命令 1517/1518 处理器("Not available…" 弹窗) |
| 0x214A30 / 0x504F50 / 0x506DF0 | 金丝雀表构建器 / CRT 初始化器(写槽)/ atexit 释放器 |
| 0x5FF730 | 引擎任务表(12 项,§5.5) |
| 0x7050A0 | 任务表描述符([0] = 0x5FF730;地址存于 mgr+0xC10) |
9. 开放项
- vtable 0x1805E0580(About 对话框类)与任务表 0x5FF730 的完整方法语义(仅析出本案所需项)。
mgr+0xC58的合法写入者:引擎在何种条件下写入0x1FA68尚未定位(代理按"已注册"取值一次性置位)。- 菜单构建期 1517/1518 绑定的判据来源(决策走间接路径,静态未闭环;当前方案无实际需求)。
- exe/dll 旁 64B
.sig自完整性算法(疑 SHA-512/RSA-512)——与许可链独立,攻击路径未触碰。 sub_18031B6A0的精确实现(已实证等价于 HMAC-MD5;内部 HashFilter 组合未逐指令还原)。
10. 附:工具与脚本清单
生产三件套
| 文件 | 作用 |
|---|---|
wininet_proxy.c + wininet_proxy.def |
劫持代理源码(299 导出;编译见 §7.3) |
| he_blob_default.h` | 编译期内置公钥数据(由 blobheader 生成;缺它无法编译) |
he_blob.bin(应用目录,可选) |
外置公钥数据(存在则优先于内置;换密钥只换它,免重编译) |
helium_license.py |
统一入口:keygen / issue / regcode / blob / blobheader / verify / selfcheck(内置私钥+公钥,单文件可用) |
helium_manager.py |
Tkinter GUI:签发 / 部署劫持 DLL / 显示编译命令 / 启动 / 本地校验 / 生成注册码(手动注册) / 状态(含 About 预期内容)/ 启动自检密钥一致性 |
wininet_proxy.c
// wininet.dll 代理 + 进程内激活补丁器(Helium Hex Editor 持久化方案)
//
// 设计(一次性、无常驻循环;详见 HeliumHexEditor_许可机制分析.md §4.7):
// Worker 线程按显式阶段执行,全部补丁在初始化阶段写完即结束(实测 ≈0.9s):
// 阶段 1 等宿主模块映射 → 2 准备转发真身 → 3 读 he_blob.bin
// → 4 定位 mgr → 5 等引擎许可轮完成(唯一必需的运行时等待:rec0 != 0)
// → 6 一次性补丁:
// 6.1 公钥重定向 c38=ourBlob / c40=548
// 6.2 防篡改期望串换血 chkstr'[5+6k] = HMAC-MD5(KEY, ourDER)[k](步长 6)
// 6.3 直调引擎分发器重跑其自身链:0xB(解密+防篡改) → 0x1D(验签编排) → 后续命令
// 6.4 About 状态开关 *(mgr+0xC58) = 0x1FA68(→ About 显示摘要)
// 6.5 金丝雀解锁(等 T0 重新加锁之后再做,避免被反制)
// 之后 Worker 返回(无循环、稳态零写入)。可选看门狗:-DWININET_WATCHDOG=1
// (1s,仅漂移时修复;默认关闭,依据 §4.7.4 漂移实验)。
//
// 定位策略(无版本硬编码偏移,全部特征自校验):
// 1) 原厂公钥 blob:在 .rdata 扫描 XOR 索引加密的 SPKI 特征
// (enc[0..3] = 30 83 00 23;解密后 plain[0..3] = 30 82 02 20,DER 长度自描述 548B);
// 2) mgr:扫描 .data 中指向该 blob 的指针(唯一命中 = mgr+0xC38)→ mgr 基址;
// 3) 金丝雀表:槽位 0x714568(机制常量)+ MAGIC 明/密文自校验,处理器表 kRv 为本版本代码地址。
//
// 公钥数据两级来源:**优先外置** he_blob.bin(XOR 索引加密的自签名 SPKI,由
// tools/gen_blob_bin.py 或 `helium_license.py blob` 生成)——换密钥只需换文件、免重编译;
// **缺失/无效则回退编译期内置数据** `he_blob_default.h: kDefaultBlob[548]`
// (由 `python helium_license.py blobheader` 生成)→ 部署只需 wininet.dll 一个文件。
// ⚠ 公钥变更后除替换 he_blob.bin 外,若依赖内置回退还须重跑 blobheader 并重新编译。
//
// 构建:x86_64-w64-mingw32-gcc -shared -O2 -static-libgcc -o wininet.dll tools/wininet_proxy.c
#include <windows.h>
#include <wininet.h>
#include <stdio.h>
#include <stdarg.h>
#include "he_blob_default.h" /* 编译期内置公钥数据(外置 he_blob.bin 缺失/无效时回退使用) */
#define OFF_C38 0xC38
#define OFF_C40 0xC40
#define OFF_F0 0xCA8
#define OFF_REC 3300
#define OFF_CHKSTR 0xC48
#define OFF_SLOT 0x714568
#define BLOB_LEN 548
#define MAGIC 0xB4A3D2E2C8789F87ULL
static volatile ULONG64 g_t0 = 0;
static HINSTANCE g_self = 0;
static const unsigned long kRv[12] = {
0x22B7C0, 0x2453D0, 0x22D980, 0x22DA80, 0x22DBC0, 0x22DCE0,
0x22DDB0, 0x23AEA0, 0x23B1E0, 0x23EC80, 0x2398A0, 0x23D440
};
/* ---- 记录恢复素材(About 显示门;LicenseType=1 / Name=popk / A14E0 摘要格式)---- */
static const wchar_t g_rec_name[] = L"popk";
static const wchar_t g_rec_digest[] = L"Pro Portable license registered to popk\r\n";
/* ---- 防篡改期望串换血缓冲(常驻,维护循环需重断言其指针)---- */
static char g_chk_new_buf[256];
static volatile int g_chk_new_ready = 0;
/* ---- hex nibble 解码(chkstr/F 输出均为大写 hex)---- */
static int hexval(int c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return -1;
}
/* ---- 自旋观察线程:跨线程纳秒级跟踪 pub(3112) 生命周期 ----
g_spin_seen: 0=未见 1=存活中 2=存活后被释放(防篡改失败) 3=观察窗结束仍存活(通过) */
static volatile LONG g_spin_seen = 0;
static volatile unsigned char g_spin_head[8];
static DWORD WINAPI SpinProc(LPVOID p)
{
ULONG64 mgr = (ULONG64)p;
unsigned long long t0 = GetTickCount64();
while (GetTickCount64() - t0 < 2000) {
volatile ULONG64 v = *(volatile ULONG64 *)(mgr + 3112);
if (v && !g_spin_seen) {
g_spin_seen = 1;
for (int b = 0; b < 8; b++) g_spin_head[b] = ((unsigned char *)v)[b];
}
if (!v && g_spin_seen == 1) { g_spin_seen = 2; break; }
Sleep(0);
}
if (g_spin_seen == 1) g_spin_seen = 3;
return 0;
}
// ---- 诊断日志(写本 DLL 同目录 he_worker.log;仅 Worker 线程调用,无 loader-lock 风险)----
static void Log(const char *fmt, ...)
{
va_list ap;
wchar_t path[MAX_PATH], *slash;
FILE *f;
GetModuleFileNameW(g_self, path, MAX_PATH);
slash = wcsrchr(path, L'\\');
if (slash) *(slash + 1) = 0; else path[0] = 0;
wcscat(path, L"he_worker.log");
f = _wfopen(path, L"a");
if (!f) return;
fprintf(f, "[%llums] ", GetTickCount64() - g_t0);
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fprintf(f, "\n");
fclose(f);
}
// ---- 真身解析(基名不同的副本,防同名自匹配)----
// 关键陷阱:LoadLibraryW("C:\Windows\System32\wininet.dll") 会因基名 wininet.dll
// 匹配到已加载的代理自身(加载器查重只看基名),GetProcAddress 拿到自己导出 ->
// 转发自递归。故先把真身复制为 wininet_orig.dll(基名不同)再加载。
// 其余 291 个导出由 wininet_proxy.def 转发到 wininet_orig.X(IE 组件链
// mshtml/urlmon/ieframe 动态解析 wininet 需要全部导出,仅 8 stub 会使其崩溃)。
static HMODULE g_real = 0;
static FARPROC g_p[8] = {0};
static const char *kNames[8] = {
"InternetCrackUrlW", "InternetReadFile", "InternetConnectW", "HttpSendRequestW",
"HttpQueryInfoW", "HttpOpenRequestW", "InternetCloseHandle", "InternetOpenW"
};
static int EnsureOrigCopy(void)
{
wchar_t src[MAX_PATH], dst[MAX_PATH], *slash;
wcscpy(src, L"C:\\Windows\\System32\\wininet.dll");
GetModuleFileNameW(g_self, dst, MAX_PATH);
slash = wcsrchr(dst, L'\\');
if (!slash) return 0;
wcscpy(slash + 1, L"wininet_orig.dll");
if (GetFileAttributesW(dst) != INVALID_FILE_ATTRIBUTES) return 1;
return CopyFileW(src, dst, FALSE) ? 1 : 0;
}
static void ResolveReal(void)
{
int i;
wchar_t path[MAX_PATH], *slash;
if (g_real) return;
if (!EnsureOrigCopy()) return;
GetModuleFileNameW(g_self, path, MAX_PATH);
slash = wcsrchr(path, L'\\');
if (!slash) return;
wcscpy(slash + 1, L"wininet_orig.dll");
g_real = LoadLibraryW(path);
if (!g_real) return;
for (i = 0; i < 8; i++) g_p[i] = (FARPROC)GetProcAddress(g_real, kNames[i]);
}
// ---- 8 导出转发(签名与 wininet.h 一致,同名导出)----
__declspec(dllexport) HINTERNET __stdcall InternetOpenW(LPCWSTR a, DWORD b, LPCWSTR c, LPCWSTR d, DWORD e)
{ ResolveReal(); return ((HINTERNET (__stdcall *)(LPCWSTR, DWORD, LPCWSTR, LPCWSTR, DWORD))g_p[7])(a,b,c,d,e); }
__declspec(dllexport) HINTERNET __stdcall InternetConnectW(HINTERNET h, LPCWSTR a, INTERNET_PORT p, LPCWSTR u, LPCWSTR w, DWORD s, DWORD f, DWORD_PTR c)
{ ResolveReal(); return ((HINTERNET (__stdcall *)(HINTERNET, LPCWSTR, INTERNET_PORT, LPCWSTR, LPCWSTR, DWORD, DWORD, DWORD_PTR))g_p[2])(h,a,p,u,w,s,f,c); }
__declspec(dllexport) HINTERNET __stdcall HttpOpenRequestW(HINTERNET h, LPCWSTR v, LPCWSTR o, LPCWSTR ve, LPCWSTR r, LPCWSTR *at, DWORD fl, DWORD_PTR c)
{ ResolveReal(); return ((HINTERNET (__stdcall *)(HINTERNET, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR *, DWORD, DWORD_PTR))g_p[5])(h,v,o,ve,r,at,fl,c); }
__declspec(dllexport) BOOL __stdcall HttpSendRequestW(HINTERNET h, LPCWSTR hd, DWORD hl, LPVOID b, DWORD bl)
{ ResolveReal(); return ((BOOL (__stdcall *)(HINTERNET, LPCWSTR, DWORD, LPVOID, DWORD))g_p[3])(h,hd,hl,b,bl); }
__declspec(dllexport) BOOL __stdcall HttpQueryInfoW(HINTERNET h, DWORD qi, LPVOID bf, LPDWORD bl, LPDWORD ix)
{ ResolveReal(); return ((BOOL (__stdcall *)(HINTERNET, DWORD, LPVOID, LPDWORD, LPDWORD))g_p[4])(h,qi,bf,bl,ix); }
__declspec(dllexport) BOOL __stdcall InternetReadFile(HINTERNET h, LPVOID bf, DWORD tn, LPDWORD rd)
{ ResolveReal(); return ((BOOL (__stdcall *)(HINTERNET, LPVOID, DWORD, LPDWORD))g_p[1])(h,bf,tn,rd); }
__declspec(dllexport) BOOL __stdcall InternetCrackUrlW(LPCWSTR u, DWORD ul, DWORD fl, URL_COMPONENTSW *uc)
{ ResolveReal(); return ((BOOL (__stdcall *)(LPCWSTR, DWORD, DWORD, URL_COMPONENTSW *))g_p[0])(u,ul,fl,uc); }
__declspec(dllexport) BOOL __stdcall InternetCloseHandle(HINTERNET h)
{ ResolveReal(); return ((BOOL (__stdcall *)(HINTERNET))g_p[6])(h); }
// ---- 特征定位 ----
// 验证一段 548B 数据是否为"XOR 索引加密的 RSA-4096 SPKI":解密头 = 30 82 02 20
static int IsEncryptedSpki(const unsigned char *p)
{
unsigned char plain[4];
int i;
for (i = 0; i < 4; i++) plain[i] = (unsigned char)(p[i] ^ i);
return plain[0] == 0x30 && plain[1] == 0x82 && plain[2] == 0x02 && plain[3] == 0x20;
}
// 在 HexControl64.dll 映像内定位原厂加密 blob(大范围扫描,不依赖节表边界)
static ULONG64 FindVendorBlob(ULONG64 base)
{
unsigned char *d = (unsigned char *)base;
ULONG n = 0x720000; // 覆盖 .text/.rdata/.data 全域
ULONG j;
if (n < BLOB_LEN) return 0;
for (j = 0x1000; j + BLOB_LEN <= n; j++) {
if (d[j] == 0x30 && d[j+1] == 0x83 && d[j+2] == 0x00 && d[j+3] == 0x23
&& IsEncryptedSpki(d + j)) {
return base + j;
}
}
return 0;
}
// 在映像内找指向 blob 的指针(可能多处)→ 用 f0 阶段小值 + c40=0x224 双判据锁定真 mgr
static ULONG64 FindMgrByBlobPtr(ULONG64 base, ULONG64 blobVA)
{
ULONG64 *d = (ULONG64 *)base;
ULONG n = 0x720000 / 8;
ULONG j;
for (j = 0x800; j < n; j++) {
if (d[j] == blobVA) {
ULONG64 mgr = base + j * 8 - OFF_C38;
long f0 = *(long *)(mgr + OFF_F0);
ULONG64 c40 = *(ULONG64 *)(mgr + OFF_C40);
// 双判据:f0 为阶段小值(或已消费 -1),且 c40 为静态常量 548
if (((ULONG)f0 <= 0xFFFF || f0 == -1) && c40 == 0x224)
return mgr;
}
}
return 0;
}
// 读本 DLL 同目录的 he_blob.bin(外置公钥数据)
static int LoadOurBlob(unsigned char *out)
{
wchar_t path[MAX_PATH], *slash;
HANDLE f;
DWORD rd = 0;
GetModuleFileNameW(g_self, path, MAX_PATH);
slash = wcsrchr(path, L'\\');
if (slash) *(slash + 1) = 0; else path[0] = 0;
wcscat(path, L"he_blob.bin");
f = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
if (f != INVALID_HANDLE_VALUE) {
ReadFile(f, out, BLOB_LEN, &rd, 0);
CloseHandle(f);
if (rd == BLOB_LEN && IsEncryptedSpki(out)) {
Log("blob source: external he_blob.bin");
return 1; /* 外置优先:换密钥只需换文件,免重编译 */
}
Log("WARN: he_blob.bin invalid (%u B) -> fallback to embedded", rd);
}
/* 回退:编译期内置数据(tools/he_blob_default.h,由 helium_license.py blobheader 生成)
→ 部署时只需 wininet.dll 一个文件,无需附带 he_blob.bin */
memcpy(out, kDefaultBlob, BLOB_LEN);
if (IsEncryptedSpki(out)) {
Log("blob source: embedded default");
return 1;
}
Log("WARN: no usable public-key blob");
return 0;
}
// ---- 金丝雀(Layer B)----
static const ULONG64 kEncMagic = 0xA4EE18F5D7F80873ULL;
static const ULONG64 kPlainMagic = 0xB4A3D2E2C8789F87ULL;
static int TryLayerB(ULONG64 base)
{
ULONG64 *P = (ULONG64 *)(*(ULONG64 *)(base + OFF_SLOT));
int i;
if (!P) return 0;
if (P[12] == kPlainMagic) return 1;
if (P[12] != kEncMagic) return 0; // 表状态异常,不动
for (i = 0; i < 12; i++) P[i] = base + kRv[i];
P[12] = kPlainMagic;
return P[12] == kPlainMagic;
}
// 金丝雀是否处于「被引擎加锁」态(T0 会在初始化期间重新加锁 → 需在其之后解锁才持久)
static int CanaryLocked(ULONG64 base)
{
ULONG64 slot = *(ULONG64 *)(base + OFF_SLOT);
return slot && *(ULONG64 *)(slot + 0x60) == kEncMagic;
}
// ---- 工作线程(v4:f0 触发定点写 Layer A + Layer B 金丝雀尽早激活 + 常驻维护;
// 无消息投递。二分探针开关:PROBE_NO_A=禁 Layer A 写,PROBE_NO_B=禁 Layer B)----
#if defined(PROBE_NO_A)
#define DO_LAYERA 0
#else
#define DO_LAYERA 1
#endif
#if defined(PROBE_NOWRITE)
#define DO_C38WRITE 0 /* 探针:不写 c38/c40(保持厂商值),仅投递命令 */
#else
#define DO_C38WRITE 1
#endif
#if defined(PROBE_NO_B)
#define DO_LAYERB 0
#else
#define DO_LAYERB 1
#endif
static DWORD WINAPI Worker(LPVOID arg)
{
ULONG64 base = 0, mgr = 0, *P;
static unsigned char ourBlob[BLOB_LEN]; // 常驻维护会长期引用该缓冲,避免栈地址
int i, haveBlob = 0, wroteA = 0, layerB = 0;
(void)arg;
/* ===== 阶段 1:等宿主模块映射(本 DLL 是其静态依赖,先于其 CRT 初始化运行)===== */
for (i = 0; i < 1000 && !base; i++) {
base = (ULONG64)GetModuleHandleW(L"HexControl64.dll");
if (!base) Sleep(5);
}
if (!base) return 0;
Log("worker start, base=0x%llx", base);
/* ===== 阶段 2:准备 .def 转发真身(wininet_orig.dll),供 IE 组件链解析全量导出 ===== */
if (EnsureOrigCopy()) Log("wininet_orig.dll ready");
else Log("WARN: copy wininet_orig.dll failed");
/* ===== 阶段 3:读外置公钥数据 he_blob.bin(换密钥免重编译)===== */
haveBlob = LoadOurBlob(ourBlob);
Log(haveBlob ? "he_blob.bin loaded" : "WARN: he_blob.bin missing/invalid -> 跳过 Layer A");
/* ===== 阶段 4:定位 mgr 单例(SPKI 特征扫描;由宿主 CRT 初始化器填充,故需重试)===== */
for (i = 0; i < 300 && !mgr; i++) {
ULONG64 vendorVA = FindVendorBlob(base);
if (vendorVA) mgr = FindMgrByBlobPtr(base, vendorVA);
if (!mgr) Sleep(10);
}
if (!mgr) { Log("WARN: mgr not located"); return 0; }
Log("mgr located @0x%llx", mgr);
/* ===== 阶段 5:等引擎许可轮完成 —— **唯一必需的运行时等待** =====
rec0(mgr+3300) != 0 ⇒ provider 决策已用「厂商」c38/c40 通过、.lic 已解析并绑定
(实测 ≈0.4s)。此后再改写 c38 才不会翻转决策(否则引擎会跳过整个许可处理)。 */
for (i = 0; i < 500 && !*(volatile ULONG64 *)(mgr + OFF_REC); i++) Sleep(10);
if (!*(ULONG64 *)(mgr + OFF_REC)) Log("WARN: license record still empty (engine chain idle?)");
/* ===== 阶段 6:一次性补丁(此后稳态不再写内存)===== */
if (DO_LAYERA && haveBlob) {
WORD cb = *(WORD *)(mgr + 2482);
/* 6.1 Layer A:公钥重定向(c38 = 我方 blob / c40 = 真实长度 548,而非 50) */
#if !defined(PROBE_NOWRITE)
*(ULONG64 *)(mgr + OFF_C38) = (ULONG64)ourBlob;
*(ULONG64 *)(mgr + OFF_C40) = BLOB_LEN;
#endif
wroteA = 1;
Log("Layer A: c38 -> our blob, c40 = %d%s", BLOB_LEN, DO_C38WRITE ? "" : " [NOWRITE probe]");
/* v9:防篡改期望串换血(核心修正)——
比较循环语义(9C3B0 @0x18009C670)实为:
sample = chkstr_copy[5 + 6*k](stride = *(mgr+3152)=5,步进 = stride+1 = 6)
vs derived[k](= HMAC-MD5(key="Error opening file ...", msg=解密公钥DER))
故只需令 chkstr'[5+6k] = HMAC-MD5(KEY, ourDER)[k](k=0..31),
比较即按构造相等。derived 用引擎自身函数 31B6A0 作 oracle 计算,
无需离线复刻(已实证其输出 == Python hmac-md5(KEY, der))。 */
{
typedef void *(__fastcall *PFN_DERIVE)(void *, const unsigned char *,
unsigned long long, const unsigned char *,
unsigned long long, unsigned long long,
unsigned long long);
PFN_DERIVE fnDerive = (PFN_DERIVE)(base + 0x31B6A0);
const unsigned char *obf = (const unsigned char *)(base + 0x5BE860);
static unsigned char g_out[32];
static unsigned char g_ourder[BLOB_LEN];
const char *s;
unsigned long long L;
int k;
for (k = 0; k < BLOB_LEN; k++)
g_ourder[k] = (unsigned char)(k ^ ourBlob[k]); /* plain[i]=i^enc[i] */
memset(g_out, 0, sizeof g_out);
((unsigned long long *)(g_out + 16))[0] = 0;
((unsigned long long *)(g_out + 24))[0] = 15;
fnDerive(g_out, g_ourder, BLOB_LEN, obf, 48, 0, 0);
s = (((unsigned long long *)(g_out + 24))[0] >= 16)
? *(const char **)g_out : (const char *)g_out;
L = ((unsigned long long *)(g_out + 16))[0];
if (L > 32) L = 32;
memset(g_chk_new_buf, '0', sizeof g_chk_new_buf);
for (k = 0; k < (int)L; k++) g_chk_new_buf[5 + 6 * k] = s[k];
g_chk_new_buf[5 + 6 * (int)L] = 0;
*(const char **)(mgr + OFF_CHKSTR) = g_chk_new_buf;
g_chk_new_ready = 1;
Log("chkstr' built (stride6): L=%llu head='%.16s' tail='%.8s'",
L, s, s + (L > 8 ? L - 8 : 0));
/* 6.3 令引擎自身链以我方公钥重跑:WM_COMMAND 通道不可靠(引擎 subclass 已被
case 0x21 注销)→ 直接调用引擎分发器函数(sub_180099E80 / sub_18009A370) */
if (cb) {
typedef unsigned int (__fastcall *PFN_DISP)(ULONG64, unsigned short, unsigned long long);
PFN_DISP fnD = (PFN_DISP)(base + 0x99E80); /* 含 case 0xB / 0x1D */
PFN_DISP fnA = (PFN_DISP)(base + 0x9A370); /* 含 0x00 / 0x0C / 0x0F / 0x1A / 0x24 */
fnD(mgr, (unsigned short)(cb + 0x0B), 1); /* 解密 + 防篡改(chkstr' 使比对通过) */
Log("engine decrypt dispatched");
Sleep(80);
fnD(mgr, (unsigned short)(cb + 0x1D), 1); /* 验签编排(我方公钥验我方签名) */
Log("engine verify dispatched");
/* 复刻 case 0x1D 编排内的后续命令(原本经窗口消息投递;此处直接分发) */
fnA(mgr, (unsigned short)(cb + 0x00), 0); Sleep(15);
fnD(mgr, (unsigned short)(cb + 0x09), 1); Sleep(15);
fnA(mgr, (unsigned short)(cb + 0x0F), 0); Sleep(15);
fnA(mgr, (unsigned short)(cb + 0x1A), 0); Sleep(15);
fnA(mgr, (unsigned short)(cb + 0x0C), 1); Sleep(15);
fnD(mgr, (unsigned short)(cb + 0x1E), 0); Sleep(15);
fnA(mgr, (unsigned short)(cb + 0x24), 1); Sleep(15);
fnD(mgr, (unsigned short)(cb + 0x20), 0);
Log("engine chain re-run done");
} else {
Log("WARN: cmdBase not ready, engine chain not re-run");
}
}
/* 6.4 About 状态行开关:置"已注册"取值 → About 显示摘要(§4.7.1 链路) */
*(ULONG64 *)(mgr + 0xC58) = 0x1FA68;
Log("About gate set (0xC58 = 0x1FA68)");
}
/* 6.5 Layer B(金丝雀):等引擎 T0 完成"重新加锁"之后再一次性解锁(早解锁会被反制) */
if (DO_LAYERB && !layerB) {
for (i = 0; i < 350 && !CanaryLocked(base); i++) Sleep(10); /* 上限 3.5s,通常立即满足 */
layerB = TryLayerB(base);
Log(layerB ? "Layer B unlocked (one-shot)" : "WARN: Layer B unlock failed");
}
Log("one-shot patching complete%s", wroteA ? "" : " (Layer A skipped: no blob)");
#if defined(WININET_WATCHDOG) || defined(PROBE_NOMAINTAIN)
/* 可选看门狗(**默认关闭**;-DWININET_WATCHDOG=1 启用):稳态零写入,仅在检测到
漂移(引擎重扫重置 c38/chkstr/记录,或 T0 重加锁)时按需修复。
-DPROBE_NOMAINTAIN 构建仅观测不改写(诊断用,见 §4.7.4 漂移实验)。 */
{
static ULONG64 last38 = 0, lastck = 0, lastgate = 0, lastp12 = 0;
static unsigned int lastr0 = 0xFFFFFFFF;
while (1) {
ULONG64 c38, gate, slot, p12;
const char *ck;
unsigned int r0;
Sleep(1000);
c38 = *(ULONG64 *)(mgr + OFF_C38);
ck = *(const char **)(mgr + OFF_CHKSTR);
r0 = *(unsigned int *)(mgr + OFF_REC);
gate = *(ULONG64 *)(mgr + 0xC58);
slot = *(ULONG64 *)(base + OFF_SLOT);
p12 = slot ? *(ULONG64 *)(slot + 0x60) : 0;
#ifdef PROBE_NOMAINTAIN
if (c38 != last38) { Log("[drift] c38=0x%llx ours=%d", c38, c38 == (ULONG64)ourBlob); last38 = c38; }
if ((ULONG64)ck != lastck) { Log("[drift] chkstr=0x%llx ours=%d", (ULONG64)ck, ck == g_chk_new_buf); lastck = (ULONG64)ck; }
if (r0 != lastr0) { Log("[drift] rec0=%u", r0); lastr0 = r0; }
if (gate != lastgate){ Log("[drift] gate0xC58=0x%llx", gate); lastgate = gate; }
if (p12 != lastp12) { Log("[drift] canaryP12=0x%llx", p12); lastp12 = p12; }
continue;
#endif
if (DO_LAYERA && wroteA && gate != 0x1FA68) {
*(ULONG64 *)(mgr + 0xC58) = 0x1FA68;
Log("re-assert About gate (drift 0x%llx)", gate);
}
if (DO_LAYERA && wroteA && r0 == 0) {
*(unsigned int *)(mgr + OFF_REC) = 1; /* rec0 */
*(unsigned int *)(mgr + OFF_REC + 4) = 1; /* LicenseType=1 */
memcpy((void *)(mgr + OFF_REC + 8), g_rec_name, sizeof(g_rec_name));
memcpy((void *)(mgr + OFF_REC + 1386), g_rec_digest, sizeof(g_rec_digest));
Log("re-assert record (engine cleared it)");
}
if (DO_LAYERA && DO_C38WRITE && wroteA && haveBlob && c38 != (ULONG64)ourBlob) {
*(ULONG64 *)(mgr + OFF_C38) = (ULONG64)ourBlob;
*(ULONG64 *)(mgr + OFF_C40) = BLOB_LEN;
Log("re-assert Layer A c38 (drift 0x%llx)", c38);
}
if (DO_LAYERA && wroteA && g_chk_new_ready && ck != g_chk_new_buf) {
*(const char **)(mgr + OFF_CHKSTR) = g_chk_new_buf;
Log("re-assert chkstr' (drift 0x%llx)", (unsigned long long)ck);
}
if (DO_LAYERB && slot && p12 != kPlainMagic && p12 == kEncMagic) {
ULONG64 *PP = (ULONG64 *)slot;
int j;
for (j = 0; j < 12; j++) PP[j] = base + kRv[j];
PP[12] = kPlainMagic;
Log("re-activate canary (T0 re-locked)");
}
}
}
#endif
return 0;
}
BOOL __stdcall DllMain(HINSTANCE h, DWORD reason, LPVOID reserved)
{
(void)reserved;
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(h);
g_self = h;
g_t0 = GetTickCount64();
#ifndef PROBE_PASSIVE
CreateThread(0, 0, Worker, 0, 0, 0);
#endif
}
return TRUE;
}
wininet_proxy.def
LIBRARY wininet
EXPORTS
AppCacheCheckManifest=wininet_orig.AppCacheCheckManifest
AppCacheCloseHandle=wininet_orig.AppCacheCloseHandle
AppCacheCreateAndCommitFile=wininet_orig.AppCacheCreateAndCommitFile
AppCacheDeleteGroup=wininet_orig.AppCacheDeleteGroup
AppCacheDeleteIEGroup=wininet_orig.AppCacheDeleteIEGroup
AppCacheDuplicateHandle=wininet_orig.AppCacheDuplicateHandle
AppCacheFinalize=wininet_orig.AppCacheFinalize
AppCacheFreeDownloadList=wininet_orig.AppCacheFreeDownloadList
AppCacheFreeGroupList=wininet_orig.AppCacheFreeGroupList
AppCacheFreeIESpace=wininet_orig.AppCacheFreeIESpace
AppCacheFreeSpace=wininet_orig.AppCacheFreeSpace
AppCacheGetDownloadList=wininet_orig.AppCacheGetDownloadList
AppCacheGetFallbackUrl=wininet_orig.AppCacheGetFallbackUrl
AppCacheGetGroupList=wininet_orig.AppCacheGetGroupList
AppCacheGetIEGroupList=wininet_orig.AppCacheGetIEGroupList
AppCacheGetInfo=wininet_orig.AppCacheGetInfo
AppCacheGetManifestUrl=wininet_orig.AppCacheGetManifestUrl
AppCacheLookup=wininet_orig.AppCacheLookup
CommitUrlCacheEntryA=wininet_orig.CommitUrlCacheEntryA
CommitUrlCacheEntryBinaryBlob=wininet_orig.CommitUrlCacheEntryBinaryBlob
CommitUrlCacheEntryW=wininet_orig.CommitUrlCacheEntryW
CreateMD5SSOHash=wininet_orig.CreateMD5SSOHash
CreateUrlCacheContainerA=wininet_orig.CreateUrlCacheContainerA
CreateUrlCacheContainerW=wininet_orig.CreateUrlCacheContainerW
CreateUrlCacheEntryA=wininet_orig.CreateUrlCacheEntryA
CreateUrlCacheEntryExW=wininet_orig.CreateUrlCacheEntryExW
CreateUrlCacheEntryW=wininet_orig.CreateUrlCacheEntryW
CreateUrlCacheGroup=wininet_orig.CreateUrlCacheGroup
DeleteIE3Cache=wininet_orig.DeleteIE3Cache
DeleteUrlCacheContainerA=wininet_orig.DeleteUrlCacheContainerA
DeleteUrlCacheContainerW=wininet_orig.DeleteUrlCacheContainerW
DeleteUrlCacheEntry=wininet_orig.DeleteUrlCacheEntry
DeleteUrlCacheEntryA=wininet_orig.DeleteUrlCacheEntryA
DeleteUrlCacheEntryW=wininet_orig.DeleteUrlCacheEntryW
DeleteUrlCacheGroup=wininet_orig.DeleteUrlCacheGroup
DeleteWpadCacheForNetworks=wininet_orig.DeleteWpadCacheForNetworks
DetectAutoProxyUrl=wininet_orig.DetectAutoProxyUrl
DispatchAPICall=wininet_orig.DispatchAPICall
DllCanUnloadNow=wininet_orig.DllCanUnloadNow
DllGetClassObject=wininet_orig.DllGetClassObject
DllInstall=wininet_orig.DllInstall
DllRegisterServer=wininet_orig.DllRegisterServer
DllUnregisterServer=wininet_orig.DllUnregisterServer
FindCloseUrlCache=wininet_orig.FindCloseUrlCache
FindFirstUrlCacheContainerA=wininet_orig.FindFirstUrlCacheContainerA
FindFirstUrlCacheContainerW=wininet_orig.FindFirstUrlCacheContainerW
FindFirstUrlCacheEntryA=wininet_orig.FindFirstUrlCacheEntryA
FindFirstUrlCacheEntryExA=wininet_orig.FindFirstUrlCacheEntryExA
FindFirstUrlCacheEntryExW=wininet_orig.FindFirstUrlCacheEntryExW
FindFirstUrlCacheEntryW=wininet_orig.FindFirstUrlCacheEntryW
FindFirstUrlCacheGroup=wininet_orig.FindFirstUrlCacheGroup
FindNextUrlCacheContainerA=wininet_orig.FindNextUrlCacheContainerA
FindNextUrlCacheContainerW=wininet_orig.FindNextUrlCacheContainerW
FindNextUrlCacheEntryA=wininet_orig.FindNextUrlCacheEntryA
FindNextUrlCacheEntryExA=wininet_orig.FindNextUrlCacheEntryExA
FindNextUrlCacheEntryExW=wininet_orig.FindNextUrlCacheEntryExW
FindNextUrlCacheEntryW=wininet_orig.FindNextUrlCacheEntryW
FindNextUrlCacheGroup=wininet_orig.FindNextUrlCacheGroup
ForceNexusLookup=wininet_orig.ForceNexusLookup
ForceNexusLookupExW=wininet_orig.ForceNexusLookupExW
FreeUrlCacheSpaceA=wininet_orig.FreeUrlCacheSpaceA
FreeUrlCacheSpaceW=wininet_orig.FreeUrlCacheSpaceW
FtpCommandA=wininet_orig.FtpCommandA
FtpCommandW=wininet_orig.FtpCommandW
FtpCreateDirectoryA=wininet_orig.FtpCreateDirectoryA
FtpCreateDirectoryW=wininet_orig.FtpCreateDirectoryW
FtpDeleteFileA=wininet_orig.FtpDeleteFileA
FtpDeleteFileW=wininet_orig.FtpDeleteFileW
FtpFindFirstFileA=wininet_orig.FtpFindFirstFileA
FtpFindFirstFileW=wininet_orig.FtpFindFirstFileW
FtpGetCurrentDirectoryA=wininet_orig.FtpGetCurrentDirectoryA
FtpGetCurrentDirectoryW=wininet_orig.FtpGetCurrentDirectoryW
FtpGetFileA=wininet_orig.FtpGetFileA
FtpGetFileEx=wininet_orig.FtpGetFileEx
FtpGetFileSize=wininet_orig.FtpGetFileSize
FtpGetFileW=wininet_orig.FtpGetFileW
FtpOpenFileA=wininet_orig.FtpOpenFileA
FtpOpenFileW=wininet_orig.FtpOpenFileW
FtpPutFileA=wininet_orig.FtpPutFileA
FtpPutFileEx=wininet_orig.FtpPutFileEx
FtpPutFileW=wininet_orig.FtpPutFileW
FtpRemoveDirectoryA=wininet_orig.FtpRemoveDirectoryA
FtpRemoveDirectoryW=wininet_orig.FtpRemoveDirectoryW
FtpRenameFileA=wininet_orig.FtpRenameFileA
FtpRenameFileW=wininet_orig.FtpRenameFileW
FtpSetCurrentDirectoryA=wininet_orig.FtpSetCurrentDirectoryA
FtpSetCurrentDirectoryW=wininet_orig.FtpSetCurrentDirectoryW
GetProxyDllInfo=wininet_orig.GetProxyDllInfo
GetUrlCacheConfigInfoA=wininet_orig.GetUrlCacheConfigInfoA
GetUrlCacheConfigInfoW=wininet_orig.GetUrlCacheConfigInfoW
GetUrlCacheEntryBinaryBlob=wininet_orig.GetUrlCacheEntryBinaryBlob
GetUrlCacheEntryInfoA=wininet_orig.GetUrlCacheEntryInfoA
GetUrlCacheEntryInfoExA=wininet_orig.GetUrlCacheEntryInfoExA
GetUrlCacheEntryInfoExW=wininet_orig.GetUrlCacheEntryInfoExW
GetUrlCacheEntryInfoW=wininet_orig.GetUrlCacheEntryInfoW
GetUrlCacheGroupAttributeA=wininet_orig.GetUrlCacheGroupAttributeA
GetUrlCacheGroupAttributeW=wininet_orig.GetUrlCacheGroupAttributeW
GetUrlCacheHeaderData=wininet_orig.GetUrlCacheHeaderData
GopherCreateLocatorA=wininet_orig.GopherCreateLocatorA
GopherCreateLocatorW=wininet_orig.GopherCreateLocatorW
GopherFindFirstFileA=wininet_orig.GopherFindFirstFileA
GopherFindFirstFileW=wininet_orig.GopherFindFirstFileW
GopherGetAttributeA=wininet_orig.GopherGetAttributeA
GopherGetAttributeW=wininet_orig.GopherGetAttributeW
GopherGetLocatorTypeA=wininet_orig.GopherGetLocatorTypeA
GopherGetLocatorTypeW=wininet_orig.GopherGetLocatorTypeW
GopherOpenFileA=wininet_orig.GopherOpenFileA
GopherOpenFileW=wininet_orig.GopherOpenFileW
HttpAddRequestHeadersA=wininet_orig.HttpAddRequestHeadersA
HttpAddRequestHeadersW=wininet_orig.HttpAddRequestHeadersW
HttpCheckDavCompliance=wininet_orig.HttpCheckDavCompliance
HttpCloseDependencyHandle=wininet_orig.HttpCloseDependencyHandle
HttpDuplicateDependencyHandle=wininet_orig.HttpDuplicateDependencyHandle
HttpEndRequestA=wininet_orig.HttpEndRequestA
HttpEndRequestW=wininet_orig.HttpEndRequestW
HttpGetServerCredentials=wininet_orig.HttpGetServerCredentials
HttpGetTunnelSocket=wininet_orig.HttpGetTunnelSocket
HttpIndicatePageLoadComplete=wininet_orig.HttpIndicatePageLoadComplete
HttpIsHostHstsEnabled=wininet_orig.HttpIsHostHstsEnabled
HttpOpenDependencyHandle=wininet_orig.HttpOpenDependencyHandle
HttpOpenRequestA=wininet_orig.HttpOpenRequestA
HttpPushClose=wininet_orig.HttpPushClose
HttpPushEnable=wininet_orig.HttpPushEnable
HttpPushWait=wininet_orig.HttpPushWait
HttpQueryInfoA=wininet_orig.HttpQueryInfoA
HttpSendRequestA=wininet_orig.HttpSendRequestA
HttpSendRequestExA=wininet_orig.HttpSendRequestExA
HttpSendRequestExW=wininet_orig.HttpSendRequestExW
HttpWebSocketClose=wininet_orig.HttpWebSocketClose
HttpWebSocketCompleteUpgrade=wininet_orig.HttpWebSocketCompleteUpgrade
HttpWebSocketQueryCloseStatus=wininet_orig.HttpWebSocketQueryCloseStatus
HttpWebSocketReceive=wininet_orig.HttpWebSocketReceive
HttpWebSocketSend=wininet_orig.HttpWebSocketSend
HttpWebSocketShutdown=wininet_orig.HttpWebSocketShutdown
IncrementUrlCacheHeaderData=wininet_orig.IncrementUrlCacheHeaderData
InternetAlgIdToStringA=wininet_orig.InternetAlgIdToStringA
InternetAlgIdToStringW=wininet_orig.InternetAlgIdToStringW
InternetAttemptConnect=wininet_orig.InternetAttemptConnect
InternetAutodial=wininet_orig.InternetAutodial
InternetAutodialCallback=wininet_orig.InternetAutodialCallback
InternetAutodialHangup=wininet_orig.InternetAutodialHangup
InternetCanonicalizeUrlA=wininet_orig.InternetCanonicalizeUrlA
InternetCanonicalizeUrlW=wininet_orig.InternetCanonicalizeUrlW
InternetCheckConnectionA=wininet_orig.InternetCheckConnectionA
InternetCheckConnectionW=wininet_orig.InternetCheckConnectionW
InternetClearAllPerSiteCookieDecisions=wininet_orig.InternetClearAllPerSiteCookieDecisions
InternetCombineUrlA=wininet_orig.InternetCombineUrlA
InternetCombineUrlW=wininet_orig.InternetCombineUrlW
InternetConfirmZoneCrossing=wininet_orig.InternetConfirmZoneCrossing
InternetConfirmZoneCrossingA=wininet_orig.InternetConfirmZoneCrossingA
InternetConfirmZoneCrossingW=wininet_orig.InternetConfirmZoneCrossingW
InternetConnectA=wininet_orig.InternetConnectA
InternetConvertUrlFromWireToWideChar=wininet_orig.InternetConvertUrlFromWireToWideChar
InternetCrackUrlA=wininet_orig.InternetCrackUrlA
InternetCreateUrlA=wininet_orig.InternetCreateUrlA
InternetCreateUrlW=wininet_orig.InternetCreateUrlW
InternetDial=wininet_orig.InternetDial
InternetDialA=wininet_orig.InternetDialA
InternetDialW=wininet_orig.InternetDialW
InternetEnumPerSiteCookieDecisionA=wininet_orig.InternetEnumPerSiteCookieDecisionA
InternetEnumPerSiteCookieDecisionW=wininet_orig.InternetEnumPerSiteCookieDecisionW
InternetErrorDlg=wininet_orig.InternetErrorDlg
InternetFindNextFileA=wininet_orig.InternetFindNextFileA
InternetFindNextFileW=wininet_orig.InternetFindNextFileW
InternetFortezzaCommand=wininet_orig.InternetFortezzaCommand
InternetFreeCookies=wininet_orig.InternetFreeCookies
InternetFreeProxyInfoList=wininet_orig.InternetFreeProxyInfoList
InternetGetCertByURL=wininet_orig.InternetGetCertByURL
InternetGetCertByURLA=wininet_orig.InternetGetCertByURLA
InternetGetConnectedState=wininet_orig.InternetGetConnectedState
InternetGetConnectedStateEx=wininet_orig.InternetGetConnectedStateEx
InternetGetConnectedStateExA=wininet_orig.InternetGetConnectedStateExA
InternetGetConnectedStateExW=wininet_orig.InternetGetConnectedStateExW
InternetGetCookieA=wininet_orig.InternetGetCookieA
InternetGetCookieEx2=wininet_orig.InternetGetCookieEx2
InternetGetCookieExA=wininet_orig.InternetGetCookieExA
InternetGetCookieExW=wininet_orig.InternetGetCookieExW
InternetGetCookieW=wininet_orig.InternetGetCookieW
InternetGetLastResponseInfoA=wininet_orig.InternetGetLastResponseInfoA
InternetGetLastResponseInfoW=wininet_orig.InternetGetLastResponseInfoW
InternetGetPerSiteCookieDecisionA=wininet_orig.InternetGetPerSiteCookieDecisionA
InternetGetPerSiteCookieDecisionW=wininet_orig.InternetGetPerSiteCookieDecisionW
InternetGetProxyForUrl=wininet_orig.InternetGetProxyForUrl
InternetGetSecurityInfoByURL=wininet_orig.InternetGetSecurityInfoByURL
InternetGetSecurityInfoByURLA=wininet_orig.InternetGetSecurityInfoByURLA
InternetGetSecurityInfoByURLW=wininet_orig.InternetGetSecurityInfoByURLW
InternetGoOnline=wininet_orig.InternetGoOnline
InternetGoOnlineA=wininet_orig.InternetGoOnlineA
InternetGoOnlineW=wininet_orig.InternetGoOnlineW
InternetHangUp=wininet_orig.InternetHangUp
InternetInitializeAutoProxyDll=wininet_orig.InternetInitializeAutoProxyDll
InternetLockRequestFile=wininet_orig.InternetLockRequestFile
InternetOpenA=wininet_orig.InternetOpenA
InternetOpenUrlA=wininet_orig.InternetOpenUrlA
InternetOpenUrlW=wininet_orig.InternetOpenUrlW
InternetQueryDataAvailable=wininet_orig.InternetQueryDataAvailable
InternetQueryFortezzaStatus=wininet_orig.InternetQueryFortezzaStatus
InternetQueryOptionA=wininet_orig.InternetQueryOptionA
InternetQueryOptionW=wininet_orig.InternetQueryOptionW
InternetReadFileExA=wininet_orig.InternetReadFileExA
InternetReadFileExW=wininet_orig.InternetReadFileExW
InternetSecurityProtocolToStringA=wininet_orig.InternetSecurityProtocolToStringA
InternetSecurityProtocolToStringW=wininet_orig.InternetSecurityProtocolToStringW
InternetSetCookieA=wininet_orig.InternetSetCookieA
InternetSetCookieEx2=wininet_orig.InternetSetCookieEx2
InternetSetCookieExA=wininet_orig.InternetSetCookieExA
InternetSetCookieExW=wininet_orig.InternetSetCookieExW
InternetSetCookieW=wininet_orig.InternetSetCookieW
InternetSetDialState=wininet_orig.InternetSetDialState
InternetSetDialStateA=wininet_orig.InternetSetDialStateA
InternetSetDialStateW=wininet_orig.InternetSetDialStateW
InternetSetFilePointer=wininet_orig.InternetSetFilePointer
InternetSetOptionA=wininet_orig.InternetSetOptionA
InternetSetOptionExA=wininet_orig.InternetSetOptionExA
InternetSetOptionExW=wininet_orig.InternetSetOptionExW
InternetSetOptionW=wininet_orig.InternetSetOptionW
InternetSetPerSiteCookieDecisionA=wininet_orig.InternetSetPerSiteCookieDecisionA
InternetSetPerSiteCookieDecisionW=wininet_orig.InternetSetPerSiteCookieDecisionW
InternetSetSecureLegacyServersAppCompat=wininet_orig.InternetSetSecureLegacyServersAppCompat
InternetSetStatusCallback=wininet_orig.InternetSetStatusCallback
InternetSetStatusCallbackA=wininet_orig.InternetSetStatusCallbackA
InternetSetStatusCallbackW=wininet_orig.InternetSetStatusCallbackW
InternetShowSecurityInfoByURL=wininet_orig.InternetShowSecurityInfoByURL
InternetShowSecurityInfoByURLA=wininet_orig.InternetShowSecurityInfoByURLA
InternetShowSecurityInfoByURLW=wininet_orig.InternetShowSecurityInfoByURLW
InternetTimeFromSystemTime=wininet_orig.InternetTimeFromSystemTime
InternetTimeFromSystemTimeA=wininet_orig.InternetTimeFromSystemTimeA
InternetTimeFromSystemTimeW=wininet_orig.InternetTimeFromSystemTimeW
InternetTimeToSystemTime=wininet_orig.InternetTimeToSystemTime
InternetTimeToSystemTimeA=wininet_orig.InternetTimeToSystemTimeA
InternetTimeToSystemTimeW=wininet_orig.InternetTimeToSystemTimeW
InternetUnlockRequestFile=wininet_orig.InternetUnlockRequestFile
InternetWriteFile=wininet_orig.InternetWriteFile
InternetWriteFileExA=wininet_orig.InternetWriteFileExA
InternetWriteFileExW=wininet_orig.InternetWriteFileExW
IsHostInProxyBypassList=wininet_orig.IsHostInProxyBypassList
IsUrlCacheEntryExpiredA=wininet_orig.IsUrlCacheEntryExpiredA
IsUrlCacheEntryExpiredW=wininet_orig.IsUrlCacheEntryExpiredW
LoadUrlCacheContent=wininet_orig.LoadUrlCacheContent
ParseX509EncodedCertificateForListBoxEntry=wininet_orig.ParseX509EncodedCertificateForListBoxEntry
PrivacyGetZonePreferenceW=wininet_orig.PrivacyGetZonePreferenceW
PrivacySetZonePreferenceW=wininet_orig.PrivacySetZonePreferenceW
ReadUrlCacheEntryStream=wininet_orig.ReadUrlCacheEntryStream
ReadUrlCacheEntryStreamEx=wininet_orig.ReadUrlCacheEntryStreamEx
RegisterUrlCacheNotification=wininet_orig.RegisterUrlCacheNotification
ResumeSuspendedDownload=wininet_orig.ResumeSuspendedDownload
RetrieveUrlCacheEntryFileA=wininet_orig.RetrieveUrlCacheEntryFileA
RetrieveUrlCacheEntryFileW=wininet_orig.RetrieveUrlCacheEntryFileW
RetrieveUrlCacheEntryStreamA=wininet_orig.RetrieveUrlCacheEntryStreamA
RetrieveUrlCacheEntryStreamW=wininet_orig.RetrieveUrlCacheEntryStreamW
RunOnceUrlCache=wininet_orig.RunOnceUrlCache
SetUrlCacheConfigInfoA=wininet_orig.SetUrlCacheConfigInfoA
SetUrlCacheConfigInfoW=wininet_orig.SetUrlCacheConfigInfoW
SetUrlCacheEntryGroup=wininet_orig.SetUrlCacheEntryGroup
SetUrlCacheEntryGroupA=wininet_orig.SetUrlCacheEntryGroupA
SetUrlCacheEntryGroupW=wininet_orig.SetUrlCacheEntryGroupW
SetUrlCacheEntryInfoA=wininet_orig.SetUrlCacheEntryInfoA
SetUrlCacheEntryInfoW=wininet_orig.SetUrlCacheEntryInfoW
SetUrlCacheGroupAttributeA=wininet_orig.SetUrlCacheGroupAttributeA
SetUrlCacheGroupAttributeW=wininet_orig.SetUrlCacheGroupAttributeW
SetUrlCacheHeaderData=wininet_orig.SetUrlCacheHeaderData
ShowCertificate=wininet_orig.ShowCertificate
ShowClientAuthCerts=wininet_orig.ShowClientAuthCerts
ShowSecurityInfo=wininet_orig.ShowSecurityInfo
ShowX509EncodedCertificate=wininet_orig.ShowX509EncodedCertificate
UnlockUrlCacheEntryFile=wininet_orig.UnlockUrlCacheEntryFile
UnlockUrlCacheEntryFileA=wininet_orig.UnlockUrlCacheEntryFileA
UnlockUrlCacheEntryFileW=wininet_orig.UnlockUrlCacheEntryFileW
UnlockUrlCacheEntryStream=wininet_orig.UnlockUrlCacheEntryStream
UpdateUrlCacheContentPath=wininet_orig.UpdateUrlCacheContentPath
UrlCacheCheckEntriesExist=wininet_orig.UrlCacheCheckEntriesExist
UrlCacheCloseEntryHandle=wininet_orig.UrlCacheCloseEntryHandle
UrlCacheContainerSetEntryMaximumAge=wininet_orig.UrlCacheContainerSetEntryMaximumAge
UrlCacheCreateContainer=wininet_orig.UrlCacheCreateContainer
UrlCacheFindFirstEntry=wininet_orig.UrlCacheFindFirstEntry
UrlCacheFindNextEntry=wininet_orig.UrlCacheFindNextEntry
UrlCacheFreeEntryInfo=wininet_orig.UrlCacheFreeEntryInfo
UrlCacheFreeGlobalSpace=wininet_orig.UrlCacheFreeGlobalSpace
UrlCacheGetContentPaths=wininet_orig.UrlCacheGetContentPaths
UrlCacheGetEntryInfo=wininet_orig.UrlCacheGetEntryInfo
UrlCacheGetGlobalCacheSize=wininet_orig.UrlCacheGetGlobalCacheSize
UrlCacheGetGlobalLimit=wininet_orig.UrlCacheGetGlobalLimit
UrlCacheReadEntryStream=wininet_orig.UrlCacheReadEntryStream
UrlCacheReloadSettings=wininet_orig.UrlCacheReloadSettings
UrlCacheRetrieveEntryFile=wininet_orig.UrlCacheRetrieveEntryFile
UrlCacheRetrieveEntryStream=wininet_orig.UrlCacheRetrieveEntryStream
UrlCacheServer=wininet_orig.UrlCacheServer
UrlCacheSetGlobalLimit=wininet_orig.UrlCacheSetGlobalLimit
UrlCacheUpdateEntryExtraData=wininet_orig.UrlCacheUpdateEntryExtraData
UrlZonesDetach=wininet_orig.UrlZonesDetach
_GetFileExtensionFromUrl=wininet_orig._GetFileExtensionFromUrl
he_blob_default.h
// 自动生成,勿手改:wininet_proxy.c 的编译期内置公钥数据
// = XOR 索引加密(enc[i] = plain[i] ^ i)的 RSA-4096 SPKI DER,548 字节
// 来源:内置公钥常量
// 生成:python helium_license.py blobheader
// ⚠ 公钥变更后需重新生成此文件并**重新编译** wininet.dll
#ifndef HE_BLOB_DEFAULT_H
#define HE_BLOB_DEFAULT_H
static const unsigned char kDefaultBlob[548] = {
0x30, 0x83, 0x00, 0x23, 0x34, 0x08, 0x00, 0x0E, 0x22, 0x8F, 0x42, 0x8D, 0xFB, 0x00, 0x0F, 0x0E,
0x11, 0x14, 0x12, 0x10, 0x96, 0x17, 0x1B, 0x17, 0x28, 0x9B, 0x18, 0x13, 0x1E, 0x9F, 0x1C, 0x1E,
0x20, 0x83, 0xF5, 0xF6, 0x4B, 0x3A, 0xDB, 0x9A, 0x22, 0xBF, 0x8C, 0xBD, 0xFF, 0x2B, 0x2E, 0x66,
0x60, 0xB9, 0x89, 0x6F, 0x65, 0x73, 0xCB, 0x71, 0x61, 0x11, 0x08, 0x4F, 0x2E, 0x80, 0xEA, 0x35,
0x1F, 0x29, 0x6B, 0xD8, 0x79, 0x72, 0x46, 0x1D, 0x8F, 0xF0, 0x71, 0x23, 0x80, 0x10, 0xAB, 0xDA,
0xF4, 0xDE, 0x55, 0xC6, 0xAF, 0x53, 0x45, 0xB1, 0xCF, 0xDD, 0x9D, 0xAF, 0x07, 0xB9, 0x60, 0x39,
0xE9, 0x56, 0xA0, 0xA8, 0x10, 0x45, 0xDE, 0x3C, 0xA2, 0x26, 0x95, 0x0C, 0xF1, 0xD3, 0x29, 0xB7,
0x02, 0xC8, 0xC3, 0x8E, 0x7F, 0x6E, 0x96, 0x64, 0x65, 0xC5, 0xAE, 0x15, 0xA9, 0xEE, 0xBF, 0x50,
0x34, 0xAC, 0x51, 0xC9, 0xB8, 0x48, 0xA6, 0x8A, 0x63, 0xE9, 0x1B, 0x63, 0xD5, 0x06, 0xE9, 0x7B,
0x26, 0x1D, 0x1F, 0x73, 0x25, 0x7C, 0xB1, 0x68, 0x69, 0xB5, 0x59, 0x3F, 0x9A, 0x60, 0x9A, 0x05,
0x06, 0x1B, 0x0D, 0x18, 0x9D, 0x21, 0xAD, 0x46, 0xC4, 0x85, 0xE2, 0x0C, 0xEB, 0x57, 0x1D, 0x6B,
0x5E, 0x03, 0x96, 0xCC, 0x3D, 0x31, 0xCE, 0x81, 0x66, 0xEA, 0x3D, 0x25, 0x48, 0xEE, 0x95, 0xAF,
0xBE, 0xA6, 0xD2, 0x68, 0xD8, 0xC8, 0x9E, 0xE6, 0xB7, 0xB7, 0x34, 0x1F, 0xEE, 0xD0, 0x76, 0xBC,
0x0A, 0x92, 0xD0, 0xD8, 0xD2, 0x55, 0x33, 0x8E, 0x5E, 0xF8, 0xFB, 0x55, 0xC6, 0x72, 0x2C, 0x3F,
0xF5, 0x35, 0xA7, 0xE4, 0x1E, 0xC1, 0xC2, 0x2F, 0xFA, 0xD1, 0x80, 0xC1, 0xED, 0xB3, 0xBF, 0xC8,
0x58, 0x1C, 0x68, 0xC5, 0xE5, 0x07, 0x5C, 0xE5, 0xA4, 0x23, 0xBF, 0x39, 0x39, 0xBB, 0x19, 0x6A,
0xA8, 0x7B, 0x75, 0xAF, 0x63, 0xEB, 0xE6, 0xF8, 0x35, 0xE6, 0x14, 0x19, 0x72, 0x86, 0x86, 0x0A,
0x2C, 0xAA, 0x4C, 0x7E, 0x83, 0xE8, 0x00, 0x2F, 0xEA, 0xF3, 0x7E, 0x31, 0x34, 0xAA, 0x52, 0x4A,
0x54, 0xED, 0x62, 0xC3, 0x20, 0xC1, 0x90, 0xCD, 0xB8, 0x8B, 0x8E, 0x27, 0x2D, 0x8C, 0xF0, 0xA4,
0xFA, 0x58, 0xDE, 0xD9, 0xA5, 0x45, 0x83, 0xB2, 0xB9, 0x83, 0x6B, 0xF4, 0xFA, 0x1A, 0x56, 0x2D,
0x8B, 0xD8, 0x61, 0x9B, 0x58, 0x75, 0x20, 0xD0, 0xF5, 0x2C, 0xBE, 0x76, 0x50, 0x74, 0x46, 0x62,
0x35, 0xBC, 0xD2, 0xE0, 0x35, 0x09, 0xD5, 0xC3, 0x72, 0xF6, 0x77, 0x70, 0x61, 0x79, 0xB8, 0x8C,
0x75, 0x93, 0x91, 0xAB, 0x76, 0xD3, 0x01, 0xAA, 0xED, 0xA6, 0x47, 0x77, 0x76, 0x3F, 0x43, 0x1D,
0x4D, 0x20, 0xAD, 0x33, 0x7E, 0x00, 0x36, 0x01, 0xEE, 0x57, 0xDD, 0x68, 0x14, 0x75, 0x71, 0x59,
0xBE, 0x58, 0x70, 0x0E, 0xBF, 0x7B, 0x12, 0x85, 0x72, 0xC9, 0xCD, 0xC0, 0xF8, 0xCB, 0x2B, 0x7D,
0x44, 0xE7, 0x4B, 0xA8, 0x0A, 0x42, 0xBB, 0xB2, 0x73, 0xE3, 0xA1, 0x1E, 0x35, 0xF7, 0xB9, 0xA9,
0x86, 0xBF, 0x22, 0xD4, 0x9D, 0xA0, 0xBE, 0xC1, 0x51, 0xBB, 0x99, 0xF9, 0x88, 0xB0, 0x33, 0xBA,
0xDE, 0x6C, 0xCF, 0xE1, 0xE1, 0xEF, 0x6B, 0xBE, 0x8D, 0x64, 0xD3, 0xB5, 0x9D, 0x57, 0x1B, 0xA6,
0xE9, 0x87, 0xD2, 0xE8, 0x2B, 0xAC, 0xD6, 0x5C, 0x37, 0x9E, 0x94, 0xFE, 0x66, 0x2E, 0xAD, 0x52,
0xEF, 0xEA, 0x71, 0x74, 0x2D, 0x0E, 0xF8, 0x32, 0xA6, 0xEB, 0x53, 0x38, 0x51, 0xC6, 0xD0, 0x6F,
0x0A, 0xE6, 0x69, 0xA1, 0x66, 0xB3, 0x0B, 0xE6, 0x64, 0xDA, 0x77, 0x22, 0x58, 0x81, 0x0B, 0x75,
0x3F, 0x24, 0x93, 0x43, 0xF7, 0xAC, 0xEB, 0x15, 0xF2, 0x03, 0x6A, 0x77, 0xB4, 0x46, 0xDF, 0x1F,
0x5D, 0xE2, 0x86, 0xA2, 0x7C, 0xC9, 0xCD, 0x36, 0x36, 0x85, 0xF2, 0x07, 0x04, 0x5B, 0xAC, 0x44,
0x88, 0x24, 0x47, 0x3B, 0x59, 0xD8, 0x75, 0x0E, 0x94, 0x0D, 0x0A, 0x21, 0x27, 0x4B, 0xFD, 0x72,
0x99, 0x23, 0x23, 0x32,
};
#endif // HE_BLOB_DEFAULT_H
helium_license.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Helium Hex Editor 许可工具链(统一入口,自包含:内置 RSA-4096 密钥,无需外部 pem/bin)
逆向依据:HeliumHexEditor_许可机制分析.md §2/§3/§9.10/§9.12/§9.14
两种注册方式(务必先看,选其一):
① 文件直写 issue(推荐,稳定):用内置私钥签发 .lic + .sig 直写 HeliumLicenses\\,
并同步生成 mgr 重定向 blob。程序双击启动即已注册态,绕开注册对话框的编码损失。
② 注册码手动粘贴 regcode(备选):生成注册对话框用注册码,
程序菜单 Help→Register 打开注册框(控件 id 1519),整段粘贴后确定。
★风险:中文系统(CP936)下注册框写 .sig 会经 WideCharToMultiByte(CP_ACP),
签名高位字节失真 → 验签失败。稳妥用法是随后再用 issue 直写刷新 HeliumLicenses\\*.sig。
子命令:
keygen 生成 RSA-4096 密钥对(private.pem / public.bin),可 --exponent 复刻厂商参数
issue 签发许可:生成 .lic + .sig 直写 HeliumLicenses\\,并输出 mgr 重定向 blob
regcode 生成注册对话框用注册码(Blowfish-OFB;SigData=签名 hex)
blob 仅生成 mgr+0xC38 重定向用的 XOR 加密公钥 blob(poc2_blob.bin 格式)
verify 本地复验:用公钥验证已有 .lic/.sig(模拟 mgr 的验签逻辑)
selfcheck 自检:内置私钥 / 内置公钥 / he_blob.bin / wininet_proxy_blob.h 四者公钥是否一致
密钥来源:默认全部使用内置密钥;若显式给出 --private/--public 路径则优先用文件(向后兼容)。
关键逆向事实(全部动态/静态实证):
- .lic = UTF-16LE 文本([Signature] 之前的部分,INI 格式),mgr 验签的消息即其磁盘字节
- .sig = 512 字节裸二进制签名(RSASSA-PKCS1v15-SHA1),无任何编码层
- 注册码 = HEX( Blowfish-OFB( 明文UTF-16LE, KEY, IV ) )
KEY = b"0x%I64X\\0" + b"0x%08X\\0" + b"%u\\0" (18B, Blowfish 可变长全用)
IV = b"kernel32" (8B = 块长)
- mgr 公钥 blob:XOR 索引加密的 SPKI DER,plain[i] = enc[i] ^ i(sub_18009C3B0)
- LicenseType 绑定语义(sub_1800A1780,运行时实测):
1/5/8 不绑定机器(8 在程序已注册态会失败)
2/6/9 绑定用户名:机器块 +0 = "COMPUTER\\用户名" 取末段,与 lic.Name 比对
3 绑定路径首段:机器块 +0x200 = "\\COMPUTERNAME" 首段为空串 → lic.ComputerName 留空即可
4/7/10 绑定三元组:ComputerName("\\COMPUTERNAME") + Processor(CPU 全名) + ID(机器 GUID)
11 加载后强制置 0(恒失败)
附加:机器块 +0x964 非 0 且 type∈[5,10] 失败;注册状态字非 0 且 type∈[8,10] 失败
"""
import argparse
import base64
import hashlib
import os
import re
import sys
# 输出强制 UTF-8:GUI 以 utf-8 解码子进程管道输出,避免 GBK 代码页机器上乱码
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8")
from Crypto.Cipher import Blowfish
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA1
# ---------------- 常量(逆向实证) ----------------
BF_KEY = b"0x%I64X\x00" + b"0x%08X\x00" + b"%u\x00" # 18 字节,[rbp+0x80]
BF_IV = b"kernel32" # 8 字节
MODULUS_BITS = 4096
SIG_SIZE = MODULUS_BITS // 8 # 512
# ---------------- 内置密钥(自包含:脚本本身即可签发/验签,另一台电脑只需本文件) ----------------
# 与项目 he_private.pem / he_public.bin / he_blob.bin / wininet_proxy_blob.h 的公钥同一把:
# SHA1(SPKI DER) = 678108305f0f144b677c49bd3ddc2271891586c4
# 注意:务必与部署的 wininet.dll(he_blob.bin)保持一致,否则验签失败——见 selfcheck。
EMBEDDED_PRIVATE_PEM = """-----BEGIN RSA PRIVATE KEY-----
MIIJJQIBAAKCAgEAotfVbx/9vQqWppbTBgBJUIi7XFFG/UZZKDJ0Er3UCl9oKZs9
NwBax7k7aMxd5ZWkjweV+wYT5peEx/Rb5D5miTfCy3QguFvKT/9nnb5H2HK5sf0L
G+ATHbzUbtWTwS+0LdNKPM0gDetgkehZi2f0toyN4LHpJ//xLMOkBv0Emqa6r7s5
hAvhbCxIp0f6s8TusiR/iYR4Nt5Th570UysQfmcQqxwNWCF/fv7UIh24c9pDAgsG
gOVZhiEhjhqv8uAV1EUH+iQkyBI4aioBXlEnqO2aNhHyqhJc2kXCxUbnlah6d6xn
7uD/Pe8eEn6LiAU8u15tl/0WOPLqZCoot0xVdMxA4ATktuqQoqQMAaHei8pp7OqR
cLWFgbpRz8YnaBLLmSPYHDBml71l9D0cOQgtZe2As2Fcg5Qqry0rPSTm0xXy88gS
tmfNhc8tHBpSLXI9Ud9ACnVAdpYupxNoCA8mPtnyjTv+lAL6QEdLdEal8tR22Tue
1y0l63o7halqJzYmHoB3OQUYZvkSM1IkHZ0Vbt19UlVa3Qk13WkOIeqlGSlGECvv
aRCb/1deNarjY50/O6On+dsu5X4yieONGw6w6geLQoJW7QGMM53JtGzlms/VYbAD
WR3iCvqQjEi7IeBd44SheMzLMT6M+AwIVqJLmDVVKE3NYxmMFBA6O1bjbbkCAREC
ggIAAmUObs+HfwProJFG3jS1yLa3dhBBC0EI1y3rHmSsh66UWvb9IrDyRrOqSRIQ
cI1+p8AcjvxTHb52qJZqw1tFRchZc+7iXRBon0tJDZ0mtB/OBmHWwGjxOOhsiSjJ
s8g7Hsqi+V1elsr9p8ryQgzSqFTj+AKdyB4d5k4r0u0h8zcoZHd1jTxo9kvyAnXD
r9H0c4+YdrprLfu9drNHW5N08szPTc94qrUy0O0oxEQzp1nMR484kPRQYBcTUSYK
Hey1BuLlw621Py1xxVNw9hfIHNZRU55hr61yToEGoQTOsyRcJ2gutHQ//SVmFtw2
wgATt3YYMy0iNYsaCQCet2bFBXVuxC8OrdgElnRewWHk48OC0i+Vl33/PF4bLO2s
+N/94x5i7MilHbrs5I9rcw4waWiHxyId2Ec+5tZ8d0OEMrXokoMNgkpBC2+f+YbP
OxkfGib9Xm+uwGS282ks1ENcqVaaBCPKC75o3O5FBqkUctvVCS+YeDzdH7mVD79X
ypoPYwbUniWLsc0EXPr+e49XI7yeTDhXe7FD2WaBYRlxvgQMNsr/UQZCUkuXygnL
hv2wcaeoLAeGOA2zVXxJchrwaG8sknRdDWPk13iY0vHlUX4MmQZl/mzhCnttTGik
DTqCorOQyQZSNKIbe9U2fi1L+3ZWrlm2f9B4nuEbA0KmrpECggEBALbiDAa5WpsO
vu5EqvZ/e3QwrSk+qtzd6Aspg0huGEdF7zpnU1G7m6lOlF8gQ9BIxWxtadlfqw3s
jtT+ZnFkwP6UtVr/Cv73MEpkyrtE/vf4pKCMO8nm/4s3gvWP22FK08okcrTy/QTW
XCG3nLdcnhPfjXsHrqcQTBKt08az9odnW4pfXkeoZ7afBI1lK11nrNN+8Ls09PJ3
NO4VTyLIgJBFjvfmacgKvjxqtzPS1FiKckWgZGN8SmA8cqFcUclGrLKudcAJCL7X
2gFiE7lDdjbSP2rlupvKrnZ3zAe8nTCCIILqR/c5v2wp9geFND5XsTg8yP3Brp7A
OT7HOgtsIqkCggEBAOPyt35FLu5J7Myb+KqSkGd5ax2WiGDbkK/wXVvHNEwVVdcp
45LPFm0qqcVxZXMIoL0CZHgPZZmIz18dKMRkmcWXU519D6+PS95KCZABe84BuuNm
uQDuXEpefC3vysk+cA7lAQVgeGpJNNs+EqXa0lwA2xwXOFtZMhv1gw9kX8KodSWd
2MRUxM1KfBkmICyIQ+/yIqLpDQC46UCbCAWmH4XB80g3eZ7zZQah090As+HWmNVb
43gLt3pTYj6ueh2Of80F5dmWVbnrHZPPmHK4fx2qfJIGCZZQk7sQdHVrASUoi09B
yye7dsMWHWIJnlvFG44SD3uRwRrBpwZGI5cw7JECggEAS04E87W75X6K2pS+3fhB
5IyDiXQoPNPYBJifeC1VSoY1VEip1lxPJ5jTrrLuvy0F/3hYwusoUQcNov9XW94x
WcTDJXgTlimMWt41XCt4C8C8QhugJfW0hJ5jGdHSzbVmRC0gLGQN1NC8hlqp8SYi
+R/Q2E51F51MgCl1UdGhv0itOPoXwyc5w6rjwcA/CFfdwH+QTRXORbidUvm3LHCt
aJUcwG3vUl7GzZVafsA5UaJrSdihzpyXGJFcYI9s+IaDWKIwfD/0itFZxFWPpqNO
y0eDhl6YIggpuE9yIU2qIwhnvW+HC3IhpQI4ITbZRtjQgJF/8AR1FDEIg0L5yHfS
CQKCAQAoOeQlV4DAo6JCOaRaVhl7rAPYC4F6gRmIdbYfQUV21patf9zdrBMENLSM
QTAFPcIDPKhRbCD8+gZ6MlJ9ArG5dQ7BcGwuCjqQo6dVpeir4jAKAxGWwKbf8o5i
dZxBoZtN7C1bTUJtG/pEzrf/Jp2Xw+puXnNbahfm0PkCt1wxaQWdOfkTln0VK0MT
fzLarqKTwVFoCwJLa+ztSIjx8COuMUkMvn7f0Jlbhfg2Dy7NgDkWl7661OQkpU2S
l0K56/hveYLqKZaoVqrd2VcjTbwUS0Mo0+OS/ykhAudgA9MGjq8sGqthXVExfF+J
xXZMbhPr5RHKgyITx9IuSJzecguhAoIBAENCDamRfp7HHtngQXvDDrOasBzioiMB
VnGx685eAKc9qaccEPEU+lecqYEIRwRwuOBoplSfquhUlfjB2bV+916h3Xmk9dyN
ROcwp2EaSVdK+1ZpomEy69nlr5pdckME15oYISuLTzcqS74x3Gbd78+aoJj69sb6
KLtLlmuzeI75DLI540p0OwfjB4tOH7UxSkRF+l9kYkxextv3zL2dxZFmJMK7HGXB
SqDltUAj0K87gzc4WyJlMlYPJ3lHjeBLE8ZM/sWGI1FTOGwqjN5Y9bt2lG15l2vI
D55qYW/yrPWCyG26CW4HuEsg3hV797EK+e29jgw+jLGcPlfHDjb342U=
-----END RSA PRIVATE KEY-----
"""
EMBEDDED_PUBLIC_DER_B64 = (
"MIICIDANBgkqhkiG9w0BAQEFAAOCAg0AMIICCAKCAgEAotfVbx/9vQqWppbTBgBJUIi7XFFG/UZZKDJ0"
"Er3UCl9oKZs9NwBax7k7aMxd5ZWkjweV+wYT5peEx/Rb5D5miTfCy3QguFvKT/9nnb5H2HK5sf0LG+A"
"THbzUbtWTwS+0LdNKPM0gDetgkehZi2f0toyN4LHpJ//xLMOkBv0Emqa6r7s5hAvhbCxIp0f6s8TusiR"
"/iYR4Nt5Th570UysQfmcQqxwNWCF/fv7UIh24c9pDAgsGgOVZhiEhjhqv8uAV1EUH+iQkyBI4aioBXlE"
"nqO2aNhHyqhJc2kXCxUbnlah6d6xn7uD/Pe8eEn6LiAU8u15tl/0WOPLqZCoot0xVdMxA4ATktuqQoqQ"
"MAaHei8pp7OqRcLWFgbpRz8YnaBLLmSPYHDBml71l9D0cOQgtZe2As2Fcg5Qqry0rPSTm0xXy88gStmf"
"Nhc8tHBpSLXI9Ud9ACnVAdpYupxNoCA8mPtnyjTv+lAL6QEdLdEal8tR22Tue1y0l63o7halqJzYmHoB"
"3OQUYZvkSM1IkHZ0Vbt19UlVa3Qk13WkOIeqlGSlGECvvaRCb/1deNarjY50/O6On+dsu5X4yieONGw6"
"w6geLQoJW7QGMM53JtGzlms/VYbADWR3iCvqQjEi7IeBd44SheMzLMT6M+AwIVqJLmDVVKE3NYxmMFBA"
"6O1bjbbkCARE="
)
def embedded_public_der():
"""内置公钥 DER(SPKI,548B)。"""
return base64.b64decode(EMBEDDED_PUBLIC_DER_B64)
def load_private_key(path=""):
"""path 非空且存在则读文件,否则用内置私钥(自包含)。"""
if path and os.path.exists(path):
return RSA.import_key(open(path, "rb").read())
return RSA.import_key(EMBEDDED_PRIVATE_PEM)
def load_public_der(path=""):
"""path 非空且存在则读文件,否则用内置公钥 DER。"""
if path and os.path.exists(path):
return open(path, "rb").read()
return embedded_public_der()
def load_public_key(path=""):
return RSA.import_key(load_public_der(path))
def build_license_text(license_type, name, computer="", processor="", uid=""):
"""构造 [Signature] 之前的明文(宽字符层面;落盘为 UTF-16LE)。
字段集与 .lic INI 解析器 sub_1800A0DB0 的键一致。"""
lines = [
"[Info]",
"LicenseType=%d" % license_type,
"Name=%s" % name,
]
if license_type in (4, 7, 10):
lines.append("ComputerName=%s" % computer)
lines.append("Processor=%s" % processor)
lines.append("ID=%s" % uid)
return "\r\n".join(lines) + "\r\n"
def sign_message(privkey, lic_bytes):
"""mgr 验签语义:消息 = .lic 文件原始磁盘字节(sub_18009D190)。
RSASSA-PKCS1v15-SHA1(Crypto++ DigestInfo 前缀 sub_1802DD0F0)。"""
h = SHA1.new(lic_bytes)
sig = pkcs1_15.new(privkey).sign(h)
assert len(sig) == SIG_SIZE
return sig
def make_lic_and_sig(priv, license_type, name, computer="", processor="", uid=""):
"""返回 (.lic 磁盘字节[含 UTF-16LE BOM], 512B 裸签名)。供 issue 与 GUI 复用。
UTF-16LE BOM 必须带:mgr 用 GetPrivateProfileStringW 解析,无 BOM 会按 ANSI 读导致键值丢失。"""
text = build_license_text(license_type, name, computer, processor, uid)
lic_bytes = b"\xff\xfe" + text.encode("utf-16-le")
return lic_bytes, sign_message(priv, lic_bytes)
def make_regcode(license_type, name, sig, computer="", processor="", uid=""):
"""注册码 = 大写 HEX( Blowfish-OFB( 明文UTF-16LE, BF_KEY, BF_IV ) )。
sig 为 512B 签名(十六进制写入 SigData)。供 regcode 子命令与 GUI 复用。"""
text = build_license_text(license_type, name, computer, processor, uid)
plain = (text + "[Signature]\r\nSigData=" + sig.hex() + "\r\n").encode("utf-16-le")
enc = Blowfish.new(BF_KEY, Blowfish.MODE_OFB, BF_IV).encrypt(plain)
return enc.hex().upper()
TYPE_MEANING = {
1: "Pro Portable(不绑定)", 2: "绑定用户名", 3: "绑定路径首段",
4: "Pro Single Computer(硬件三元组)", 5: "Pro Portable(不绑定)",
6: "绑定用户名", 7: "Pro Single Computer(硬件三元组)",
8: "不绑定(程序已注册态会失败)", 9: "绑定用户名",
10: "Pro Single Computer(硬件三元组)", 11: "无效(程序加载时强制作废)",
}
def parse_license(lic_bytes):
"""解析 .lic(UTF-16LE INI,[Signature] 之前的部分),返回字段 dict。"""
txt = lic_bytes.decode("utf-16-le", errors="replace").split("[Signature]")[0]
fields = {}
for line in txt.splitlines():
line = line.strip()
if line.startswith("[") or "=" not in line:
continue
k, v = line.split("=", 1)
fields[k.strip()] = v.strip()
return fields
def describe_license(lic_bytes):
"""返回 (字段 dict, 类型 int, 人话描述行列表),供 verify/issue 输出。"""
f = parse_license(lic_bytes)
try:
t = int(f.get("LicenseType", "0"))
except ValueError:
t = 0
meaning = TYPE_MEANING.get(t, "未知类型(程序绑定校验将失败)")
rows = ["LicenseType=%d (%s)" % (t, meaning), "Name=%s" % f.get("Name", "")]
if t in (4, 7, 10):
rows.append("ComputerName=%s" % f.get("ComputerName", ""))
rows.append("Processor=%s" % f.get("Processor", ""))
rows.append("ID=%s" % f.get("ID", ""))
return f, t, rows
# ---------------- 子命令 ----------------
def cmd_keygen(args):
"""RSA-4096 密钥对。指数默认 65537(Crypto++ verifier 从 DER 读 e,任意素数可),
如需复刻厂商参数可 --exponent 17。"""
key = RSA.generate(MODULUS_BITS, e=args.exponent)
priv_pem = key.export_key()
pub_der = key.publickey().export_key(format="DER") # X.509 SubjectPublicKeyInfo
with open(args.private, "wb") as f:
f.write(priv_pem)
with open(args.public, "wb") as f:
f.write(pub_der)
print("[+] private -> %s (%d B)" % (args.private, len(priv_pem)))
print("[+] public -> %s (%d B SPKI DER)" % (args.public, len(pub_der)))
print("[+] modulus bits=%d e=%d" % (key.n.bit_length(), key.e))
def xor_index_blob(pub_der):
"""mgr+0xC38 blob 编码:enc[i] = plain[i] ^ i(sub_18009C3B0 解密例程的逆)。"""
return bytes((b ^ i) & 0xFF for i, b in enumerate(pub_der))
def cmd_blob(args):
pub_der = load_public_der(args.public)
blob = xor_index_blob(pub_der)
with open(args.out, "wb") as f:
f.write(blob)
print("[+] mgr blob -> %s (%d B;mgr_redirect.ps1 将重定向 mgr+0xC38/0xC40 到它)" % (args.out, len(blob)))
# 自检:解密往返
dec = bytes((b ^ i) & 0xFF for i, b in enumerate(blob))
assert dec == pub_der
print("[+] xor roundtrip OK")
def cmd_blobheader(args):
"""生成 wininet_proxy.c 的**编译期内置公钥头**(默认 tools/he_blob_default.h)。
生产代理优先读同目录外置 he_blob.bin;缺失时回退到该内置数据 →
部署只需 wininet.dll 一个文件(换密钥仍可只换 he_blob.bin,无需重编译)。
**修改公钥后必须重新生成本头并重新编译 wininet.dll。**"""
der = load_public_der(args.public)
if len(der) != 548:
sys.exit("公钥 DER 长度异常:%d(期望 548,RSA-4096 SPKI)" % len(der))
blob = xor_index_blob(der)
rows = []
for i in range(0, len(blob), 16):
rows.append(" " + ", ".join("0x%02X" % b for b in blob[i:i + 16]) + ",")
src = args.public if (args.public and os.path.exists(args.public)) else "内置公钥常量"
text = (
"// 自动生成,勿手改:wininet_proxy.c 的编译期内置公钥数据\n"
"// = XOR 索引加密(enc[i] = plain[i] ^ i)的 RSA-4096 SPKI DER,548 字节\n"
"// 来源:%s\n"
"// 生成:python helium_license.py blobheader\n"
"// ⚠ 公钥变更后需重新生成此文件并**重新编译** wininet.dll\n"
"#ifndef HE_BLOB_DEFAULT_H\n#define HE_BLOB_DEFAULT_H\n\n"
"static const unsigned char kDefaultBlob[548] = {\n%s\n};\n\n"
"#endif // HE_BLOB_DEFAULT_H\n" % (src, "\n".join(rows)))
with open(args.out, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
print("[+] 内置公钥头 -> %s(%d 字节 blob;公钥 SHA1=%s)"
% (args.out, len(blob), __import__("hashlib").sha1(der).hexdigest()))
print("[!] 请重新编译 wininet.dll 使内置数据生效(编译命令见 GUI 的“显示编译命令”或分析文档 §7.3)")
def cmd_issue(args):
"""签发许可(推荐路径):直写 HeliumLicenses\\*.lic/*.sig + 输出 mgr blob。
绕开注册对话框的 WideCharToMultiByte(CP_ACP) 编码损失(sub_1800FEBA0)。
默认用内置私钥;显式 --private 文件路径则优先用文件。"""
priv = load_private_key(args.private)
# 验签消息 = .lic 磁盘全部字节(含 UTF-16LE BOM),与 mgr 的 sub_18009D190 读法一致。
lic_bytes, sig = make_lic_and_sig(priv, args.type, args.name,
args.computer, args.processor, args.uid) # 512B 裸签名
os.makedirs(args.license_dir, exist_ok=True)
lic_path = os.path.join(args.license_dir, args.basename + ".lic")
sig_path = os.path.join(args.license_dir, args.basename + ".sig")
with open(lic_path, "wb") as f:
f.write(lic_bytes)
with open(sig_path, "wb") as f:
f.write(sig)
print("[+] wrote %s (%d B)" % (lic_path, len(lic_bytes)))
print("[+] wrote %s (%d B)" % (sig_path, len(sig)))
# 签发自检 + 许可内容回显
lic_check = open(lic_path, "rb").read()
sig_check = open(sig_path, "rb").read()
fields, ltype, rows = describe_license(lic_check)
print("[+] ---- 许可内容 ----")
for r in rows:
print(" " + r)
if ltype in (2, 6, 9):
print("[!] 类型 %d 绑定用户名:Name 应为目标机器的登录用户名" % ltype)
if ltype in (4, 7, 10):
print("[!] 类型 %d 绑定三元组:ComputerName 需带前导反斜杠、ID 需为机器 GUID" % ltype)
try:
pkcs1_15.new(priv).verify(SHA1.new(lic_check), sig_check)
print("[+] 自检 SIGNATURE VALID(磁盘字节可被验签)")
except (ValueError, TypeError):
print("[-] 自检 SIGNATURE INVALID(异常,请检查磁盘写入)")
# mgr 公钥 blob(供 mgr_redirect.ps1)
pub_der = priv.publickey().export_key(format="DER")
blob = xor_index_blob(pub_der)
with open(args.blob, "wb") as f:
f.write(blob)
print("[+] mgr blob -> %s (%d B)" % (args.blob, len(blob)))
# 顺带输出同内容注册码(UI 路径;SigData=磁盘 .sig 的 hex,中文系统 .sig 会失真,仅供对照)
if args.regcode:
code = make_regcode(args.type, args.name, sig,
args.computer, args.processor, args.uid)
with open(args.regcode, "w") as f:
f.write(code)
print("[+] regcode -> %s (%d hex chars) [注意:UI 路径 .sig 经 CP_ACP,中文系统失真]" % (args.regcode, len(code)))
def cmd_regcode(args):
"""生成注册对话框用注册码(Blowfish-OFB,见分析文档 §1.1)。
签名来源优先级:`--sig HEX` > `--lic 文件`(用私钥签)> **缺省自签**(用内置/指定私钥
对本次 LicenseType/Name 现签一个 .lic 并签名)——故单文件即可独立生成注册码。
注意:中文系统(CP936)下注册对话框写 .sig 走 WideCharToMultiByte(CP_ACP) 会失真。"""
if args.sig:
sig = bytes.fromhex(args.sig)
elif args.lic:
priv = load_private_key(args.private)
sig = sign_message(priv, open(args.lic, "rb").read())
else:
priv = load_private_key(args.private)
lic_bytes, sig = make_lic_and_sig(priv, args.type, args.name,
args.computer, args.processor, args.uid)
print("[.] 未提供 --sig/--lic:已用内置私钥现签一份许可(%d B)" % len(lic_bytes))
code = make_regcode(args.type, args.name, sig,
args.computer, args.processor, args.uid)
with open(args.out, "w") as f:
f.write(code)
print("[+] regcode -> %s (%d hex chars;粘贴到 Help→Register 的输入框)" % (args.out, len(code)))
print("[!] 中文系统(CP936)下注册对话框写 .sig 走 WideCharToMultiByte(CP_ACP),"
"签名高位字节将失真;确需 UI 注册时,注册后请再用 issue 直写刷新 HeliumLicenses\\*.sig")
def cmd_verify(args):
"""本地复验(复刻 mgr:sub_18009D190 → sub_1802FD4F0 → PKCS1v15-SHA1)。
校验字节链:.sig 必须 512B;消息 = .lic 全部字节。默认用内置公钥。"""
pub = load_public_key(args.public)
lic = open(args.lic, "rb").read()
sig = open(args.sig, "rb").read()
print("[.] .lic=%dB .sig=%dB (expect sig=%dB)" % (len(lic), len(sig), SIG_SIZE))
# 许可内容解析输出(LicenseType 绑定校验由程序加载时另行执行)
fields, ltype, rows = describe_license(lic)
print("[.] ---- 许可信息 ----")
for r in rows:
print(" " + r)
if ltype == 0:
print("[!] LicenseType 缺失/非数字 —— 程序绑定校验将失败")
if len(sig) != SIG_SIZE:
print("[-] signature size mismatch -> mgr 将走失败分支")
return 1
try:
pkcs1_15.new(pub).verify(SHA1.new(lic), sig)
print("[+] SIGNATURE VALID —— mgr 验签将通过(flags 置位,功能解锁)")
return 0
except (ValueError, TypeError):
print("[-] SIGNATURE INVALID")
return 1
def _default_scan_dirs():
"""selfcheck 搜索 he_blob.bin / wininet_proxy_blob.h 的候选目录。"""
here = os.path.dirname(os.path.abspath(__file__))
return [os.getcwd(), here, os.path.normpath(os.path.join(here, ".."))]
def _find_file(dirs, *relatives):
for d in dirs:
for rel in relatives:
p = os.path.join(d, rel)
if os.path.exists(p):
return p
return None
def _parse_kblob(header_path):
"""从 C 头文件解析 `kBlob[…]` / `kDefaultBlob[…]` = {0x.., …} 的字节
(两处数组均为 XOR 加密形态:wininet_proxy_blob.h(version 探针)与
he_blob_default.h(wininet 代理编译期内置数据))。"""
src = open(header_path, "r", encoding="utf-8", errors="replace").read()
m = re.search(r"\w*Blob\s*\[[^\]]*\]\s*=\s*\{(.*?)\}", src, re.S)
if not m:
return None
return bytes(int(x, 16) for x in re.findall(r"0x([0-9A-Fa-f]{2})", m.group(1)))
def run_selfcheck(dirs=None):
"""自校验:内置私钥 / 内置公钥 / he_blob.bin / wininet_proxy_blob.h 的公钥是否一致。
返回 (ok, lines)。供 CLI 与 GUI 复用(dirs 为搜索目录列表,默认脚本目录/cwd/上级)。"""
if dirs is None:
dirs = _default_scan_dirs()
lines = []
ok = True
emb = embedded_public_der()
priv_der = RSA.import_key(EMBEDDED_PRIVATE_PEM).publickey().export_key(format="DER")
sha = hashlib.sha1(emb).hexdigest()
mismatch_tail = "劫持 DLL 的公钥与脚本不一致,需用 gen_blob_bin.py 重新生成 he_blob.bin"
# (a) 内置私钥导出公钥 == 内置公钥常量
if priv_der == emb:
lines.append("[+] (a) 内置私钥导出的公钥 == 内置公钥常量(SHA1=%s,%dB)" % (sha, len(emb)))
else:
ok = False
lines.append("[-] (a) 内置私钥与内置公钥常量不一致!%s" % mismatch_tail)
# (b) he_blob.bin(XOR 索引加密)还原后 == 内置公钥
blob_path = _find_file(dirs, "he_blob.bin")
if blob_path:
blob = open(blob_path, "rb").read()
dec = bytes((b ^ i) & 0xFF for i, b in enumerate(blob))
if dec == emb:
lines.append("[+] (b) %s XOR 还原 == 内置公钥(一致,%dB)" % (blob_path, len(blob)))
else:
ok = False
lines.append("[-] (b) %s 与内置公钥不一致!%s" % (blob_path, mismatch_tail))
else:
lines.append("[i] (b) 未找到 he_blob.bin(部署 wininet.dll 前须用 blob 子命令生成)")
# (c) tools/archive/wininet_proxy_blob.h 中 kBlob 还原后 == 内置公钥
hdr_path = _find_file(dirs, os.path.join("tools", "archive", "wininet_proxy_blob.h"),
os.path.join("archive", "wininet_proxy_blob.h"))
if hdr_path:
kb = _parse_kblob(hdr_path)
if kb is None:
ok = False
lines.append("[-] (c) 无法解析 %s 中的 kBlob" % hdr_path)
else:
dec = bytes((b ^ i) & 0xFF for i, b in enumerate(kb))
if dec == emb:
lines.append("[+] (c) %s kBlob XOR 还原 == 内置公钥(一致,%dB)" % (hdr_path, len(kb)))
else:
ok = False
lines.append("[-] (c) %s kBlob 与内置公钥不一致!%s" % (hdr_path, mismatch_tail))
else:
lines.append("[i] (c) 未找到 tools/archive/wininet_proxy_blob.h(跳过)")
# (d) tools/he_blob_default.h 内置数据还原后 == 内置公钥(DLL 编译期回退数据,勿过期)
dhdr = _find_file(dirs, os.path.join("tools", "he_blob_default.h"),
"he_blob_default.h")
if dhdr:
kb = _parse_kblob(dhdr)
if kb is None:
ok = False
lines.append("[-] (d) 无法解析 %s 中的 kDefaultBlob" % dhdr)
else:
dec = bytes((b ^ i) & 0xFF for i, b in enumerate(kb))
if dec == emb:
lines.append("[+] (d) %s kDefaultBlob XOR 还原 == 内置公钥(一致,%dB)" % (dhdr, len(kb)))
else:
ok = False
lines.append("[-] (d) %s 内置数据与内置公钥不一致!请重跑 `blobheader` 并**重新编译** "
"wininet.dll" % dhdr)
else:
lines.append("[i] (d) 未找到 tools/he_blob_default.h(编译前需 `blobheader` 生成)")
lines.append("[=] 自检结论:%s"
% ("公钥一致,可正常签发/部署" if ok else "存在不一致,请按上方提示处理"))
return ok, lines
def cmd_selfcheck(args):
ok, lines = run_selfcheck()
for ln in lines:
print(ln)
return 0 if ok else 1
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("keygen", help="生成 RSA-4096 密钥对")
p.add_argument("--exponent", type=int, default=65537)
p.add_argument("--private", default="he_private.pem")
p.add_argument("--public", default="he_public.bin")
p.set_defaults(fn=cmd_keygen)
p = sub.add_parser("issue", help="签发许可(.lic+.sig 直写 + mgr blob)")
p.add_argument("--private", default="", help="私钥 PEM 路径;留空用内置私钥")
p.add_argument("--type", type=int, default=1, help="LicenseType:1/5 不绑定,2/6/9 文件名,3 路径首段,4/7/10 硬件三元组")
p.add_argument("--name", default="Test")
p.add_argument("--computer", default="")
p.add_argument("--processor", default="")
p.add_argument("--uid", default="")
p.add_argument("--license-dir", default="HeliumLicenses")
p.add_argument("--basename", default="he_custom")
p.add_argument("--blob", default="tools/poc2_blob.bin")
p.add_argument("--regcode", default="", help="可选:同时输出 UI 注册码文件")
p.set_defaults(fn=cmd_issue)
p = sub.add_parser("regcode", help="生成注册对话框注册码")
p.add_argument("--private", default="")
p.add_argument("--lic", default="")
p.add_argument("--sig", default="", help="签名的 hex(若无私钥)")
p.add_argument("--type", type=int, default=1)
p.add_argument("--name", default="Test")
p.add_argument("--computer", default="")
p.add_argument("--processor", default="")
p.add_argument("--uid", default="")
p.add_argument("--out", default="regkey_final.txt")
p.set_defaults(fn=cmd_regcode)
p = sub.add_parser("blob", help="生成 mgr 重定向 blob")
p.add_argument("--public", default="", help="公钥 DER 路径;留空用内置公钥")
p.add_argument("--out", default="tools/poc2_blob.bin")
p.set_defaults(fn=cmd_blob)
p = sub.add_parser("blobheader", help="生成 wininet_proxy.c 的编译期内置公钥头(tools/he_blob_default.h)")
p.add_argument("--public", default="", help="公钥 DER 文件(缺省用内置公钥)")
p.add_argument("--out", default=os.path.join("tools", "he_blob_default.h"))
p.set_defaults(fn=cmd_blobheader)
p = sub.add_parser("verify", help="本地复验 .lic/.sig")
p.add_argument("--public", default="", help="公钥 DER 路径;留空用内置公钥")
p.add_argument("--lic", required=True)
p.add_argument("--sig", required=True)
p.set_defaults(fn=cmd_verify)
p = sub.add_parser("selfcheck", help="自检:内置密钥与 he_blob.bin/代理头文件公钥是否一致")
p.set_defaults(fn=cmd_selfcheck)
args = ap.parse_args()
sys.exit(args.fn(args))
if __name__ == "__main__":
main()
helium_manager.py
# -*- coding: utf-8 -*-
"""Helium Hex Editor 许可管理 GUI(Tkinter,无第三方依赖)
设计原则:
- 最小外部依赖:仅调用 tools/helium_license.py(签发/校验,逻辑复杂保留为脚本);
机器信息/激活状态读取、wininet.dll 部署、程序启动全部由本 GUI 内置(ctypes/subprocess);
- 小白友好:许可类型选择后自动启用/冻结字段并给出解释;支持粘贴硬件信息自动解析;
- 路径可配置:程序路径默认自动探测(本脚本所在目录及其父目录),可手动指定并记忆。
用法:python tools\\helium_manager.py
"""
import json
import os
import re
import queue
import subprocess
import sys
import threading
import tkinter as tk
from tkinter import ttk, messagebox
import ctypes
from ctypes import wintypes
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_ROOT = (HERE if os.path.exists(os.path.join(HERE, "HeliumHexEditor64.exe"))
else os.path.dirname(HERE))
CONFIG = os.path.join(HERE, "helium_gui_config.json")
# 同目录的 helium_license.py:自检/签发/注册码复用其函数(脚本自包含,无需外部密钥文件)
sys.path.insert(0, HERE)
import helium_license as hlic # noqa: E402
TYPES = {
1: "1 - Pro Portable(便携,不绑定机器)",
2: "2 - 绑定用户名(COMPUTER\\USER 的 USER 部分)",
3: "3 - 绑定路径首段(ComputerName 留空即可匹配)",
4: "4 - Pro Single Computer(硬件三元组)",
5: "5 - Pro Portable(便携,不绑定机器)",
6: "6 - 绑定用户名(同 2)",
8: "8 - 不绑定(已注册状态下会失败,少用)",
9: "9 - 绑定用户名(同 2)",
10: "10 - Pro Single Computer(硬件三元组,同 4)",
11: "11 - 无效类型(程序会强制作废,仅用于验证拒绝逻辑)",
}
TYPE_DESC = {
1: "不绑定机器/用户,任何机器可用。最简单,推荐个人使用。",
2: "许可的 Name 字段须等于当前 Windows 用户名(机器块 +0 = COMPUTER\\USER 取末段)。",
3: "许可的 ComputerName 与机器块 +0x200 的首段比对;实测机器块为 \\COMPUTERNAME,"
"首段为空串,因此本字段留空即可通过。",
4: "硬件三元组绑定:ComputerName(形如 \\DESKTOP-XXX)、Processor(CPU 全名)、"
"ID(机器 GUID)。可用下方\"粘贴硬件信息\"自动填充。",
5: "同类型 1,不绑定。",
6: "同类型 2(绑定用户名)。",
8: "绑定校验恒通过,但当程序处于已注册状态时会判定失败——一般不用。",
9: "同类型 2(绑定用户名)。",
10: "同类型 4(硬件三元组)。",
11: "加载时会被强制置 0 并判定失败——仅用于验证程序确实拒绝了无效许可。",
}
# ---------------- ctypes:进程/内存读取(替代外部 PS 脚本) ----------------
_k32 = ctypes.windll.kernel32
PSAPI = ctypes.windll.psapi
# 64 位下必须显式声明签名,否则句柄/指针按 c_int 截断(OverflowError)
_k32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
_k32.OpenProcess.restype = wintypes.HANDLE
_k32.ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID,
ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
_k32.ReadProcessMemory.restype = wintypes.BOOL
PSAPI.EnumProcessModulesEx.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.HMODULE),
wintypes.DWORD, ctypes.POINTER(wintypes.DWORD),
wintypes.DWORD]
PSAPI.EnumProcessModulesEx.restype = wintypes.BOOL
PSAPI.GetModuleFileNameExW.argtypes = [wintypes.HANDLE, wintypes.HMODULE, wintypes.LPWSTR,
wintypes.DWORD]
PSAPI.GetModuleFileNameExW.restype = wintypes.DWORD
PSAPI.GetModuleInformation.argtypes = [wintypes.HANDLE, wintypes.HMODULE, ctypes.c_void_p,
wintypes.DWORD]
PSAPI.GetModuleInformation.restype = wintypes.BOOL
PSAPI.GetModuleFileNameExW.restype = wintypes.DWORD
TH32CS_SNAPPROCESS = 0x2
TH32CS_SNAPMODULE = 0x8
TH32CS_SNAPMODULE32 = 0x10
INVALID_HANDLE = ctypes.c_void_p(-1).value
class PROCESSENTRY32W(ctypes.Structure):
_fields_ = [("dwSize", wintypes.DWORD), ("cntUsage", wintypes.DWORD),
("th32ProcessID", wintypes.DWORD),
("th32DefaultHeapID", ctypes.POINTER(wintypes.ULONG)),
("th32ModuleID", wintypes.DWORD), ("cntThreads", wintypes.DWORD),
("th32ParentProcessID", wintypes.DWORD), ("pcPriClassBase", ctypes.c_long),
("dwFlags", wintypes.DWORD), ("szExeFile", wintypes.WCHAR * 260)]
class MODULEENTRY32W(ctypes.Structure):
_fields_ = [("dwSize", wintypes.DWORD), ("th32ModuleID", wintypes.DWORD),
("GlblcntUsage", wintypes.DWORD), ("ProccntUsage", wintypes.DWORD),
("modBaseAddr", ctypes.POINTER(ctypes.c_byte)),
("modBaseSize", wintypes.DWORD), ("hModule", wintypes.HMODULE),
("szModule", wintypes.WCHAR * 256), ("szExePath", wintypes.WCHAR * 260)]
def find_pid(exe_name="HeliumHexEditor64.exe"):
snap = _k32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
entry = PROCESSENTRY32W(); entry.dwSize = ctypes.sizeof(entry)
pid = 0
if _k32.Process32FirstW(snap, ctypes.byref(entry)):
while True:
if entry.szExeFile == exe_name:
pid = entry.th32ProcessID
break
if not _k32.Process32NextW(snap, ctypes.byref(entry)):
break
_k32.CloseHandle(snap)
return pid
def find_module_base(pid, module_name="HexControl64.dll"):
"""psapi EnumProcessModulesEx + GetModuleInformation(比 Toolhelp 模块快照更稳)。"""
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
LIST_MODULES_ALL = 0x03
h = _k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
if not h:
return 0
try:
needed = wintypes.DWORD(0)
if not PSAPI.EnumProcessModulesEx(h, None, 0, ctypes.byref(needed), LIST_MODULES_ALL):
return 0
count = needed.value // ctypes.sizeof(wintypes.HMODULE)
if count <= 0:
return 0
arr = (wintypes.HMODULE * count)()
if not PSAPI.EnumProcessModulesEx(h, arr, needed.value, ctypes.byref(needed),
LIST_MODULES_ALL):
return 0
class MODULEINFO(ctypes.Structure):
_fields_ = [("lpBaseOfDll", wintypes.LPVOID),
("SizeOfImage", wintypes.DWORD),
("EntryPoint", wintypes.LPVOID)]
base = 0
for i in range(count):
name = ctypes.create_unicode_buffer(260)
PSAPI.GetModuleFileNameExW(h, arr[i], name, 260)
if name.value.lower().endswith(module_name.lower()):
mi = MODULEINFO()
if PSAPI.GetModuleInformation(h, arr[i], ctypes.byref(mi),
ctypes.sizeof(mi)):
base = mi.lpBaseOfDll or 0
break
return base
finally:
_k32.CloseHandle(h)
def read_mem(pid, addr, size):
buf = ctypes.create_string_buffer(size)
got = ctypes.c_size_t(0)
h = _k32.OpenProcess(0x1FFFFF, False, pid)
if not h:
return None
ok = _k32.ReadProcessMemory(h, ctypes.c_void_p(addr), buf, size, ctypes.byref(got))
_k32.CloseHandle(h)
return buf.raw[:got.value] if ok else None
def read_wstr(pid, addr, maxlen):
raw = read_mem(pid, addr, maxlen * 2)
if not raw:
return ""
s = raw.decode("utf-16-le", errors="replace")
return s.split("\x00", 1)[0]
def read_machine_info():
"""纯 ctypes 读取 mgr 机器块(mgr+64):+0 / +0x200 / +0x400 / +0x462。"""
pid = find_pid()
if not pid:
return None, "程序未运行(先启动 HeliumHexEditor64)"
base = find_module_base(pid)
if not base:
return None, "HexControl64.dll 未映射"
mgr = base + 0x711310
block = mgr + 64
info = {
"path": read_wstr(pid, block + 0x000, 256),
"f200": read_wstr(pid, block + 0x200, 256),
"processor": read_wstr(pid, block + 0x400, 49),
"id": read_wstr(pid, block + 0x462, 128),
}
return info, ("PID=%d base=0x%X" % (pid, base))
def read_activation_state():
"""读取激活状态:许可记录(mgr+3300, About 判据) / licdoc / 金丝雀 P12 / c38 /
f0(mgr+0xCA8,MBA 状态字——兼作状态机阶段与验签结果位,同一字段)。"""
pid = find_pid()
if not pid:
return None
base = find_module_base(pid)
if not base:
return None
mgr = base + 0x711310
raw = read_mem(pid, mgr + 0xCA8, 12)
f0 = int.from_bytes(raw[0:4], "little", signed=True) if raw else None
slot_raw = read_mem(pid, base + 0x714568, 8)
p12 = None
if slot_raw:
slot = int.from_bytes(slot_raw, "little")
p12raw = read_mem(pid, slot + 0x60, 8)
if p12raw:
p12 = int.from_bytes(p12raw, "little")
c38 = int.from_bytes(read_mem(pid, mgr + 0xC38, 8) or b"\x00" * 8, "little")
rec0 = int.from_bytes(read_mem(pid, mgr + 3300, 4) or b"\x00" * 4, "little")
ltype = int.from_bytes(read_mem(pid, mgr + 3304, 4) or b"\x00" * 4, "little")
digest = read_wstr(pid, mgr + 3300 + 1386, 200)
licdoc = int.from_bytes(read_mem(pid, mgr + 3272, 8) or b"\x00" * 8, "little")
about_gate = int.from_bytes(read_mem(pid, mgr + 0xC58, 8) or b"\x00" * 8, "little")
return {"f0": f0, "p12": p12, "c38": c38, "pid": pid, "base": base,
"rec0": rec0, "ltype": ltype, "digest": digest, "licdoc": licdoc,
"about_gate": about_gate}
# ---------------- 工具函数 ----------------
def make_blob_from_public(pub_path, out_path):
"""he_public.bin(SPKI DER) -> he_blob.bin(XOR 索引加密),并做结构自校验。"""
data = open(pub_path, "rb").read()
if not (data[:1] == b"\x30" and data[1:2] == b"\x82"):
raise ValueError("he_public.bin 不是 SPKI DER")
plain_len = (data[2] << 8) | data[3]
if 4 + plain_len != len(data):
raise ValueError("DER 长度不自洽")
blob = bytes((b ^ i) & 0xFF for i, b in enumerate(data))
open(out_path, "wb").write(blob)
return out_path
def run_async(logq, args, cwd):
def worker():
try:
logq.put("$ " + " ".join(args) + "\n")
creation = 0x08000000 if os.name == "nt" else 0
p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True,
encoding="utf-8", errors="replace",
creationflags=creation)
for line in p.stdout:
logq.put(line)
p.wait()
logq.put("[exit %d]\n" % p.returncode)
except Exception as exc: # noqa: BLE001
logq.put("[error] %r\n" % (exc,))
threading.Thread(target=worker, daemon=True).start()
# ---------------- 界面 ----------------
class App:
def __init__(self, master):
self.master = master
master.title("Helium Hex Editor 许可管理器")
self.logq = queue.Queue()
self.vars = {}
self._build()
self._load_cfg()
self._apply_type_ui()
master.after(100, self._pump_log)
master.after(300, self.on_selfcheck)
self.log("提示:本目录部署 wininet.dll 后,程序双击启动即为已注册态(无需每次操作)。")
# ---------- 界面构建 ----------
def _build(self):
frm = ttk.Frame(self.master, padding=10)
frm.pack(fill="both", expand=True)
row = 0
ttk.Label(frm, text="程序路径(HeliumHexEditor64.exe):").grid(row=row, column=0, sticky="w")
self.exe_var = tk.StringVar()
e = ttk.Entry(frm, textvariable=self.exe_var, width=52)
e.grid(row=row, column=1, columnspan=3, sticky="we")
ttk.Button(frm, text="浏览…", command=self.on_browse_exe).grid(row=row, column=4, sticky="w")
row += 1
ttk.Label(frm, text="许可目录(HeliumLicenses):").grid(row=row, column=0, sticky="w")
self.licdir_var = tk.StringVar()
ttk.Entry(frm, textvariable=self.licdir_var, width=52).grid(
row=row, column=1, columnspan=3, sticky="we")
ttk.Button(frm, text="浏览…", command=self.on_browse_licdir).grid(row=row, column=4, sticky="w")
row += 1
ttk.Separator(frm, orient="horizontal").grid(row=row, column=0, columnspan=5, sticky="we", pady=4)
row += 1
ttk.Label(frm, text="LicenseType:").grid(row=row, column=0, sticky="w")
self.type_var = tk.StringVar(value=TYPES[1])
cb = ttk.Combobox(frm, textvariable=self.type_var, state="readonly", width=46,
values=list(TYPES.values()))
cb.grid(row=row, column=1, columnspan=3, sticky="we")
cb.bind("<<ComboboxSelected>>", lambda _e: self._apply_type_ui())
row += 1
self.desc_var = tk.StringVar(value=TYPE_DESC[1])
ttk.Label(frm, textvariable=self.desc_var, foreground="#0a5",
wraplength=560).grid(row=row, column=0, columnspan=5, sticky="w")
row += 1
def field(label, key):
nonlocal row
lbl = ttk.Label(frm, text=label)
lbl.grid(row=row, column=0, sticky="w")
v = tk.StringVar()
ent = ttk.Entry(frm, textvariable=v, width=44)
ent.grid(row=row, column=1, columnspan=3, sticky="we")
self.vars[key] = {"var": v, "entry": ent, "label": lbl}
row += 1
field("Name(类型 2/6/9 = Windows 用户名):", "name")
field("ComputerName(类型 4/7/10 形如 \\COMPUTER;类型 3 留空):", "computer")
field("Processor(类型 4/7/10 CPU 全名):", "processor")
field("ID(类型 4/7/10 机器 GUID):", "uid")
field("许可文件名 basename:", "basename")
row += 1
ttk.Label(frm, text="粘贴硬件信息(每行一项,顺序不限,自动归类):").grid(
row=row, column=0, sticky="nw")
self.hwtext = tk.Text(frm, height=4, wrap="word")
self.hwtext.grid(row=row, column=1, columnspan=3, sticky="we")
ttk.Button(frm, text="解析并\n自动填充", command=self.on_parse_hw).grid(
row=row, column=4, sticky="w")
row += 1
ttk.Button(frm, text="从运行中的程序读取机器信息", command=self.on_machine).grid(
row=row, column=1, sticky="w")
row += 1
ttk.Separator(frm, orient="horizontal").grid(row=row, column=0, columnspan=5, sticky="we", pady=4)
row += 1
btns = ttk.Frame(frm)
btns.grid(row=row, column=0, columnspan=5, sticky="we", pady=4)
for text, cmd in (
("生成密钥对(自动同步公钥)", self.on_keygen),
("签发许可", self.on_issue),
("生成本地注册码(手动注册)", self.on_regcode),
("本地校验许可", self.on_verify),
("部署/更新 wininet.dll", self.on_deploy),
("显示/复制 DLL 编译命令", self.on_compile_hint),
("启动程序(纯启动即激活)", self.on_launch),
("查看激活状态", self.on_status),
):
ttk.Button(btns, text=text, command=cmd).pack(side="left", padx=3)
row += 1
ttk.Label(frm, text="注册码(手动注册:程序菜单 Help→Register,把下面整段粘贴进注册框 id 1519):").grid(
row=row, column=0, columnspan=5, sticky="w")
row += 1
self.regcode_box = tk.Text(frm, height=5, wrap="char")
self.regcode_box.grid(row=row, column=0, columnspan=4, sticky="we")
ttk.Button(frm, text="复制注册码", command=self.on_copy_regcode).grid(
row=row, column=4, sticky="nw")
row += 1
self.regchk = tk.BooleanVar(value=False)
ttk.Checkbutton(frm, text="强制重新生成密钥与许可(覆盖现有)", variable=self.regchk).grid(
row=row, column=0, columnspan=3, sticky="w")
row += 1
frm.columnconfigure(1, weight=1)
frm.rowconfigure(row, weight=1)
self.logbox = tk.Text(frm, height=16, wrap="word", state="disabled")
self.logbox.grid(row=row, column=0, columnspan=5, sticky="nsew")
# ---------- 类型联动 ----------
def _apply_type_ui(self):
t = self._lic_type()
self.desc_var.set(TYPE_DESC.get(t, ""))
en_name = t in (1, 2, 3, 5, 6, 8, 9, 11)
en_triple = t in (4, 7, 10)
en_comp3 = t == 3
self.vars["name"]["entry"].state(["!disabled"] if en_name else ["disabled"])
self.vars["computer"]["entry"].state(["!disabled"] if (en_triple or en_comp3) else ["disabled"])
self.vars["processor"]["entry"].state(["!disabled"] if en_triple else ["disabled"])
self.vars["uid"]["entry"].state(["!disabled"] if en_triple else ["disabled"])
# 字段标签随类型动态化(说明跟随语义变化)
name_lbl = "Name(显示名,不参与校验):"
comp_lbl = "ComputerName:"
proc_lbl = "Processor:"
uid_lbl = "ID:"
if t in (2, 6, 9):
name_lbl = "Name(类型 %d:必须为当前 Windows 用户名,已自动填充):" % t
elif t in (1, 5, 8):
name_lbl = "Name(类型 %d:不校验,随意填写):" % t
if t == 3:
comp_lbl = "ComputerName(类型 3:留空即可匹配路径首段):"
elif en_triple:
comp_lbl = "ComputerName(类型 %d:\\COMPUTERNAME,带前导反斜杠):" % t
if en_triple:
proc_lbl = "Processor(类型 %d:CPU 全名):" % t
uid_lbl = "ID(类型 %d:机器 GUID):" % t
self.vars["name"]["label"].config(text=name_lbl)
self.vars["computer"]["label"].config(text=comp_lbl)
self.vars["processor"]["label"].config(text=proc_lbl)
self.vars["uid"]["label"].config(text=uid_lbl)
# 类型 2/6/9:自动填充当前 Windows 用户名(与机器块 +0 的末段语义一致)
if t in (2, 6, 9):
cur = os.environ.get("USERNAME", "")
old = self.vars["name"]["var"].get().strip()
if cur and old != cur:
self.vars["name"]["var"].set(cur)
self.log("[name] 类型 %d 绑定用户名,已自动填入当前用户名:%s" % (t, cur))
def _lic_type(self):
return int(self.type_var.get().split(" ")[0])
# ---------- 配置 ----------
def _load_cfg(self):
cfg = {}
try:
with open(CONFIG, "r", encoding="utf-8") as f:
cfg = json.load(f)
except Exception:
pass
self.exe_var.set(cfg.get("exe_path", os.path.join(DEFAULT_ROOT, "HeliumHexEditor64.exe")))
self.licdir_var.set(cfg.get("license_dir", os.path.join(DEFAULT_ROOT, "HeliumLicenses")))
for k, v in cfg.get("fields", {}).items():
if k in self.vars:
self.vars[k]["var"].set(v)
def _save_cfg(self):
try:
with open(CONFIG, "w", encoding="utf-8") as f:
json.dump({"exe_path": self.exe_var.get(),
"license_dir": self.licdir_var.get(),
"fields": {k: v["var"].get() for k, v in self.vars.items()}},
f, ensure_ascii=False, indent=1)
except Exception:
pass
# ---------- 日志 ----------
def log(self, text):
self.logq.put(text + "\n")
def _pump_log(self):
try:
while True:
line = self.logq.get_nowait()
self.logbox.configure(state="normal")
self.logbox.insert("end", line)
self.logbox.see("end")
self.logbox.configure(state="disabled")
except queue.Empty:
pass
self.master.after(120, self._pump_log)
# ---------- 路径 ----------
def _paths(self):
exe = self.exe_var.get().strip() or os.path.join(DEFAULT_ROOT, "HeliumHexEditor64.exe")
root = os.path.dirname(exe)
licdir = self.licdir_var.get().strip() or os.path.join(root, "HeliumLicenses")
return {"exe": exe, "root": root, "licdir": licdir,
"priv": os.path.join(root, "he_private.pem"),
"pub": os.path.join(root, "he_public.bin"),
"blob": os.path.join(root, "he_blob.bin")}
def on_browse_exe(self):
from tkinter import filedialog
p = filedialog.askopenfilename(title="选择 HeliumHexEditor64.exe",
filetypes=[("可执行文件", "*.exe")])
if p:
self.exe_var.set(p)
self.licdir_var.set(os.path.join(os.path.dirname(p), "HeliumLicenses"))
self._save_cfg()
def on_browse_licdir(self):
from tkinter import filedialog
p = filedialog.askdirectory(title="选择 HeliumLicenses 目录")
if p:
self.licdir_var.set(p)
self._save_cfg()
# ---------- 动作 ----------
def on_parse_hw(self):
guid_re = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
lines = [ln.strip() for ln in self.hwtext.get("1.0", "end").splitlines() if ln.strip()]
if not lines:
messagebox.showinfo("提示", "请先把硬件信息粘贴到文本框(每行一项,顺序不限)")
return
filled = []
for ln in lines:
if guid_re.match(ln):
self.vars["uid"]["var"].set(ln)
filled.append("ID=" + ln)
elif ln.startswith("\\"):
self.vars["computer"]["var"].set(ln)
filled.append("ComputerName=" + ln)
elif "intel" in ln.lower() or "amd" in ln.lower() or "cpu" in ln.lower():
self.vars["processor"]["var"].set(ln)
filled.append("Processor=" + ln)
elif "\\" in ln:
name = ln.split("\\")[-1]
self.vars["name"]["var"].set(name)
filled.append("Name(用户名)=" + name)
else:
filled.append("未识别(忽略):" + ln)
self._save_cfg()
self.log("[parse] " + ";".join(filled))
messagebox.showinfo("解析完成", "已自动填充:\n" + "\n".join(filled))
def on_machine(self):
def worker():
info, msg = read_machine_info()
self.logq.put("[machine] " + msg + "\n")
if not info:
return
t = self._lic_type()
self.logq.put("[machine] %r\n" % info)
if t in (4, 7, 10):
self.master.after(0, lambda: (
self.vars["computer"]["var"].set(info["f200"]),
self.vars["processor"]["var"].set(info["processor"]),
self.vars["uid"]["var"].set(info["id"])))
elif t in (2, 6, 9):
name = info["path"].split("\\")[-1]
self.master.after(0, lambda: self.vars["name"]["var"].set(name))
self.logq.put("[machine] 已按当前类型自动填充对应字段\n")
threading.Thread(target=worker, daemon=True).start()
def on_keygen(self):
self._gen_keys(lambda: self.log(
"[hint] 密钥与公钥数据已生成(he_blob.bin 同步完成),可直接签发许可"))
def _gen_keys(self, cb=None):
"""GUI 内置生成 RSA-4096 密钥对(e=17),并同步 he_blob.bin。"""
p = self._paths()
def worker():
try:
self.logq.put("[keys] 生成 RSA-4096 密钥对(e=17,约需数秒)…\n")
from Crypto.PublicKey import RSA
key = RSA.generate(4096, e=17)
with open(p["priv"], "wb") as f:
f.write(key.export_key())
with open(p["pub"], "wb") as f:
f.write(key.publickey().export_key(format="DER"))
make_blob_from_public(p["pub"], p["blob"])
self.logq.put("[keys] 已生成:\n %s\n %s\n %s\n"
% (p["priv"], p["pub"], p["blob"]))
except Exception as exc: # noqa: BLE001
self.logq.put("[keys] 生成失败: %r\n" % (exc,))
if cb:
self.master.after(0, cb)
threading.Thread(target=worker, daemon=True).start()
def _ensure_keys(self, cb):
p = self._paths()
if os.path.exists(p["priv"]) and os.path.exists(p["pub"]):
cb()
return
self.log("[keys] 未找到密钥文件,自动生成中…")
self._gen_keys(cb)
def on_issue(self):
self._save_cfg()
t = self._lic_type()
name = self.vars["name"]["var"].get().strip() or "User"
base = self.vars["basename"]["var"].get().strip() or "he_custom"
# 类型 2/6/9 签发前校验:用户名与当前登录用户不一致时明确提示
if t in (2, 6, 9):
cur = os.environ.get("USERNAME", "")
if cur and name != cur:
if not messagebox.askyesno(
"确认",
"类型 %d 绑定用户名:许可 Name=「%s」,与当前登录用户「%s」不一致。\n"
"该许可在本机的用户名比对将失败(除非确为其他用户签发)。\n继续签发?"
% (t, name, cur)):
return
need_triple = t in (4, 7, 10)
missing = need_triple and not (
self.vars["computer"]["var"].get().strip()
and self.vars["processor"]["var"].get().strip()
and self.vars["uid"]["var"].get().strip())
if missing:
if not messagebox.askyesno(
"提示", "硬件三元组不完整。\n是否先从运行中的程序读取本机信息?"):
return
self.on_machine()
self.master.after(2500, lambda: self._ensure_keys(
lambda: self._issue_now(t, name, base)))
return
self._ensure_keys(lambda: self._issue_now(t, name, base))
def _issue_now(self, t, name, base):
p = self._paths()
args = [sys.executable, os.path.join(HERE, "helium_license.py"), "issue",
"--type", str(t), "--name", name, "--basename", base,
"--license-dir", p["licdir"],
"--private", p["priv"], "--blob", p["blob"]]
for key, flag in (("computer", "--computer"), ("processor", "--processor"),
("uid", "--uid")):
v = self.vars[key]["var"].get().strip()
if v:
args += [flag, v]
run_async(self.logq, args, cwd=p["root"])
self.master.after(1500, self._verify_quiet)
def _verify_quiet(self):
base = self.vars["basename"]["var"].get().strip() or "he_custom"
p = self._paths()
lic = os.path.join(p["licdir"], base + ".lic")
sig = os.path.join(p["licdir"], base + ".sig")
if not (os.path.exists(lic) and os.path.exists(sig)):
return
run_async(self.logq, [
sys.executable, os.path.join(HERE, "helium_license.py"), "verify",
"--public", p["pub"], "--lic", lic, "--sig", sig], cwd=HERE)
def on_deploy(self):
p = self._paths()
src = os.path.join(HERE, "wininet.dll")
if not os.path.exists(src):
messagebox.showerror("错误", "tools\\wininet.dll 不存在——请先编译 wininet_proxy.c"
"(点“显示/复制 DLL 编译命令”查看命令)")
return
import shutil
try:
shutil.copy2(src, os.path.join(p["root"], "wininet.dll"))
except PermissionError:
messagebox.showerror(
"文件被占用",
"wininet.dll 正在被运行中的程序占用。\n请先关闭 Helium Hex Editor,再点一次部署。")
return
# 真身副本(基名不同):291 个 .def 转发导出的目标 + Worker 兜底
orig = os.path.join(p["root"], "wininet_orig.dll")
if not os.path.exists(orig):
try:
shutil.copy2(r"C:\Windows\System32\wininet.dll", orig)
self.log("[deploy] wininet_orig.dll 已复制(真身副本)")
except Exception as exc: # noqa: BLE001
self.log("[deploy] wininet_orig.dll 复制失败: %r(Worker 首次运行会兜底复制)" % (exc,))
pub = p["pub"] if os.path.exists(p["pub"]) else os.path.join(HERE, "he_public.bin")
if os.path.exists(pub):
try:
make_blob_from_public(pub, p["blob"])
except Exception as exc: # noqa: BLE001
self.log("[blob] 生成失败: %r" % (exc,))
self.log("[deploy] wininet.dll 已部署到 " + p["root"]
+ "(重启程序即激活;he_blob.bin 为可选的外置公钥,缺失时用 DLL 内置公钥)")
# ---------- 编译命令提示 ----------
@staticmethod
def _find_gcc():
import shutil
for c in (r"D:\Program\cygwin64\bin\x86_64-w64-mingw32-gcc.exe",
r"C:\cygwin64\bin\x86_64-w64-mingw32-gcc.exe",
r"C:\msys64\mingw64\bin\x86_64-w64-mingw32-gcc.exe"):
if os.path.exists(c):
return c
return shutil.which("x86_64-w64-mingw32-gcc") or "<x86_64-w64-mingw32-gcc.exe 的完整路径>"
def on_compile_hint(self):
"""在界面中给出劫持 DLL 的完整编译/部署命令(可复制)。"""
p = self._paths()
gcc = self._find_gcc()
cmd = (
":: ===== Helium 劫持 DLL 编译 / 部署命令(在项目根目录执行)=====\n"
":: 注意:修改过公钥时,务必先刷新“内置公钥数据”,否则 DLL 内置回退仍是旧公钥\n\n"
":: (1) 生成/刷新编译期内置公钥头(tools\\he_blob_default.h)\n"
"python helium_license.py blobheader\n\n"
":: (2) 编译代理 DLL(-Itools 用于找到 he_blob_default.h;.def 提供 291 个转发导出)\n"
"\"%s\" -shared -O2 -static-libgcc -Itools "
"tools\\wininet_proxy.c tools\\wininet_proxy.def -o tools\\wininet.dll\n\n"
":: (3) 部署到应用目录(与 HeliumHexEditor64.exe 同级,本窗口已可一键部署)\n"
"copy /Y tools\\wininet.dll .\\wininet.dll\n\n"
":: 说明:公钥“内置”在 DLL 里,部署只需 wininet.dll 一个文件;\n"
":: 若同目录另放 he_blob.bin(可选),则外置优先 → 以后换密钥只需换它,免重编译。\n"
"python helium_license.py blob --public he_public.bin --out .\\he_blob.bin\n\n"
":: 自检(内置私钥/公钥、he_blob.bin、代理内嵌 blob 三者是否一致)\n"
"python helium_license.py selfcheck"
% gcc)
self.log("[compile] 编译命令已生成(见弹窗,可全选复制):")
for line in cmd.splitlines():
self.log(" " + line)
self._show_text_dialog("劫持 DLL 编译 / 部署命令", cmd)
def _show_text_dialog(self, title, text):
"""可选择的文本弹窗(便于复制命令)。"""
win = tk.Toplevel(self.master)
win.title(title)
win.transient(self.master)
box = tk.Text(win, width=100, height=20, wrap="none")
box.insert("1.0", text)
box.pack(fill="both", expand=True, padx=6, pady=6)
def _copy():
self.master.clipboard_clear()
self.master.clipboard_append(text)
self.log("[compile] 命令已复制到剪贴板")
bar = ttk.Frame(win)
bar.pack(fill="x", padx=6, pady=(0, 6))
ttk.Button(bar, text="复制全部命令", command=_copy).pack(side="left")
ttk.Button(bar, text="关闭", command=win.destroy).pack(side="right")
def on_launch(self):
p = self._paths()
if not os.path.exists(os.path.join(p["root"], "wininet.dll")):
if messagebox.askyesno("提示", "应用目录尚未部署 wininet.dll,现在部署?"):
self.on_deploy()
return
self._save_cfg()
try:
subprocess.Popen([p["exe"]], cwd=p["root"])
self.log("[launch] 已启动 " + p["exe"] + "(wininet.dll 将自动完成激活)")
except Exception as exc: # noqa: BLE001
messagebox.showerror("错误", "启动失败: %r" % (exc,))
def on_verify(self, quiet=False):
base = self.vars["basename"]["var"].get().strip() or "he_custom"
p = self._paths()
lic = os.path.join(p["licdir"], base + ".lic")
sig = os.path.join(p["licdir"], base + ".sig")
if not (os.path.exists(lic) and os.path.exists(sig)):
if not quiet:
messagebox.showwarning("提示", "许可文件不存在:%s" % lic)
return
run_async(self.logq, [
sys.executable, os.path.join(HERE, "helium_license.py"), "verify",
"--public", p["pub"], "--lic", lic, "--sig", sig], cwd=HERE)
def on_status(self):
st = read_activation_state()
if not st:
self.log("[status] 程序未运行或 DLL 未映射")
return
MAGIC = 0xB4A3D2E2C8789F87
vendor = st["base"] + 0x6F35E0
# 许可记录(mgr+3300):About 显示的总开关(A14E0 门 = rec0 非零)
if st["rec0"] and st["digest"]:
rec = "已填充(LicenseType=%d)About 摘要='%s'" % (st["ltype"], st["digest"].strip())
elif st["rec0"]:
rec = "已填充但摘要为空"
else:
rec = "未填充(About 将空白;v6 代理会在装载轮内 rec0 锚定写 c38)"
licdoc = "在载" if st["licdoc"] else "未载/已释放"
canary = "已解锁" if st["p12"] == MAGIC else ("加锁" if st["p12"] == 0xA4EE18F5D7F80873
else "0x%X" % (st["p12"] or 0))
redirected = "外置 blob(劫持生效)" if st["c38"] != vendor else "原厂 blob"
# About 状态行开关(§4.7):*(mgr+0xC58)==0x1FA68 → About 显示摘要(已注册)
about_ok = (st["about_gate"] == 0x1FA68)
about = ("已注册(显示摘要)" if about_ok
else "未注册(显示 Unregistered 文案)0x%X" % st["about_gate"])
# f0(mgr+0xCA8) = 验签结果位/状态机阶段同一字段(MBA 态):
# 0x88 态 = 验签链运行且失败;-1/随机初值有歧义,不作通过判据(§9-6 修正)
f0s = "0x%X" % (st["f0"] & 0xFFFFFFFFFFFFFFFF) if st["f0"] is not None else "?"
self.log("[status] About状态行=%s;记录=%s;licdoc=%s;金丝雀=%s;c38=%s;状态字=%s(PID=%d)"
% (about, rec, licdoc, canary, redirected, f0s, st["pid"]))
# About 实际会渲染的文本(打开 Help→About 的预期内容)
expected = (st["digest"].strip()
if (about_ok and st["digest"]) else
"Unregistered : some features are disabled like Save, Export, Join, Split, "
"Disassembly, Cryptography, Plugins, ...")
self.log("[status] About 预期显示内容:%s" % expected)
self.log("[status] 判读:About 状态行由 *(mgr+0xC58) 决定(==0x1FA68 显示摘要);"
"金丝雀解锁 = 受限功能可用;代理在初始化阶段一次性完成全部写入"
"(≈0.9s,无驻留循环),可选看门狗 -DWININET_WATCHDOG=1 仅按需修复漂移。")
# ---------- 内置密钥自检 ----------
def on_selfcheck(self):
"""启动时校验 helium_license 内置公钥与 he_blob.bin/代理头文件是否一致。"""
p = self._paths()
def worker():
try:
ok, lines = hlic.run_selfcheck([p["root"], HERE, os.getcwd()])
for ln in lines:
self.logq.put("[selfcheck] " + ln + "\n")
if not ok:
self.logq.put("[selfcheck][!] ★警告:劫持 DLL 的公钥与脚本不一致,"
"需用 gen_blob_bin.py 重新生成 he_blob.bin,否则验签必失败!\n")
except Exception as exc: # noqa: BLE001
self.logq.put("[selfcheck] 自检异常: %r\n" % (exc,))
threading.Thread(target=worker, daemon=True).start()
# ---------- 生成注册码(手动注册) ----------
def on_regcode(self):
"""按当前界面字段直写 .lic/.sig,并据 .sig 生成注册码(供注册框手动粘贴)。"""
self._save_cfg()
t = self._lic_type()
name = self.vars["name"]["var"].get().strip() or "User"
base = self.vars["basename"]["var"].get().strip() or "he_custom"
computer = self.vars["computer"]["var"].get().strip()
processor = self.vars["processor"]["var"].get().strip()
uid = self.vars["uid"]["var"].get().strip()
p = self._paths()
def worker():
try:
# 私钥:优先用文件,缺失则用 helium_license 内置私钥(自包含)
priv = hlic.load_private_key(p["priv"])
lic_bytes, sig = hlic.make_lic_and_sig(priv, t, name, computer, processor, uid)
# 先直写 .lic/.sig(.sig 以落盘为准),再据磁盘 .sig 生成注册码
os.makedirs(p["licdir"], exist_ok=True)
lic_path = os.path.join(p["licdir"], base + ".lic")
sig_path = os.path.join(p["licdir"], base + ".sig")
with open(lic_path, "wb") as f:
f.write(lic_bytes)
with open(sig_path, "wb") as f:
f.write(sig)
sig_disk = open(sig_path, "rb").read()
code = hlic.make_regcode(t, name, sig_disk, computer, processor, uid)
out = os.path.join(p["root"], "regcode_ui.txt")
with open(out, "w", encoding="ascii") as f:
f.write(code)
self.logq.put("[regcode] 已直写 %s / %s\n" % (lic_path, sig_path))
self.logq.put("[regcode] 注册码已保存 -> %s(%d 个十六进制字符)\n" % (out, len(code)))
self.master.after(0, lambda: self._show_regcode(code, out))
except Exception as exc: # noqa: BLE001
self.logq.put("[regcode] 生成失败: %r\n" % (exc,))
threading.Thread(target=worker, daemon=True).start()
def _show_regcode(self, code, out):
self.regcode_box.delete("1.0", "end")
self.regcode_box.insert("1.0", code)
self.regcode_box.see("1.0")
self.log("[regcode] 用法:程序菜单 Help→Register 打开注册框(控件 id 1519),"
"把上面文本框整段粘贴后确定。")
self.log("[regcode] ★注意:中文系统(CP936)下注册框写 .sig 会经 WideCharToMultiByte(CP_ACP) "
"导致签名失真、验签失败;稳妥做法是用『签发许可』文件直写路径,"
"或在 UI 注册后用文件直写刷新 HeliumLicenses\\*.sig(本按钮已直写 .sig)。")
messagebox.showinfo(
"注册码已生成",
"注册码已显示在上方文本框,并保存到:\n%s\n\n"
"用法:程序菜单 Help→Register,把整段注册码粘贴进注册框(id 1519)后确定。\n\n"
"注意:中文系统(CP936)下注册框写入的 .sig 经 CP_ACP 会失真、验签失败——"
"推荐直接用『签发许可』文件直写,或用文件直写刷新 HeliumLicenses\\*.sig 兜底。" % out)
def on_copy_regcode(self):
code = self.regcode_box.get("1.0", "end").strip()
if not code:
messagebox.showinfo("提示", "尚未生成注册码,请先点『生成本地注册码(手动注册)』")
return
self.master.clipboard_clear()
self.master.clipboard_append(code)
self.log("[regcode] 注册码已复制到剪贴板(%d 字符)" % len(code))
def main():
root = tk.Tk()
App(root)
root.mainloop()
if __name__ == "__main__":
main()
ps
helium_manager.py gui可能还是有问题的




浙公网安备 33010602011771号