AIGC标识 【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)

三个许可相关程序集(LicenseManagerJam.License.CommonJam.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 自定义加密基础设施Class55SymmetricAlgorithm 子类)为 Babel 运行时反混淆流,其 CreateEncryptor 仅把 Key/IV 复制到参数并返回自身,GenerateKeySystem.Random 生成 16 字节——与许可数据加密无关,属混淆器基础设施。

1.3 去混淆处理

已用 de4dot(de4dot-cex 版)对 Jam.License.Validation.dll
Jam.License.Common.dll 去混淆,产物:

  • extracted\Jam.License.Validation-clean.dll
  • extracted\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.exeJam.dll 均为原生程序集(PowerShell AssemblyName.GetAssemblyName
    BadImageFormatException,无 .NET 清单)。
  • 原生侧通过 COM 创建 JamCOMObjectFactoryJam.Interop.dll[ComVisible(true)]
    Guid 0B87D366-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)

  1. \uE003.WaitOne() 加锁;
  2. 注册表无该键 且 栈 \uE001 非空 → 弹出程序集逐个调 \uE006(asm) 注册其全部类型;
  3. 仍未命中 → \uE007():遍历 AppDomain.CurrentDomain.GetAssemblies() 全部程序集补扫;
  4. \uE002[名称]Activator.CreateInstance
  5. 类型不存在 → 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 启动时按序执行(全部为间接调用,无类名字面量):

  1. 依赖预加载 sub_22A1B70:3 处直接调用 vtable +1064 槽位(含空串与
    Jam.Interop.dllLicenseManager.exe 路径),随后 16 次调 sub_22A1AE0 注册其余依赖
    (合计 18 项文件 = 11 项 JAM 相关 + 7 项 .NET Standard 运行库,清单见 poc/POC.md §17.7)。
    sub_22A1AE0sub_13F8CF0 拼接路径 → sub_1542F50 存在性检查 → 存在则经目标
    vtable +1064 槽位注册(模块类型初始化器触发点)。
  2. CLR SxS 宿主 sub_12E3900mscoree.dllCLRCreateInstanceICLRMetaHost
    承载托管库。
  3. 工厂创建入口 sub_12EA520(约 44KB):先对承载程序集做 WinVerifyTrust 签名验证 +
    证书嵌入比对,通过后创建 JamCOMObjectFactory 单例(getter sub_12E9980 / RTTI 创建
    sub_12E98D0,全局 qword_3186DC0)。
  4. 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_22A1F98off_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 LicenseConfigurationJam.License.Common),JSON 属性顺序:

顺序 字段 含义
0 ProductName 产品名
1 ProductVersion 产品版本
2 ProductGroupId 产品组 ID(用于系统/用户许可文件名生成)
3 ProductGroupName 产品组名(用于密钥派生熵、portable 文件名)
4 SignatureVersion 签名版本(参与密钥派生熵)
5-7 (未公开或后续字段)
8 TrialLengthDays 试用期天数;>0IsTrialSupported = 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 迁移器)
    • \uE03E portable 存储(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))
      无任何分隔符
  • 联网:\uE044 API 客户端(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 其他枚举

LicenseTypeNone=0, Perpetual=1(永久), Trial=2(试用), Freeware=3, Subscription=4(订阅)
ProductEditionNone=0, Classic=1, DataCentral=2, Enterprise=3(=EnterpriseEdition), Personal=4, Professional=5, SmallBusiness=6, Standard=7
ActivationTypeUserAndSystem=0(激活绑定用户+系统)、System=1(仅绑定系统)
ReminderTypeNone=0, Standard=1, Reseller=2
OnlineActivationFailureReasonKeyInvalid=0, VersionOutOfMaintenance=1, SubscriptionExpired=2

5.3 试用机制

IsTrialSupported = TrialLengthDays > 0。试用由两个管理器实现,职责已由反编译确认:

Full 版试用管理器 \uE020(继承 \uE018——"服务器签发"模型:

  • 构造:(SignedLicenseConfiguration, 存储提供器, API 客户端包装, 编译日期)
    存储提供器为 portable 存储\uE03EAppContext.BaseDirectory\license)。
  • 读取(\uE010):从 portable 存储读取 SignedDto<LicenseInformation>(服务器签名签发的
    Trial 许可信息),完整校验链:
    1. FileNotFoundExceptionLicenseState.NotFound
    2. 验签/格式/JSON 异常(CryptographicException/FormatException/InvalidOperationException/
      JsonException)→ LicenseState.Corrupted
    3. Content.State == Corrupted 或状态校验(\uE008)不通过 → NotFound
    4. 有效(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):
    1. 显式传入加密串 → 解密 → 交 \uE023 验证;
    2. 存储无记录(首次运行)→ 新试用:过期时间 = 今天 + TrialLengthDays从当天开始);
    3. 存储有记录 → 读 TrialLicenseData → 解密 → 验证;
    4. 15 个月重置规则\uE004):expirationDate.Date.AddMonths(15) <= 今天
      重新开始试用(再次 今天 + TrialLengthDays)——即旧试用数据超过 15 个月即作废重来;
    5. FileNotFoundExceptionNotFound;解密/JSON 异常 → Corrupted(并记录
      License check 0x... failed. 日志)。
  • 写入(\uE012):仅接受 LicenseType.Trial;生成 TrialLicenseData(加密过期串) 存入存储。

