AIGC标识 【AI】Helium Hex Editor v2.8.7.1许可机制分析

Helium Hex Editor v2.8.7.1许可机制分析

分析对象HeliumHexEditor64.exe v2.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 0xBsub_18009C3B0plain[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 包装)

算法与模式判定(运行时实抓 + 数学比对):沿 SetKeyWithIVsub_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 按模数长度内部校验。
  • ⚠️ 注册框路径的编码损失:注册框写 .sigWideCharToMultiByte(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 + 6kv17 起始为 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 ^ KEYKEY = 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_18024B590MessageBoxW("Not available for unregistered version")
姊妹 sub_180215200 同构发射 1518sub_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_18024B5900x18023338E)、case 1518 → sub_18024B6700x18023339C)。
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 链崩溃):

  1. 真身加载必须用基名不同的副本wininet_orig.dll):加载器查重只看基名,
    直接 LoadLibrary("…\System32\wininet.dll") 会命中代理自身 → 转发自递归;
  2. 导出表全量覆盖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

注册方式二选一

  1. 文件直写(推荐)helium_license.py issue 生成 .lic+.sig 落到 HeliumLicenses\
  2. 注册码手动注册:GUI/脚本生成注册码(§1.1 算法),粘贴到程序 Help → Register
    (输入框控件 id 1519)。注意:注册框写 .sigWideCharToMultiByte(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. 开放项

  1. vtable 0x1805E0580(About 对话框类)与任务表 0x5FF730 的完整方法语义(仅析出本案所需项)。
  2. mgr+0xC58 的合法写入者:引擎在何种条件下写入 0x1FA68 尚未定位(代理按"已注册"取值一次性置位)。
  3. 菜单构建期 1517/1518 绑定的判据来源(决策走间接路径,静态未闭环;当前方案无实际需求)。
  4. exe/dll 旁 64B .sig 自完整性算法(疑 SHA-512/RSA-512)——与许可链独立,攻击路径未触碰。
  5. 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可能还是有问题的

image-20260910212237714

image-20260910212305522

image-20260910212358464

posted @ 2026-09-10 21:32  DirWangK  阅读(63)  评论(0)    收藏  举报