AIGC标识 【AI】WizTree 4.32 x64 portable 许可分析

WizTree 4.32 x64 portable 许可分析

分析对象:WizTree64.exe(Delphi x64,4.32 版,构建日期 2026-08-05)
分析方法:IDA Pro 静态反编译 + x64dbg 远程动态验证
本文中所有函数地址为 IDA 静态地址;动态调试时需加重定位偏移。

wiztree_4_32_portable

PE64
    Operation system: Windows (Server 2003) [AMD64, 64位, GUI]
    Linker: Turbo linker (8.00)
    Compiler: Embarcadero Object Pascal(Delphi) (36.0 (29.0.52161.7750))
    Language: Object Pascal(Delphi)
    Library: Visual Component Library
    Tool: Embarcadero Delphi (12.0 Athens)
    Sign tool: Windows Authenticode (2.0) [PKCS #7]
    Overlay: Binary [偏移 = 0x00a73e00, 大小 = 0x2d80]
        Certificate: Windows Authenticode (2.0) [PKCS #7]
Hash: 
    MD5: e0597271a9bd8f89f04eaed75bd6087f
    SHA1: cbd5b3726886ee55f6946f28979e8597dd6c9174
    SHA256: 6bffb6b63d96ff19824641e8ced04e5e0f648696073d82a508f1bb1a21e7eb46


目录


第一部分 许可体系与代码格式

1. 许可体系概述

WizTree 采用"支持者代码(Supporter Code)"机制(对应 EULA 中的 supporter code / Enterprise License):

  • 免费供个人使用;商业使用需购买 supporter code(按用户数分级售卖);
  • 程序内置三种许可档位,在"关于"页展示(判定顺序:用户数==999 → Enterprise;有效天数!=0 → N user;否则 → Supporter):
    • Enterprise license:用户数字段 == 999(企业不限用户);
    • N user license:用户数 N(1/5/10…),显示 <N> user license,存在有效期("Upgrades until: <日期>");
    • Supporter license:有效天数 == 0(个人免费,无有效期 "Lifetime"),此时用户数任意非 999。

用户数 = byte2(A2)*256 + byte2(A1)ParseSupporterCodeFields_7A18D0 的 a5 输出,见 §3.2);
About 页显示逻辑(UpdateLicenseDisplay_AboutPage_B95300,见 §13)中,该字段同时用于 ==999 的 Enterprise 判断与 "%d user license" 的拼接,故其语义即授权用户数

许可校验核心位于:

  • TfrmSupporterCode 窗体(输入支持者代码的对话框):0xB0CAEF(RTTI 名),处理函数集中在 0xB0DC35(btnOKClick);
  • 主窗体 About 页显示逻辑:UpdateLicenseDisplay_AboutPage_B953000xB95300);
  • 一组 0x7A 段的许可算法函数(见第三部分 §8~§10)。

校验分两条路径:

  1. 在线路径:向 http://antibody-software.com/checkkey.php?reginfo=<包> 发 HTTP 请求,服务器返回的 chk 响应字段 1 必须为 1(见 §6);
  2. 离线路径:当 URL.DLL!InetIsOffline() 报告系统离线时,跳过在线校验,仅凭本地格式校验 + 版本日期校验即可通过(见 §7)。

2. 关键字符串定位

2.1 明文(非混淆)字符串

字符串 地址 用途
TfrmSupporterCode 0xB0CAEF 支持者代码窗体
Enter Supporter Code 0xB0ED88 窗体标题
Supporter Code: 0xB0EDC0 输入框标签
Setting the license key of the installed version of WizTree requires administrator access... 0xB0EE68 安装版需管理员提示
Sorry, your supporter code is only valid for versions of WizTree with version date 0xB0E824 / 0xB95FAC 版本日期过期提示
Supporter code not valid for this version 0xB96248 About 页过期显示
Enterprise license / user license / Supporter license 0xB96320/54/7C 档位显示(Enterprise / N user / Supporter)
KeyValidUntil 0x518A65 附近 INI 键:许可支持版本日期
LicenseKey 0x82443D / 0xB40D11 旧版键名(安装版注册表)

2.2 混淆字符串(经 DeobfuscateStr_XorIndex3_7A22A0 解密,见 §10.1)