Trial 有效期验证器 \uE023IVerifier<DateTime, LicenseState>,防时间作弊核心):
构造 (TimeProvider, compileDate 编译日期, SignedLicenseConfiguration)Verify(expirationDate)

  1. compileDate > now(UTC)(编译日期在未来,系统时间被回拨到编译前)→ Corrupted
  2. compileDate.Date.AddMonths(30) <= now.Date(编译日期已超过 30 个月前)→ Expired
    (防大幅回拨时间重置试用);
  3. 允许天数 num = TrialLengthDays;若配置了 PreviousTrialLengthDays
    num = Max(TrialLengthDays, PreviousTrialLengthDays)(兼容历史更长试用期);
  4. expirationDate - now > num 天(剩余天数超过允许最大值,过期时间被伪造/拉长)→ Corrupted
  5. expirationDate.Date <= now.DateExpired
  6. 其余 → Valid

时间基准统一为 TimeProvider\uE021TimeProvider.System),配合 §4.2 中 6 小时周期的
时间回拨监控(\uE01F)。

IL 证据(de4dot 清理 IL,tools\validation_clean.ilClass12.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.ilClass27,行 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)判断目标盘是否可移动/可移除;
    若是可移动盘则 \uE00DNotSupportedException(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.ilLicenseManagerFactory.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.ilClass10,行 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> 写入流程:

    1. 深拷贝数据对象,置 Timestamp = UtcNow
    2. Signature 置 null 后序列化(净化转换器集合,排除带 \uE003 标记的转换器);
    3. DigitalSignatureSignerRsa.Sign(序列化字节, 私钥XML)
    4. 回填签名 → 输出最终 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.ilLicenseManagerFactory.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=privateKeybool_0=isApiContext
new JsonSignatureVerifier(publicKey)(内部 new DigitalSignatureVerifierRsa())。

8.3 JsonSignatureVerifier 细节

  • 兼容 signature / Signature 两种字段大小写。
  • 规范化(Serialize(Deserialize))消除了字段顺序/空白差异,确保客户端与服务端签名原始串一致。

IL 证据(de4dot 清理 IL,tools\common_clean.ilJsonSignatureVerifier,行 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)

ByteSequenceIEnumerable<byte> 封装,提供 ToBase64() / ToHex()"0x"+hex)/
FromBase64 与序列相等比较。

9.2 用途

  • ActivationRequest 携带 SystemId / UserId / DomainId;
  • 激活响应中的标识需与本地一致,否则拒绝(防拷许可到别的机器)。

10. 密钥泄露与攻击面

10.1 已提取密钥

extracted\keys_dump.txt 包含两个 RSA-2048 密钥对的 XML:

  1. ServerSideSigningPublicKey —— 服务器侧签名公钥(仅公钥)。
  2. 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)
      的私钥来源即此内嵌密钥。
  • 这是许可体系中最关键的削弱点:验证信任根(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 安装流程)

  1. TryCreate(key):按 LicenseKey 规范化/格式化校验 key 格式(§12);
  2. 无效 → \uE01C.\uE000() 返回 NotFound
  3. 有效 → API \uE001.\uE002(key) 获取签名 JSON 许可信息;
  4. \uE018.\uE004 验签 + 反序列化;
  5. 服务器返回 Trial 但产品不支持 → 报错;
  6. 状态 Valid → 做过期检查、产品版本检查、整体有效性检查;
  7. HttpRequestException 时,若 \uE005(需要激活)→ 置 ManualActivationRequired

11.3 激活必要性判断

\uE017.\uE001(activationState, licenseInformation)

  • Valid / Expired 状态 → 需要激活;
  • Expired 时:若为 Perpetual 且 ExpireDate 晚于今天 → 不需要激活(旧永久许可宽限)。

\uE017.\uE002(licenseState, licenseInformation):更新必要性判断,逻辑同上。

11.4 写入许可(\uE012

  • 仅支持 PerpetualSubscription 写入(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) 完整链路:
    1. JsonSerializer.Deserialize<ManualActivation>(jsa, \uE001)——外层 无签名要求
    2. \uE018.\uE00A(LicenseInformation.GetRawText(), key) 校验(LicenseKey.Equals 未被打补丁
      content.licenseKey 必须与对话框输入密钥一致);返回码 1=Corrupted / 2=Expired /
      3=WrongLicense,0=通过;
    3. \uE040.ActivateAsync(key, Activation.GetRawText())\uE041.Verify 被补丁恒 Valid
      三个静态判定(token 100663991-100663993)取用户激活 \uE005 优先
      DomainId==null && SystemId!=null && UserId!=null → 写入用户存储
      %LocalAppData%\JAM Software\Licenses\<SHA256(seed)大写>)。
  • CLIlicense 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+userIddomainId=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.ilLicenseKey.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-ZI/O 后与 Base32 剩余字母对齐)。
  • 说明:8→B0→O1→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(构造器调用)→ 抛 FormatExceptionbool_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)、ActivationManagerFactoryActivationStateIActivationManager/IActivationVerifier\uE043/\uE044(API 客户端包装)
机器标识 AdBasedDomainIdentifierProvider / WinApiDomainIdentifierProvider / DomainIdentifierProvider / IDomainIdentifierProvider(域标识两种来源,§9)
通知 IUserNotifierDebugNotifier / EventLogNotifier / MessageBoxNotifier / AbstractUserNotifier
UI DialogFactoryManualActivationViewModelLicenseKeyDialogResources 等(激活/试用过期对话框)

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

image-20260815175212143

image-20260815175230750

posted @ 2026-08-15 18:03  DirWangK  阅读(45)  评论(1)    收藏  举报