【AI】TreeSize 9.8.2.2303许可(License)机制分析报告
TreeSize 9.8.2.2303许可(License)机制分析报告
目标:
LicenseManager.exe(含其依赖的托管许可库)
分析工具:dnSpy(MCP)、de4dot、ILSpy、Mono.Cecil、PowerShell 反射
结论性质:本文档全部内容基于程序集反编译/反射/IL 级观察,实事求是,未做推测性编造;无法确证处均已注明"待确认"。
1. 目标与程序集概况
1.1 文件属性
TreeSize.exe
PE64
Operation system: Windows (Vista) [AMD64, 64位, GUI]
Linker: Microsoft Linker (8.0)
Compiler: Embarcadero Delphi (12.0 Athens) [Enterprise]
Language: Object Pascal (Delphi)
Sign tool: Windows Authenticode (2.0) [PKCS #7]
(Heur) Protection: Generic [Strange resources]
(Heur) Packer: Generic [Resources like Quick Batch File Compiler]
(Heur) Licensing: Contains [Strings]
Resource: Binary [偏移 = 0x0346a584, 大小 = 0x1050]
Format: Resource String Table [Resources]
Resource: Binary [偏移 = 0x04410c90, 大小 = 0x06e4]
Format: RC Data [Resources]
Data: ZLIB data [ZLIB compression best]
Overlay: Binary [偏移 = 0x0470a600, 大小 = 0x3980]
Certificate: WinAuth (2.0) [PKCS #7]
MD5: f3e8fa02f49791ca5a11761290299fe4
SHA1: b5581ecfe6758bcdcc6deb650b4f8cf167c3da22
SHA256: 8f87d779fa9325dfcbb81c4e13d5821282abe4bada3dc9308be2040feb322990
LicenseManager.exe
PE32
Operation system: Windows (95) [I386, 32位, GUI]
Linker: Microsoft Linker
Language: MSIL/C#
Library: Serilog
Library: .NET Framework (4.8, CLR 4.0.30319)
Sign tool: Windows Authenticode (2.0) [PKCS #7]
Protector: Babel .NET (1.0-2.X)
(Heur) Protection: Obfuscation [Modified managed EP + CLR constructor + Virtualization + Anti-ILDASM + Math mutations + Watermark]
(Heur) Packer: Generic [Section #0 (".text") compressed + High entropy]
Debug data: Records [codeview]
(Heur) Debug data: Contains [Absolute PDB path]
Overlay: Binary [偏移 = 0x00202400, 大小 = 0x35f8]
Certificate: WinAuth (2.0) [PKCS #7]
MD5: 2eb77c4c29b6a071655babab3fe54fae
SHA1: 0ca6f1debc077d013c28ceffd0983b25ae9518f8
SHA256: 553aaaf3d55cc1133b5c7c2aec731c43de0cd2bdf182aaa904e0f65c5784d6f4
| 文件 | 类型 | 说明 |
|---|---|---|
LicenseManager.exe |
PE32 / MSIL / C# / .NET Framework 4.8 (CLR 4.0.30319) | 许可管理 CLI 入口,版本 6.3.1.0 |
TreeSize.exe |
原生程序集(非 .NET,Delphi/C++ 系) | 主程序,不在本报告范围内做反编译 |
Jam.dll |
原生程序集 | 原生核心库 |
Jam.Interop.dll |
托管 / .NET | 原生主程序 ↔ 托管库的 COM 互操作桥(版本 3.1.0.0) |
Jam.License.Validation.dll |
托管 / .NET | 许可验证核心库(Babel 混淆) |
Jam.License.Common.dll |
托管 / .NET | 许可 DTO、签名、序列化基础库(Babel 混淆) |
Jam.Logging.dll |
托管 / .NET | 日志库(版本 1.0.2.0) |
三个许可相关程序集(LicenseManager、Jam.License.Common、Jam.License.Validation)均为
版本 6.3.1.0,PublicKeyToken = 13acf979d16e8a17(强名称签名)。
1.2 保护手段(来自检测器与人工确认)
- Protector: Babel .NET (1.0-2.X)
- 检测器结论:Modified managed EP + CLR constructor + Virtualization + Anti-ILDASM + Math mutations + Watermark
- 人工确认的混淆手段:
- 控制流混淆:大量
for(;;)+ 整数运算(异或/加减)跳转的假分支(如if (-78 <= -106) goto ...),反编译后表现为死代码与打乱顺序,逻辑本体不变。 - 字符串加密:字符串字面量被抽离,运行时通过
\uE04C类动态解密(嵌入资源 + 运行时 Emit 生成解密代码);解密后的明文(如静态 AES 密钥种子)已人工提取,见 §7。 - 类型/成员重命名:私有类型命名为
\uE0xx(Jam.License.Validation / Common),Jam.Interop 内为\uE000~\uE002;de4dot 去混淆后为Class0/Class1/Struct0等。 - Babel 自定义加密基础设施:
Class55(SymmetricAlgorithm子类)为 Babel 运行时反混淆流,其CreateEncryptor仅把 Key/IV 复制到参数并返回自身,GenerateKey用System.Random生成 16 字节——与许可数据加密无关,属混淆器基础设施。
- 控制流混淆:大量
1.3 去混淆处理
已用 de4dot(de4dot-cex 版)对 Jam.License.Validation.dll 与
Jam.License.Common.dll 去混淆,产物:
extracted\Jam.License.Validation-clean.dllextracted\Jam.License.Common-clean.dll
clean 版因与原始程序集同名冲突,dnSpy MCP 无法加载分析,改用 PowerShell 反射
(捕获 ReflectionTypeLoadException)与 Mono.Cecil(ILSpy 目录自带)读取其 IL。
extracted\clean_validation_types.txt 记录了 clean 版类型映射。
2. 整体架构与调用关系
2.1 组件关系(已确认)
┌────────────────────┐ COM ┌──────────────────────┐ 引用 ┌─────────────────────────┐
│ TreeSize.exe (原生) │ ───────▶ │ Jam.Interop.dll │ ───────▶ │ Jam.License.Validation │
│ (Delphi 系, 非托管) │ │ (托管 COM 桥) │ │ (许可验证核心) │
└────────────────────┘ └──────────────────────┘ └───────────┬─────────────┘
│ 引用
┌──────────────────────┐ ┌─────▼─────────────┐
│ LicenseManager.exe │ ───────────▶ │ Jam.License.Common │
│ (许可管理 CLI, 托管) │ │ (DTO/签名/序列化) │
└──────────────────────┘ └───────────────────┘
原生主程序调用方式(已确认机制):
TreeSize.exe与Jam.dll均为原生程序集(PowerShellAssemblyName.GetAssemblyName抛
BadImageFormatException,无 .NET 清单)。- 原生侧通过 COM 创建
JamCOMObjectFactory(Jam.Interop.dll,[ComVisible(true)],
Guid0B87D366-9E0A-4B94-9AB6-0B343B5AAD76,接口IJamCOMObjectFactory),再调用其
Create(string pClassName)按类名字符串反射实例化托管对象。 JamCOMObjectFactory挂接AppDomain.AssemblyResolve,按需从自身所在目录加载依赖程序集
(含Jam.License.Validation.dll);加载后遍历所有模块调用模块类型初始化器(TypeInitializer)。- 许可库类型均无
[ComVisible]导出(find_by_attribute在
Jam.License.Common/Jam.License.Validation中结果为 0)——Activator.CreateInstance
反射创建不要求 COM 可见性,COM 可见性仅影响类型库/注册表导出。 - 原生侧实际调用的类名(已动态确认,见 poc/POC.md §17.7):TreeSize.exe 启动流程中,原生 CLR 宿主对
JamCOMObjectFactory.Create(...)传入"ComLicenseFactory"(短名),命中
Jam.Licensing.Com.ComLicenseFactory的短名注册键,随后原生侧经该工厂的 COM 方法
(CreateLicenseManager(IntPtr)等)继续创建许可管理器。IDA 静态分析确认原生二进制中
无任何类名字面量(ComLicenseFactory/Jam.Licensing等字符串均不存在),类名仅存在于
托管侧类型表,经 COM vtable 槽位间接传入——静态路径不可达,动态 hook 为决定性证据。
托管侧入口: LicenseManager.exe(独立 CLI),读同目录 LicenseConfiguration.json
(SignedLicenseConfiguration,已签名配置)后经 LicenseManagerFactory.CreateLicenseManager
构建许可管理器,见 §4;CLI 行为见 poc/NOTES.md §11。
2.2 Jam.Interop.JamCOMObjectFactory 类型解析机制(IL 级)
JamCOMObjectFactory 是原生 ↔ 托管桥的入口。以下逻辑经 dnSpy MCP(原始程序集)与 de4dot
清理 IL(tools\jam_interop_clean.il)逐条对照一致。
静态字段(.cctor 初始化 4 项):
| 字段 | 类型 | 职责 |
|---|---|---|
\uE000 |
Logger(Jam.Logging) | 日志 |
\uE001 |
ConcurrentStack<Assembly> |
待注册程序集栈 |
\uE002 |
ConcurrentDictionary<string, Type> |
类型注册表(短名 + 全名双键) |
\uE003 |
Mutex |
查找/注册互斥锁 |
Create(string pClassName) 只是两段包装:
Create = \uE002() // 惰性初始化(校验静态字段非 null)
\uE008(pClassName) // 按名查找并 Activator.CreateInstance
\uE008(string)(类型解析核心,de4dot 名 smethod_8):
\uE003.WaitOne()加锁;- 注册表无该键 且 栈
\uE001非空 → 弹出程序集逐个调\uE006(asm)注册其全部类型; - 仍未命中 →
\uE007():遍历AppDomain.CurrentDomain.GetAssemblies()全部程序集补扫; \uE002[名称]→Activator.CreateInstance;- 类型不存在 →
ClassOrInterfaceNotFoundException;其余异常 →JamInteropException包装。
\uE006(Assembly)(类型注册):对程序集中每个类型写双键(TryAdd,ordinal 大小写敏感):
TryAdd(type.Name, type) // 短名
TryAdd(type.Namespace + "." + type.Name, type) // 全名
注册表使用
ConcurrentDictionary<string,Type>默认比较器——原生侧传"ComLicenseFactory"命中短名键;
若传大小写变体(如"jam.licensing.com.comlicensefactory")将找不到(TryAdd/查询均区分大小写)。
\uE007()(全程序集扫描):对 GetAssemblies() 全部程序集执行 \uE006,兜底补注册延迟加载的程序集。
LoadAssembly(...)(原生侧 vtable +80 槽位调用):用 CLR 宿主加载目标程序集后,遍历所有模块
触发模块类型初始化器(TypeInitializer);\uE004/\uE005(AssemblyResolve)负责从自身目录按需
解析依赖程序集。
IL 证据(de4dot 清理 IL,tools\jam_interop_clean.il,行号可复核)
Create(string) 本体(行 4052-4061)——仅是两段包装:
.method public hidebysig newslot virtual final instance object Create(string pClassName) cil managed
{
// 代码大小 12 (0xc)
IL_0000: call void Jam.Interop.JamCOMObjectFactory::smethod_2() // 惰性初始化
IL_0005: ldarg.1
IL_0006: call object Jam.Interop.JamCOMObjectFactory::smethod_8(string) // 按名解析+创建
IL_000b: ret
}
\uE002()(Create 第一步,行 3274-3330,synchronized)核心序列:
注册 LoggerFactory → JamUtilsCoreCOMObject.Initialize() → smethod_0()/smethod_1()(AssemblyResolve
回调初始化)→ 向 AppDomain.CurrentDomain 挂接 add_AssemblyResolve:
IL_0000: newobj Class9::.ctor
IL_0005: call LogManager::set_LoggerFactory(Class9)
IL_000a: call JamUtilsCoreCOMObject::get_Instance
IL_000f: callvirt JamUtilsCoreCOMObject::Initialize
IL_0021: call JamCOMObjectFactory::smethod_0
IL_0026: call JamCOMObjectFactory::smethod_1
IL_002b: call AppDomain::get_CurrentDomain
IL_0030: ldsfld ResolveEventHandler Class0::resolveEventHandler_0
...
IL_0051: callvirt AppDomain::add_AssemblyResolve
IL_0056: ldc.i4.1
IL_0057: stsfld bool JamCOMObjectFactory::bool_1 // 仅初始化一次
\uE008(string)(Create 第二步,行 4063-4177,smethod_8)核心:先查 ConcurrentDictionary
注册表,未命中则 ConcurrentStack 弹出待注册程序集逐批补注册,仍无则 smethod_7() 全程序集扫描,
最终 get_Item + Activator.CreateInstance:
IL_000b: ldsfld ConcurrentDictionary<string,Type>::concurrentDictionary_0
IL_0010: ldarg.0
IL_0011: callvirt ConcurrentDictionary::ContainsKey(pClassName)
IL_0016: brtrue.s IL_0037 // 已注册 → 直接创建
IL_001b: ldsfld ConcurrentStack<Assembly>::concurrentStack_0
IL_0020: ldloca.s V_0
IL_0022: callvirt ConcurrentStack::TryPop(&asm)
IL_0027: brfalse.s IL_002c
IL_002f: ldloc.0
IL_0030: call JamCOMObjectFactory::smethod_6(asm) // 逐个注册该程序集全部类型
IL_0035: br.s IL_000b // 循环直到命中
...
IL_004d: call JamCOMObjectFactory::smethod_7() // AppDomain.GetAssemblies() 全扫兜底
IL_0052: ldsfld ConcurrentDictionary::concurrentDictionary_0
IL_0057: ldarg.0
IL_0058: callvirt ConcurrentDictionary::get_Item(pClassName)
IL_005d: call [mscorlib]System.Activator::CreateInstance(Type)
IL_0062: stloc.1
// catch KeyNotFoundException → ClassOrInterfaceNotFoundException
// catch Exception → JamInteropException(HandleException 日志后重抛)
\uE006(Assembly)(类型注册,行 4019 附近,smethod_6)——双键写入的直接证据:
IL_00ab: ldsfld ConcurrentDictionary<string,Type>::concurrentDictionary_0
IL_00b0: ldloc.s V_6
IL_00b2: callvirt MemberInfo::get_Name()
IL_00b7: ldloc.s V_6
IL_00b9: callvirt ConcurrentDictionary::TryAdd(Name, type) // 短名键
IL_00bf: ldsfld ConcurrentDictionary::concurrentDictionary_0
IL_00c4: ldloc.s V_6
IL_00c6: callvirt Type::get_Namespace()
IL_00cb: ldstr "."
IL_00d0: callvirt MemberInfo::get_Name()
IL_00d7: call [mscorlib]System.String::Concat(string,string,string) // "命名空间.类型名"
IL_00dc: ldloc.s V_6
IL_00de: callvirt ConcurrentDictionary::TryAdd(全名, type) // 全名键
2.3 原生侧托管桥初始化链(IDA 反编译)
TreeSize.exe 启动时按序执行(全部为间接调用,无类名字面量):
- 依赖预加载
sub_22A1B70:3 处直接调用 vtable +1064 槽位(含空串与
Jam.Interop.dll、LicenseManager.exe路径),随后 16 次调sub_22A1AE0注册其余依赖
(合计 18 项文件 = 11 项 JAM 相关 + 7 项 .NET Standard 运行库,清单见 poc/POC.md §17.7)。
sub_22A1AE0用sub_13F8CF0拼接路径 →sub_1542F50存在性检查 → 存在则经目标
vtable +1064 槽位注册(模块类型初始化器触发点)。 - CLR SxS 宿主
sub_12E3900:mscoree.dll→CLRCreateInstance→ICLRMetaHost
承载托管库。 - 工厂创建入口
sub_12EA520(约 44KB):先对承载程序集做 WinVerifyTrust 签名验证 +
证书嵌入比对,通过后创建JamCOMObjectFactory单例(gettersub_12E9980/ RTTI 创建
sub_12E98D0,全局qword_3186DC0)。 - vtable 调用:
+80槽位 →LoadAssembly(...);+32槽位 →Create(...)
(动态实测实参为"ComLicenseFactory",见 poc/POC.md §17.7)。
IDA 反编译证据(TreeSize.exe,x64)
sub_22A1AE0(单依赖注册:拼路径 → 存在性检查 → vtable+1064)完整反编译:
__int64 __fastcall sub_22A1AE0(__int64 a1, __int64 a2)
{
_QWORD vars38[2];
vars38[0] = 0;
sub_13F8CF0(vars38, *(_QWORD *)(a1 + 72), a2); // 拼接目标目录 + 文件名
if ( (unsigned __int8)sub_1542F50(vars38[0]) ) // 存在性检查(FileExists)
(*(void (__fastcall **)(_QWORD, _QWORD))(***(_QWORD ***)(a1 + 104) + 1064LL))
(*(_QWORD **)(a1 + 104), vars38[0]); // vtable +1064 槽位注册
return sub_4138A0(vars38);
}
sub_22A1AE0 对应 x64 汇编(IDA,29 条指令,地址 0x22A1AE0——与上方 C 反编译逐句对应):
sub_22A1AE0 (.text @ 0x22a1ae0):
22a1ae0 push rbp
22a1ae1 sub rsp, 40h ; 局部帧(含 var_s38 路径缓冲)
22a1ae5 mov rbp, rsp
22a1ae8 mov [rbp+var_s20], rcx ; a1 = 宿主对象
22a1aec mov [rbp+var_s28], rdx ; a2 = 文件名(宽字符串)
22a1af0 mov [rbp+var_s38], 0 ; vars38[0] = 0
22a1af9 lea rcx, [rbp+var_s38] ; &vars38
22a1afd mov rax, [rbp+var_s20]
22a1b01 mov rdx, [rax+48h] ; *(a1+72) 目标目录
22a1b05 mov r8, [rbp+var_s28] ; 文件名
22a1b09 call sub_13F8CF0 ; 拼接完整路径
22a1b0e mov rcx, [rbp+var_s38]
22a1b12 call sub_1542F50 ; 存在性检查
22a1b17 test al, al
22a1b19 jz short loc_22A1B36 ; 不存在 → 跳过注册
22a1b1b mov rax, [rbp+var_s20]
22a1b1f mov rax, [rax+68h] ; *(a1+104) → 对象体
22a1b23 mov rax, [rax] ; *vtable
22a1b26 mov rcx, rax
22a1b29 mov rdx, [rbp+var_s38] ; 完整路径
22a1b2d mov rax, [rax]
22a1b30 call qword ptr [rax+428h] ; vtable +0x428 = +1064 槽位注册
22a1b36 nop
22a1b37 lea rcx, [rbp+var_s38]
22a1b3b call sub_4138A0 ; 释放路径缓冲
22a1b40 lea rsp, [rbp+40h]
22a1b44 pop rbp
22a1b45 retn
注:
call qword ptr [rax+428h]即 C 反编译中的vtable +1064槽位间接调用
(0x428 = 1064),mov rdx, [rax+48h]即*(a1 + 72)目录成员。
sub_22A1B70(依赖预加载:vtable+1064 的 19 次调用)关键段落:
sub_40A850(&vars38, 0); // 空串
(*(+1064))(*a2, vars38); // 直接注册(空项)
sub_40A850(&vars30, 0);
sub_488AD0(vars48, vars30); // 程序目录
sub_13F8CF0(&vars28, vars48[0], &off_22A1D88); // Jam.Interop.dll
(*(+1064))(*a2, vars28);
sub_13F8CF0(&vars20, vars48[0], &off_22A1DB4); // LicenseManager.exe
(*(+1064))(*a2, vars20);
sub_22A1AE0(_0, &off_22A1DE8); // LicenseManager.config
sub_22A1AE0(_0, &off_22A1E20); // AuthenticodeExaminer.dll
sub_22A1AE0(_0, &off_22A1E60); // Jam.License.Common.dll
sub_22A1AE0(_0, &off_22A1E9C); // Jam.License.Shared.dll
sub_22A1AE0(_0, &off_22A1ED8); // Jam.License.Validation.dll
sub_22A1AE0(_0, &off_22A1F1C); // Jam.Logging.dll
sub_22A1AE0(_0, &off_22A1F48); // Microsoft.Bcl.AsyncInterfaces.dll
sub_22A1AE0(_0, &off_22A1F98); // Microsoft.Bcl.HashCode.dll
sub_22A1AE0(_0, L"System.Buffers.dll");
sub_22A1AE0(_0, L"System.Memory.dll");
sub_22A1AE0(_0, L"System.Numerics.Vectors.dll");
sub_22A1AE0(_0, L"System.Runtime.CompilerServices.Unsafe.dll");
sub_22A1AE0(_0, L"System.Text.Encodings.Web.dll");
sub_22A1AE0(_0, L"System.Text.Json.dll");
sub_22A1AE0(_0, L"System.Threading.Tasks.Extensions.dll");
sub_22A1AE0(_0, &off_22A21C0); // WindowsFirewallHelper.dll
注:
off_22A1D88~off_22A1F98、off_22A21C0指向各依赖的宽字符串字面量(首字符依次为
J/L/L/A/J/J/J/J/M/M/W),文件名与 poc/POC.md §17.7 清单一致。
sub_12EA520(工厂入口)的 WinVerifyTrust + 错误码白名单 + 证书比对:
// 准备 WINTRUST_DATA(88 字节):无 UI、无吊销检查缓存
sub_40DBA0(pWVTData, 88, 0);
pWVTData[0] = 88; // cbStruct
pWVTData[6] = 2; // WTD_REVOCATION_CHECK_NONE
pWVTData[7] = 0;
pWVTData[8] = 1; // WTD_UI_NONE
vars318 = 4096; // WTD_CACHE_ONLY_URL_RETRIEVAL
vars2F8 = &vars328;
vars348 = WinVerifyTrust(HWND_MESSAGE | 0x2, pgActionID, pWVTData);
// 白名单:以下"签名失败"码视为可接受
v5 = !vars348
|| vars348 == -2146762495 // TRUST_E_NOSIGNATURE(0x800B0100)
|| vars348 == -2146762490 // CERT_E_UNTRUSTEDROOT
|| vars348 == -2146885614 // CERT_E_CRITICAL
|| vars348 == -2146885613 // CERT_E_VALIDITYPERIODNESTING
|| vars348 == -2146762484 // CERT_E_CN_NO_MATCH
|| vars348 == -2146762482 // CERT_E_UNTRUSTEDTESTROOT
|| vars348 == -2146762480 // CERT_E_WRONG_USAGE
|| vars348 == -2146869243 // CRYPT_E_REVOKED
|| vars348 == -2146885594; // CERT_E_EXPIRED
vars34E = v5;
// 随后从 PE 的 WIN_CERTIFICATE 提取签名证书(Certificate / Certificateheader,
// LPWIN_CERTIFICATE),与两枚内嵌 DER 证书(JAM Software GmbH 代码签名证书,
// 见下方 0x12eab1c / 0x12eac21 的字面量)逐字节比对:
sub_413FC0(&vars3F8, L"h6fE49u10QevFBDBbvamekUtPeZHmluiPJ3qXrZrg4GcORvIELoVMJDmUU7KGs308207B1308..."); // 证书①
sub_413FC0(&vars410, L"VNRMR82ZHUYHTNTB68AH6C7Y41P2BIQX0GY6UI8G1LZ03FMPLH5TEI1Y6KIE88XO308207B1308..."); // 证书②
// sub_415E60(..., 63/62/58, len) 逐段解码比对
sub_12E3900(CLR SxS 宿主)关键序列——mscoree.dll 导出 CLRCreateInstance 逐级取接口:
ModuleHandleW_1 = GetModuleHandleW(&off_12E3EBC); // mscoree.dll
if ( !ModuleHandleW_1 ) ModuleHandleW_1 = LoadLibraryW(&off_12E3EBC);
TMessageManager_Write(..., L"TSxsLoader successfully loaded mscoree.dll", 5, 0);
vars100 = (CLRCreateInstance)sub_42BED0(ModuleHandleW_1, &off_12E3F8E); // 取导出
v14 = vars100(&unk_2EF22E0, off_316EBF0, &varsF8); // → ICLRMetaHost
v15 = (*(+24))(varsF8, L"v4.0.30319", off_3170060, &varsF0); // GetRuntime(L"v4.0.30319") → ICLRRuntimeInfo
varsCC = 1;
varsC4 = (*(+96))(varsF0, &varsCC, 0, &varsC8); // GetInterface(CLSID_CorRuntimeHost,...)
if ( (!varsC4 || varsC4 == 122) && varsC8 == 1 )
(*(+88))(varsF0, varsCC, sub_4157F0(vars110)); // CorRuntimeHost::Start
v18 = (*(+72))(varsF0, varsE0, varsD0, a2); // 默认 AppDomain 内执行(加载托管桥)
sub_41BE50(&qword_3186DC8, *a2);
原生二进制中检索
ComLicenseFactory/ComLicenseManager/Jam.Licensing
(find_regex 与 UTF-16LE find_bytes)均无匹配:类名只存在于托管侧类型表,原生侧仅持有
vtable 槽位偏移与启动期按索引调用约定。
3. LicenseConfiguration.json 签名配置
3.1 字段(JsonPropertyOrder)
record LicenseConfiguration(Jam.License.Common),JSON 属性顺序:
| 顺序 | 字段 | 含义 |
|---|---|---|
| 0 | ProductName |
产品名 |
| 1 | ProductVersion |
产品版本 |
| 2 | ProductGroupId |
产品组 ID(用于系统/用户许可文件名生成) |
| 3 | ProductGroupName |
产品组名(用于密钥派生熵、portable 文件名) |
| 4 | SignatureVersion |
签名版本(参与密钥派生熵) |
| 5-7 | (未公开或后续字段) | |
| 8 | TrialLengthDays |
试用期天数;>0 时 IsTrialSupported = true |
| 9 | PreviousTrialLengthDays |
历史试用期天数 |
| 10 | ActivationType |
激活类型(UserAndSystem=0 / System=1) |
| 11 | RequiresDemoKeyRegistration |
是否需要 Demo Key 注册(此时可用 Trial 类型写入许可) |
3.2 签名配置加载
- 文件本身是
SignedDto<LicenseConfiguration>(RSA 签名 JSON,见 §8)。 SignedLicenseConfiguration使用静态反序列化选项:SignedJsonConverter<LicenseConfiguration>( null, Resources.LicenseConfigurationRsaPublicKey, false)——即用内嵌于 Common 库资源
LicenseConfigurationRsaPublicKey的 RSA 公钥验签;该公钥即为
extracted\keys_dump.txt中的ServerSideSigningPublicKey(见 §10.2)。- 配置对象的所有属性 getter 均实时重新
Deserialize(每次读取都重新解析签名 JSON),
无本地持久化缓存。 - 编译日期由
SignedLicenseConfigurationCompileDateProvider提供(嵌入在签名配置中,
用于试用期起算,见 §5.3)。
4. 许可管理器架构(三层)
LicenseManagerFactory.CreateLicenseManager(config, useUserStorage) 组装三个管理器实例并
交给门面 \uE01E 组合(均为 Jam.License.Validation 内混淆类型):
| 实例 | 类型 | 职责 |
|---|---|---|
| Full | \uE017 |
正式许可管理器:双存储(系统 DPAPI + portable AES),支持在线/手动激活 |
| Full 版 Trial | \uE020 |
Full 产品试用:portable 存储 + API,读服务器签名的 SignedDto<LicenseInformation> |
| Portable 版 Trial | \uE021 |
Portable(便携版)试用:DPAPI 系统存储 + 硬编码口令 PBKDF2 加密过期时间 |
| 门面 | \uE01E |
组合以上管理器,对外提供统一查询/安装/更新接口 |
| 日期监控 | \uE01F(TimeProvider.System, 6h) |
每 6 小时检查一次系统时间回拨 |
静态字段:许可根目录 \uE001 = %ProgramData%\JAM Software\Licenses(系统级);
用户级目录为 %LocalAppData%\JAM Software\Licenses(UserAndSystem 激活时使用)。
Trial 管理器的详细职责与校验链见 §5.3。
4.1 Full 管理器 \uE017 的组装细节
- 文件名提供器:
\uE03B(ProductGroupId, false)—— 系统/用户许可文件名(SHA256 种子 → hex 大写)\uE03C(ProductGroupName, SignatureVersion, false)—— portable 许可文件名
- 存储:
\uE037系统存储(DPAPI)+ 旧文件名 → 新文件名迁移逻辑(\uE033迁移器)\uE03Eportable 存储(AES-CBC,格式Base64(IV).Base64(密文)),目录
Path.Combine(AppContext.BaseDirectory, "license")- DPAPI 熵 =
SHA512(UTF8(ProductGroupName + SignatureVersion.ToString(InvariantCulture)))
(由LicenseManagerFactory.\uE005(name, version)计算:SHA512(UTF8(name + version)),
无任何分隔符)
- 联网:
\uE044API 客户端(OnlineActivationClient,包装ApiClient)
4.2 门面 \uE01E / 日期监控
- 门面统一了"查询许可状态、判断是否需激活/更新、安装、卸载"等接口。
- 日期监控
\uE01F注入TimeProvider.System,用于检测系统时间被回拨(试用期作弊防护)。
5. 许可状态与试用机制
5.1 LicenseState 枚举(Jam.License.Common)
| 值 | 名称 | 含义 |
|---|---|---|
| 0 | Unknown |
未知 |
| 1 | Valid |
有效 |
| 2 | Invalid |
无效 |
| 3 | Expired |
已过期 |
| 4 | Corrupted |
数据损坏 |
| 5 | NotFound |
未找到许可 |
| 6 | VersionNotSupported |
版本不支持 |
| 7 | ManualActivationRequired |
需要手动激活(在线激活失败时进入) |
5.2 其他枚举
LicenseType:None=0, Perpetual=1(永久), Trial=2(试用), Freeware=3, Subscription=4(订阅)
ProductEdition:None=0, Classic=1, DataCentral=2, Enterprise=3(=EnterpriseEdition), Personal=4, Professional=5, SmallBusiness=6, Standard=7
ActivationType:UserAndSystem=0(激活绑定用户+系统)、System=1(仅绑定系统)
ReminderType:None=0, Standard=1, Reseller=2
OnlineActivationFailureReason:KeyInvalid=0, VersionOutOfMaintenance=1, SubscriptionExpired=2
5.3 试用机制
IsTrialSupported = TrialLengthDays > 0。试用由两个管理器实现,职责已由反编译确认:
Full 版试用管理器 \uE020(继承 \uE018)——"服务器签发"模型:
- 构造:
(SignedLicenseConfiguration, 存储提供器, API 客户端包装, 编译日期);
存储提供器为 portable 存储(\uE03E,AppContext.BaseDirectory\license)。 - 读取(
\uE010):从 portable 存储读取SignedDto<LicenseInformation>(服务器签名签发的
Trial 许可信息),完整校验链:FileNotFoundException→LicenseState.NotFound;- 验签/格式/JSON 异常(
CryptographicException/FormatException/InvalidOperationException/
JsonException)→LicenseState.Corrupted; Content.State == Corrupted或状态校验(\uE008)不通过 →NotFound;- 有效(
Valid)时先做配置匹配(\uE005)与 key 检查(\uE009,决定是否重新拉取刷新),
再做过期检查(\uE006,不过 →Expired)与版本检查(\uE007,不过 →
VersionNotSupported)。
\uE00B/\uE00C/\uE00F/\uE012全部抛NotImplementedException—— 只读管理器
(Trial 许可由服务器在线签发后经其他路径写入 portable 存储)。
Portable 版试用管理器 \uE021——"本地生成"模型:
- 构造:
(SignedLicenseConfiguration, 存储提供器, IVerifier<DateTime, LicenseState>, TimeProvider);
存储提供器为 DPAPI 系统存储(\uE037,%ProgramData%\JAM Software\Licenses,
文件名由 Trial 提供器\uE03B(ProductGroupId, true)生成)。 - 试用期数据:
TrialLicenseData(record(string LicenseKey, int SchemaVersion=2)),
其 LicenseKey 字段实际承载$1$Base64(IV)$Base64(密文)加密串(\uE021.\uE003(expirationDate)
生成),解密为过期时间DateTime(\uE021.\uE001解析,见 §7.3)。 - 查询逻辑(
\uE00C):- 显式传入加密串 → 解密 → 交
\uE023验证; - 存储无记录(首次运行)→ 新试用:
过期时间 = 今天 + TrialLengthDays(从当天开始); - 存储有记录 → 读
TrialLicenseData→ 解密 → 验证; - 15 个月重置规则(
\uE004):expirationDate.Date.AddMonths(15) <= 今天时
重新开始试用(再次今天 + TrialLengthDays)——即旧试用数据超过 15 个月即作废重来; FileNotFoundException→NotFound;解密/JSON 异常 →Corrupted(并记录
License check 0x... failed.日志)。
- 显式传入加密串 → 解密 → 交
- 写入(
\uE012):仅接受LicenseType.Trial;生成TrialLicenseData(加密过期串)存入存储。
Trial 有效期验证器 \uE023(IVerifier<DateTime, LicenseState>,防时间作弊核心):
构造 (TimeProvider, compileDate 编译日期, SignedLicenseConfiguration),Verify(expirationDate):
compileDate > now(UTC)(编译日期在未来,系统时间被回拨到编译前)→Corrupted;compileDate.Date.AddMonths(30) <= now.Date(编译日期已超过 30 个月前)→Expired
(防大幅回拨时间重置试用);- 允许天数
num = TrialLengthDays;若配置了PreviousTrialLengthDays,
num = Max(TrialLengthDays, PreviousTrialLengthDays)(兼容历史更长试用期); expirationDate - now > num 天(剩余天数超过允许最大值,过期时间被伪造/拉长)→Corrupted;expirationDate.Date <= now.Date→Expired;- 其余 →
Valid。
时间基准统一为 TimeProvider(\uE021 用 TimeProvider.System),配合 §4.2 中 6 小时周期的
时间回拨监控(\uE01F)。
IL 证据(de4dot 清理 IL,tools\validation_clean.il,Class12.Verify,行 12698-12868)——
五个判定分支与错误码逐一对应:
// ── 分支 1:compileDate > now(UTC) → Corrupted(0xa04c0022)
IL_0000: ldarg.0
IL_0001: ldfld System.DateTime Class12::dateTime_0 // compileDate
IL_0006: call DateTimeOffset::op_Implicit(DateTime)
IL_000b: ldarg.0
IL_000c: ldfld System.TimeProvider Class12::timeProvider_0
IL_0011: callvirt TimeProvider::GetUtcNow()
IL_0016: call DateTimeOffset::op_GreaterThan
IL_001b: brfalse.s IL_0020
...
IL_0029: ldc.i4 0xa04c0022
IL_0040: callvirt ILog::ErrorFormat("License check 0x{0:X} failed.", ...)
IL_0045: ldc.i4.4 // ret 4 = Corrupted
// ── 分支 2:compileDate.Date.AddMonths(30) <= now.Date → Expired(0xa04c0024)
IL_0047: ldarg.0
IL_0048: ldfld System.DateTime Class12::dateTime_0
IL_0050: call DateTime::get_Date()
IL_0058: ldc.i4.s 30
IL_005a: call DateTime::AddMonths(int32) // +30 个月
IL_0060: ldfld TimeProvider Class12::timeProvider_0
IL_0065: callvirt TimeProvider::GetUtcNow()
IL_006d: call DateTimeOffset::get_Date()
IL_0072: call DateTime::op_LessThanOrEqual
...
IL_0091: ldc.i4 0xa04c0024
IL_00a1: ldc.i4.3 // ret 3 = Expired
// ── 允许天数:TrialLengthDays;有 PreviousTrialLengthDays 时取 Max
IL_00a3: ldarg.0
IL_00a4: ldfld SignedLicenseConfiguration Class12::signedLicenseConfiguration_0
IL_00a9: callvirt SignedLicenseConfiguration::get_TrialLengthDays() // → V_2
IL_00af: ...
IL_00bb: ldloca.s V_3
IL_00bd: call Nullable<int32>::get_HasValue()
IL_00c2: brfalse.s IL_00ee
IL_00d5: callvirt SignedLicenseConfiguration::get_TrialLengthDays()
IL_00e0: stloc.3
IL_00e3: call Nullable<int32>::get_Value()
IL_00e8: call [mscorlib]System.Math::Max(int32, int32) // Max(Trial, Previous)
IL_00ed: stloc.2
// ── 分支 3:剩余天数(expiration - now) > num 天 → Corrupted(0xa04c0023)
IL_00ee: ldarg.1
IL_00ef: call DateTimeOffset::op_Implicit(pData)
IL_00f5: ldfld TimeProvider Class12::timeProvider_0
IL_00fa: callvirt TimeProvider::GetUtcNow()
IL_00ff: call DateTimeOffset::op_Subtraction
IL_0106: ldloca.s V_4
IL_0108: call TimeSpan::get_TotalDays() // 剩余天数(double)
IL_010d: ldloc.2
IL_010e: conv.r8
IL_010f: ble.un.s IL_0135 // 未超过 → 跳过
...
IL_0123: ldc.i4 0xa04c0023
IL_0133: ldc.i4.4 // ret 4 = Corrupted
// ── 分支 4:pData.Date <= now.Date → Expired(0xa04c0025,Debug 级日志)
IL_0135: ldarga.s pData
IL_0137: call DateTime::get_Date()
IL_013d: ldfld TimeProvider Class12::timeProvider_0
IL_0142: callvirt TimeProvider::GetUtcNow()
IL_0147: stloc.1
IL_0148: ldloca.s V_1
IL_014a: call DateTimeOffset::get_Date()
IL_014f: call DateTime::op_LessThanOrEqual
IL_0154: brfalse.s IL_0159
...
IL_016e: ldc.i4 0xa04c0025
IL_017e: ldc.i4.3 // ret 3 = Expired
// ── 分支 5:其余 → Valid
IL_0180: ldc.i4.1
IL_0181: ret // ret 1 = Valid
6. 存储位置与磁盘格式(核心)
6.1 汇总表
| 存储 | 目录 | 文件名 | 加密 | 磁盘格式 |
|---|---|---|---|---|
| 系统许可(Full) | %ProgramData%\JAM Software\Licenses |
SHA256(种子) → hex 大写 |
DPAPI (LocalMachine) | Base64(DPAPI(data)) |
| 用户许可(Full) | %LocalAppData%\JAM Software\Licenses |
同上(用户侧提供器) | DPAPI (LocalMachine) | Base64(DPAPI(data)) |
| Portable 许可(Full) | AppContext.BaseDirectory\license |
\uE03C 提供器生成 |
AES-256-CBC (PKCS7) | Base64(IV) + "." + Base64(密文) |
试用(Full 版 \uE020) |
AppContext.BaseDirectory\license |
\uE03C 提供器生成 |
AES-256-CBC (PKCS7) | SignedDto<LicenseInformation> JSON(签名) |
试用(Portable 版 \uE021) |
%ProgramData%\JAM Software\Licenses |
Trial 提供器 \uE03B(id, true) |
DPAPI + AES-256-CBC (PBKDF2) | TrialLicenseData → $1$Base64(IV)$Base64(密文) |
注 1:
LicenseManager.exe的 portable 目录基于AppContext.BaseDirectory,即
LicenseManager.exe所在目录(随主程序目录)。
注 2:Portable 版试用的 TrialLicenseData 在 DPAPI 系统存储目录中;其LicenseKey字段
承载$1$...加密过期串(见 §5.3)。
6.2 系统存储 \uE037(DPAPI 细节——经修正的结论)
- 写入:
ProtectedData.Protect(明文, null, DataProtectionScope.LocalMachine)—— 使用
null entropy。 - 读取:先尝试带 entropy 解(entropy 见 §7.1),捕获
CryptographicException后
回退到 null entropy 再解一次。即兼容"旧版带熵写入"与"当前 null 熵写入"两种文件。 - 文件内容为
Convert.ToBase64String(DPAPI密文)。 - 删除文件时捕获
DirectoryNotFoundException/IOException/UnauthorizedAccessException
并静默吞掉(删除失败不影响主流程)。
IL 证据(de4dot 清理 IL,tools\validation_clean.il,Class27,行 25520-26100)
读取(Struct41.MoveNext,带 entropy 失败回退 null entropy):
// 读文件 → FromBase64String → 带 entropy 尝试解密
IL_009b: call Convert::FromBase64String(string)
IL_00a2: ldloc.s V_5 // Base64 密文
IL_00a4: ldloc.1
IL_00a5: ldfld uint8[] Class27::byte_0 // entropy(§7.1)
IL_00aa: ldc.i4.1 // DataProtectionScope.LocalMachine
IL_00ab: call ProtectedData::Unprotect(byte[], byte[], scope)
IL_00b0: stloc.s V_6
IL_00b2: leave.s IL_00d2
} // end .try
catch [mscorlib]System.Security.Cryptography.CryptographicException
{
IL_00b4: pop
IL_00b5: ldsfld ILog Class27::ilog_0
IL_00ba: ldstr "DPAPI decryption with entropy failed, falling back to null entropy."
IL_00c0: callvirt ILog::Info(object, Exception)
IL_00c5: ldloc.s V_5
IL_00c7: ldnull // entropy = null
IL_00c8: ldc.i4.1 // LocalMachine
IL_00c9: call ProtectedData::Unprotect(byte[], null, LocalMachine) // 回退重解
IL_00ce: stloc.s V_6
写入(Struct43.MoveNext,null entropy + LocalMachine + Base64):
IL_0017: ldloc.1
IL_0018: ldfld Interface5 Class27::interface5_0
IL_001d: callvirt Interface5::imethod_0() // 存储目录
IL_0023: ldarg.0
IL_0024: ldfld uint8[] Class27/Struct43::byte_0 // 明文
IL_0029: ldnull // entropy = null
IL_002a: ldc.i4.1 // DataProtectionScope.LocalMachine
IL_002b: call ProtectedData::Protect(byte[], null, LocalMachine)
IL_0030: call Convert::ToBase64String(uint8[])
IL_0035: stloc.3 // → 写入文件(Base64 文本)
6.3 Portable 存储 \uE03E(AES-CBC 细节)
- 序列化(写入):
Convert.ToBase64String(IV) + "." + Convert.ToBase64String(密文)
—— IV 在前。 - 解析(读取):
.Split('.'),反编译元组命名中array[0]被命名为"密文"、array[1]
被命名为"IV",但实际文件布局为 IV 在前(以序列化代码为准)。 - 解密顺序:先尝试静态密钥(§7.2),失败后回退动态密钥(§7.4)。
- 目标盘判断:
\uE016通过总线类型(总线类型 7 = IEEE 1394)或DriveType.Removable、
PhysicalDrive 的 STORAGE_PROPERTY(DeviceBusType)判断目标盘是否可移动/可移除;
若是可移动盘则\uE00D抛NotSupportedException(portable 许可不允许装在可移动介质上)。
6.4 文件名提供器
\uE03B:系统/用户许可文件名 ——SHA256(种子).ToString().ToUpperInvariant()(hex 大写)。
种子含 ProductGroupId。\uE03C:portable 文件名 —— 种子含 ProductGroupName + SignatureVersion。
7. 密钥派生与加密(核心公式)
7.1 DPAPI entropy
entropy = SHA512( UTF8( name + version.ToString(InvariantCulture) ) ) // 无任何分隔符
由 LicenseManagerFactory.\uE005(name, version) 计算(Encoding.UTF8.GetBytes(name + version) →
SHA512.Create().ComputeHash)。调用方:
- Full 管理器:
\uE005(ProductGroupName, SignatureVersion) - Trial 管理器:
\uE005(ProductName, 1.0m)(版本常量1.0)
当前写入用 null entropy,entropy 仅用于兼容旧文件的读取回退(见 §6.2)。
IL 证据(de4dot 清理 IL,tools\validation_clean.il,LicenseManagerFactory.smethod_5,行 9565-9611):
.method private hidebysig static uint8[] smethod_5(string string_1, valuetype [mscorlib]System.Decimal decimal_0) cil managed
{
// 代码大小 63 (0x3f)
IL_0000: call [mscorlib]System.Text.Encoding::get_UTF8()
IL_0005: ldarg.0 // name
IL_0006: ldarga.s decimal_0
IL_0008: call CultureInfo::get_InvariantCulture()
IL_000d: call Decimal::ToString(IFormatProvider) // version.ToInvariantString()
IL_0012: call String::Concat(string, string) // name + version(无分隔符)
IL_0017: callvirt Encoding::GetBytes(string)
IL_001c: stloc.0
IL_001d: call SHA512::Create()
...
IL_0023: ldloc.1
IL_0024: ldloc.0
IL_0025: callvirt HashAlgorithm::ComputeHash(byte[]) // SHA512 摘要
IL_002a: stloc.2
IL_003d: ldloc.2
IL_003e: ret
}
7.2 Portable 静态 AES 密钥(已提取明文种子)
key = SHA256( UTF8( "G{'h^;GTAXa]<GjS5?^J[2bb~^UpTT=K" + ProductGroupName + SignatureVersion.ToString(InvariantCulture) ) )
其中 G{'h^;GTAXa]<GjS5?^J[2bb~^UpTT=K 为 Babel 字符串解密器在运行时解出的静态密钥种子
(已从内存/反编译中确认)。算法为 AES-256-CBC,IV 随机生成,填充 PKCS7。
7.3 Trial(Portable 版试用 \uE021)PBKDF2 密钥
key = Rfc2898DeriveBytes( password, IV, 10000, SHA1 ) // 输出 32 字节 → AES-256
password = "uPqGSXB7b&C9@&5jyk_gamC#SNY_L!Yq" // \uE021 静态字段,硬编码
- 口令为
\uE021类静态只读字段(已明文确认)。 - 加密内容为过期时间的 InvariantCulture 字符串;加密串布局:
- 加密:
"$1$" + Base64(IV) + "$" + Base64(密文)(版本标记固定为1) - 解密:
Split('$')→array[0]=="1"(否则InvalidOperationException)、
array[1]=IV、array[2]=密文;长度非 3 抛ArgumentException - 格式校验正则:
^\s*\$1\$.{24}\$.{44}\s*$(IV 24 字符 / 密文 44 字符 Base64)
- 加密:
- 每次写入随机 IV,密文不同但密钥可复算。
IL 证据(de4dot 清理 IL,tools\validation_clean.il,Class10,行 12127-12343)
smethod_1(PBKDF2 密钥派生,口令 Class10::string_0 + IV + 10000 轮 + SHA1 → 32 字节):
.method private hidebysig static uint8[] smethod_1(string string_1, uint8[] byte_0, int32 int_0) cil managed
{
// 代码大小 46 (0x2e)
.locals init (Rfc2898DeriveBytes V_0, uint8[] V_1)
IL_0000: ldarg.0 // password = Class10::string_0
IL_0001: ldarg.1 // salt = IV
IL_0002: ldc.i4 0x2710 // 10000 轮
IL_0007: call HashAlgorithmName::get_SHA1()
IL_000c: newobj Rfc2898DeriveBytes::.ctor(string, byte[], int32, HashAlgorithmName)
...
IL_0012: ldloc.0
IL_0013: ldarg.2 // 32(字节,即 AES-256 密钥长)
IL_0014: callvirt DeriveBytes::GetBytes(int32)
IL_0019: stloc.1
IL_002c: ldloc.1
IL_002d: ret
}
smethod_2(DateTime)(加密 → "$1$" + Base64(IV) + "$" + Base64(密文))关键序列:
IL_0000: ldarga.s dateTime_0
IL_0002: call DateTimeFormatInfo::get_InvariantInfo()
IL_0007: call DateTime::ToString(IFormatProvider) // 过期时间 → 字符串
IL_000d: call Aes::Create()
IL_0014: ldc.i4 0x100 // KeySize = 256
IL_0019: callvirt SymmetricAlgorithm::set_KeySize(int32)
IL_001f: callvirt SymmetricAlgorithm::GenerateIV() // 每次随机 IV
IL_0025: ldsfld string Class10::string_0 // 口令
IL_002b: callvirt SymmetricAlgorithm::get_IV()
IL_0032: call Class10::smethod_1(password, IV, 32) // → 密钥
IL_0037: callvirt SymmetricAlgorithm::set_Key(byte[])
... CreateEncryptor(Key, IV) → CryptoStream(Write) → ToBase64String(密文) → V_6 ...
IL_0085: callvirt SymmetricAlgorithm::get_IV()
IL_008a: call Convert::ToBase64String(byte[]) // → V_7(IV)
IL_0091: ldstr "$1$"
IL_0098: ldstr "$"
IL_009f: call String::Concat(string,string,string,string) // "$1$" + V_7 + "$" + V_6
IL_00a4: stloc.s V_8
IL_00fc: ldloc.s V_8
IL_00fe: ret
smethod_0(string)(解密)入参校验与布局解析:
IL_0032: ldarg.0
IL_0033: callvirt String::Trim()
IL_0038: ldc.i4.1 // 分隔符数组长度 1
IL_003f: ldc.i4.s 36 // '$' (0x24)
IL_0042: stelem.i2
IL_0043: ldc.i4.1 // StringSplitOptions
IL_0044: callvirt String::Split(char[], StringSplitOptions) // 按 '$' 拆 3 段
IL_004a: ldloc.0
IL_004d: ldc.i4.3
IL_004e: beq.s IL_0060 // 非 3 段 → ArgumentException
IL_0063: ldstr "1"
IL_0068: call String::op_Inequality(array[0], "1")
IL_0075: ldstr "Unknown version of encrypted content." // 版本非 1 → InvalidOperationException
IL_0080: call Aes::Create()
IL_0087: ldc.i4 0x100
IL_0092: ldloc.0
IL_0093: ldc.i4.1
IL_0094: ldelem.ref
IL_0095: call Convert::FromBase64String(array[1]) // IV
IL_009a: callvirt SymmetricAlgorithm::set_IV(byte[])
IL_00a0: ldsfld string Class10::string_0
IL_00a6: callvirt SymmetricAlgorithm::get_IV()
IL_00ab: ldc.i4.s 32
IL_00ad: call Class10::smethod_1(password, IV, 32) // 同一密钥
IL_00b2: callvirt SymmetricAlgorithm::set_Key(byte[])
... CreateDecryptor → StreamReader::ReadToEnd → DateTime::Parse(s, InvariantInfo) ...
7.4 Portable 动态回退密钥(防静态分析第二层)
key = SHA256( UTF8( string.Format("{0}{1}{2}{3}", 卷序列号, 磁盘总大小(TotalSize), ProductGroupName, SignatureVersion) ) )
- 卷序列号与磁盘总大小来自
DriveInfo(目标盘所在卷)。 - 当静态密钥解密失败(文件被用另一台机器/另一种格式写入)时回退到该动态密钥。
- 注意
string.Format的四段之间无分隔符,顺序为:卷序列号 → TotalSize →
ProductGroupName → SignatureVersion。
7.5 AES 辅助封装 \uE016(Jam.License.Validation)
\uE000(加密):返回(密文, IV)二元组,AES-CBC-PKCS7,IV 随机。\uE001(解密):输入密文与 IV。\uE007(选钥):根据上下文选择静态(§7.2)或动态(§7.4)密钥。
8. RSA 签名与 SignedDto 序列化(核心安全机制)
8.1 算法
- RSA-2048 + SHA256 + PKCS#1 v1.5 padding。
- 签名器
DigitalSignatureSignerRsa.Sign(bytes, privateKeyXml):
SHA256 摘要 →RSACryptoServiceProvider.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)。 - 验签器
DigitalSignatureVerifierRsa.VerifySignature(publicKeyXml, bytes, signature):
VerifyHash(...);另有 CspBlob 形式的重载。 - 默认密钥长度
RsaKeySize.Rsa2048。
IL 证据(de4dot 清理 IL,tools\common_clean.il)
DigitalSignatureSignerRsa.Sign(IEnumerable<byte>, privateKeyXml)(行 3908-3985):
// 代码大小 104 (0x68)
.locals init (RSACryptoServiceProvider V_0, SHA256 V_1, uint8[] V_2, ByteSequence V_3)
IL_0000: ldarg.0
IL_0001: call get_KeySizeRsa() // 构造器置为 Rsa2048
IL_0006: newobj RSACryptoServiceProvider::.ctor(int32)
IL_000c: ldc.i4.0
IL_000d: callvirt RSACryptoServiceProvider::set_PersistKeyInCsp(bool)
IL_0013: ldloc.0
IL_0014: ldarg.2
IL_0015: callvirt AsymmetricAlgorithm::FromXmlString(privateKeyXml) // 载入私钥 XML
IL_001a: call SHA256::Create()
IL_0021: ldarg.1
IL_0022: call Enumerable::ToArray<byte>(pData)
IL_0027: callvirt HashAlgorithm::ComputeHash(byte[]) // SHA256 摘要
IL_002c: stloc.2
IL_002d: ldloc.0
IL_002e: ldloc.2
IL_002f: call HashAlgorithmName::get_SHA256()
IL_0034: call RSASignaturePadding::get_Pkcs1()
IL_0039: callvirt RSA::SignHash(hash, SHA256, Pkcs1) // RSA 签名
IL_003e: newobj ByteSequence::.ctor(IEnumerable<byte>)
IL_0043: stloc.3
IL_0066: ldloc.3
IL_0067: ret
DigitalSignatureVerifierRsa.VerifySignature(publicKeyXml, data, signature)(行 4094-4151):
IL_0000: ldarg.0
IL_0001: call get_KeySizeRsa()
IL_0006: newobj RSACryptoServiceProvider::.ctor(int32)
IL_000c: ldc.i4.0
IL_000d: callvirt set_PersistKeyInCsp(bool)
IL_0013: ldloc.0
IL_0014: ldarg.1
IL_0015: callvirt AsymmetricAlgorithm::FromXmlString(publicKeyXml)
IL_001a: ldarg.2
IL_001b: call Enumerable::ToArray<byte>(data)
IL_0020: call smethod_0(byte[]) // SHA256.Create().ComputeHash
IL_0025: stloc.1
IL_0026: ldloc.0
IL_0027: ldloc.1
IL_0028: ldarg.3
IL_0029: callvirt ByteSequence::ToArray() // 签名
IL_002e: ldsfld HashAlgorithmName hashAlgorithmName_0 // .cctor 置 SHA256
IL_0033: call RSASignaturePadding::get_Pkcs1()
IL_0038: callvirt RSA::VerifyHash(hash, signature, SHA256, Pkcs1)
IL_003d: stloc.2
IL_0050: ldloc.2
IL_0051: ret
另有
VerifySignature(ByteSequence publicKeyBlob, ...)重载(行 4034-4092):
用ImportCspBlob(publicKeyBlob.ToArray())导入公钥(CAPI2 CspBlob 形式),验签主流程相同。
8.2 SignedDto 模式
所有与服务器交互的载荷都是签名 JSON:
{
"content": { ...业务数据 T... },
"schemaVersion": "1.0",
"signature": "<Base64 RSA 签名>",
"timestamp": "2026-08-14T00:00:00Z"
}
-
SignedDtoConverter<T>写入流程:- 深拷贝数据对象,置
Timestamp = UtcNow; - 将
Signature置 null 后序列化(净化转换器集合,排除带\uE003标记的转换器); DigitalSignatureSignerRsa.Sign(序列化字节, 私钥XML);- 回填签名 → 输出最终 JSON。
- 深拷贝数据对象,置
-
读取流程:
JsonSignatureVerifier先将签名字段写 null,再对整份 JSON 做规范化
(JsonSerializer.Serialize(Deserialize(...))),之后验签:- 签名 null/空 → 返回 false;
- Base64 格式非法 → 返回 false;
- 上下文:API 响应验签失败抛
JsonException;本地文件验签失败抛CryptographicException。
-
密钥装配(
LicenseManagerFactory.\uE004(),已确认):所有许可存储/网络载荷共用同一组
反序列化选项:new JsonSerializerOptions(JsonSerializerDefaults.Web) { Converters = { new SignedDtoConverter<LicenseInformation>( Resource.ClientSideSigningPrivatePublicKey, // 私钥(签名) Resource.ServerSideSigningPublicKey, // 公钥(验签) false) } }.AddAllJamLicenseConverters();即
SignedDtoConverter的构造签名为(privateKeyXml, publicKeyXml, bool),签名用客户端
私钥、验签用服务器公钥——两者均内嵌于Jam.License.Validation.Resources.Resource。
IL 证据(de4dot 清理 IL,tools\validation_clean.il,LicenseManagerFactory.smethod_4,行 9545-9563):
.method private hidebysig static class [System.Text.Json]System.Text.Json.JsonSerializerOptions smethod_4() cil managed
{
// 代码大小 39 (0x27)
IL_0000: ldc.i4.1 // JsonSerializerDefaults.Web
IL_0001: newobj JsonSerializerOptions::.ctor(JsonSerializerDefaults)
IL_0006: dup
IL_0007: callvirt JsonSerializerOptions::get_Converters()
IL_000c: call Resources.Resource::get_ClientSideSigningPrivatePublicKey() // 私钥
IL_0011: call Resources.Resource::get_ServerSideSigningPublicKey() // 公钥
IL_0016: ldc.i4.0 // isApiContext = false
IL_0017: newobj SignedDtoConverter<LicenseInformation>::.ctor(string,string,bool)
IL_001c: callvirt ICollection<JsonConverter>::Add(converter)
IL_0021: call JsonSerializerOptionsExtensions::AddAllJamLicenseConverters(options)
IL_0026: ret
}
SignedDtoConverter<TData> 构造器(tools\common_clean.il 行 17398-17421):
new DigitalSignatureSignerRsa() → 存 string_0=privateKey、bool_0=isApiContext →
new JsonSignatureVerifier(publicKey)(内部 new DigitalSignatureVerifierRsa())。
8.3 JsonSignatureVerifier 细节
- 兼容
signature/Signature两种字段大小写。 - 规范化(Serialize(Deserialize))消除了字段顺序/空白差异,确保客户端与服务端签名原始串一致。
IL 证据(de4dot 清理 IL,tools\common_clean.il,JsonSignatureVerifier,行 16819-17190)
Verify(string)(行 16842-16883)——解析 JSON 文档后转发:
IL_0000: ldarg.1
IL_0003: initobj JsonDocumentOptions
IL_000a: call JsonDocument::Parse(string, JsonDocumentOptions)
IL_000f: stloc.0
IL_0010: ldarg.0
IL_0011: ldloc.0
IL_0012: call Verify(JsonDocument) // 转发文档重载
IL_0017: stloc.2
IL_002a: ldloc.2
IL_002b: ret
Verify(JsonDocument)(行 16885-17190 关键段)——signature/Signature 大小写双查询:
IL_0000: ldarg.1
IL_0001: callvirt JsonDocument::get_RootElement()
IL_0008: ldloca.s V_6
IL_000a: ldstr "signature"
IL_000f: ldloca.s V_1
IL_0011: call JsonElement::TryGetProperty("signature", &V_1)
IL_0016: brfalse.s IL_001b // 未命中 → 尝试 "Signature"
IL_001e: ldstr "signature"
IL_0023: stloc.0 // 命中 "signature"
IL_0024: br.s IL_004d
IL_0026: ldarg.1
IL_0027: callvirt JsonDocument::get_RootElement()
IL_002e: ldloca.s V_6
IL_0030: ldstr "Signature"
IL_0035: ldloca.s V_1
IL_0037: call JsonElement::TryGetProperty("Signature", &V_1)
IL_0041: ldc.i4.0
IL_0042: brtrue IL_00dd // 两个都无 → 返回 false
IL_0047: ldstr "Signature"
IL_004c: stloc.0
IL_004d: ldloca.s V_1
IL_004f: call JsonElement::GetRawText() // 签名原始文本
IL_0054: stloc.2
// ... Convert.FromBase64String 失败 → false;签名内容为空 → false ...
// ... 签名写 null 后 Serialize(Deserialize(root)) 规范化 → VerifyHash ...
8.4 序列化转换器注册
JsonSerializerOptionsExtensions.AddAllJamLicenseConverters():
| 转换器 | 用途 |
|---|---|
\uE001 |
ByteSequence 转换器 |
\uE002 |
LicenseKey 转换器(规范化/格式化) |
\uE004 |
Version 转换器 |
JsonStringEnumConverter<ActivationType/ProductEdition/LicenseType/LicenseState> |
枚举按字符串序列化 |
\uE003 |
SignedDtoConverter 排除标记(防递归签名) |
9. 机器标识(绑定用指纹)
9.1 标识集合
| 标识 | 来源 | 算法 |
|---|---|---|
| SystemId | HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid(Registry64) |
直接使用 |
| UserId | Windows 身份(本地用户 'L' + UserName.ToUpperInvariant();域用户 'A' + User.Value) |
SHA256 → ByteSequence |
| DomainId | AD DomainIdentifier;非域环境走 Win32 API 回退 |
— |
| DriveId | GetVolumeInformation IOCTL(含 PhysicalDrive 的 STORAGE_PROPERTY;判断可移动/总线类型 7) |
— |
ByteSequence:IEnumerable<byte> 封装,提供 ToBase64() / ToHex()("0x"+hex)/
FromBase64 与序列相等比较。
9.2 用途
ActivationRequest携带 SystemId / UserId / DomainId;- 激活响应中的标识需与本地一致,否则拒绝(防拷许可到别的机器)。
10. 密钥泄露与攻击面
10.1 已提取密钥
extracted\keys_dump.txt 包含两个 RSA-2048 密钥对的 XML:
- ServerSideSigningPublicKey —— 服务器侧签名公钥(仅公钥)。
- ClientSideSigningPrivatePublicKey —— 完整私钥(含 P/Q/DP/DQ/InverseQ/D)。
10.2 公钥的分布与密钥使用方
- 配置公钥
LicenseConfigurationRsaPublicKey内嵌于Jam.License.Common资源,
用于校验LicenseConfiguration.json。 Jam.License.Validation.Resources.Resource内嵌 ClientSideSigningPrivatePublicKey(私钥)
与 ServerSideSigningPublicKey(公钥),经LicenseManagerFactory.\uE004()注入
SignedDtoConverter:客户端写(签名)用私钥、读(验签)用公钥(见 §8.2)。- 已确认使用该签名体系的场景:portable/系统/用户许可文件的序列化、激活/去激活请求、
激活响应解析、Trial 许可信息。
10.3 安全结论(基于事实)
- 客户端内嵌完整签名私钥(
ClientSideSigningPrivatePublicKey),意味着:- 可离线伪造任何 SignedDto(激活响应、SignedLicenseConfiguration、Trial 许可信息),
只要验签侧使用该公钥; SignedDtoConverter写入时用DigitalSignatureSignerRsa.Sign(..., privateKeyXml)
的私钥来源即此内嵌密钥。
- 可离线伪造任何 SignedDto(激活响应、SignedLicenseConfiguration、Trial 许可信息),
- 这是许可体系中最关键的削弱点:验证信任根(RSA 私钥)位于客户端本身。
- 服务器公钥(ServerSideSigningPublicKey)仅能验证服务器签名,但客户端回执由客户端私钥
签发时,服务器公钥无法伪造响应。
10.4 攻击面总结
| 层面 | 描述 |
|---|---|
| 签名信任根 | 客户端内嵌完整私钥 → 可离线伪造签名数据 |
| 本地存储 | DPAPI LocalMachine(同机任意用户可 Unprotect);AES 静态密钥种子已明文暴露 |
| 试用期 | PBKDF2 口令已明文暴露;时间回拨有 6h 监控但可绕过 |
| 激活流程 | 存在 30 天本地验证时间窗;ManualActivationRequired 状态可被伪造响应填充 |
| 机器绑定 | MachineGuid/用户标识可克隆(同一机器镜像) |
11. 激活流程(在线 / 手动 / 去激活)
11.1 API 端点(OnlineActivationClient,\uE044 族)
| 方法 | 端点 | 载荷 |
|---|---|---|
| POST | {host}api/v3/activation |
SignedDto<ActivationRequest> |
| POST | {host}api/v3/deactivation |
SignedDto<DeactivationRequest> |
| GET | {host}api/v3/license?licenseKey=<Formatted> |
— |
11.2 在线激活校验链(Full 管理器 \uE017.\uE00C 安装流程)
TryCreate(key):按 LicenseKey 规范化/格式化校验 key 格式(§12);- 无效 →
\uE01C.\uE000()返回NotFound; - 有效 → API
\uE001.\uE002(key)获取签名 JSON 许可信息; \uE018.\uE004验签 + 反序列化;- 服务器返回 Trial 但产品不支持 → 报错;
- 状态
Valid→ 做过期检查、产品版本检查、整体有效性检查; - 抛
HttpRequestException时,若\uE005(需要激活)→ 置ManualActivationRequired。
11.3 激活必要性判断
\uE017.\uE001(activationState, licenseInformation):
- 非
Valid/Expired状态 → 需要激活; Expired时:若为 Perpetual 且ExpireDate晚于今天 → 不需要激活(旧永久许可宽限)。
\uE017.\uE002(licenseState, licenseInformation):更新必要性判断,逻辑同上。
11.4 写入许可(\uE012)
- 仅支持
Perpetual与Subscription写入(RequiresDemoKeyRegistration例外以 Trial 写入); - 以
UTF8(licenseKey.Normalized)作为数据写入 portable 存储(§6.3); - 激活响应按 Activation 类型(系统型 / 域型 / 完整型)分别写入系统或用户存储。
11.5 手动激活(.jsa 文件)
两条入口(读取同一 DTO):
- GUI 手动激活对话框(TreeSize 应用内 / LicenseManager 对话框):
ManualActivationViewModel
构造时按"<ProductName>-<版本>"(\uE003.ProductName+ 版本字符串,实测TreeSize-9.8.2)生成
两个桌面路径:请求文件TreeSize-9.8.2-Activation-Request.jsar(本地生成、发送服务器)与
响应文件TreeSize-9.8.2-Activation.jsa(服务器签发、用户选择加载)。
响应文件不存在时弹 "The activation response could not be found"。
ProcessActivationResponseAsync(mode=0)完整链路:JsonSerializer.Deserialize<ManualActivation>(jsa, \uE001)——外层 无签名要求;\uE018.\uE00A(LicenseInformation.GetRawText(), key)校验(LicenseKey.Equals 未被打补丁,
content.licenseKey必须与对话框输入密钥一致);返回码 1=Corrupted / 2=Expired /
3=WrongLicense,0=通过;\uE040.ActivateAsync(key, Activation.GetRawText()):\uE041.Verify被补丁恒Valid;
三个静态判定(token 100663991-100663993)取用户激活\uE005优先:
DomainId==null && SystemId!=null && UserId!=null→ 写入用户存储
(%LocalAppData%\JAM Software\Licenses\<SHA256(seed)大写>)。
- CLI:
license activate --activationResponse <*.jsa>(poc/NOTES.md §11.2,Class9)。
.jsa 数据格式(ManualActivation DTO = activation + licenseInformation 两个
JsonElement + schemaVersion:1;Web options → camelCase):
{
"activation": {
"content": {
"licenseKey": "<42位Base32>", "productVersion": "9.8.2",
"systemId": "<任意非空串>", "userId": "<Base64>", "domainId": null,
"schemaVersion": 4
},
"schemaVersion": 1, "signature": "<Base64 RSA-SHA256>", "timestamp": "<ISO8601>"
},
"licenseInformation": {
"content": { "customerId":1, "entitlements":[], "expireDate":"<未来UTC>",
"licenseKey":"<与 activation 相同>", "licenseType":"Perpetual",
"maintenance":{"endDate":"<未来>","reminderType":0,"autoRenewal":true},
"product":{"id":1,"name":"TreeSize","edition":"Classic"},
"schemaVersion":1, "state":"Valid" },
"schemaVersion": 1, "signature": "<Base64 RSA-SHA256>", "timestamp": "<ISO8601>"
},
"schemaVersion": 1
}
伪造要点(Generator --gen-jsa 已实现,见 poc/POC.md §17.3 路径 C):
Activation构造函数(token 100663641)自动置SchemaVersion=4;- 填
systemId+userId(domainId=null)即命中用户激活判定\uE005→ 用户存储最简路径; - 两个内部载荷用 ClientSide 私钥真签名(补丁已恒真可绕过,真签与运行时验签完全一致);
- 关键约束:
licenseInformation.content.licenseKey必须与激活对话框输入的密钥一致
(LicenseKey.Equals未被打补丁),否则返回错误码 3(WrongLicense)。
本地验证有 30 天时间窗(服务器无法联网时仍可工作)。
12. LicenseKey 算法
12.1 规范化(Normalized)
key.ToUpperInvariant()
.Replace("-", "")
.Replace("0", "O")
.Replace("1", "I")
.Replace("8", "B")
.Trim()
IL 证据(de4dot 清理 IL,tools\common_clean.il,LicenseKey.smethod_1,行 2706-2731):
.method private hidebysig static string smethod_1(string string_2) cil managed
{
// 代码大小 72 (0x48)
IL_0000: ldarg.0
IL_0001: callvirt String::ToUpperInvariant()
IL_0006: ldstr "-"
IL_000b: ldsfld String::Empty
IL_0010: callvirt String::Replace("-", "") // 去连字符
IL_0015: ldstr "0"
IL_001a: ldstr "O"
IL_001f: callvirt String::Replace("0", "O") // 混淆字符归一
IL_0024: ldstr "1"
IL_0029: ldstr "I"
IL_002e: callvirt String::Replace("1", "I")
IL_0033: ldstr "8"
IL_0038: ldstr "B"
IL_003d: callvirt String::Replace("8", "B")
IL_0042: callvirt String::Trim()
IL_0047: ret
}
12.2 格式校验
- 正则
^[A-Z2-7]{42}$:42 位,字母表为 Base32 字母表(去0/1/8干扰字符;
2-7数字 +A-Z去I/O后与 Base32 剩余字母对齐)。 - 说明:
8→B、0→O、1→I的替换使得手输易混淆字符归一。
IL 证据(de4dot 清理 IL,tools\common_clean.il,行 2828-2864 与 2866-2892)
.cctor(两个正则编译并缓存):
IL_0000: ldstr "[A-Z2-7]{{{0}}}(\?!$)" // 格式化模板
IL_0005: ldc.i4.6 // 分组长度 6
IL_0006: box Int16
IL_000b: call String::Format(template, 6)
IL_0010: ldc.i4.0 // RegexOptions.None
IL_0011: ldc.r8 30.
IL_001a: call TimeSpan::FromSeconds(30.) // 30s 超时
IL_001f: newobj Regex::.ctor(pattern, options, timeout)
IL_0024: stsfld Regex LicenseKey::regex_0 // [A-Z2-7]{6}(?!$)
IL_0029: ldstr "^[A-Z2-7]{42}$"
IL_002e: ldc.i4.8 // RegexOptions.IgnoreCase
IL_002f: ldc.r8 2.
IL_0038: call TimeSpan::FromSeconds(2.)
IL_003d: newobj Regex::.ctor(pattern, options, timeout)
IL_0042: stsfld Regex LicenseKey::regex_1 // ^[A-Z2-7]{42}$
smethod_2(校验路径,行 2828-2864):smethod_1(规范化)后 regex_1.IsMatch,
未匹配时 bool_0=true(构造器调用)→ 抛 FormatException;bool_0=false(TryCreate)→ 返回 false:
IL_0040: ldarg.0
IL_0041: call LicenseKey::smethod_1(string) // 规范化
IL_0046: stloc.0
IL_0047: ldsfld Regex LicenseKey::regex_1
IL_004c: ldloc.0
IL_004d: callvirt Regex::IsMatch(string)
IL_0054: ldc.i4.1
IL_0057: ldc.i4.0
IL_0058: brtrue.s IL_0070 // 匹配 → 成功
IL_005a: ldarg.2
IL_005b: brfalse.s IL_0060
IL_0063: ldstr "The passed license key is not in a recognized format."
IL_0068: newobj FormatException::.ctor(string) // 构造器路径抛异常
IL_006d: throw
IL_006e: ldc.i4.0 // TryCreate 路径返回 false
IL_006f: ret
IL_0070: ldarg.1
IL_0071: ldloc.0
IL_0072: stind.ref // out 规范化结果
IL_0073: ldc.i4.1
IL_0074: ret
12.3 格式化(显示用)
- 每 6 字符插入
-:正则[A-Z2-7]{6}(?!$)→$0-(如ABCDEF-GHIJKL-...)。 FormattedShort:取格式化串尾部 20 字符(用于 UI 简短显示)。TypeConverter内嵌类\uE000支持字符串 ↔ LicenseKey 转换。
IL 证据(de4dot 清理 IL,tools\common_clean.il,行 2620-2631 与 2743-2775)
smethod_0(分组插入 -):
.method private hidebysig static string smethod_0(string string_2) cil managed
{
// 代码大小 17 (0x11)
IL_0000: ldsfld Regex LicenseKey::regex_0 // [A-Z2-7]{6}(?!$)
IL_0005: ldarg.0
IL_0006: ldstr "$0-"
IL_000b: callvirt Regex::Replace(string, "$0-") // 每 6 位后补连字符
IL_0010: ret
}
TryCreate(行 2743-2775)——公开的解析入口,smethod_2(key, out, false) 成功后构造:
.method public hidebysig static bool TryCreate(string pLicenseKey, [out] LicenseKey& pResult) cil managed
{
IL_0000: ldarg.1
IL_0001: ldnull
IL_0002: stind.ref // pResult = null
IL_0003: ldarg.0
IL_0006: ldc.i4.0
IL_0007: call LicenseKey::smethod_2(key, out, false) // 校验 + 规范化
IL_000c: brfalse.s IL_0011
IL_000e: ldc.i4.1
IL_000f: br.s IL_0012
IL_0011: ldc.i4.0
IL_0012: brtrue.s IL_0016
IL_0014: ldc.i4.0
IL_0015: ret // 校验失败 → false
IL_0016: ldarg.1
IL_0017: ldloc.0
IL_0018: newobj LicenseKey::.ctor(string) // 规范化串构造
IL_001d: stind.ref
IL_001e: ldc.i4.1
IL_001f: ret
}
13. 基础类型清单(附录)
13.1 Jam.License.Common
| 类型 | 说明 |
|---|---|
Entitlement |
record (int ProductID, string Name, string Metric, int? Limit, bool IsUnlimited) |
ProductInformation |
record (int ID, string Name, ProductEdition Edition, int ProductGroupID) |
MaintenanceInformation |
record (DateTime EndDate, ReminderType ReminderType, bool AutoRenewal) |
Activation |
{ LicenseKey, Version ProductVersion, String SystemId, ByteSequence UserId, ByteSequence DomainId, SchemaVersion } |
ActivationRequest / DeactivationRequest |
同 Activation 结构 |
ActivationDtoV1 |
仅 SystemId |
ActivationDtoV2 |
SystemId + HashedUserInfo |
SignedActivationDtoV1 / V2 |
对应签名版本 |
ManualActivation |
Activation + LicenseInformation 两个 JsonElement |
WebClientWrapperDto<T> |
通用包装 |
SignedJsonConverter<TData> |
包装 SignedDtoConverter,读写 Content 字段 |
Resources |
内嵌 RSA 公钥等资源 |
13.2 Jam.License.Validation(补充)
| 类型 | 说明 |
|---|---|
TrialLicenseData |
record (string LicenseKey, int SchemaVersion=2);试用数据载体,LicenseKey 字段实际存放 $1$... 加密过期串(§5.3/§7.3) |
LicenseFileSchemeVersion |
枚举 Latest / JamLicense30AndUp / JamLicense50AndUp / JamLicense60AndUp(许可文件格式版本迁移标记) |
\uE023 |
IVerifier<DateTime, LicenseState> Trial 有效期验证器(编译日期/最大天数校验,§5.3) |
\uE01F |
时间回拨监控(TimeProvider + 6h 周期,§4.2) |
\uE033 |
旧文件名 → 新文件名迁移器((rootDir, portableProvider, systemProvider)) |
\uE019 |
SignedLicenseConfigurationCompileDateProvider(从签名配置取编译日期,\uE006 调用 GetCompileDateUtc) |
| 存储提供器族 | 接口 \uE03A/\uE039 及实现:\uE037(DPAPI 系统存储)、\uE03E(AES portable 存储);两者均带 \uE014<T>(泛型读)/\uE015<T>(泛型写) |
| 在线激活 | OnlineActivationClient(\uE042)、ActivationManagerFactory、ActivationState、IActivationManager/IActivationVerifier、\uE043/\uE044(API 客户端包装) |
| 机器标识 | AdBasedDomainIdentifierProvider / WinApiDomainIdentifierProvider / DomainIdentifierProvider / IDomainIdentifierProvider(域标识两种来源,§9) |
| 通知 | IUserNotifier:DebugNotifier / EventLogNotifier / MessageBoxNotifier / AbstractUserNotifier |
| UI | DialogFactory、ManualActivationViewModel、LicenseKeyDialogResources 等(激活/试用过期对话框) |
13.3 dnSpy MCP 全量类型索引(缓存,2026-08-15)
获取方法(可复现):dnSpy MCP → search_types(assembly_name="Jam.License.Validation", query="*", names_only=true, page_size=500);
Common 同理。混淆名(\uE0xx)用实际 Unicode 字符传递。
Jam.License.Validation —— 共 190 类型(混淆类型为主,命名空间类完整保留):
<Module>; Microsoft.CodeAnalysis.EmbeddedAttribute;
System.Runtime.CompilerServices.{IsUnmanagedAttribute,NullableAttribute,NullableContextAttribute,
RefSafetyRulesAttribute,ExtensionMarkerAttribute,IsExternalInit};
混淆根类型: \uE006,\uE009,\uE00A`1,\uE00B..\uE01D,\uE01E(嵌套 \uE000..\uE006),\uE01F(嵌套 \uE000),
\uE020(嵌套 \uE000,\uE001),\uE021(嵌套 \uE000,\uE001),\uE022,\uE023,\uE024,\uE025,\uE026(嵌套 \uE000),
\uE027(嵌套 \uE000..\uE002),\uE028(嵌套 \uE000),\uE029,\uE02A,\uE02B,\uE02C,\uE02D,\uE02E,\uE02F,
\uE030..\uE03C,\uE03D,\uE03E(嵌套 \uE000..\uE005),\uE03F(嵌套 <>c),\uE040(嵌套 \uE000..\uE00B),
\uE041,\uE042,\uE043,\uE044,\uE045..\uE04E,\uE04F..\uE054
明文根类型: Jam.License.Validation.{LicenseManagerFactory,SignedLicenseConfigurationCompileDateProvider};
含嵌套: \uE017/…\uE005、\uE018/…\uE002、\uE019 等
命名空间类型:
.UI: DialogFactory(嵌套 \uE000)、ILicenseKeyDialog、LicenseKeyDialogResources、
ManualActivationDialogResources、ManualLicenseUpdateDialogResources、TrialExpiredDialogResources、
UserControls.ManualActivationViewModel(嵌套 \uE000)
.Resources.Resource (公钥宿主, 补丁 1 目标)
.Persistence: LicenseFileSchemeVersion、TrialLicenseData
.Notifications: AbstractUserNotifier、DebugNotifier、EventLogNotifier、IUserNotifier、MessageBoxNotifier
.Clients: ApiClient(嵌套 \uE000`2、<>c、<>c__13`1、<>c__19`1、\uE001`1..\uE006`2)、
ApiException、IApiClient
.Clients.OnlineActivation: ActivationException、ActivationManagerFactory、ActivationState、
IActivationManager、IActivationVerifier、OnlineActivationClient(嵌套 \uE000..\uE003)
.Clients.Identification: AdBasedDomainIdentifierProvider、DomainIdentifierProvider、
IDomainIdentifierProvider、WinApiDomainIdentifierProvider
BabelObfuscatorAttribute
Jam.License.Common —— 共 76 类型:
<Module>; Microsoft.CodeAnalysis.EmbeddedAttribute;
System.Runtime.CompilerServices.{NullableAttribute,NullableContextAttribute,IsExternalInit};
ActivationType、ByteSequence、Entitlement、ILicenseManager、LicenseChangedEventArgs、
LicenseChangingEventArgs、LicenseConfiguration、LicenseException、LicenseKey(嵌套 \uE000)、
LicenseNotFoundException、LicenseState、LicenseType、OnlineActivationFailureReason、ProductEdition、
ProductInformation、Resources、SignedLicenseConfiguration、
Validation.IVerifier`2、Signature.{DigitalSignatureSignerRsa,DigitalSignatureVerifierRsa,
IDigitalSignatureSigner,IDigitalSignatureVerifier,RsaKeySize}、
ExtensionMethods.{ConvertibleToBytesExtensions,SignedDtoExtensions}、
DTOs.{DebugLicenseInformation,EntitlementExtensions(嵌套 <>c),IConvertibleToBytes,ICreationTimeAware,
ILicenseInformation,ISignedDto,ISignedDto`1,IVersionedDto,LicenseInformation,SignedDto`1,VersionedDto,
Maintenance.{MaintenanceRequestDtoV1,MaintenanceResponseDtoV1,MaintenanceResponseDtoV2,
SignedMaintenanceResponseDtoV2},
Activation.{Activation,ActivationDtoV1,ActivationDtoV2,ActivationRequest,ActivationRequestDtoV1,
ActivationRequestDtoV2,DeactivationRequest,DeactivationRequestDtoV1,DeactivationRequestDtoV2,
ManualActivation,SignedActivationDtoV1,SignedActivationDtoV2,SignedActivationRequestDtoV2,
SignedDeactivationRequestDtoV2,WebClientWrapperDto`1}},
Converters.JSON.{JsonSerializerOptionsExtensions,JsonSignatureVerifier,SignedDtoConverter`1(嵌套 <>c),
SignedJsonConverter`1},
Clients.Maintenance.{MaintenanceInformation,ReminderType};
混淆根类型: \uE000..\uE004
BabelObfuscatorAttribute
注:
\uE041(Validation)= 激活验证器实现类(见 poc/POC.md §17.9),\uE040= OnlineActivation API 客户端族(含
\uE040/\uE003等 12 个嵌套),\uE000(Common)= 签名服务提供器族。token 需assembly_name
消歧(token 仅模块内唯一)。
ps



浙公网安备 33010602011771号