混淆原文(宽字符) 解密结果
ivwp;-,aovjbnfz-rmetvcqe/alm.akebihex,shq=qefkmfn? http://antibody-software.com/checkkey.php?reginfo=
Hlweslbl!gqrnp#IE8#11v;6 Internal error ID: 10t86
Qnfrarg#cnlwabv#sndww\pf rwspmkfr` Please contact software supplier
Sgdirvqau kln!dbimgg.!Pfarmm <"! Registration failed. Reason = "
\lwicmgy/xb.ogw` antibody.za.net
\lwicmgy,qlfuubrd,`ol` antibody-software.com

注:Internal error ID: 10t86 的解密结果含字符 t(0x76^2=0x74),为程序内混淆原文所致,保持如实记录。


3. 支持者代码格式

3.1 整体布局

代码由 8 组 4 位十六进制字符组成,组间以连字符分隔,共 39 字符:

XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
│←─A1─→│←─A2─→│←csumA→│←─自由区(Key3)─→│←校验和→│

去掉连字符后为 32 个十六进制字符,含义如下(s[i] 为第 i 个字符,1 起始):

位置 长度 含义
1–8 8 A1(32 位十六进制整数,第 1、2 组)
9–16 8 A2(32 位十六进制整数,第 3、4 组)
17–18 2 csumA = CalcChecksumByte_7A19B0(A1, 17, 5, 70)
19–20 2 csumB = CalcChecksumByte_7A19B0(A1, 12, 9, 3)
21–28 8 自由区(许可信息,随 Key3 一起保存)
29–32 4 CalcCodeSuffixChecksum_7A1A40(第1–28字符)

3.2 派生态(用户数 / 有效天数)

A1A2 派生出的许可字段(ParseSupporterCodeFields_7A18D0,见 §8.2):

字段A (a3) = byte3(A1)*256 + byte0(A1)
字段B (a4) = byte3(A2)*256 + byte0(A2)
用户数 (a5) = byte2(A2)*256 + byte2(A1)      // 999=Enterprise; 其它N且有效天数!=0 → "N user license"
有效天数 (a6) = byte1(A2)*256 + byte1(A1)    // 自 2009-01-01 的天数; 0 表示终身(实测保持 0)
字段E (a7) = a6

其中:

  • 用户数 = byte2(A2)*256 + byte2(A1):About 页据此判定档位——999 = 企业 Enterprise;其它 N 且有效天数非 0 时拼为 "N user license";有效天数为 0 时显示个人 Supporter(此时用户数任意非 999);
  • 有效天数 = byte1(A2)*256 + byte1(A1),含义为"许可支持的最大版本日期 = EncodeDate(2009,1,1) + 有效天数";该值为 0 时按终身处理(About 页显示 Lifetime,且跳过版本日期校验);
  • 有效天数非 0 时,About 页显示 Upgrades until: <日期>,并在构建日期晚于该日期时判定"Supporter code not valid for this version"。

第二部分 激活流程与校验路径

4. 输入界面:TfrmSupporterCode

  • TfrmSupporterCode_FormCreate0xB0ECA2):设置按钮文案 OK/Cancel、标题 Enter Supporter Code、标签 Supporter Code:
  • TfrmSupporterCode_FormShow0xB0EE1A):若为安装版(非便携版)且当前非管理员(sub_7A45D0 为假且 off_C64830 未置位),提示"Setting the license key of the installed version of WizTree requires administrator access - please restart WizTree as administrator and try again."并发送 WM_CLOSE 关闭窗体;
  • 输入控件为 TMaskEdit edLicense(RTTI 0xF103E0),输入限制为十六进制字符。

5. [OK] 按钮处理:TfrmSupporterCode_btnOKClick(0xB0DC35)

5.1 完整处理流程

完整处理流程(伪代码,行为由反编译与汇编交叉确认):

btnOKClick(Self, Sender):
  code = edLicense.Text
  bad_format = (CheckSupporterCodeFormat_7A1AE0(code) == 0)   # 格式校验失败置位
  if not CheckHostsBlock_7A28F0():                            # hosts 拦截检测
      ShowMessage("Internal error ID: 10t86\r\rPlease contact software supplier")
      return
  machine = GetMachineCode_7A2100(&hwid)                      # 卷序列号 + 硬件ID
  if bad_format:
      ShowMessage("Invalid Key - Please try again.\r\r"
                  "It is recommended that you copy and paste your supporter code.\r\r"
                  "Only use digits 0 to 9 and letters A to F.")
      return
  # 解析代码分组 (1-4)+(6-9) -> A1; (11-14)+(16-19) -> A2; "$" 前缀使 StrToInt 按十六进制解析
  ParseSupporterCodeFields_7A18D0(A1, A2, &f1, &f2, &type, &days, &e)
  # 组包: 以 ',' 与 "5," 拼接: IntToStr(f1) "," IntToStr(f2) "5," "%8.8X"%machine "," IntToStr(hwid) "," "1"
  query = obfuscate_7A17A0(payload)          # XOR 混淆
  query  = HexEncodeString_7A14C0(query)     # 逐字符转十六进制
  if qword_D55150:                           # URL.DLL!InetIsOffline
      if qword_D55150(0):                    # 系统离线
          chk = ""                           # 跳过在线校验 (离线激活路径)
      else:
          chk = HTTP_GET("http://antibody-software.com/checkkey.php?reginfo=" + query)
  WriteString("chk", chk)
  x1 = A1 ^ machine;  x2 = A2 ^ machine
  if chk != "":                             # 有在线响应
      ParseChkResponse_7A2470(chk, &f1, &f2, &s3, &s4, &s5)
      if f1 != 1:                            # 服务器判无效
          WriteString("Key1",""); WriteString("Key2",""); WriteString("Key3","")
          ShowMessage("Registration failed. Reason = " + s5 + "\"")
          return
  if days != 0 and (int)GetBuildDate_B9CD10() > (int)(EncodeDate(2009,1,1) + days):
      ShowMessage("Sorry, your supporter code is only valid for versions of WizTree with "
                  "version date <许可日期> or earlier.\r\n"
                  "This version of WizTree has version date <构建日期>.\r\n\r\n"
                  "Please install an older version of WizTree with a suitable version date, "
                  "or purchase a new supporter code.")
      return
  # 保存 Key1/Key2/Key3 (机器绑定)
  WriteString("Key1", HexEncode("%4.4X%4.4X" % (x1)))
  WriteString("Key2", HexEncode("%4.4X%4.4X" % (x2)))
  WriteString("Key3", HexEncode(obfuscate_7A17A0(code 第21字符起)))
  if not 已置重启标志: ShowMessage("Thank you for your support!\r\rWizTree will now restart.")
  ModalResult = 1

关键点

  • 分组解析采用 "$" + 前4位 + 中4位StrToInt 的方式,$ 前缀使 Delphi 的 StrToInt 按十六进制解析,从而得到 32 位整数 A1/A2;
  • 在线判定完全依赖服务器 chk 响应字段 1 == 1;
  • 离线(InetIsOffline 为真)时跳过在线校验,直接进入版本日期校验并保存 Key;
  • Key1/Key2 用机器码(卷序列号)异或加密后再存储,实现机器绑定(见 §11.2)。

5.2 版本日期校验(关键汇编片段)

0xB0DBE0 起(btnOKClick 内):

addsd xmm0, xmm1                ; EncodeDate(2009,1,1) + 有效天数(double)
movsd [rbp+var_s2C0], xmm0      ; 保存许可支持版本日期
call sub_B9CD10                 ; 构建日期 2026-08-05 -> xmm0
call sub_40C8E0                 ; rax = (int)构建日期
mov  rbx, rax
movsd xmm0, [rbp+var_s2C0]
call sub_40C8E0                 ; rax = (int)许可支持版本日期
cmp  rbx, rax
jle  loc_B0DDC9                 ; 构建日期 <= 许可日期 -> 通过(保存Key)
; 否则显示 "Sorry, your supporter code is only valid for versions..."

注:有效天数 == 0 时跳过本校验(终身码不受版本日期限制)。


6. 在线校验与 chk 响应解析

6.1 在线查询包

btnOKClick 组包(明文,随后整体做 ObfuscateStr_Xor18i + HexEncodeString):

IntToStr(f1) + "," + IntToStr(f2) + "5," + Format('%8.8X', 机器码) + "," + IntToStr(hwid) + "," + "1"

请求:GET http://antibody-software.com/checkkey.php?reginfo=<上述包加密后>

6.2 ParseChkResponse_7A2470(0x7A2470)— chk 响应解析

void ParseChkResponse(chk, &f1, &f2, &s3, &s4, &s5) {
    s = HexDecodeString(chk);          // 逐字符 hex 解码
    s = ObfuscateStr_Xor18i(s);        // XOR 还原
    f1 = StrToInt(SplitStrByDelim(s, ','));   // 字段1: 是否有效 (1=有效)
    f2 = StrToInt(SplitStrByDelim(s, ','));
    s3 = HexDecodeString(SplitStrByDelim(s, ','));
    s4 = HexDecodeString(SplitStrByDelim(s, ','));
    s5 = HexDecodeString(SplitStrByDelim(s, ','));
}

响应为逗号分隔的 5 个字段;f1 == 1 表示注册码有效,否则清除已存 Key 并提示 Registration failed. Reason = <s5>"


7. 离线许可验证(激活关键路径)

7.1 离线放行条件(InetIsOffline)

btnOKClick 中,qword_D55150 指向 URL.DLL 导出的 InetIsOffline(由 InitInetIsOfflinePtr_B0EFC0(0xB0EFC0)在启动时加载):

qword_D55158 = LoadLibraryW("URL.DLL");
qword_D55150 = GetProcAddress(qword_D55158, "InetIsOffline");

流程判定:

if (qword_D55150 != NULL && qword_D55150(0) == TRUE)   // InetIsOffline 返回真(离线)
    chk = "";                                          // 跳过 HTTP 在线校验
else
    chk = HTTP_GET(...);                               // 在线校验

if (chk != "") { ... 服务器判定 ... }                   // 有响应才判定
if (!日期校验失败) { 保存 Key1/Key2/Key3; 成功 }

7.2 hosts 拦截检查 CheckHostsBlock_7A28F0 / ScanHostsForBlockedDomains_7A26B0

  • CheckHostsBlock_7A28F0(0x7A28F0):管理员(sub_458280()==1)时读 %WindowsDir%\hosts,否则读 %SystemDir%\drivers\etc\hosts
  • ScanHostsForBlockedDomains_7A26B0(0x7A26B0):逐行读取 hosts,剔除 # 注释后,若某行包含解密域名 antibody.za.netantibody-software.com,则判定被拦截(返回假);
  • 被拦截时 btnOKClick 直接弹 Internal error ID: 10t86 / Please contact software supplier无法进入离线激活

7.3 离线激活条件汇总

离线激活只需满足以下三个条件:

  1. CheckHostsBlock_7A28F0 通过(hosts 未把 antibody-software.com / antibody.za.net 指向本地,见 §7.2);
  2. CheckSupporterCodeFormat_7A1AE0 通过(39 字符 8 组 / 去连字符 32 字符且尾部校验和正确,见 §9.1);
  3. 版本日期校验通过:有效天数 == 0(终身)EncodeDate(2009,1,1)+有效天数 >= 构建日期(2026-08-05)

第三部分 核心算法

8. 机器码获取与许可字段解析

8.1 GetMachineCode_7A2100(0x7A2100)

int GetMachineCode(&hwid) {
    drive = 系统盘路径;                      // sub_40A050(0)
    GetVolumeInformationW(drive, ..., &VolumeSerialNumber, ...);
    if (成功) serial = VolumeSerialNumber; else serial = 0;
    if (off_C63498(drive, &x, &y, &z))      // 自定义硬件ID回调
        *hwid = y; else *hwid = 0;
    return serial;                           // 机器码 = 系统盘卷序列号
}

测试机实测:vol C: = 185B-EE8A(0x185BEE8A),与调试器在 ParseSupporterCodeFields_7A18D0 入口观察到的输入值完全一致(未注册时 A1 = 机器码 ^ 0)。

8.2 ParseSupporterCodeFields_7A18D0(0x7A18D0)

void ParseSupporterCodeFields(A1, A2, &f1, &f2, &users, &days, &e) {
    *f1    = byte3(A1)*256 + byte0(A1);
    *f2    = byte3(A2)*256 + byte0(A2);
    *users = byte2(A2)*256 + byte2(A1);      // 授权用户数: 999=Enterprise; N且有效天数!=0 → "N user license"
    *days  = byte1(A2)*256 + byte1(A1);
    *e     = *days;
    if (*days == 0) {                        // 本应默认"当前日期-2009-01-01", 但实际不生效
        v = (int)qword_C17E78;               // qword_C17E78 恒为 0(从未被初始化, 运行验证确认)
        if (v > 0)                           // 恒不成立
            *days = v - (int)EncodeDate(2009, 1, 1);
    }
}

注意:反编译中存在"*days == 0 时默认设为 当前日期-2009-01-01"的分支,但其数据源 qword_C17E78 在启动流程中从未被写入(0xC64430 的指针引用未对应任何写操作,xrefs 全程序仅 2 处:0x7A1967 的 movsd 读取与 0xC64430 的数据引用;x64dbg 运行确认其值恒为 0),故 (int)0.0 = 0v > 0 恒不成立,*days 实际保持 0。即有效天数 = 0 表示终身,不会被改写为当前日期。


9. 本地校验函数

9.1 CheckSupporterCodeFormat_7A1AE0(0x7A1AE0)— 格式校验

// 输入: 代码字符串(可带连字符)
sub_458EB0(transform, code, byte_7A1BE8=0x2D('-'), 0, 1);  // 移除全部 '-' 字符
s = transform;
if (Length(s) == 32) {
    tail4 = Copy(s, 29, 4);          // 第 29-32 字符
    s28   = Copy(s, 1, 28);          // 前 28 字符
    ok = (tail4 == CalcCodeSuffixChecksum_7A1A40(s28));
}
return ok;

即:去连字符后必须恰好 32 个十六进制字符,且末 4 位等于前 28 位的累加校验和。

9.2 CalcCodeSuffixChecksum_7A1A40(0x7A1A40)— 尾部 4 位校验和

v3 = 66;  v4 = 190;
for (i = 1..28) {
    v4 += code[i];  if (v4 > 0xFF) v4 -= 255;
    v3 += v4;       if (v3 > 0xFF) v3 -= 255;
}
result = Format('%04X', v4 + (v3 << 8));

Fletcher 式模 255 累加,初始值 (66, 190)。

9.3 CalcChecksumByte_7A19B0(0x7A19B0)— 校验和字节

char CalcChecksumByte(a1, a2, a3, a4) {
    v4 = a3 % 3;
    if ((a2 % 25) & 1)  v5 = a4 & (a1 >> (v4 & 0x3F));   // 奇: 与
    else                v5 = a4 | (a1 >> (v4 & 0x3F));   // 偶: 或
    return (char)(v5 ^ (a1 >> ((a2 % 25) & 0x3F)));
}

代码第 17-18 位:CalcChecksumByte(A1, 17, 5, 70);第 19-20 位:CalcChecksumByte(A1, 12, 9, 3)

9.4 ValidateCodeLocal_7A1BF0(0x7A1BF0)— 完整本地校验(About 页判定)

int ValidateCodeLocal(code, chkList) {
    if (!CheckSupporterCodeFormat_7A1AE0(code)) return 1;   // 格式错误
    s = code 去 '-' 后的字符串;
    // 1) 与 3 个硬编码代码比较 (off_C17E80, 见 §14.4)
    for (i = 0; i < 3; i++)
        if (s == 硬编码代码[i] 去'-') { result = 2; break; }
    A1 = StrToInt("$" + Copy(s,1,8));   // 第 1-8 字符
    A2 = StrToInt("$" + Copy(s,9,8));   // 第 9-16 字符
    if (chkList != NULL) {              // 与 chk 列表逐项比较(在线白名单)
        for (item in Split(chkList, ','))
            if (item 与 A1/A2 取反比较匹配) { result = 2; break; }
    }
    if (result != 2) {
        ok1 = (Copy(s,17,2) == CalcChecksumByte(A1, 17, 5, 70));
        ok2 = (Copy(s,19,2) == CalcChecksumByte(A1, 12, 9, 3));
        result = (ok1 && ok2) ? 0 : 3;
    }
    return result;   // 0=有效 1=格式错 2=硬编码/chk匹配 3=校验和错
}

注意:btnOKClick 不调用 ValidateCodeLocal_7A1BF0,只调用 CheckSupporterCodeFormat_7A1AE0;本地校验和仅用于 About 页显示与离线路径之外的口径。


10. 字符串混淆与编码

10.1 DeobfuscateStr_XorIndex3_7A22A0(0x7A22A0)— 静态字符串解密

对每个宽字符:result[i] = code[i] XOR (i & 3)(i 从 1 起始)。用于上述混淆字符串(URL、错误信息、hosts 域名)。

for (i = 1; i <= Length(s); i++)
    out += Chr(Ord(s[i]) ^ (i & 3));

10.2 ObfuscateStr_Xor18i_7A17A0(0x7A17A0)— 运行期混淆(双向可逆)

对每个宽字符(i 从 0 起始):result[i] = code[i] XOR ((18*i) % 255)。用于在线查询包和 Key3 存储。

for (i = 0; i < Length(s); i++)
    out += Chr(Ord(s[i]) ^ ((18 * i) % 255));

10.3 HexEncodeString_7A14C0 / HexDecodeString_7A15B0(0x7A14C0 / 0x7A15B0)

  • 编码:每字符 Format('%2.2X', Ord(c)) 拼接(1 字符 → 2 个十六进制字符);
  • 解码:每两字符十六进制 → 1 字节。

10.4 SplitStrByDelim_7A2390(0x7A2390)

按分隔符(dword_7A26AC = ',')切分:取首个分隔符前的子串,并消费原串。


第四部分 存储、显示与注册表

11. Key 存储与重建

11.1 存储位置

设置文件由 GetConfigIniPath_7A48A0(0x7A48A0)决定:

  • 命令行/注册表指定 configfolder<configfolder>\WizTree3.ini
  • 否则若 exe 同目录存在 WizTree3.ini → 便携模式,使用同目录文件;
  • 否则使用 %AppData%\WizTree3.ini

许可相关键位于 [frmWizTreeMain] 节(测试机实测确认):

Key1=...   Key2=...   Key3=...   chk=...   KeyValidUntil=...

11.2 写入(btnOKClick 成功路径)

x1 = A1 ^ 机器码;   x2 = A2 ^ 机器码;
Key1 = HexEncode(Format('%4.4X%4.4X', x1))     // 低16位 + 高16位
Key2 = HexEncode(Format('%4.4X%4.4X', x2))
Key3 = HexEncode(ObfuscateStr_Xor18i(代码第21字符起))
chk  = 服务器响应(离线为空串)

Format('%4.4X%4.4X') 中精度 4 会将每个值截断为 4 位十六进制,即分别取低、高 16 位。

11.3 重建:RebuildCodeFromStoredKeys_B94EB0(0xB94EB0)

machine = GetMachineCode_7A2100(&hwid)
a1 = HexDecode(Key1) 解析为 32 位  → 还原 A1 = a1 ^ machine
a2 = HexDecode(Key2) 解析为 32 位  → 还原 A2 = a2 ^ machine
k3 = ObfuscateStr_Xor18i(HexDecode(Key3))        // 还原第 21 字符起部分
code = Format('%4.4X-%4.4X-%4.4X-%4.4X-%s', A1, A2, k3)

重建结果与原始支持者代码一致(keygen 已做往返验证,见附录 A.4)。测试机未注册时(Key1/Key2 为空,按 0 解析),重建代码的 A1 = 机器码 = 0x185BEE8A,与调试器观察到的 ParseSupporterCodeFields_7A18D0 输入一致。


12. 安装版注册表镜像(ConfigOverride)

12.1 机制概述

安装版(非便携)WizTree 的设置除写入 INI 外,还会在管理员模式下把受白名单控制的设置项镜像到注册表:

HKLM\Software\Antibody Software\WizTree\ConfigOverride\<节名>\<值名>
  • 根键:HKEY_LOCAL_MACHINEmov rdx, 0x80000002,故写入需要管理员权限);
  • 节名:对应 JvAppStorage 节,例如 frmWizTreeMain
  • 值名:设置名,许可键 Key1Key2Key3chkKeyValidUntil 均在镜像白名单字符串数组(off_C5E848,共 128 项,包含 Key1/Key2/Key3/chk 等,地址 0xB5019C~0xB501E0)中
  • 值类型:字符串(TRegistry_WriteString,读取 INI 中同名值写入)。

12.2 写入流程 SaveAdminSettingsToRegistry_B83850(0xB83850)

if (便携标志 off_C64830)   → 便携版,不适用,退出
else if (IsAdminSettingsMode_7A45D0(1))   → 管理员模式
    TRegistry(RootKey=HKLM)
    if 存在 \Software\Antibody Software\WizTree\ConfigOverride:
        TRegistry_GetKeyNames → 对每个节名调用 MirrorKeySettingsToRegistry_B83570
        MirrorKeySettingsToRegistry_B83570:
            OpenKey(ConfigOverride\<节名>)
            TRegistry_GetValueNames → 对每个值名调用 WriteOneSettingToRegistry_B83190
            WriteOneSettingToRegistry_B83190:
                若 IsSettingInAdminMirrorWhitelist_B518A0(节,名) 为真:
                    读 INI 中同名值 → TRegistry_WriteString(值名, 值)
                否则: 删除该注册表值(readonly 不允许删除等)
        TJvFormPlacement_SetAppStoragePath(INI路径)   // 恢复存储
else → "Not in admin mode",不适用

调用点:

  • TfrmWizTreeMain_fstoreSavePlacement(0xB85044)——设置保存时(退出/改设置);
  • sub_B60930(0xB611A0)。

12.3 读取流程 LoadAdminSettingsFromRegistry_B52320(0xB52320)

start(程序启动,0xBA2115)阶段调用:

if (便携标志 off_C64830) → 便携版,不适用,退出
else:
    TRegistry(RootKey=HKLM) + TIniFile(GetConfigIniPath)
    if 存在 ConfigOverride:
        OpenKeyReadOnly(ConfigOverride)
        GetKeyNames → 对每个节名调用 LoadRegistryMirrorKey_B52190
        LoadRegistryMirrorKey_B52190:
            OpenKeyReadOnly(ConfigOverride\<节名>)
            GetValueNames → 对每个值名调用 LoadRegistryValueIntoIni_B51C20
            LoadRegistryValueIntoIni_B51C20:
                若白名单允许: TRegistry_ReadString(值名) → 写入 TIniFile(节,名,值)

即:注册表中的管理员设置会在启动时覆盖/合并进 INI(管理员强制设置对普通用户生效)。

12.4 与许可的关系

  • 安装版(非便携、非可移动盘)设置许可证时,TfrmSupporterCode_FormShow(0xB0EE1A)检测到非管理员模式即提示 "Setting the license key of the installed version of WizTree requires administrator access - please restart WizTree as administrator and try again." 并关闭窗体;
  • 以管理员运行时,激活成功路径写入的 Key1/Key2/Key3/chk/KeyValidUntil(先写 INI,随后由 fstoreSavePlacement 镜像)会出现在 HKLM\Software\Antibody Software\WizTree\ConfigOverride\frmWizTreeMain\ 下;
  • 因此安装版修改许可(尤其是为所有用户统一授权)需要管理员权限的根本原因是写入 HKLM 的 ConfigOverride 镜像。

说明:字符串 LicenseKey(0x82443D / 0xB40D11)经核对是第三方组件 TControlData(富文本类组件)RTTI 的发布字段名,与 WizTree 自身许可无关;WizTree 实际使用 Key1/Key2/Key3/chk/KeyValidUntil 作为许可设置键名。


13. About 页许可显示:UpdateLicenseDisplay_AboutPage_B95300(0xB95300)

sub_B95300(a1=主窗体):
  code = RebuildCodeFromStoredKeys_B94EB0(Key1, Key2, Key3)   // 重建代码
  A1 = "$"+Copy(code,1,4)+Copy(code,6,4) 解析为十六进制
  A2 = "$"+Copy(code,11,4)+Copy(code,16,4) 解析为十六进制
  ParseSupporterCodeFields_7A18D0(A1, A2, ...)
  ret = ValidateCodeLocal_7A1BF0(code, NULL)
  if (ret == 0):                       // 本地校验通过
      if (有效天数 != 0):
          KeyValidUntil = (int)(EncodeDate(2009,1,1) + 有效天数)   // 写入 INI
          if (构建日期 > KeyValidUntil):
              显示 "Supporter code not valid for this version"    // 过期
      else:
          KeyValidUntil = 0            // 终身
      // 读取 chk 并解析出注册名(字段3)后, 显示:
      //   "Thank you for your support! <名字>"
      // 用户数: 用户数==999 -> "Enterprise license" (企业不限用户)
      //         有效天数!=0 -> "<用户数> user license", "Upgrades until: <日期>"
      //         否则        -> "Supporter license", "Lifetime"

启动时 TfrmWizTreeMain_FormCreate(0xB7F74C,调用点 0xB7F74C)会调用本函数完成 About 页许可展示。


第五部分 暗桩与隐蔽校验

14. 暗桩与隐蔽校验分析

14.1 PE 完整性暗桩("EXE is corrupt please reinstall")

触发位置TfrmWizTreeMain_actScanExecute(扫描动作,0xB7572D):

cmp cs:dword_D55280, 1
jz  loc_B7576F               ; D55280 == 1 → 正常继续
; D55280 != 1 → 暗桩
lea rdx, <混淆串>            ; "DZF hq#cnpquqv#pmgbsd\"qeh lpt`nof"
call DeobfuscateStr_XorIndex3_7A22A0   ; → "EXE is corrupt please reinstall"
mov rcx, <消息>
call sub_787820              ; 弹出错误对话框
call sub_412540              ; System.Halt → 退出程序

