【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
- WizTree 4.32 x64 portable 许可分析
- 第一部分 许可体系与代码格式
- 第二部分 激活流程与校验路径
- 第三部分 核心算法
- 第四部分 存储、显示与注册表
- 第五部分 暗桩与隐蔽校验
- 附录
- 附录 A. 离线注册码生成(keygen)与离线激活文件
- 附录 B. 函数地址映射表
- py
- ps
第一部分 许可体系与代码格式
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_B95300(0xB95300); - 一组 0x7A 段的许可算法函数(见第三部分 §8~§10)。
校验分两条路径:
- 在线路径:向
http://antibody-software.com/checkkey.php?reginfo=<包>发 HTTP 请求,服务器返回的 chk 响应字段 1 必须为 1(见 §6); - 离线路径:当
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 派生态(用户数 / 有效天数)
由 A1、A2 派生出的许可字段(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_FormCreate(0xB0ECA2):设置按钮文案 OK/Cancel、标题Enter Supporter Code、标签Supporter Code:;TfrmSupporterCode_FormShow(0xB0EE1A):若为安装版(非便携版)且当前非管理员(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.net或antibody-software.com,则判定被拦截(返回假);- 被拦截时 btnOKClick 直接弹
Internal error ID: 10t86/Please contact software supplier,无法进入离线激活。
7.3 离线激活条件汇总
离线激活只需满足以下三个条件:
CheckHostsBlock_7A28F0通过(hosts 未把antibody-software.com/antibody.za.net指向本地,见 §7.2);CheckSupporterCodeFormat_7A1AE0通过(39 字符 8 组 / 去连字符 32 字符且尾部校验和正确,见 §9.1);- 版本日期校验通过:有效天数 == 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 = 0,v > 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_MACHINE(mov rdx, 0x80000002,故写入需要管理员权限); - 节名:对应 JvAppStorage 节,例如
frmWizTreeMain; - 值名:设置名,许可键
Key1、Key2、Key3、chk、KeyValidUntil均在镜像白名单字符串数组(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!InetIsOffline(InitInetIsOfflinePtr_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_B8D440:GET 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)。
给定 A1、A2、自由区 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)=0、byte1(A2)=0 → 有效天数=0 |
Supporter license - Lifetime |
enterprise(终身) |
byte2(A1)=0xE7、byte2(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


浙公网安备 33010602011771号