标志来源TfrmWizTreeMain_FormCreate,0xB7EA90):

call TJclPeImage_GetCertificateList
mov eax, [rax+10h]           ; 证书列表对象 +0x10 (容量/计数)
mov dword_D55284, eax
cmp dword_D55284, 1
jnz skip                     ; 计数 != 1 → 跳过校验和
call TJclPeImage_VerifyCheckSum
movzx rax, al
mov dword_D55284, eax        ; 校验和结果
skip:
mov eax, dword_D55284
mov dword_D55280, eax        ; 供 actScanExecute 使用

触发条件(前置判定,0xB7EA81):

movapd xmm0, xmm6            ; Now()
subsd  xmm0, [rbp+var_sB0]   ; Now() - 文件日期(天)
call TDateTimeToInt_40C8E0
cmp rax, 1Eh                 ; 文件年龄 <= 30 天
jle  skip_check              ; 则跳过完整性检查

即:当程序文件时间距今超过 30 天时执行证书/校验和检查;校验和失败或证书计数非 1 时,dword_D55280 != 1,此后每次执行扫描动作都会弹出 "EXE is corrupt please reinstall" 并直接退出程序

影响:对二进制做字节级补丁(尤其是改动 .text 且未同步 PE Checksum)后,一旦文件年龄超过 30 天,程序将在扫描时自爆。破解/补丁时需注意同步修正 PE 校验和字段,或绕过 dword_D55280 判定。

14.2 hosts 拦截检测(在线校验前置闸门)

机制与离线路径中的检查完全相同(详见 §7.2)。从反制角度看,它的作用是:

  • 阻止通过修改 hosts 将 antibody-software.com / antibody.za.net 指向本地来"离线化"在线校验;
  • 被拦截时 btnOKClick 直接弹 Internal error ID: 10t86 / Please contact software supplier,无法进入离线激活。

14.3 离线/在线校验切换(InetIsOffline)

qword_D55150 = URL.DLL!InetIsOfflineInitInetIsOfflinePtr_B0EFC0 启动时加载)。btnOKClick 中 InetIsOffline()==TRUE(系统离线)→ 跳过在线 HTTP 校验,直接走本地校验+版本日期校验。这是离线激活得以成立的"后门"(详见 §7.1)。

14.4 硬编码支持代码列表

off_C17E80 中 3 个硬编码代码(去连字符后与用户代码比较,命中返回 2):

94F5-BCF6-F4F6-2CF7-64AC-9CF7-301D-A855
3A56-A3AE-39F4-A332-C3C5-833A-E3D8-4F1E
EEE7-EEF6-0E03-AEF7-CE89-4EF5-EEDC-5AAA

(注意:ValidateCodeLocal_7A1BF0 返回 2 时调用方按"不等于 0"处理,故这 3 个代码不会在 About 页显示许可信息。)

14.5 机器绑定

Key1/Key2 存储前与系统盘卷序列号异或(A ^ 机器码),换机器后即使复制 WizTree3.ini 也无法恢复出有效代码(About 页本地校验失败)。Key3 额外做 ObfuscateStr_Xor18i 混淆 + hex 编码。存储细节见 §11.2。

14.6 调试日志

InitDebugLog_7A42D0(0x7A42D0)初始化,TraceLogWrite_7A4020(0x7A4020)写入:

%LocalAppData%\WizTree3\DebugLogs\wiztreelog<yyyymmddhhnnss>.log

记录大量内部事件("FormCreate"、"SaveAdminSettingsToRegistry start" 等)。若 byte_C18424 被置位则启用,会暴露内部路径、注册表操作细节(信息泄露面)。

14.7 更新检查(无强制逻辑)

CheckForUpdates_B8D440GET http://antibody-software.com/files/wiztreeversion.php?ver=4.32(新装追加 &newinstall=1),更新 INI 中 LastUpdateCheck,仅提示更新,无强制退出/限制。

14.8 反调试说明

导入表未发现 IsDebuggerPresent / CheckRemoteDebuggerPresent / NtQueryInformationProcess / OutputDebugStringA 等典型反调试 API;存在 QueryPerformanceCounter(winmm 的 timeGetTime、kernel32 的 QueryPerformanceCounter),用于性能计时(扫描统计),未发现基于计时的反调试陷阱。


附录

附录 A. 离线注册码生成(keygen)与离线激活文件

A.1 生成算法(wiztree_keygen.py

脚本自动获取机器码(GetVolumeInformationW 读取系统盘卷序列号,与程序 GetMachineCode_7A2100 同 API);自动获取失败时会提示手工输入(vol C: 输出形如 185B-EE8A)。

给定 A1A2、自由区 8 位十六进制(默认 00000000):

G5   = '%02X%02X' % (CalcChecksumByte(A1,17,5,70), CalcChecksumByte(A1,12,9,3))
body = '%08X%08X' % (A1, A2) + G5 + free8            # 28 字符
code = body + CalcCodeSuffixChecksum_7A1A40(body)    # 32 字符
code = 每 4 位一组加 '-'                             # 39 字符

用户数与字段关系(脚本按档位自动计算 byte1(A1)/byte1(A2)/byte2(A1)/byte2(A2),其余字节可配置):

档位 字段取值 About 页显示
lifetime(默认) byte1(A1)=0byte1(A2)=0 → 有效天数=0 Supporter license - Lifetime
enterprise(终身) byte2(A1)=0xE7byte2(A2)=0x03 → 用户数=999;byte1 全 0 Enterprise license - Lifetime
enterprise --days N 用户数=999;byte1(A2)*256+byte1(A1)=N≥6425 Enterprise license + Upgrades until: <日期>
user --days N --typenum T 有效天数 N≥6425;用户数 T <T> user license + Upgrades until: <日期>

字段布局说明(byte0 为最低字节):

  • A1 = [byte3][byte2][byte1][byte0]A2 = [byte3][byte2][byte1][byte0]
  • 授权用户数 = byte2(A2)*256 + byte2(A1)(999 = Enterprise 不限用户;N 且有效天数非 0 = N user license;有效天数=0 时任意非 999 值为 Supporter)
  • 有效天数 = byte1(A2)*256 + byte1(A1)(自 2009-01-01,≥6425 才通过构建日期 2026-08-05 的版本校验)
  • 两者使用不同的字节(byte2 vs byte1),互不冲突,故 Enterprise 与终身可以同时成立user 用户数与天数也不互相约束。

命令行用法

python wiztree_keygen.py [lifetime|enterprise|user] [options]

  --days N         有效天数(自2009-01-01); user 必填; enterprise 留空=终身(0), 填写则限时
  --free8 HEX      自由区8位hex, 第21-28字符 (默认 00000000)
  --machine HEX    机器码8位hex (默认自动获取)
  --ini            输出 WizTree3.ini 追加内容
  --a1hi/--a1mid/--a1lo    A1 的 byte3/byte2/byte0 (默认 0x12/0x34/0x78)
  --a2hi/--a2mid/--a2lo    A2 的 byte3/byte2/byte0 (默认 0x9A/0xBC/0xF0)

示例:

python wiztree_keygen.py lifetime                # 终身 Supporter license - Lifetime
python wiztree_keygen.py enterprise --ini        # Enterprise license(终身) + 输出 INI 内容
python wiztree_keygen.py enterprise --days 6631  # Enterprise license 限时 (2027-02-27)
python wiztree_keygen.py user --days 6656 --typenum 5 --ini   # 5 user license
python wiztree_keygen.py enterprise --a1lo 42 --free8 01020304   # 自定义字段

A.2 图形界面(wiztree_keygen_gui.py

现代化深色图形界面,纯 Python 标准库 tkinter 实现,零第三方依赖(无需 pip install),直接运行:

python wiztree_keygen_gui.py

界面特性:

  • DPI 自适应SetProcessDpiAwareness + tk scaling,字体/布局/窗口随系统缩放自动适配(125%/150% 等),窗口不超出屏幕;
  • 左侧配置区可滚动:内容超出时出现滚动条,支持鼠标滚轮;
  • 输入校验(GUI/CLI 共用 parse_hex_value/parse_hex_byte/parse_int_value/parse_machine_code):非法输入给出具体错误并标红对应输入框;空值自动补默认(机器码空→自动获取,自由区空→00000000,A1/A2 字节空→默认,user 天数空→最小可过校验天数 6425);
  • 许可档位:分段按钮选择 lifetime / enterprise / user,选中后显示对应说明并自动联动字段(enterprise 锁定 byte2=0xE7/0x03 且用户数显示 999、有效天数留空=终身;user 显示"用户数"输入框;切换档位自动保存/恢复 byte2 与用户数配置);
  • 机器码:一键"自动获取"(与程序同 API 读取卷序列号)或手动填写;
  • 高级选项(可折叠):有效天数(含 ≥6425 提示)、user 用户数(999=Enterprise(不限用户) · 其它 N="N user license")、自由区、A1/A2 六个字节输入(每个带字节含义说明;byte1 由有效天数自动计算);
  • INI 自动写入:自动定位 WizTree3.ini(exe 同目录便携 > %AppData%),支持浏览选择;一键"写入 INI"将 Key1/Key2/Key3/chk/KeyValidUntil 以行级编辑方式写入 [frmWizTreeMain] 节(更新已有键、追加缺失键、保留文件其余内容与格式),写入前自动备份为 WizTree3.ini.bak,并弹出机器码绑定确认;
  • 生成后自动写入:勾选"生成后自动写入 INI"(开启时一次性确认机器码绑定),之后每次"生成注册码"成功即静默自动写入 INI(同样自动备份 .bak),状态栏提示结果,无需再点"写入 INI";
  • 生成结果:注册码、许可档位显示、有效期、本地校验/重建校验状态、Key1/Key2/Key3、INI 追加内容,一键复制。

A.3 离线激活文件(WizTree3.ini 预置)

对给定机器码 M(系统盘卷序列号,vol C: 可得):

Key1 = HexEncode(Format('%04X%04X', (A1^M)&0xFFFF, (A1^M)>>16))
Key2 = HexEncode(Format('%04X%04X', (A2^M)&0xFFFF, (A2^M)>>16))
Key3 = HexEncode(ObfuscateStr_Xor18i(代码第21字符起))
chk  = (空)
KeyValidUntil = 0   (终身码) / (int)(EncodeDate(2009,1,1)+有效天数)  (限时/Enterprise)

将该 5 个键追加到 WizTree3.ini[frmWizTreeMain] 节即可完成离线预注册(无需运行程序输入代码)。


附录 B. 函数地址映射表

B.1 许可与算法函数

静态地址 名称(IDA 已重命名) 功能
0x7A18D0 ParseSupporterCodeFields_7A18D0 解析代码字段(用户数/有效天数)
0x7A1AE0 CheckSupporterCodeFormat_7A1AE0 格式校验(32 字符 + 尾部校验和)
0x7A1A40 CalcCodeSuffixChecksum_7A1A40 尾部 4 位校验和
0x7A1BF0 ValidateCodeLocal_7A1BF0 完整本地校验(0=有效)
0x7A19B0 CalcChecksumByte_7A19B0 校验和字节
0x7A2100 GetMachineCode_7A2100 机器码(卷序列号 + 硬件ID)
0x7A22A0 DeobfuscateStr_XorIndex3_7A22A0 字符串解密 XOR(i&3)
0x7A17A0 ObfuscateStr_Xor18i_7A17A0 字符串混淆 XOR(18i%255)
0x7A14C0 HexEncodeString_7A14C0 逐字符 hex 编码
0x7A15B0 HexDecodeString_7A15B0 hex 解码
0x7A2390 SplitStrByDelim_7A2390 按分隔符切分
0x7A2470 ParseChkResponse_7A2470 解析服务器 chk 响应
0x7A28F0 CheckHostsBlock_7A28F0 hosts 拦截检测入口
0x7A26B0 ScanHostsForBlockedDomains_7A26B0 扫描 hosts 中拦截域名
0xB95300 UpdateLicenseDisplay_AboutPage_B95300 About 页许可显示
0xB94EB0 RebuildCodeFromStoredKeys_B94EB0 由 Key1/2/3 重建代码
0xB9CD10 GetBuildDate_B9CD10 构建日期 EncodeDate(2026,8,5)
0x40C8E0 TDateTimeToInt_40C8E0 TDateTime(double) 取整
0xB0EFC0 InitInetIsOfflinePtr_B0EFC0 加载 URL.DLL!InetIsOffline
0x7A48A0 GetConfigIniPath_7A48A0 确定 WizTree3.ini 路径
0xB0DC35 TfrmSupporterCode_btnOKClick [OK] 按钮处理(激活主流程)
0xB0ECA2 TfrmSupporterCode_FormCreate 窗体初始化
0xB0EE1A TfrmSupporterCode_FormShow 窗体显示(管理员检查)
0xB7F74C TfrmWizTreeMain_FormCreate 主窗体创建(调用许可显示、PE 完整性检查)

B.2 安装版管理员设置 / 注册表相关

静态地址 名称(IDA 已重命名) 功能
0xB83850 SaveAdminSettingsToRegistry_B83850 安装版+管理员时将设置(含 Key1/2/3/chk/KeyValidUntil)镜像到 HKLM ConfigOverride
0xB52320 LoadAdminSettingsFromRegistry_B52320 启动时读取 HKLM ConfigOverride 并写入 INI
0xB83570 MirrorKeySettingsToRegistry_B83570 镜像单个节(如 frmWizTreeMain)到 ConfigOverride<节>
0xB83190 WriteOneSettingToRegistry_B83190 写/删单个注册表值(白名单过滤)
0xB52190 LoadRegistryMirrorKey_B52190 读取 ConfigOverride<节> 下的值
0xB51C20 LoadRegistryValueIntoIni_B51C20 将注册表值写入 INI(管理员设置生效)
0xB518A0 IsSettingInAdminMirrorWhitelist_B518A0 白名单判定(off_C5E848 的 128 个设置名)
0xB516C0 BuildAdminMirrorWhitelist_B516C0 构建管理员镜像白名单
0x7A45D0 IsAdminSettingsMode_7A45D0 判定是否可写管理员设置(管理员/可移动盘)
0x7A2E90 EnumerateSystemDrives_7A2E90 枚举系统盘(含 NoDrives 策略过滤)
0x7A4020 TraceLogWrite_7A4020 调试跟踪日志写入
0x7A42D0 InitDebugLog_7A42D0 初始化调试日志(DebugLogs 目录)
0xB8D440 CheckForUpdates_B8D440 检查更新(version.php)

py

离线使用,在线不可用!

wiztree_keygen.py

# -*- coding: utf-8 -*-
"""
WizTree 4.32 (x64) 离线支持者代码生成器 / WizTree3.ini 预置生成
================================================================
基于对 WizTree64.exe 许可校验逻辑的静态分析 (详见 WizTree_License_Analysis.md):

离线激活路径 (TfrmSupporterCode_btnOKClick @ 0xB0DC35):
  1) CheckHostsBlock_7A28F0  - hosts 未拦截 antibody-software.com
  2) CheckSupporterCodeFormat_7A1AE0 - 去连字符后 32 字符, 且 chars29-32 ==
     CalcCodeSuffixChecksum_7A1A40(chars1-28)
  3) 机器码 GetMachineCode_7A2100 (系统盘卷序列号)
  4) ParseSupporterCodeFields_7A18D0 -> 有效天数字段 = byte1(A2)*256+byte1(A1)
  5) URL.DLL!InetIsOffline() 为真(离线) -> 跳过在线 HTTP 校验
  6) 版本日期校验: 构建日期(2026-08-05) <= EncodeDate(2009,1,1)+有效天数
     (有效天数 == 0 时视为终身, 直接通过)
  7) 保存 Key1/Key2/Key3 到 WizTree3.ini [frmWizTreeMain]

代码格式: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX (8 组 4 位十六进制)
  G1G2 = A1 (第1-8字符)      G3G4 = A2 (第9-16字符)
  G5   = csumA(2) + csumB(2)  G6G7 = 自由区(第21-28字符, 存入 Key3)
  G8   = 校验和(第29-32字符)

校验和:
  csumA = CalcChecksumByte_7A19B0(A1, 17, 5, 70) = (70 & (A1>>2)) ^ (A1>>17)  低字节
  csumB = CalcChecksumByte_7A19B0(A1, 12, 9, 3)  = (3 | A1) ^ (A1>>12)         低字节
  G8    = CalcCodeSuffixChecksum_7A1A40(chars1-28) (Fletcher 式累加, v3=66 v4=190 mod 255)

存储 (INI [frmWizTreeMain], 机器绑定):
  Key1 = hex(ObfuscateStr_Xor18i 之前) 的 8 位hex 为 "%4.4X%4.4X" % (A1 ^ 机器码)
  Key2 = "%4.4X%4.4X" % (A2 ^ 机器码) 再逐字符 hex 编码
  Key3 = ObfuscateStr_Xor18i(chars21+) 再逐字符 hex 编码
"""

# ---------------- 机器码(卷序列号)获取 ----------------

def get_system_drive():
    """获取系统盘根路径 (如 C:\\), 与 GetMachineCode_7A2100 使用的 sub_40A050(0) 一致"""
    import os
    d = os.environ.get('SystemDrive', '')
    if d:
        return d.rstrip('\\') + '\\'
    # 回退: 用 Windows 目录所在盘
    windir = os.environ.get('WINDIR', 'C:\\Windows')
    return windir[:2] + '\\'

def get_volume_serial(drive=None):
    """通过 GetVolumeInformationW 获取卷序列号 (与 GetMachineCode_7A2100 相同 API)"""
    drive = drive or get_system_drive()
    try:
        import ctypes
        buf = ctypes.create_unicode_buffer(261)
        fsname = ctypes.create_unicode_buffer(261)
        serial = ctypes.c_uint32(0)
        ok = ctypes.windll.kernel32.GetVolumeInformationW(
            ctypes.c_wchar_p(drive), buf, 261,
            ctypes.byref(serial), None, None, fsname, 261)
        if ok:
            return serial.value
    except Exception:
        pass
    return None

def resolve_machine_code():
    """自动获取机器码(卷序列号); 失败时提示用户手工输入 (格式如 185BEE8A 或 185B-EE8A)"""
    serial = get_volume_serial()
    if serial is not None:
        return serial
    print('无法自动获取卷序列号。')
    print('请在 cmd 中运行  vol C:  查看卷序列号(形如 185B-EE8A), 然后手工输入。')
    while True:
        s = input('请输入卷序列号 (hex, 可含连字符, 如 185BEE8A): ').strip().upper()
        s = s.replace('-', '')
        try:
            if len(s) == 8:
                return int(s, 16)
        except ValueError:
            pass
        print('格式无效, 请重新输入 8 位十六进制数。')

# ---------------- 与二进制一致的算法实现 ----------------

def calc_checksum_19B0(a1, a2, a3, a4):
    """sub_7A19B0: char CalcChecksumByte(a1,a2,a3,a4)"""
    v4 = a3 % 3
    if (a2 % 0x19) & 1:
        v5 = a4 & (a1 >> (v4 & 0x3F))
    else:
        v5 = a4 | (a1 >> (v4 & 0x3F))
    return (v5 ^ (a1 >> ((a2 % 0x19) & 0x3F))) & 0xFF

def calc_suffix_1A40(s):
    """sub_7A1A40: 4 位十六进制累加和 (v3=66, v4=190, >255 时减 255)"""
    v3, v4 = 66, 190
    for ch in s:
        v4 += ord(ch)
        if v4 > 0xFF:
            v4 -= 255
        v3 += v4
        if v3 > 0xFF:
            v3 -= 255
    return '%04X' % (v4 + (v3 << 8))

def obfuscate_7A17A0(s):
    """sub_7A17A0: 逐字符 XOR (18*i) % 255 (i 从 0 开始), 双向可逆"""
    return ''.join(chr(ord(c) ^ ((18 * i) % 255)) for i, c in enumerate(s))

def hex_encode(s):
    """sub_7A14C0: 每字符格式化为 %2.2X 后拼接"""
    return ''.join('%02X' % ord(c) for c in s)

def hex_decode(s):
    """sub_7A15B0: 两字符十六进制 -> 一字节"""
    return ''.join(chr(int(s[i:i+2], 16)) for i in range(0, len(s), 2))

# ---------------- 生成与校验 ----------------

def validate_local(code):
    """模拟 ValidateCodeLocal_7A1BF0 (v44=0): 0=有效"""
    s = code.replace('-', '')
    if len(s) != 32:
        return 1
    if s[28:32] != calc_suffix_1A40(s[:28]):
        return 1
    a1 = int(s[0:8], 16)
    if s[16:18] != '%02X' % calc_checksum_19B0(a1, 17, 5, 70):
        return 3
    if s[18:20] != '%02X' % calc_checksum_19B0(a1, 12, 9, 3):
        return 3
    return 0

def make_code(a1, a2, free8='00000000'):
    """生成 39 字符支持者代码 (8 组 4 位十六进制)"""
    g5 = '%02X%02X' % (calc_checksum_19B0(a1, 17, 5, 70),
                       calc_checksum_19B0(a1, 12, 9, 3))
    body28 = '%08X%08X%s%s' % (a1, a2, g5, free8)
    s = body28 + calc_suffix_1A40(body28)
    code = '-'.join(s[i:i+4] for i in range(0, 32, 4))
    assert validate_local(code) == 0, validate_local(code)
    return code

# 构建日期 2026-08-05 —— 版本日期校验的最小有效天数基准
import datetime
MIN_VALID_DAYS = datetime.date(2026, 8, 5)

def _days_since_2009_01_01(d):
    return (d - datetime.date(2009, 1, 1)).days

def _license_max_date(days):
    return datetime.date(2009, 1, 1) + datetime.timedelta(days=days)

def compute_type_bytes(lictype, days=None, a1mid=0x34, a2mid=0xBC, typenum=None):
    """按许可档位计算字节字段:
    返回 (byte1(A1), byte1(A2), byte2(A1), byte2(A2))
    有效天数 = byte1(A2)*256 + byte1(A1)
    授权用户数 = byte2(A2)*256 + byte2(A1)   (999 = Enterprise)
    """
    min_days = _days_since_2009_01_01(MIN_VALID_DAYS)   # 6425
    if lictype == 'lifetime':
        return (0x00, 0x00, a1mid & 0xFF, a2mid & 0xFF)  # 有效天数=0(终身), 用户数任意(≠999)
    if lictype == 'enterprise':
        b2a, b2b = 0xE7, 0x03              # 用户数 = 3*256 + 0xE7 = 999
        if not days:                       # 终身 Enterprise (有效天数=0)
            return (0x00, 0x00, b2a, b2b)
        if days < min_days:
            raise ValueError('有效天数需 >= %d (构建日期 2026-08-05)' % min_days)
        b1b, b1a = divmod(days, 256)
        return (b1a, b1b, b2a, b2b)
    if lictype == 'user':
        d = days if days and days > 0 else min_days
        if d < min_days:
            raise ValueError('有效天数需 >= %d (构建日期 2026-08-05)' % min_days)
        b1b, b1a = divmod(d, 256)
        if typenum is not None:            # 用户数 = byte2(A2)*256 + byte2(A1)
            b2a, b2b = typenum & 0xFF, (typenum >> 8) & 0xFF
        else:
            b2a, b2b = 0, 0                # 默认用户数 0 (About 页显示 "0 user license")
        return (b1a, b1b, b2a, b2b)
    raise ValueError('未知许可档位: %s' % lictype)

def parse_field_bytes(s):
    """解析 8 位 hex 字符串为 int"""
    s = s.strip()
    return int(s, 16) if s.lower().startswith('0x') else int(s, 16)

def make_code_by_fields(lictype, days=None, free8='00000000', typenum=None,
                        a1hi=0x12, a1mid=0x34, a1lo=0x78,
                        a2hi=0x9A, a2mid=0xBC, a2lo=0xF0):
    """按许可档位与可配置字段生成支持者代码。
    字段布局 (byte0 最低位):
      A1 = [a1hi(byte3)] [byte2] [byte1] [a1lo(byte0)]
      A2 = [a2hi(byte3)] [byte2] [byte1] [a2lo(byte0)]
      byte1(A1)/byte1(A2) = 有效天数, byte2(A1)/byte2(A2) = 授权用户数(999=Enterprise)
    enterprise: byte2 固定 (0xE7, 0x03) -> 用户数 999; 不填 days 即终身
    user: byte2 由 typenum 决定 (默认 0 -> 用户数 0)
    lifetime: byte2 取 a1mid/a2mid (自由)
    """
    b1a, b1b, b2a, b2b = compute_type_bytes(lictype, days, a1mid, a2mid, typenum)
    a1 = ((a1hi & 0xFF) << 24) | (b2a << 16) | ((b1a & 0xFF) << 8) | (a1lo & 0xFF)
    a2 = ((a2hi & 0xFF) << 24) | (b2b << 16) | ((b1b & 0xFF) << 8) | (a2lo & 0xFF)
    return make_code(a1, a2, free8)

def describe_code(code):
    """解析代码字段并生成人类可读描述"""
    s = code.replace('-', '')
    a1 = int(s[0:8], 16); a2 = int(s[8:16], 16)
    usercount = ((a2 >> 16) & 0xFF) * 256 + ((a1 >> 16) & 0xFF)
    days = ((a2 >> 8) & 0xFF) * 256 + ((a1 >> 8) & 0xFF)
    if usercount == 999:
        kind = 'Enterprise license'
    elif days != 0:
        kind = '%d user license' % usercount
    else:
        kind = 'Supporter license'
    if days == 0:
        valid = 'Lifetime'
    else:
        valid = 'Upgrades until: %s' % _license_max_date(days)
    return kind, valid, days

def build_keys(code, machine):
    """根据机器码生成 Key1/Key2/Key3 (写入 WizTree3.ini [frmWizTreeMain])
    Delphi Format('%4.4X%4.4X') 的精度=4 会截断到 4 位, 故显式取低/高 16 位"""
    s = code.replace('-', '')
    a1 = int(s[0:8], 16)
    a2 = int(s[8:16], 16)
    k1 = ((a1 ^ machine) & 0xFFFF, ((a1 ^ machine) >> 16) & 0xFFFF)
    k2 = ((a2 ^ machine) & 0xFFFF, ((a2 ^ machine) >> 16) & 0xFFFF)
    key1 = hex_encode('%04X%04X' % k1)
    key2 = hex_encode('%04X%04X' % k2)
    key3 = hex_encode(obfuscate_7A17A0(code[20:]))   # chars21+ (0-based 20)
    return key1, key2, key3

def build_ini(code, machine, valid_days=0):
    """生成 WizTree3.ini 许可段内容 (追加到 [frmWizTreeMain])"""
    k1, k2, k3 = build_keys(code, machine)
    lines = ['[frmWizTreeMain]', 'Key1=%s' % k1, 'Key2=%s' % k2, 'Key3=%s' % k3,
             'chk=', 'KeyValidUntil=%d' % valid_days]
    return '\r\n'.join(lines)

def rebuild_code(key1, key2, key3, machine):
    """模拟 RebuildCodeFromStoredKeys_B94EB0: Key1/2 与机器码异或还原代码"""
    def parse_key(k):
        s = hex_decode(k)              # 逐字符 hex 解码
        lo = int(s[0:4], 16)           # 前 4 位 = 低 16 位
        hi = int(s[4:8], 16)           # 后 4 位 = 高 16 位
        return (hi << 16) | lo
    a1 = parse_key(key1) ^ machine
    a2 = parse_key(key2) ^ machine
    k3 = obfuscate_7A17A0(hex_decode(key3))   # XOR 两次还原
    return '%04X-%04X-%04X-%04X-%s' % (a1 >> 16, a1 & 0xFFFF,
                                       a2 >> 16, a2 & 0xFFFF, k3)

# ---------------- 输入校验 (GUI/CLI 共用) ----------------

def parse_hex_value(s, name, default=None, lo=0, hi=0xFFFFFFFF):
    """解析十六进制输入: 空串/None 返回 default; 非法或越界抛 ValueError"""
    s = (s or "").strip()
    if not s:
        if default is None:
            raise ValueError("%s 不能为空" % name)
        return default
    try:
        v = int(s, 16)
    except ValueError:
        raise ValueError("%s 必须是十六进制数 (如 1F 或 0x1F)" % name)
    if not (lo <= v <= hi):
        raise ValueError("%s 范围应为 0x%X ~ 0x%X" % (name, lo, hi))
    return v

def parse_hex_byte(s, name, default):
    """解析单个字节 (0x00-0xFF), 空则用 default"""
    return parse_hex_value(s, name, default=default, lo=0, hi=0xFF)

def parse_int_value(s, name, default=None, lo=0, hi=0x7FFFFFFF):
    """解析十进制整数: 空串/None 返回 default; 非法或越界抛 ValueError"""
    s = (s or "").strip()
    if not s:
        if default is None:
            raise ValueError("%s 不能为空" % name)
        return default
    try:
        v = int(s, 10)
    except ValueError:
        raise ValueError("%s 必须是整数" % name)
    if not (lo <= v <= hi):
        raise ValueError("%s 范围应为 %d ~ %d" % (name, lo, hi))
    return v

def parse_machine_code(s):
    """机器码: 8 位十六进制 (允许连字符); 空返回 None (由调用方自动获取)"""
    s = (s or "").replace("-", "").strip()
    if not s:
        return None
    if len(s) != 8:
        raise ValueError("机器码必须是 8 位十六进制 (如 185BEE8A)")
    try:
        v = int(s, 16)
    except ValueError:
        raise ValueError("机器码必须是 8 位十六进制 (如 185BEE8A)")
    return v

# ---------------- WizTree3.ini 自动写入 ----------------

def find_wiztree_ini():
    """定位 WizTree3.ini (与程序 GetConfigIniPath_7A48A0 一致):
    优先 exe 同目录(便携), 其次 %AppData%\\WizTree3.ini, 最后当前目录"""
    import os
    here = os.path.dirname(os.path.abspath(__file__))
    cands = [os.path.join(here, 'WizTree3.ini')]
    appdata = os.environ.get('APPDATA', '')
    if appdata:
        cands.append(os.path.join(appdata, 'WizTree3.ini'))
    cands.append(os.path.join(os.getcwd(), 'WizTree3.ini'))
    for c in cands:
        if os.path.isfile(c):
            return c
    return cands[0]                  # 默认 exe 同目录

def _read_ini_text(path):
    """读取 INI, 自动识别编码 (utf-8-sig / 系统 ANSI)"""
    for enc in ('utf-8-sig', 'mbcs'):
        try:
            with open(path, 'r', encoding=enc) as f:
                return f.read(), enc
        except (UnicodeDecodeError, UnicodeError):
            continue
    with open(path, 'r', encoding='mbcs', errors='replace') as f:
        return f.read(), 'mbcs'

def apply_keys_to_ini(path, keys, backup=True):
    """在 [frmWizTreeMain] 节更新/追加许可键 (行级编辑, 保留文件其余内容与格式)。
    keys: 有序 dict {键名: 值}; 值 None 表示删除该键。
    返回写入后的路径; 写入前自动备份为 <path>.bak"""
    import os
    import shutil
    if backup and os.path.isfile(path):
        shutil.copy2(path, path + '.bak')
    if os.path.isfile(path):
        text, enc = _read_ini_text(path)
        lines = text.splitlines()
    else:
        lines, enc = [], 'mbcs'
        os.makedirs(os.path.dirname(path), exist_ok=True)

    # 定位 [frmWizTreeMain] 节
    sec = -1
    for i, ln in enumerate(lines):
        if ln.strip().lower() == '[frmwiztreemain]':
            sec = i
            break
    if sec == -1:
        if lines and lines[-1].strip():
            lines.append('')
        lines.append('[frmWizTreeMain]')
        sec = len(lines)
        lines.append('')
    # 节结束位置
    end = len(lines)
    for i in range(sec + 1, len(lines)):
        s = lines[i].lstrip()
        if s.startswith('[') and s.rstrip().endswith(']'):
            end = i
            break

    remaining = dict(keys)
    updated = set()
    new_body = []
    for ln in lines[sec + 1:end]:
        if '=' in ln and not ln.lstrip().startswith((';', '#')):
            k = ln.split('=', 1)[0].strip()
            if k.lower() in {x.lower() for x in remaining}:
                real = [x for x in remaining if x.lower() == k.lower()][0]
                v = remaining[real]
                if v is None:
                    continue          # 删除该键
                new_body.append('%s=%s' % (real, v))
                updated.add(real)
                continue
        new_body.append(ln)
    for k, v in remaining.items():
        if k not in updated and v is not None:
            new_body.append('%s=%s' % (k, v))

    lines[sec + 1:end] = new_body
    with open(path, 'w', encoding=enc, newline='') as f:
        f.write('\r\n'.join(lines))
        f.write('\r\n')
    return path

def build_lic_ini_keys(code, machine, valid_days=0):
    """由代码/机器码生成 {键名: 值} 有序字典, 供 apply_keys_to_ini 使用"""
    k1, k2, k3 = build_keys(code, machine)
    return [('Key1', k1), ('Key2', k2), ('Key3', k3),
            ('chk', ''), ('KeyValidUntil', str(valid_days))]

def _main():
    import argparse
    p = argparse.ArgumentParser(
        prog='wiztree_keygen.py',
        description='WizTree 4.32 x64 离线支持者代码生成器 / WizTree3.ini 预置生成',
        epilog='示例:\n'
               '  python wiztree_keygen.py lifetime                 # 终身 Supporter license (默认)\n'
               '  python wiztree_keygen.py enterprise --ini         # Enterprise license + 输出 INI\n'
               '  python wiztree_keygen.py user --days 6656 --ini   # 限时 user license\n'
               '  python wiztree_keygen.py lifetime --machine 185BEE8A\n'
               '  python wiztree_keygen.py enterprise --a1lo 42 --free8 01020304',
        formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument('lictype', nargs='?', default='lifetime',
                   choices=['lifetime', 'enterprise', 'user'],
                   help='许可档位 (默认 lifetime)')
    p.add_argument('--days', type=int, default=None,
                   help='有效天数(自2009-01-01起); user 必填, enterprise 留空=终身')
    p.add_argument('--typenum', type=int, default=None,
                   help='user 授权用户数 (999=Enterprise, 其它N="N user license"; 默认 0)')
    p.add_argument('--free8', default='00000000', help='自由区8位hex, 第21-28字符 (默认 00000000)')
    p.add_argument('--machine', default=None, help='机器码8位hex (默认自动获取)')
    p.add_argument('--ini', action='store_true', help='输出 WizTree3.ini 追加内容')
    p.add_argument('--a1hi', default='0x12', help='A1 byte3 (默认 0x12)')
    p.add_argument('--a1mid', default='0x34', help='A1 byte2 (默认 0x34)')
    p.add_argument('--a1lo', default='0x78', help='A1 byte0 (默认 0x78)')
    p.add_argument('--a2hi', default='0x9A', help='A2 byte3 (默认 0x9A)')
    p.add_argument('--a2mid', default='0xBC', help='A2 byte2, 参与用户数计算 (默认 0xBC)')
    p.add_argument('--a2lo', default='0xF0', help='A2 byte0 (默认 0xF0)')
    a = p.parse_args()

    # 输入校验: 非法输入抛 ValueError (附具体提示), 空值自动补默认
    machine = parse_machine_code(a.machine)
    if machine is None:
        machine = resolve_machine_code()
    days = (parse_int_value(str(a.days), "有效天数", lo=0, hi=65535)
            if a.days is not None else None)
    typenum = (parse_int_value(str(a.typenum), "用户数", lo=0, hi=65535)
               if a.typenum is not None else None)
    free8 = "%08X" % parse_hex_value(a.free8, "自由区", default=0, lo=0, hi=0xFFFFFFFF)
    print('机器码(卷序列号) = 0x%08X' % machine)
    print('许可档位        = %s' % a.lictype)

    code = make_code_by_fields(
        a.lictype, days=days, free8=free8, typenum=typenum,
        a1hi=parse_hex_byte(a.a1hi, "A1 byte3", 0x12),
        a1mid=parse_hex_byte(a.a1mid, "A1 byte2", 0x34),
        a1lo=parse_hex_byte(a.a1lo, "A1 byte0", 0x78),
        a2hi=parse_hex_byte(a.a2hi, "A2 byte3", 0x9A),
        a2mid=parse_hex_byte(a.a2mid, "A2 byte2", 0xBC),
        a2lo=parse_hex_byte(a.a2lo, "A2 byte0", 0xF0))

    kind, valid, days = describe_code(code)
    print('\n[生成代码] %s' % code)
    print('许可显示      : %s' % kind)
    print('有效期        : %s' % valid)
    print('有效天数      : %d%s' % (days, '' if days else ' (终身)'))
    print('本地校验      :', '通过' if validate_local(code) == 0 else '失败')

    k1, k2, k3 = build_keys(code, machine)
    print('Key1 =', k1)
    print('Key2 =', k2)
    print('Key3 =', k3)
    rebuilt = rebuild_code(k1, k2, k3, machine)
    print('重建码(模拟 sub_B94EB0):', rebuilt)
    print('重建码 == 原码:', rebuilt == code)

    if a.ini:
        print('\n--- WizTree3.ini 追加内容 ---')
        print(build_ini(code, machine, valid_days=days))

if __name__ == '__main__':
    _main()

wiztree_keygen_gui.py

# -*- coding: utf-8 -*-
"""
WizTree 离线注册码生成器 - 图形界面版 (纯标准库 tkinter, 零第三方依赖)
======================================================================
- DPI 自适应: SetProcessDpiAwareness + tk scaling, 随系统缩放自动适配字体/布局
- 左侧配置区可滚动 (Canvas + Scrollbar + 滚轮)
- 复用 wiztree_keygen.py 的许可算法
运行:  python wiztree_keygen_gui.py
"""

import sys
import os
import ctypes
import tkinter as tk
from tkinter import messagebox, filedialog

# 允许直接运行本文件时导入同目录的 wiztree_keygen
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from wiztree_keygen import (
    get_volume_serial, resolve_machine_code,
    make_code_by_fields, validate_local, describe_code,
    build_keys, build_ini, rebuild_code, MIN_VALID_DAYS,
    _days_since_2009_01_01, _license_max_date,
    find_wiztree_ini, apply_keys_to_ini, build_lic_ini_keys,
    parse_hex_value, parse_hex_byte, parse_int_value, parse_machine_code,
)

MIN_DAYS = _days_since_2009_01_01(MIN_VALID_DAYS)   # 6425


def _get_dpi_scale():
    """获取系统 DPI 缩放比例 (96 = 100%)。失败时返回 1.0"""
    try:
        if sys.platform == "win32":
            try:
                ctypes.windll.shcore.SetProcessDpiAwareness(1)   # System DPI aware
            except Exception:
                ctypes.windll.user32.SetProcessDPIAware()
            hdc = ctypes.windll.user32.GetDC(0)
            dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88)     # LOGPIXELSX
            ctypes.windll.user32.ReleaseDC(0, hdc)
            if dpi > 0:
                return dpi / 96.0
    except Exception:
        pass
    return 1.0


DPI_SCALE = _get_dpi_scale()

# ---------- 配色 (深色现代化) ----------
C = dict(
    bg="#1e1e2e", card="#27273a", card2="#30304a",
    entry="#171724",            # 启用输入框: 深色"输入槽" (比卡片暗)
    entry_dis="#3c3c56",        # 禁用输入框: 灰亮化 (明显"灰掉")
    entry_dis_fg="#6b6b82",     # 禁用输入框文字: 淡灰
    text="#e4e4e7", sub="#9a9ab0", accent="#3b8ed0", accent_d="#2a6f9e",
    line="#4a4a6a", scroll="#4a4a66",
)

TYPE_INFO = {
    "lifetime": "终身支持者许可\nAbout 页显示: Supporter license - Lifetime\n无到期日, 不受版本日期限制",
    "enterprise": "企业版许可 (用户数=999, 不限用户)\nAbout 页显示: Enterprise license\n留空\"有效天数\"= 终身 Enterprise",
    "user": "限时用户授权\nAbout 页显示: <用户数> user license\n用户数: 999=Enterprise(不限) · 其它 N=\"N user license\"\n需填写\"有效天数\"(>=6425 天)",
}


def _mkfont(size, bold=False, family=None):
    """字体按 DPI 缩放 (point 由 tk scaling 统一放大, 这里只做基础尺寸)"""
    fam = family or "Microsoft YaHei"
    return (fam, size, "bold" if bold else "normal")


class WizTreeKeygenApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.scale = DPI_SCALE
        # tk scaling: 让 point 字体/布局随系统 DPI 自动放大
        try:
            self.tk.call("tk", "scaling", self.scale * 96.0 / 72.0)
        except tk.TclError:
            pass
        self.title("WizTree 离线注册码生成器")
        # 窗口尺寸: 按 DPI 缩放, 且不超过屏幕 92%/90%
        sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
        w = min(int(1080 * self.scale), int(sw * 0.92))
        h = min(int(820 * self.scale), int(sh * 0.9))
        self.geometry("%dx%d" % (w, h))
        self.minsize(int(820 * self.scale), int(600 * self.scale))
        self.configure(bg=C["bg"])
        self.advanced_open = False
        self._err_entries = []
        self._last_gen = None
        self._last_output = ""
        self._cur_type = "lifetime"
        self._saved_mid = None        # 切到 enterprise 前保存的 a1mid/a2mid 原值
        self._saved_typenum = None    # 切到 enterprise 前保存的用户数原值
        self._build_ui()
        self._init_machine_default()
        self._on_type_change("lifetime")

    # ---------------- 控件工厂 ----------------

    def _card(self, parent, color=None):
        return tk.Frame(parent, bg=color or C["card"], padx=16, pady=14)

    def _label(self, parent, text, size=10, bold=False, color=None, anchor="w"):
        return tk.Label(parent, text=text, bg=parent["bg"], fg=color or C["text"],
                        font=_mkfont(size, bold), anchor=anchor, justify="left")

    def _entry(self, parent, width=None):
        e = tk.Entry(parent, bg=C["entry"], fg=C["text"], insertbackground=C["text"],
                     relief="flat", font=_mkfont(9, family="Consolas"),
                     highlightthickness=1, highlightbackground=C["line"],
                     highlightcolor=C["accent"],
                     disabledbackground=C["entry_dis"],
                     disabledforeground=C["entry_dis_fg"])
        if width:
            e.configure(width=width)
        return e

    def _button(self, parent, text, cmd, accent=False, width=None, height=1):
        bg = C["accent"] if accent else C["card2"]
        fg = "#ffffff" if accent else C["text"]
        hover = C["accent_d"] if accent else C["entry"]
        b = tk.Button(parent, text=text, command=cmd, bg=bg, fg=fg,
                      activebackground=hover, activeforeground="#ffffff",
                      relief="flat", bd=0, font=_mkfont(9, accent),
                      cursor="hand2", highlightthickness=0,
                      padx=10, pady=2)
        if width:
            b.configure(width=width)
        if height > 1:
            b.configure(height=height)
        return b

    # ---------------- UI 构建 ----------------

    def _build_ui(self):
        # 标题
        tk.Label(self, text="WizTree 离线注册码生成器", bg=C["bg"], fg=C["text"],
                 font=_mkfont(18, True)).pack(anchor="w", padx=22, pady=(12, 0))
        tk.Label(self, text="基于 WizTree 4.32 x64 许可算法逆向分析 · 机器绑定 Key1/Key2/Key3 生成",
                 bg=C["bg"], fg=C["sub"], font=_mkfont(9)).pack(anchor="w", padx=22, pady=(0, 6))

        # 左右栏: PanedWindow 支持拖动分隔条调节宽度
        body = tk.PanedWindow(self, orient=tk.HORIZONTAL, bg=C["bg"], bd=0,
                              relief="flat", sashwidth=6, sashrelief="flat",
                              sashpad=1, background=C["bg"])
        body.pack(fill="both", expand=True, padx=16, pady=(0, 12))

        # ============ 左栏 (可滚动) ============
        left_wrap = tk.Frame(body, bg=C["card"])
        body.add(left_wrap, minsize=int(340 * self.scale), stretch="always")
        left_wrap.grid_rowconfigure(0, weight=1)
        left_wrap.grid_columnconfigure(0, weight=1)

        self.canvas = tk.Canvas(left_wrap, bg=C["card"], highlightthickness=0, bd=0)
        self.canvas.grid(row=0, column=0, sticky="nsew")
        sbar = tk.Scrollbar(left_wrap, orient="vertical", command=self.canvas.yview,
                            bg=C["card"], troughcolor=C["card"],
                            activebackground=C["scroll"],
                            relief="flat", bd=0, width=9)
        sbar.grid(row=0, column=1, sticky="ns")
        self.canvas.configure(yscrollcommand=sbar.set)

        self.left = tk.Frame(self.canvas, bg=C["card"], padx=14, pady=10)
        self._win_id = self.canvas.create_window((0, 0), window=self.left, anchor="nw")
        self.left.bind("<Configure>",
                       lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
        self.canvas.bind("<Configure>",
                         lambda e: self.canvas.itemconfigure(self._win_id, width=e.width))
        self._bind_wheel(self.canvas)

        self.left.grid_columnconfigure(0, weight=1)
        self._build_left()
        # 必须在子控件创建完成后递归绑定滚轮, 否则新建控件收不到事件
        self._bind_wheel(self.left)
        self._build_right(body)

    def _bind_wheel(self, widget):
        widget.bind("<MouseWheel>", self._on_wheel)
        for c in widget.winfo_children():
            self._bind_wheel(c)

    def _on_wheel(self, e):
        if sys.platform == "darwin":
            self.canvas.yview_scroll(int(-e.delta), "units")
        else:
            self.canvas.yview_scroll(int(-e.delta / 120), "units")

    def _build_left(self):
        L = self.left
        # 1. 许可档位
        self._label(L, "许可档位", 11, True).grid(row=0, column=0, sticky="w", pady=(0, 4))
        seg = tk.Frame(L, bg=C["card"])
        seg.grid(row=1, column=0, sticky="ew", pady=(0, 4))
        self.type_btns = {}
        for i, (key, label) in enumerate([("lifetime", "Lifetime"),
                                          ("enterprise", "Enterprise"),
                                          ("user", "User")]):
            b = tk.Button(seg, text=label, relief="flat", bd=0, cursor="hand2",
                          font=_mkfont(10), highlightthickness=0, padx=6, pady=3,
                          command=lambda k=key: self._on_type_change(k))
            b.grid(row=0, column=i, sticky="ew", padx=(0, 4) if i < 2 else (0, 0))
            seg.grid_columnconfigure(i, weight=1)
            self.type_btns[key] = b
        self.type_info = self._label(L, "", 9, color=C["sub"])
        self.type_info.grid(row=2, column=0, sticky="w", pady=(0, 8))

        # 2. 机器码
        self._label(L, "机器码 (系统盘卷序列号)", 11, True).grid(row=3, column=0, sticky="w", pady=(0, 4))
        mc = tk.Frame(L, bg=C["card"])
        mc.grid(row=4, column=0, sticky="ew")
        mc.grid_columnconfigure(0, weight=1)
        self.machine_entry = self._entry(mc)
        self.machine_entry.grid(row=0, column=0, sticky="ew", padx=(0, 6), ipady=2)
        self._button(mc, "自动获取", self._auto_machine).grid(row=0, column=1)
        self._label(L, "机器码绑定 Key1/Key2 的异或值, 不同机器码生成结果不同",
                    8, color=C["sub"]).grid(row=5, column=0, sticky="w", pady=(3, 6))

        # 3. 高级选项
        self._button(L, "▸ 高级选项 (可选字段)", self._toggle_advanced).grid(
            row=6, column=0, sticky="ew", pady=(4, 4))
        self.adv = tk.Frame(L, bg=C["card2"], padx=12, pady=8)
        self.adv.grid(row=7, column=0, sticky="ew")
        self.adv.grid_columnconfigure(1, weight=1)
        self._build_advanced()

        # 4. 生成按钮
        gb = self._button(L, "生成注册码", self._generate, accent=True, height=2)
        gb.grid(row=8, column=0, sticky="ew", pady=(10, 6))
        gb.configure(font=_mkfont(13, True))
        self.gen_btn = gb

        # 5. INI 面板
        ini = tk.Frame(L, bg=C["card2"], padx=12, pady=10)
        ini.grid(row=9, column=0, sticky="ew")
        ini.grid_columnconfigure(0, weight=1)
        self._label(ini, "INI 文件 (自动写入许可键)", 10, True).grid(
            row=0, column=0, columnspan=3, sticky="w", pady=(0, 4))
        self.ini_entry = self._entry(ini)
        self.ini_entry.grid(row=1, column=0, columnspan=3, sticky="ew", ipady=2)
        self.ini_entry.insert(0, find_wiztree_ini())
        btns = tk.Frame(ini, bg=C["card2"])
        btns.grid(row=2, column=0, columnspan=3, sticky="ew", pady=(6, 2))
        btns.grid_columnconfigure(2, weight=1)
        self._button(btns, "自动定位", self._locate_ini).grid(row=0, column=0, padx=(0, 4))
        self._button(btns, "浏览...", self._browse_ini).grid(row=0, column=1)
        self.write_btn = self._button(btns, "写入 INI", lambda: self._write_ini(True))
        self.write_btn.grid(row=0, column=2, sticky="e")
        self.auto_write_var = tk.BooleanVar(value=False)
        tk.Checkbutton(ini, text="生成后自动写入 INI", variable=self.auto_write_var,
                       command=self._on_auto_write_toggle, bg=C["card2"], fg=C["text"],
                       selectcolor=C["card2"], activebackground=C["card2"],
                       activeforeground=C["text"], font=_mkfont(9), bd=0,
                       highlightthickness=0, anchor="w").grid(
            row=3, column=0, columnspan=3, sticky="w", pady=(2, 0))
        self._label(ini, "写入前自动备份为 WizTree3.ini.bak; 键值机器绑定, 请确认目标机机器码一致",
                    8, color=C["sub"]).grid(row=4, column=0, columnspan=3, sticky="w", pady=(2, 0))

    def _build_right(self, body):
        right = self._card(body)
        body.add(right, minsize=int(300 * self.scale), stretch="always")
        right.grid_columnconfigure(0, weight=1)
        right.grid_rowconfigure(1, weight=1)
        self._label(right, "生成结果", 11, True).grid(row=0, column=0, sticky="w", pady=(0, 6))
        self.result = tk.Text(right, bg=C["entry"], fg=C["text"], relief="flat",
                              font=_mkfont(11, family="Consolas"), wrap="none",
                              insertbackground=C["text"], padx=10, pady=10,
                              highlightthickness=1, highlightbackground=C["line"],
                              highlightcolor=C["accent"])
        self.result.grid(row=1, column=0, sticky="nsew")
        foot = tk.Frame(right, bg=C["card"])
        foot.grid(row=2, column=0, sticky="ew", pady=(8, 0))
        foot.grid_columnconfigure(0, weight=1)
        self.status = self._label(foot, "", 9, color=C["sub"])
        self.status.grid(row=0, column=0, sticky="w")
        self._button(foot, "复制全部结果", self._copy_all).grid(row=0, column=1)

    def _build_advanced(self):
        r = 0
        self._label(self.adv, "有效天数", 9).grid(row=r, column=0, sticky="w", padx=(0, 8), pady=(3, 2))
        self.days_entry = self._entry(self.adv, width=11)
        self.days_entry.grid(row=r, column=1, sticky="w", pady=(3, 2))
        self.days_hint = self._label(self.adv, "", 8, color=C["sub"])
        self.days_hint.grid(row=r, column=2, sticky="w", padx=(8, 0), pady=(3, 2))
        self.adv.grid_columnconfigure(2, weight=1)
        r += 1

        self._label(self.adv, "用户数", 9).grid(row=r, column=0, sticky="w", padx=(0, 8))
        self.typenum_entry = self._entry(self.adv, width=11)
        self.typenum_entry.insert(0, "0")          # 默认用户数 0
        self.typenum_entry.grid(row=r, column=1, sticky="w")
        self._label(self.adv, "N=\"N user license\"(有效期内) · 999=Enterprise(不限用户) · 0 且无有效天数=Supporter(个人)", 8,
                    color=C["sub"]).grid(row=r, column=2, sticky="w", padx=(8, 0))
        r += 1

        self._label(self.adv, "自由区", 9).grid(row=r, column=0, sticky="w", padx=(0, 8))
        self.free8_entry = self._entry(self.adv, width=11)
        self.free8_entry.insert(0, "00000000")     # 默认自由区全 0
        self.free8_entry.grid(row=r, column=1, sticky="w")
        self._label(self.adv, "代码第 21-28 字符, 随 Key3 保存, 可任意填写", 8,
                    color=C["sub"]).grid(row=r, column=2, sticky="w", padx=(8, 0))
        r += 1

        self._label(self.adv, "A1 字节", 9).grid(row=r, column=0, sticky="w", padx=(0, 8), pady=(6, 2))
        a1 = tk.Frame(self.adv, bg=C["card2"])
        a1.grid(row=r, column=1, columnspan=2, sticky="w", pady=(6, 2))
        self.a1hi = self._byte_entry(a1, "byte3", "0x12", "最高字节")
        self.a1mid = self._byte_entry(a1, "byte2", "0x34", "参与类型计算")
        self.a1lo = self._byte_entry(a1, "byte0", "0x78", "最低字节")
        r += 1

        self._label(self.adv, "A2 字节", 9).grid(row=r, column=0, sticky="w", padx=(0, 8), pady=(0, 2))
        a2 = tk.Frame(self.adv, bg=C["card2"])
        a2.grid(row=r, column=1, columnspan=2, sticky="w", pady=(0, 2))
        self.a2hi = self._byte_entry(a2, "byte3", "0x9A", "最高字节")
        self.a2mid = self._byte_entry(a2, "byte2", "0xBC", "参与用户数计算")
        self.a2lo = self._byte_entry(a2, "byte0", "0xF0", "最低字节")
        r += 1

        self._label(self.adv, "byte1(A1)/byte1(A2) 由\"有效天数\"自动计算; 用户数 = byte2(A2)*256 + byte2(A1)",
                    8, color=C["sub"]).grid(row=r, column=0, columnspan=3, sticky="w", pady=(2, 4))
        self._button(self.adv, "恢复默认字段", self._reset_advanced, width=14).grid(
            row=r + 1, column=0, columnspan=3, sticky="w", pady=(6, 0))

    def _byte_entry(self, parent, name, default, hint):
        f = tk.Frame(parent, bg=C["card2"])
        f.pack(side="left", padx=(0, 8))
        tk.Label(f, text=name, bg=C["card2"], fg=C["sub"], font=_mkfont(8)).pack()
        e = self._entry(f, width=5)
        e.insert(0, default)
        e.pack()
        tk.Label(f, text=hint, bg=C["card2"], fg=C["sub"], font=_mkfont(7)).pack()
        return e

    # ---------------- 交互 ----------------

    def _init_machine_default(self):
        """启动时自动填入机器码默认值 (获取失败则留空)"""
        s = get_volume_serial()
        if s is not None:
            self.machine_entry.delete(0, "end")
            self.machine_entry.insert(0, "%08X" % s)

    def _on_type_change(self, t):
        prev = self._cur_type
        self._cur_type = t
        for k, b in self.type_btns.items():
            if k == t:
                b.configure(bg=C["accent"], fg="#ffffff")
            else:
                b.configure(bg=C["card2"], fg=C["text"])
        self.type_info.configure(text=TYPE_INFO[t])
        is_user = t == "user"
        is_ent = t == "enterprise"
        state = "normal" if (is_user or is_ent) else "disabled"
        self.days_entry.configure(state=state)
        self.days_hint.configure(
            text=("≥ %d 天才能通过 2026-08-05 构建版本校验" % MIN_DAYS) if is_user
            else ("留空 = 终身 Enterprise; 填写则限时 (≥ %d 天)" % MIN_DAYS) if is_ent
            else "终身许可无需填写")
        # 用户数输入框: 仅 user 档位生效; enterprise 实际固定 999, 显示同步为 999 避免歧义
        if is_ent:
            if prev != "enterprise":
                self._saved_typenum = self.typenum_entry.get()   # 保存用户配置
            self.typenum_entry.configure(state="normal")         # disabled 下 delete/insert 被忽略, 先启用再改值
            self.typenum_entry.delete(0, "end")
            self.typenum_entry.insert(0, "999")
            self.typenum_entry.configure(state="disabled")
        else:
            if prev == "enterprise" and self._saved_typenum is not None:
                self.typenum_entry.configure(state="normal")
                self.typenum_entry.delete(0, "end")
                self.typenum_entry.insert(0, self._saved_typenum)  # 恢复用户配置
            self.typenum_entry.configure(state="normal" if is_user else "disabled")
        if is_user and not self.days_entry.get().strip():
            self.days_entry.insert(0, str(MIN_DAYS))     # 默认最小可过校验天数
        if is_ent and self.days_entry.get().strip() == str(MIN_DAYS):
            self.days_entry.delete(0, "end")             # 清掉 user 档位的自动默认, 使 enterprise 回到终身
        # enterprise 锁定 byte2 (0xE7/0x03); 切出时恢复原值, 避免默认值残留导致档位错乱
        if is_ent:
            if prev != "enterprise":
                self._saved_mid = (self.a1mid.get(), self.a2mid.get())
            for e, v in ((self.a1mid, "0xE7"), (self.a2mid, "0x03")):
                e.delete(0, "end")               # 先改值再禁用: disabled 状态下 delete/insert 会被忽略
                e.insert(0, v)
                e.configure(state="disabled")
        elif prev == "enterprise" and self._saved_mid is not None:
            for e, v in ((self.a1mid, self._saved_mid[0]), (self.a2mid, self._saved_mid[1])):
                e.configure(state="normal")
                e.delete(0, "end")
                e.insert(0, v)
        else:
            self.a1mid.configure(state="normal")
            self.a2mid.configure(state="normal")

    def _toggle_advanced(self):
        self.advanced_open = not self.advanced_open
        if self.advanced_open:
            self.adv.grid()
        else:
            self.adv.grid_remove()

    def _auto_machine(self):
        s = get_volume_serial()
        if s is None:
            self.status.configure(text="自动获取失败, 请在 cmd 运行 vol C: 后手动填写")
            return
        self.machine_entry.delete(0, "end")
        self.machine_entry.insert(0, "%08X" % s)
        self.status.configure(text="已自动获取机器码 0x%08X" % s)

    def _mark_err(self, entry, msg):
        """将输入框标红并显示错误信息"""
        entry.configure(highlightbackground="#c0392b", highlightcolor="#c0392b")
        if entry not in self._err_entries:
            self._err_entries.append(entry)
        self.status.configure(text="输入错误: %s" % msg, fg="#e74c3c")

    def _clear_err(self):
        """清除所有输入框的红框标记"""
        for e in self._err_entries:
            e.configure(highlightbackground=C["line"], highlightcolor=C["accent"])
        self._err_entries = []
        self.status.configure(fg=C["sub"])

    def _reset_advanced(self):
        """恢复高级字段为默认值"""
        self.days_entry.delete(0, "end")
        self.typenum_entry.delete(0, "end")
        self.typenum_entry.insert(0, "999" if self._cur_type == "enterprise" else "0")
        self.free8_entry.delete(0, "end")
        self.free8_entry.insert(0, "00000000")
        for k, v in (("a1hi", "0x12"), ("a1mid", "0x34"), ("a1lo", "0x78"),
                     ("a2hi", "0x9A"), ("a2mid", "0xBC"), ("a2lo", "0xF0")):
            e = getattr(self, k)
            e.delete(0, "end")
            e.insert(0, v)
        self._clear_err()
        self.status.configure(text="已恢复默认字段")

    def _generate(self):
        self._clear_err()
        try:
            lictype = self._cur_type

            # 机器码: 空则自动获取
            try:
                machine = parse_machine_code(self.machine_entry.get())
            except ValueError as e:
                self._mark_err(self.machine_entry, str(e))
                raise
            if machine is None:
                machine = resolve_machine_code()

            # 有效天数 (user/enterprise 才可填)
            days = None
            if lictype in ("user", "enterprise"):
                try:
                    d = self.days_entry.get().strip()
                    days = parse_int_value(d, "有效天数", lo=0, hi=65535) if d else None
                except ValueError as e:
                    self._mark_err(self.days_entry, str(e))
                    raise

            # 用户数 (user)
            typenum = None
            if lictype == "user":
                try:
                    typenum = parse_int_value(self.typenum_entry.get(),
                                              "用户数", default=0, lo=0, hi=65535)
                except ValueError as e:
                    self._mark_err(self.typenum_entry, str(e))
                    raise

            # 自由区: 空则默认 00000000
            try:
                free8 = "%08X" % parse_hex_value(self.free8_entry.get(), "自由区",
                                                 default=0, lo=0, hi=0xFFFFFFFF)
            except ValueError as e:
                self._mark_err(self.free8_entry, str(e))
                raise

            # A1/A2 字节: 空则用默认值
            byte_fields = [(self.a1hi, "A1 byte3", 0x12),
                           (self.a1mid, "A1 byte2", 0x34),
                           (self.a1lo, "A1 byte0", 0x78),
                           (self.a2hi, "A2 byte3", 0x9A),
                           (self.a2mid, "A2 byte2", 0xBC),
                           (self.a2lo, "A2 byte0", 0xF0)]
            vals = []
            for ent, nm, dflt in byte_fields:
                try:
                    vals.append(parse_hex_byte(ent.get(), nm, dflt))
                except ValueError as e:
                    self._mark_err(ent, str(e))
                    raise
            a1hi, a1mid, a1lo, a2hi, a2mid, a2lo = vals

            # 生成 (make_code_by_fields 内部还有版本日期等校验)
            try:
                code = make_code_by_fields(
                    lictype, days=days, free8=free8, typenum=typenum,
                    a1hi=a1hi, a1mid=a1mid, a1lo=a1lo,
                    a2hi=a2hi, a2mid=a2mid, a2lo=a2lo)
            except ValueError as e:
                self._mark_err(self.days_entry, str(e))
                raise

            kind, valid, days = describe_code(code)
            k1, k2, k3 = build_keys(code, machine)
            rebuilt = rebuild_code(k1, k2, k3, machine)

            out = []
            out.append("机器码      : 0x%08X" % machine)
            out.append("")
            out.append("注册码      : %s" % code)
            out.append("许可显示: %s" % kind)
            out.append("有效期      : %s" % valid)
            if days:
                out.append("有效天数    : %d" % days)
            out.append("本地校验    : %s" % ("通过 ✓" if validate_local(code) == 0 else "失败 ✗"))
            out.append("重建校验    : %s" % ("一致 ✓" if rebuilt == code else "不一致 ✗"))
            out.append("")
            out.append("Key1 = %s" % k1)
            out.append("Key2 = %s" % k2)
            out.append("Key3 = %s" % k3)
            out.append("")
            out.append("--- WizTree3.ini [frmWizTreeMain] 追加内容 ---")
            out.append(build_ini(code, machine, valid_days=days))

            self.result.delete("1.0", "end")
            self.result.insert("1.0", "\n".join(out))
            self._last_output = "\n".join(out)
            self._last_gen = (code, machine, days)
            self.write_btn.configure(bg=C["accent"], fg="#ffffff")
            self.status.configure(text="生成成功 · 可写入 INI 或复制结果", fg=C["sub"])
            if self.auto_write_var.get():
                self._write_ini(confirm=False)
        except ValueError as e:
            self.status.configure(text="输入错误: %s" % e, fg="#e74c3c")
            self.result.delete("1.0", "end")
            self.result.insert("1.0", "错误: %s" % e)

    def _locate_ini(self):
        self.ini_entry.delete(0, "end")
        self.ini_entry.insert(0, find_wiztree_ini())
        self.status.configure(text="INI 路径已定位")

    def _browse_ini(self):
        p = filedialog.askopenfilename(title="选择 WizTree3.ini",
                                       filetypes=[("INI 文件", "*.ini"), ("所有文件", "*.*")])
        if p:
            self.ini_entry.delete(0, "end")
            self.ini_entry.insert(0, p)

    def _on_auto_write_toggle(self):
        if self.auto_write_var.get():
            ok = messagebox.askyesno(
                "开启自动写入",
                "开启后每次\"生成注册码\"成功都会自动写入 INI:\n%s\n\n"
                "键值按机器码绑定, 请确保目标机与此一致。\n"
                "每次写入前自动备份为 .bak。\n\n确认开启?" % self.ini_entry.get().strip())
            if not ok:
                self.auto_write_var.set(False)
        else:
            self.status.configure(text="已关闭自动写入")

    def _write_ini(self, confirm=True):
        if self._last_gen is None:
            if confirm:
                messagebox.showwarning("未生成", "请先点击\"生成注册码\"")
            return
        code, machine, days = self._last_gen
        path = self.ini_entry.get().strip()
        if not path:
            if confirm:
                messagebox.showwarning("路径为空", "请填写或选择 WizTree3.ini 路径")
            else:
                self.status.configure(text="自动写入失败: INI 路径为空")
            return
        if confirm and not messagebox.askyesno(
                "确认写入",
                "将以下许可键写入:\n%s\n\n"
                "键值已按机器码 0x%08X 绑定, 请确认目标机与此一致。\n"
                "写入前会备份原文件为 %s.bak。\n\n继续?" % (path, machine, path)):
            return
        try:
            keys = build_lic_ini_keys(code, machine, valid_days=days)
            apply_keys_to_ini(path, dict(keys), backup=True)
            self.status.configure(text="已写入 %s (原文件已备份为 .bak)" % path)
            if confirm:
                messagebox.showinfo("写入成功", "许可键已写入:\n%s\n\n"
                                    "重启 WizTree 后生效 (About 页查看)。" % path)
        except Exception as e:
            self.status.configure(text="写入失败: %s" % e)
            if confirm:
                messagebox.showerror("写入失败", "错误: %s" % e)

    def _copy_all(self):
        if not self._last_output:
            return
        self.clipboard_clear()
        self.clipboard_append(self._last_output)
        self.status.configure(text="已复制到剪贴板")


if __name__ == "__main__":
    app = WizTreeKeygenApp()
    app.mainloop()

ps

image-20260830125316436

posted @ 2026-08-30 13:18  DirWangK  阅读(44)  评论(1)    收藏  举报