- 摘要(TL;DR)
样本是什么
一个离线机器绑定授权校验程序。它读取本机 MachineGuid 与系统盘卷序列号,经 SHA-256 生成"机器码" MX2-<64 hex>,再要求用户输入 LX2-<128 hex>(即 64 字节 raw r||s 的 ECDSA-P256 签名)。签名消息为 "CMX/Verify/v2|" + <机器码后 64 位 hex>,公钥以 XOR 密钥流加密存放于 .data。程序另有 --seal 模式,用于打印/比对 .text 段的自校验值 TextSeal(自定义 FNV-1a-64),期望值由作者工具以 DPAPI 密文写入注册表 HKCU\Software\OfflineCrackme\TextSeal。
密码学本体是否可靠
可靠。ECDSA-P256 + SHA-256 的验签实现对标准库(bcrypt)调用完全正确,签名编码为 P1363 raw r||s,域参数、消息格式、哈希均无缺陷。在没有作者私钥的前提下,纯"算号"在数学上不可行(ECDLP)。
但保护体系仍然被完全击穿,原因不在密码学,而在工程实现:
| # |
缺陷 |
CWE |
后果 |
| F-1 |
公钥密文的密钥流仅由 .text 派生,而 .text 在攻击时不必修改 |
CWE-1188 / CWE-656 |
可用同一密钥流把攻击者自己的公钥写回 .data |
| F-2 |
.data 中仅 16 字节 PUBSTOREDV1MARK1 标记参与完整性校验,其后 64 字节公钥密文完全不受保护 |
CWE-353 / CWE-345 |
公钥可被任意替换而不触发任何检查 |
| F-3 |
注册表封印是用户级 DPAPI 密文,而 DPAPI 的加密密钥属于当前登录用户 |
CWE-522 / CWE-269 |
任何本机用户都能自行生成合法封印 |
| F-4 |
最终判定链末端的 jnz 是单点决定性分支 |
CWE-693 |
2 字节 NOP 即可强制进入成功分支(无注册码/无封印) |
| F-5 |
--seal 只读不写,二进制中不存在任何注册表写入路径 |
CWE-754 |
封印机制无法自举,攻击者可随意控制其内容 |
| F-6 |
反调试仅 IsDebuggerPresent + CheckRemoteDebuggerPresent,且结果只与最终判定 AND |
CWE-693 |
无实际防护价值 |
攻击路径 A(推荐,端到端通过,退出码 0)
- 用
SHA256("AZOTH-crackme-v2-keygen-demo-2026") 派生自己的 P-256 私钥 d_att;
- 把
Q_att = d_att·G 的 64 字节大端 X||Y 异或同一密钥流后写回 file offset 0x25011..0x25050(只改 64 字节数据,不改任何代码);
- 用
d_att 对 "CMX/Verify/v2|" + hex_upper(machineFP) 签名,输出 LX2-<128 hex>;
- 用
CryptProtectData 写入 HKCU\Software\OfflineCrackme\TextSeal = DPAPI("%016x" % FNV(.text));
- 运行 → 全部 8 个判定项同时通过。
攻击路径 B(更简)
把 file offset 0x1097 的 75 47(jnz short loc_140001CE0)改成 90 90(nop;nop),任意注册码即可打印"验证成功"。
1. 任务、环境与交付物
1.1 工单
逆向分析 E:\pi-profile\逆向\crackme.rar,输出完整详细的报告。
口径声明:解包 → 静态/动态逆向 → 还原注册算法 → 产出可复现的完整报告 + 可运行的 keygen / 破解工具。
1.2 工作目录与产物
E:\pi-profile\20260919-2210_crackme-reverse\
├─ TASK.md 任务状态载体
├─ docs\
│ ├─ REPORT.md ← 本报告
│ └─ NOTES.md 逆向工作笔记(地址/常量/伪代码速查)
├─ sample\
│ ├─ crackme.rar 原始压缩包(76558 B)
│ └─ extract\crackme.exe 解包后的原始样本(未修改)
├─ src\
│ ├─ crackme_lib.py 算法复现库(含 DPAPI/注册表/PE 操作)
│ ├─ keygen.py 破解 + 注册码生成器(攻击路径 A)
│ ├─ patch_bypass.py 2 字节 NOP 补丁器 + 证据矩阵脚本(攻击路径 B)
│ ├─ verify_code.py 注册码独立校验器(纯 Python P-256)
│ └─ strings_extract.py ASCII/UTF-16 字符串提取器
├─ keys\
│ ├─ pubkey_derived.txt 从 .data 解出的作者公钥(X/Y, hex)
│ ├─ pubkey_raw.bin 作者公钥 raw X||Y(64 字节)
│ ├─ crackme_keygen.exe 攻击路径 A 产物(换公钥版,可正常通过)
│ └─ crackme_nop.exe 攻击路径 B 产物(2 字节补丁版)
└─ logs\
├─ strings_all.txt / strings.json
├─ keygen_run.txt keygen 运行记录
├─ evidence_matrix.txt 6 组对照实验结果
├─ evidence_outputs.txt 每组实验的完整程序输出
└─ ida\
├─ crackme.exe.asm IDA 反汇编(51,918 行)
├─ crackme.exe.i64 IDA 数据库
├─ decompiled.c Hex-Rays 反编译(557,568 B / 557 个函数)
└─ decompile_all.py Hex-Rays 批量反编译脚本
1.3 分析环境
| 用途 |
工具 |
版本 / 路径 |
| 解包 |
bsdtar (Windows 内置) |
C:\Windows\System32\tar.exe — libarchive 3.8.8 |
| 静态反汇编/反编译 |
IDA Pro |
8.3 (Hex-Rays) E:\Tools\MetaSword\Tools\Reverse\IDA\idat64.exe |
| 辅助静态 |
radare2 |
E:\Tools\MetaSword\Tools\Reverse\radare2\radare2.exe |
| 动态调试 |
x64dbg |
E:\Tools\MetaSword\Tools\Reverse\x64dbg\x64\ |
| 脚本 |
Python |
3.14.7(cryptography 50.0.0 / pycryptodome 3.23.0 / capstone 5.0.7 / pefile) |
Python 环境中没有 ecdsa 包,故 verify_code.py 内含一套完全独立的纯 Python P-256 实现(见 §5.4),避免"用库验库"的循环论证。
1.4 解包
本机未安装 7-Zip / WinRAR / Bandizip(which unrar rar 7z 全空)。压缩包为 RAR5 格式(签名 Rar!\x1a\x07\x01\x00),未加密。Windows 自带 tar.exe 基于 libarchive,原生支持 RAR5 读取:
cd sample/extract && /c/Windows/System32/tar.exe -xvf ../crackme.rar
# → crackme.exe (退出码 0)
2. 静态分析
2.1 PE 结构概览
Machine : 0x8664 (AMD64)
NumberOfSections : 6
TimeDateStamp : 1789744644 → 2026-09-18 15:17:24 UTC
Linker version : 14.51 (Visual Studio 2022 v17.11+)
Subsystem : 3 (CONSOLE)
ImageBase : 0x140000000
AddressOfEntryPoint : RVA 0x2B70
SizeOfImage : 0x2D000
SizeOfHeaders : 0x400
DllCharacteristics : 0x8160 (HIGH_ENTROPY_VA | DYNAMIC_BASE | NX_COMPAT | TERMINAL_SERVER_AWARE)
Characteristics : 0x22 (EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE)
CheckSum : 0x0 (未计算)
SizeOfStackReserve : 0x100000 SizeOfHeapReserve : 0x100000
Data Directories : 有 DEBUG (IMAGE_DEBUG_TYPE_CODEVIEW, RVA 0x23A40 / size 0x1C) 与 LOAD_CONFIG
无 TLS、无延迟导入、无导出、无 SECURITY(未签名)
Rich Header : 存在
2.2 节表
| 名称 |
RVA |
VirtualSize |
PointerToRaw |
SizeOfRawData |
熵 |
特征 |
.text |
0x1000 |
0x19450 |
0x400 |
0x19600 |
6.480 |
RX |
.rdata |
0x1B000 |
0xB4FC |
0x19A00 |
0xB600 |
5.085 |
R |
.data |
0x27000 |
0x16C0 |
0x25000 |
0xC00 |
2.211 |
RW |
.pdata |
0x29000 |
0x153C |
0x25C00 |
0x1600 |
5.075 |
R |
.fptable |
0x2B000 |
0x100 |
0x27200 |
0x200 |
0.000 |
RW |
.reloc |
0x2C000 |
0x678 |
0x27400 |
0x800 |
4.931 |
R |
- 熵值全部正常,无壳、无加密壳、无 native 混淆(无 OLLVM / 无控制流平坦化痕迹)。
.fptable 为本样本特有节名,内容为 0 填充,代码中无任何引用 → 疑似链接器自定义节或预留槽位。
- RVA→文件偏移换算(报告全文通用):
.text : file = RVA - 0x1000 + 0x400
.rdata: file = RVA - 0x1B000 + 0x19A00
.data : file = RVA - 0x27000 + 0x25000
2.3 导入表(保护机制的骨架)
bcrypt.dll — 密码学原语
BCryptOpenAlgorithmProvider BCryptCloseAlgorithmProvider
BCryptImportKeyPair BCryptVerifySignature BCryptDestroyKey
BCryptGetProperty BCryptCreateHash BCryptHashData
BCryptFinishHash BCryptDestroyHash
CRYPT32.dll
CryptUnprotectData ← 仅解密(DPAPI);全程序不存在 CryptProtectData
ADVAPI32.dll
RegGetValueA ← 仅读取;全程序不存在 RegSetValueExA / RegCreateKeyEx
KERNEL32.dll(与授权逻辑直接相关的部分)
IsDebuggerPresent CheckRemoteDebuggerPresent GetCurrentProcess
GetWindowsDirectoryA GetVolumeInformationA GetModuleFileNameA / W
CreateFileA / W ReadFile SetFilePointer / SetFilePointerEx
VirtualProtect GetStdHandle GetConsoleScreenBufferInfo
SetConsoleTextAttribute SetConsoleOutputCP ReadConsoleW / WriteConsoleW
QueryPerformanceCounter GetSystemTimeAsFileTime LoadLibraryExW / GetProcAddress
GetProcessHeap / HeapAlloc / HeapReAlloc / HeapFree / HeapSize
FlsAlloc / FlsGetValue / FlsSetValue / FlsFree
InitializeCriticalSectionEx EncodePointer SetEnvironmentVariableW
ExitProcess / TerminateProcess
(其余为 MSVC UCRT / CRT 启动集)
关键观察:程序从不写注册表,也从不用 CryptProtectData 生成封印。封印只能由作者的外部工具生成(见 F-5)。
2.4 字符串
.rdata 起始 RVA 0x1B000 / file 0x19A00。程序自有的字符串全部集中在其头部(0x1B3F0 ~ 0x1B78x),其余为 CRT/bcrypt 静态依赖。
| RVA |
内容 |
用途 |
| 0x1B3F0 |
PUBSTOREDV1MARK1 |
.data 完整性标记(16 字节,无 NUL) |
| 0x1B400 |
--seal |
封印诊断模式开关 |
| 0x1B407 |
TEXT_SEAL=%016llx\n |
打印 .text FNV |
| 0x1B41A |
MISSING |
封印缺失 |
| 0x1B422 |
OK |
封印匹配 |
| 0x1B425 |
MISMATCH |
封印不匹配 |
| 0x1B42E |
SEALREG=%s\n |
封印状态行 |
| 0x1B43A |
FNV_FAIL=%s\n |
.text 读取失败原因 |
| 0x1B447 |
机器码: MX2-%s\n |
机器码显示格式 |
| 0x1B45C / 0x1B45D |
\n / \n |
提示前缀 |
| 0x1B45F |
%s 请输入注册码 (LX2-..., 输入 q 退出):\n > |
交互提示 |
| 0x1B498 |
按回车键退出... |
退出提示 |
| 0x1B4AE |
GetModuleFileName |
失败标签 |
| 0x1B4C0 |
CreateFile |
失败标签 |
| 0x1B4CB |
ReadHeader |
失败标签 |
| 0x1B4D6 |
DOS |
失败标签 |
| 0x1B4DA |
NT |
失败标签 |
| 0x1B4DD |
NoText |
失败标签 |
| 0x1B4E4 |
Malloc |
失败标签 |
| 0x1B4EB |
ReadText |
失败标签 |
| 0x1B4F4 |
TextSeal |
注册表值名 |
| 0x1B4FD |
Software\OfflineCrackme |
注册表路径 |
| 0x1B515 |
MachineGuid |
HKLM 值名 |
| 0x1B521 |
SOFTWARE\Microsoft\Cryptography |
HKLM 路径 |
| 0x1B541 |
%08X |
卷序列号格式 |
| 0x1B546 |
0123456789ABCDEF |
hex 表 |
| 0x1B557 / 0x1B559 / 0x1B55E |
q / quit / exit |
退出命令 |
| 0x1B564 (UTF-16) |
SHA256 |
bcrypt 算法名 |
| 0x1B572 (UTF-16) |
HashDigestLength |
bcrypt 属性名 |
| 0x1B594 (UTF-16) |
ECDSA_P256 |
bcrypt 算法名 |
| 0x1B5AA (UTF-16) |
ECCPUBLICBLOB |
bcrypt blob 类型 |
| 0x1B5C6 |
离线验证 Crackme v2 (ECDSA-P256 加固版) |
标题 |
| 0x1B5F7 |
50 × = |
分隔线 |
| 0x1B62A |
机器码: (无法读取本机指纹) |
指纹失败分支(同时置 byte_140027BB8 = 1) |
| 0x1B652 |
(请把上面这台机器的机器码发给作者以获取注册码) |
指纹成功分支,无条件打印 |
| 0x1B699 |
\n[×] 验证失败:注册码无效或与当前机器不匹配。 |
失败 |
| 0x1B6DC |
请核对机器码后重新向作者索取注册码。\n |
失败 |
| 0x1B718 |
\n[√] 验证成功!注册码有效,感谢支持正版 :)\n |
成功 |
| 0x1B757 |
\n[×] 未输入注册码。 |
空输入 |
IDA 的反编译器把 4 个宽字符串渲染成单字符 "E" / "H",实为 L"ECCPUBLICBLOB" 与 L"HashDigestLength",已在反汇编中核对。
2.5 函数地图与调用链
start (VA 0x140002B70, RVA 0x2B70)
└─ sub_1400029F0 CRT 启动: __scrt_common_main_seh 等价物
├─ sub_140002BE0 / sub_140002B90 / sub_140002CC0 ... 安全 cookie / 异常表注册
└─ sub_140001000(argc, argv) ★★ 全部 crackme 逻辑 ★★
保护逻辑只存在于 3 个函数中(已用交叉引用确认无遗漏):
| 函数 |
地址 |
职责 |
sub_140001000 |
0x140001000 |
主流程:横幅、机器码、输入、解析、反调试、自校验、签名验证、判定 |
sub_140002190 |
0x140002190 |
读取并解密注册表封印(DPAPI) |
sub_1400022E0 |
0x1400022E0 |
计算 32 字节机器指纹(SHA-256) |
辅助函数(全部为薄封装):
| 函数 |
等价 API |
函数 |
等价 API |
sub_140002480 |
bcrypt SHA-256 |
sub_1400025B0 |
读取自身 .text 节原始字节 |
sub_1400027D0 |
vfprintf(stdout, ...) |
sub_140002840 |
vsnprintf |
sub_140006040 |
__acrt_iob_func |
sub_1400060CC |
_fileno |
sub_1400060F4 |
_isatty |
sub_1400064F0 |
fflush |
sub_1400066C0 |
fgets |
sub_1400066D0 |
isspace |
sub_140006870 |
strcmp |
sub_140006A10 |
free |
sub_140006B74 |
getchar |
sub_140006FA0 |
strtoull(s, NULL, 16) |
sub_140009CD0 |
malloc |
sub_1400191E0 |
memset |
sub_1400195A0 |
memcpy |
sub_140019C30 |
strcmp |
sub_140019CB0 |
strncpy |
sub_140019E30 |
strlen |
sub_140002900 / 29C0 / 29D0 / 2B90 / 2BE0 / 2C20 / 2CC0 / 2D60 / 2D90 / 2DC0 / 2E00 / 2EC0 … 2FB0 全部为 MSVC CRT 启动与安全机制(_security_cookie = 0x2B992DDFA232、__fastfail、Fls 回调、setlocale 等),不含任何 crackme 逻辑。
2.6 构建痕迹
- Linker 14.51 +
CONSOLE + Rich Header + SizeOfHeaders=0x400 + EncodePointer / FlsAlloc / __security_cookie → 标准 MSVC x64 控制台工程,未经过混淆器。
.pdata 完整 → 未开启 /GUARD:NO 之类裁剪,无手工汇编。
- 无 TLS 回调、无延迟导入、无资源节。
3. 程序行为逐步还原
以下伪代码直接对应 logs/ida/decompiled.c 中 sub_140001000 的逐段,变量名保留 IDA 命名以便与反编译输出对照。
3.1 顶层分支
if ( a1 < 2 || strcmp(argv[1], "--seal") ) // a1 = argc
{
/* ───────── 正常验证模式 ───────── */
}
/* ───────── 封印诊断模式(fall-through) ───────── */
return 0;
3.2 机器码计算与显示
SetConsoleOutputCP(0xFDE9); // 65001 = UTF-8
v5 = sub_1400022E0(pbOutput); // 32 字节机器指纹
for (j = 1; j != 33; j += 2) // 32 字节 → 64 个 hex 字符
{
v14 = pbOutput[j - 1];
v116[j*2 - 2] = "0123456789ABCDEF"[v14 >> 4]; // 实际写入 v117[0..63]
v116[j*2 - 1] = "0123456789ABCDEF"[v14 & 0xF];
v15 = pbOutput[j];
v117[j*2 ... ] = ...; // 同上,偶数位置
}
v119 = 0;
v117 是一段 64 字节连续栈缓冲(IDA 反编译把它拆成 v117 + v118[3],实际 rbp-0x428 ~ rbp-0x3E8),存放机器指纹的 64 个大写 hex 字符。
显示:
if ( v5 ) // 指纹读取成功(正常路径)
{
printf(" 机器码: MX2-%s\n", v117); // byte_14001B447
printf(" (请把上面这台机器的机器码发给作者以获取注册码)"); // word_14001B652 ← 无条件打印
}
else // 指纹读取失败(无 MachineGuid / 卷序列号取不到 / WindowsDirectory < 3)
{
printf(" 机器码: (无法读取本机指纹)"); // word_14001B62A
byte_140027BB8 = 1; // 见 §3.10:共享"异常"标志 → 必定失败
}
修正(2026-09-19 实机复核):word_14001B652 属于指纹成功分支,无条件打印(实测程序输出确实同时出现机器码与该提示);word_14001B62A 才是失败分支。
byte_140027BB8 是两处共用的"异常"标志:①指纹读取失败(decompiled.c:176);②检测到调试器(decompiled.c:358)。二者都会把最终判定里的 (byte_140027BB8 ^ 1) 项清零 → 必定失败。
sub_1400022E0 — 机器指纹生成
__int64 sub_1400022E0(PUCHAR pbOutput)
{
memset(pvData, 0, 128);
cbGuid = 128;
if ( RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography",
"MachineGuid", RRF_RT_REG_SZ /*2*/, NULL, pvData, &cbGuid)
|| GetWindowsDirectoryA(Buffer, 0x104) < 3 )
return 0; // 失败
RootPathName = Buffer[0]; // 系统盘盘符, 如 'C'
strcpy(pcbData, ":\\"); // → "C:"
GetVolumeInformationA(&RootPathName, 0, 0, &serial, 0, 0, 0, 0);
memcpy(pbInput, "CMX/Machine/v2|", 15);
v4 = strlen(pvData); // MachineGuid 长度
memcpy(pbInput + 15, pvData, v4);
pbInput[v4 + 15] = '|';
v5 = snprintf(pbInput + v4 + 16, "%08X", serial); // 8 位大写 hex 卷序列号
return sub_140002480(pbInput, v4 + v5 + 16, pbOutput); // SHA-256, 共 60 字节
}
规范:SHA-256( "CMX/Machine/v2|" + MachineGuid + "|" + %08X(系统盘卷序列号) ) → 32 字节 → 大写 hex → 前缀 MX2-。
3.3 输入与格式校验
memset(buf, 0, 512);
if (argc < 2) {
printf("%s 请输入注册码 (LX2-..., 输入 q 退出):\n > ", prefix);
fgets(buf, 512, stdin);
} else {
strncpy(buf, argv[1], 511);
}
// isspace 双向裁剪得到有效长度 v33
if (argc >= 2 && 输入为空) { printf("\n[×] 未输入注册码。\n"); 等待回车; return 1; }
if (交互模式 && (strcmp(buf,"q")==0 || strcmp(buf,"quit")==0 || strcmp(buf,"exit")==0)) return 0;
if ( v33 == 132 && *(DWORD*)buf == 0x2D32584C ) // 0x2D32584C → "LX2-"
{ /* 进入解析 */ }
0x2D32584C 小端字节序为 4C 58 32 2D = ASCII "LX2-"。
132 = 4 + 128:即 LX2- 后恰好 128 个十六进制字符(64 字节签名的 hex)。
3.4 注册码解析
for (k = 0; k != 64; ++k)
{
v37 = buf[4 + 2*k]; // 高 nibble 字符
v40 = buf[4 + 2*k + 1]; // 低 nibble 字符
// 0-9 → c-48 ; a-f → c-87 ; A-F → c-55 ; 其他 → -1 并 break
v35 = ((v38 | v41) >= 0); // 解析成功标志
if ((v38 | v41) < 0) break;
pbSignature[k] = (16 * v38) | v41;
}
解析结果 pbSignature[0..63] 即 ECDSA P1363 raw 签名 r(32BE) || s(32BE)。
3.5 反调试
if ( IsDebuggerPresent()
|| (CheckRemoteDebuggerPresent(GetCurrentProcess(), &flag) && flag) )
{
byte_140027BB8 = 1; // 只在最终判定中 AND 进去
}
仅用于把 v77 的判定位置 0,无主动干扰、无 NtGlobalFlag/PEB 检查、无 ptrace 等价物、无时间差检测、无自篡改。F-6。
3.6 TextSeal 自校验
sub_1400025B0 读取自身文件:
GetModuleFileNameA(0, Filename, 0x104);
hFile = CreateFileA(Filename, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
ReadFile(hFile, Buffer, 0x1000, &n, 0); // 读节表头
if (*(WORD*)Buffer != 0x5A4D /*MZ*/) return fail("DOS");
if (*(DWORD*)(Buffer + e_lfanew) != 0x4550 /*PE\0\0*/) return fail("NT");
// 遍历 6 个节头, 匹配节名 ".text"
if (节名 != ".text") return fail("NoText");
SetFilePointer(hFile, SectionHeader->PointerToRawData, 0, FILE_BEGIN);
ReadFile(hFile, malloc(SectionHeader->SizeOfRawData), SizeOfRawData, &n, 0);
*a1 = SizeOfRawData; // = 0x19600
于是 TextSeal = FNV-1a-64(.text 段原始字节),其中:
offset_basis = 0x14650FB0739D0383 ← 非标准! 二进制内硬编码
prime = 0x100000001B3 ← 标准 FNV-1a 64 位素数
h = prime * (byte ^ h) ← 逐字节
主流程在 4 处独立重复这段 FNV 计算(编译器把 4 字节展开成 h=P*(P*(P*(P*(h^b0)^b1)^b2)^b3) 的等价形式),分别存入 v45、v2、v63、v70;并两次调用 sub_140002190 读取注册表封印:
v45 = FNV(.text); // 第 1 次
if ( sub_140002190(&stored) ) { // 读注册表(DPAPI)
v2 = FNV(.text); // 第 2 次
v53 = readOK & (v2 == stored);
} else v53 = 0;
/* ... 签名验证 ... */
v63 = FNV(.text); // 第 3 次
if ( sub_140002190(&stored) ) {
v70 = FNV(.text); // 第 4 次
v73 = readOK & (v70 == stored);
} else v73 = 0;
sub_140002190 — 封印读取(DPAPI)
if ( !RegGetValueA(HKEY_CURRENT_USER, "Software\\OfflineCrackme", "TextSeal",
RRF_RT_REG_BINARY /*8*/, NULL, pvData, &cbData /*=1024*/) )
{
pDataIn = { pvData, cbData };
if ( CryptUnprotectData(&pDataIn, NULL, NULL, NULL, NULL, 0, &pDataOut) )
{
memset(v9, 0, 128);
memcpy(v9, pDataOut.pbData, min(pDataOut.cbData, 127));
LocalFree(pDataOut.pbData);
v5 = strtoull(v9, NULL, 16); // 期望 16 个 hex 字符
*a1 = v5;
return v5 != 0;
}
}
return 0;
注册表值 HKCU\Software\OfflineCrackme\TextSeal 必须是 REG_BINARY 且内容为 DPAPI 密文,明文是 16 字符的十六进制 TextSeal 值。
3.7 签名消息与密钥流派生
// 消息 = "CMX/Verify/v2|" + 64 个机器指纹 hex 字符
memcpy(v124, "CMX/Verify/v2|", 14);
*(__m128i*)&v124[14] = _mm_load_si128(&v117); // 前 16 字符
*(__m128i*)&v124[30] = _mm_load_si128(&v118[0]); // 次 16 字符
*(__m128i*)&v124[46] = v118[1];
*(__m128i*)&v124[62] = v118[2];
v59 = sub_140002480(v124, 0x4E /*78*/, pbHash); // SHA-256
公钥解密:
BCryptOpenAlgorithmProvider(&hAlg, L"ECDSA_P256", 0, 0);
// ① 完整性标记检查(唯一被校验的 .data 内容)
if ( _mm_movemask_epi8(_mm_cmpeq_epi8(_mm_loadu_si128(&xmmword_140027000), MARKER))
!= 0xFFFF ) // MARKER = "PUBSTOREDV1MARK1"
goto fail; // .data RVA 0x27000 前 16 字节
// ② 读取自身 .text 原始字节
v84 = sub_1400025B0(&size); // size = 0x19600
if (!v84 || size <= 0x3F) goto fail;
// ③ 从 .text 派生 64 字节密钥流
h = 0x14650FB0739D0383;
n = 0;
for (i = 0; i < size && n < 64; ++i)
{
h = 0x100000001B3 * (v84[i] ^ h);
if (i % (size >> 6) == 0) // size>>6 = 103936>>6 = 1624
pbInput[n++] = HIBYTE(h); // 取高 8 位
}
// ④ 解密(RVA 0x27011 起 64 字节)
v89 = xmmword_140027011 ^ pbInput[ 0..15]; // → X[ 0..15]
v90 = xmmword_140027021 ^ pbInput[16..31]; // → X[16..31]
v91 = xmmword_140027031 ^ pbInput[32..47]; // → Y[ 0..15]
v92 = xmmword_140027041 ^ pbInput[48..63]; // → Y[16..31]
*(QWORD*)pbInput = 0x2031534345; // "ECS1" + 0x20 + 000000
// (BCRYPT_ECDSA_PUBLIC_P256_MAGIC=0x31534345, cbKey=32)
*(__m128i*)&pbInput[8] = v89;
*(__m128i*)&pbInput[24] = v90;
*(__m128i*)&pbInput[40] = v91;
*(__m128i*)&pbInput[56] = v92; // → 72 字节 BCRYPT_ECCKEY_BLOB
关键:四条 xmm 载入全部发生在四条 store 之前(见反汇编 0x1400019DC..0x140001A0F),同一块栈缓冲先当密钥流、后被改写为公钥 blob,逻辑正确无自覆盖。
.data 布局(RVA 0x27000 / file 0x25000):
| 偏移 |
长度 |
内容 |
是否被完整性校验 |
+0x00 |
16 |
PUBSTOREDV1MARK1 |
✅ 是(唯一) |
+0x10 |
1 |
0x00 |
❌ 否 |
+0x11 |
64 |
keystream[i] XOR pubkey_raw[i](X‖Y) |
❌ 否 |
+0x51 |
余 |
0 |
❌ 否 |
3.8 ECDSA 验签与最终判定
BCryptImportKeyPair(hAlg, 0, L"ECCPUBLICBLOB", &hKey, pbInput, 0x48, 0);
v60 = (BCryptVerifySignature(hKey, NULL, pbHash, 0x20, pbSignature, 0x40, 0) == 0);
// 机器指纹确定性复查
v2 = (sub_1400022E0(v134) && 逐字节(pbOutput,v134) && 逐字节(v122,v135));
v77 = v73 // 注册表封印(FNV == stored) 第 2 次
& (v63 == v45) // 4 次 FNV 互相一致
& v59 // SHA-256 成功
& v35 // 注册码 hex 解析成功
& v2 // 机器指纹可复现
& v60 // ★ ECDSA 验签通过
& v53 // 注册表封印 第 1 次
& (byte_140027BB8 ^ 1); // 无反调试器
if (v77 == 1) 绿色 + "\n[√] 验证成功!注册码有效,感谢支持正版 :)\n";
else 红色 + "\n[×] 验证失败:注册码无效或与当前机器不匹配。" +
" 请核对机器码后重新向作者索取注册码。\n";
if (argc >= 2) { printf("按回车键退出..."); 等待回车; }
return v77 ^ 1; // 退出码: 0 = 成功
反汇编末端的决定性分支(RVA 0x1C97,file 0x1097):
140001C8D movzx ebx, cs:byte_140027BB8
140001C94 xor bl, 1
140001C97 and bl, r14b
140001C9A and bl, al
140001C9C cmp bl, 1
140001C9E 75 47 jnz short loc_140001CE0 ← ★ 单点决定成功/失败
140001CA0 ...
140001CE0 ... (红色失败分支)
3.9 --seal 诊断模式
v99 = sub_1400025B0(&size); // 读 .text
if (!v99) { … 报告 FNV_FAIL=%s … }
v102 = FNV(.text); // 逐字节 FNV-1a
*(QWORD*)&stored = 0;
v105 = sub_140002190(&stored); // 读注册表封印
printf("TEXT_SEAL=%016llx\n", v102);
s = v105 ? (stored == v102 ? "OK" : "MISMATCH") : "MISSING";
printf("SEALREG=%s\n", s);
if (v102 == 0 && qword_140027BB0)
printf("FNV_FAIL=%s\n", qword_140027BB0); // 具体失败标签
return r13d; // = 0
该模式只读不写,不产生任何注册表写入。
4. 算法规格(可移植描述)
4.1 常量表
| 名称 |
值 |
出现位置 |
| FNV offset basis |
0x14650FB0739D0383 |
所有自校验与密钥流派生(非标准) |
| FNV prime |
0x100000001B3 |
同上(标准 FNV-1a 64) |
| 密钥流 step |
SizeOfRawData(.text) >> 6 = 103936 >> 6 = 1624 |
公钥解密 |
.data 公钥密文偏移 |
RVA 0x27011 / file 0x25011,64 字节 |
公钥解密 |
.data 标记偏移 |
RVA 0x27000 / file 0x25000,16 字节 |
完整性检查 |
| 机器指纹前缀 |
"CMX/Machine/v2 | " (15 字节) |
| 签名消息前缀 |
"CMX/Verify/v2 | " (14 字节) |
| P-256 曲线 |
secp256r1 / NIST P-256 / prime256v1 |
L"ECDSA_P256" |
| 注册码前缀 |
"LX2-" → 小端 DWORD 0x2D32584C |
格式校验 |
| 机器码前缀 |
"MX2-" |
显示格式 |
| 签名格式 |
P1363 raw r‖s,各 32 字节大端,共 64 字节 |
BCryptVerifySignature(..., 0x40, 0) |
| 哈希 |
SHA-256(32 字节) |
L"SHA256" |
| BCRYPT blob magic |
0x31534345 = ECS1(BCRYPT_ECDSA_PUBLIC_P256_MAGIC),cbKey = 32 |
BCRYPT_ECCKEY_BLOB |
| 注册表路径 |
HKCU\Software\OfflineCrackme / 值名 TextSeal / REG_BINARY |
sub_140002190 |
| 完整性标记 |
PUBSTOREDV1MARK1 |
.data+0x00 |
4.2 机器码算法
guid = HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid (REG_SZ)
drive = GetWindowsDirectoryA()[0] (如 'C')
serial = GetVolumeInformationA(drive + ":\")->VolumeSerialNumber (DWORD)
msg = "CMX/Machine/v2|" + guid + "|" + sprintf("%08X", serial) (60 字节)
fp = SHA-256(msg) (32 字节)
机器码 = "MX2-" + hex_upper(fp) (68 字符)
签名消息 = "CMX/Verify/v2|" + hex_upper(fp) (78 字节)
z = SHA-256(签名消息) (32 字节)
注册码 = "LX2-" + hex_upper(ECDSA_P256_sign(d_author, z)) (132 字符)
4.3 公钥提取算法
keystream[0..63]:
h = 0x14650FB0739D0383
for i in 0 .. 103935:
h = (0x100000001B3 * (text_raw[i] XOR h)) mod 2^64
if i mod 1624 == 0: emit HIBYTE(h)
pubkey_raw = (file[0x25011 .. 0x25050]) XOR keystream
X = pubkey_raw[0..31]; Y = pubkey_raw[32..63]
5. 动态验证与实验结果
5.1 --seal 与 FNV 复算
$ ./crackme.exe --seal
TEXT_SEAL=2d3117e8dd257598
SEALREG=MISSING
Python 独立复算:
FNV-1a-64(offset=0x14650FB0739D0383, prime=0x100000001B3) over .text raw(0x400..0x19A00)
→ 2d3117e8dd257598 ✅ 与程序输出完全一致
FNV-1a-64 over 整个文件 → f468f83ee1861716 (参考值,非程序使用)
SEALREG=MISSING 证实:程序自身从未写入过封印,且当前系统无该键值。
5.2 公钥提取
keystream (64 B):
44426e92952be4ebfe5a2bb355eda304e0135cf5d933918b0790d58bb024f1b1
aa6dc92e79715d4a7f88d872587ecda71752ee6845880a367771ecee41d5ba50
密文 file[0x25011..0x25050]:
6aa4f1e5f5e865f5e965fa99c8f9e7415dcd4d75bf67653ae8c89d359147e644
3700e6fdf00a9bfa72f8682144d5cd6c86a8278ee85d2a2ae3a4de1b1deb1ca4
XOR ⇒
X = 0x2ee69f7760c3811e173fd12a9d144445bdde11806654f4b1ef5848be216317f5
Y = 0x9d6d2fd3897bc6b00d70b0531cab00cb91fac9e6add5201c94d532f55c3ea6f4
曲线校验 (Y² - (X³ + aX + b)) mod p == 0 → True ✅
a = p − 3,
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B,
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
公钥落在 P-256 上,证明解密与密钥流派生完全正确,不是巧合的随机字节。
5.3 机器码复算
本机实测:
MachineGuid = d863bef0-aed9-4a1f-82a5-a9a62318e7d5
C: 卷序列号 = 0x1A8455F9
程序输出 = MX2-6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
Python 复算 = 6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB ✅ 一致
签名消息 = CMX/Verify/v2|6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
SHA-256(消息) = 2999451ce8d57c506ca81675e62ffc5cb042c292622c229c158518ca227a17bf
5.4 验签复算(纯 Python,零依赖)
src/verify_code.py 内含独立的 P-256 实现(仿射坐标 + 扩展欧几里得求逆 + 双击点乘),不依赖 cryptography/ecdsa/OpenSSL:
$ python src/verify_code.py --exe keys/crackme_keygen.exe "LX2-4632...5F22"
注册码 : LX2-463225DC4908210F9CCEF92606D34322FE92A0662384865E282719635F4DA04C...
长度 : 132
ECDSA-P256 验签 : 通过
同时 keygen.py --run 中程序本体也给出了 [√] 验证成功。两条独立路径(纯 Python 数学 + Windows bcrypt)结论一致。
5.5 证据矩阵
src/patch_bypass.py --evidence 自动执行 6 组对照实验(完整输出见 logs/evidence_outputs.txt):
| # |
二进制 |
注册表封印 |
退出码 |
判定 |
说明 |
| 1 |
crackme.exe(原版) |
无 |
1 |
失败 |
原版 + 攻击者注册码,无封印 |
| 2 |
crackme.exe(原版) |
有(原 .text FNV) |
1 |
失败 |
即使封印齐全,原版仍需作者私钥 |
| 3 |
crackme_nop.exe |
有 |
1 |
仅打印成功 |
2 字节 NOP 只影响打印分支,退出码仍为真实 v77^1 |
| 4 |
crackme_keygen.exe |
无 |
1 |
失败 |
换公钥版 + 攻击者注册码;封印缺失 → 失败 |
| 5 |
crackme_keygen.exe |
有(伪造) |
0 |
成功 |
端到端完全通过(攻击路径 A) |
| 6 |
crackme_nop.exe |
无 |
1 |
仅打印成功 |
2 字节 NOP 补丁 + 任意注册码,无需封印 |
实验 2 是本次分析最重要的对照:它证明密码学本体确实无法被"算号"攻破 —— 换个说法:作者的 ECDSA 部署是有效的,失效的是它的外包装。
实验 5 证明:不掌握任何作者密钥、不改动一个代码字节,即可获得真实的端到端通过(退出码 0)。
6. 安全性评估
6.1 密码学实现评审(结论:正确)
| 检查点 |
结果 |
| 曲线 |
P-256 标准参数,无 custom curve 后门 |
| 公钥导入 |
BCRYPT_ECCKEY_BLOB + ECCPUBLICBLOB,magic/长度校验正确 |
| 公钥点校验 |
bcrypt 导入时验证点在曲线上 |
| 签名格式 |
P1363 raw r‖s,64 字节,cbSignature=0x40 一致 |
| 验签 API 参数 |
pPaddingInfo=NULL,dwFlags=0,无算法降级空间 |
| 哈希 |
SHA-256,HashDigestLength 运行时校验为 32(sub_140002480) |
| 消息绑定 |
消息含机器指纹 → 签名与机器绑定,无跨机重放 |
| 消息分隔符 |
`"CMX/Verify/v2 |
| 随机数依赖 |
验证侧不需要随机数 |
| 侧信道 |
BCryptVerifySignature 为常量时间实现 |
不存在可被利用的密码学缺陷。纯 keygen(不含补丁)在数学上被 ECDLP 阻断。
6.2 工程实现缺陷
F-1 / F-2 —— 公钥可被替换(致命)
公钥的"加密"密钥流完全由 .text 内容决定,而 .text 与公钥密文之间没有任何绑定关系。攻击者不需要修改 .text 就能重写公钥密文;同时 .data 中唯一被校验的内容是前 16 字节标记,64 字节公钥密文处于完整性保护的空窗。
// 唯一的 .data 完整性检查 —— 只覆盖 16 字节:
if (_mm_movemask_epi8(_mm_cmpeq_epi8(_mm_loadu_si128(&xmmword_140027000), "PUBSTOREDV1MARK1")) != 0xFFFF)
goto fail;
F-3 —— 封印使用用户级 DPAPI(致命)
CryptUnprotectData(..., dwFlags = 0) 的密钥由当前用户的主密钥派生。程序只读取封印、从不生成封印,而 CryptProtectData 对任何本机用户都可用 → 封印对攻击者完全透明。若改用机器级 DPAPI(CRYPTPROTECT_LOCAL_MACHINE)或由作者服务端签名,难度会显著提升(但仍无法阻止路径 A)。
F-4 —— 单点决定性分支(致命,但只能骗打印)
判定链最终坍缩为一个 cmp bl,1 / jnz。把 jnz(2 字节)改成 nop;nop 即进入成功分支。作者显然意识到了单点风险,故把 FNV 计算冗余了 4 份 —— 但所有冗余都汇入同一个 bl,冗余完全失效。
另外,return v77 ^ 1 使用的是真实的 v77,而非分支结果,因此 NOP 补丁后程序会"打印成功但退出码为 1"。这是作者唯一意外的"反制",但毫无实际意义。
F-5 —— --seal 只读不写
二进制中不存在 RegSetValueExA / RegCreateKeyEx / CryptProtectData 导入。封印必须由作者外部工具安装 → 攻击者拥有封印的完全控制权,自校验形同虚设。
F-6 —— 反调试形同虚设
仅 IsDebuggerPresent() + CheckRemoteDebuggerPresent(),结果只 AND 进最终判定。既不干扰调试器,也不检测硬件断点/单步陷阱,对 x64dbg / IDA 调试无任何阻力(本报告全程未遇到任何调试对抗)。
其他观察
.fptable 节为 0 填充且无引用 —— 噪音。
CheckSum = 0 —— 未参与校验。
- 无 PE 签名 —— 可被任意修改后重打包。
- 4 次 FNV 计算完全冗余,可合并为 1 次 + 1 个比较,代码体积亦能缩小。
6.3 攻击路径 A — 公钥替换 + 封印伪造(推荐,0 代码修改)
仅修改 .data 中 64 字节密文 + 写入注册表;.text 与所有代码字节保持原样。
# 1) 派生攻击者私钥
d_att = int.from_bytes(sha256(b"AZOTH-crackme-v2-keygen-demo-2026").digest(), "big") % n
# 2) 把自己的公钥按同一密钥流加密后写回 .data
ks = keystream(.text) # 未变,因为 .text 未改
Q_att = d_att * G # 64 字节 X||Y 大端
patched = exe[:0x25011] + xor(Q_att, ks) + exe[0x25051:]
# ↑ 16 字节 PUBSTOREDV1MARK1 标记完好, 完整性检查通过
# 3) 对机器码签名
z = SHA256("CMX/Verify/v2|" + hex_upper(machine_fp))
code = "LX2-" + hex_upper(ecdsa_p256_sign(d_att, z)) # raw r||s, 64 字节
# 4) 伪造 DPAPI 封印
blob = CryptProtectData(b"%016x" % FNV(.text)) # 用户级 DPAPI
registry.SetValue(HKCU\Software\OfflineCrackme, "TextSeal", REG_BINARY, blob)
# 5) 运行 → 8/8 判定项通过, 退出码 0
一键执行:
python src/keygen.py --run
实测输出(节选,完整见 logs/keygen_run.txt):
STEP 1/3 — 把自己生成的 P-256 公钥写入 .data(数据补丁)
私钥(SHA256 派生): 4fbe981093468fa365b6a6e6c3b0583c420d3793a9155020d140d5bcdc951ee6
新公钥 X : 4338bdc6275722d8c8dd4eb2513a94d29bfbce37eb3b795d223f96302d07e5e6
新公钥 Y : b36fee2e0378581300e378fefff843584ac5361a68605b343063483498b77f2a
新公钥在曲线上 : True
TextSeal 未变 : 2d3117e8dd257598
STEP 2/3 — 用我们的私钥对机器码签名, 生成 LX2- 注册码
机器码 : MX2-6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
注册码 : LX2-463225DC4908210F9CCEF92606D34322FE92A0662384865E282719635F4DA04C
33DDD73C9372D729219FE2B69E0C104626CE49CB75DE9F923227B644BA115F22
注册码长度: 132 (期望 132)
独立验签通过 : True
STEP 3/3 — 伪造 DPAPI 注册表封印
封印明文 : 2d3117e8dd257598
DPAPI 密文长度: 246 字节
验证 — 运行破解版
[√] 验证成功!注册码有效,感谢支持正版 :)
exit code : 0
为什么这个攻击能成立(因果链)
- 公钥密文的密钥来源(
.text)与公钥密文的存储位置(.data)之间没有密码学绑定 → 只要 .text 不变,就能用同一密钥流加密任何数据;
.data 的完整性检查只覆盖 16 字节标记 → 64 字节公钥密文是自由写入区;
- 封印的信任根是"当前用户",而攻击者就是当前用户 → 封印可自签;
- 因此验签、自校验、反调试三项全部可被"合法"满足。
6.4 攻击路径 B — 2 字节 NOP
file offset 0x1097: 75 47 (jnz short loc_140001CE0)
→ 90 90 (nop ; nop)
python src/patch_bypass.py --make
./keys/crackme_nop.exe "<任意 132 字符的 LX2- 串>"
效果:直接打印 [√] 验证成功,注册码内容任意、无需封印、无需签名。
代价:.text 被修改 → TextSeal 从 2d3117e8dd257598 变为 dd2564e237e67884;但因为该分支已被 NOP,v53/v73 的取值不再影响控制流。退出码仍为 1(return v77 ^ 1 用的是真实 v77)。
若希望退出码也归零,只需额外把 return v77 ^ 1 的求值改为常量 —— 或直接采用路径 A。
6.5 其他可行路径
| 路径 |
做法 |
备注 |
| 内存补丁 |
运行时把 BCryptVerifySignature 返回值改写为 0(或 hook 0x140001C9E 的 jnz) |
需调试器/注入,触发反调试标志但不影响 NOP 后的逻辑 |
| Hook bcrypt |
IAT hook BCryptVerifySignature / BCryptImportKeyPair |
同上 |
| 封印伪造 + 原版 |
只伪造封印 → 仍然失败(实验 2),证明密码学有效 |
反例,非攻击 |
| 私钥恢复 |
需作者签名样本 + nonce 重用 |
本样本不提供任何签名,不可行 |
| 曲线攻击 |
P-256 无已知有效攻击 |
不可行 |
6.6 缺陷严重度汇总
| ID |
缺陷 |
CWE |
可利用性 |
严重度 |
| F-2 |
.data 公钥密文不受完整性保护 |
CWE-353 Missing Support for Integrity Check |
无需私钥、无需调试器 |
严重 |
| F-1 |
公钥密钥流可由只读数据重建 |
CWE-1188 / CWE-656 |
同上 |
严重 |
| F-3 |
授权封印基于用户级 DPAPI |
CWE-522 / CWE-269 |
本机任意用户 |
严重 |
| F-4 |
判定链单点分支 |
CWE-693 Protection Mechanism Failure |
2 字节补丁 |
严重 |
| F-5 |
封印只读不写 |
CWE-754 |
削弱 F-1/F-2 的补偿 |
中 |
| F-6 |
反调试仅两个 API 且不主动干预 |
CWE-693 |
无阻力 |
低 |
7. 复现步骤
# ── 0. 解包(RAR5, 无密码, 使用 Windows 内置 bsdtar) ──────────────
cd sample/extract && /c/Windows/System32/tar.exe -xvf ../crackme.rar
# ── 1. 静态反汇编 / 反编译 ────────────────────────────────────────
"E:/Tools/MetaSword/Tools/Reverse/IDA/idat64.exe" -B -A crackme.exe
"E:/Tools/MetaSword/Tools/Reverse/IDA/idat64.exe" -A -S"decompile_all.py" crackme.exe
# 注意: 不要附加多余参数(如 -parm.txt), 否则 IDA 以 exit=4 静默失败
# ── 2. 算法复现自检(公钥提取 + 机器码 + FNV, 无需私钥) ────────────
python src/keygen.py --selftest
# ── 3. 注册码独立校验(纯 Python P-256) ────────────────────────────
python src/verify_code.py --demo
python src/verify_code.py --exe keys/crackme_keygen.exe "LX2-<128hex>"
# ── 4. 攻击路径 A: 公钥替换 + 封印伪造 + 生成注册码 ──────────────
python src/keygen.py --run
# ── 5. 攻击路径 B: 2 字节 NOP 补丁 ────────────────────────────────
python src/patch_bypass.py --make
# ── 6. 6 组对照实验 ──────────────────────────────────────────────
python src/patch_bypass.py --evidence
# ── 7. 清理(删除注册表封印并恢复) ────────────────────────────────
python src/keygen.py --restore
8. 给作者的加固建议
按收益/成本排序:
- 把公钥移出可写节并做完整性绑定(针对 F-1/F-2)
- 将公钥(或其哈希)纳入 RSA/ECDSA 自签名;或在
.text 中对公钥做编译期嵌入(立即数),使密钥流派生与公钥存储不可分离;
- 至少要哈希全部 80 字节(标记 + 1 + 64 密文 + 填充),而非仅前 16 字节。
- 封印改用机器级 DPAPI 或作者签名(针对 F-3)
CryptProtectData(..., CRYPTPROTECT_LOCAL_MACHINE),或干脆让封印本身是一段作者私钥签名;
- 更彻底:把"binary hash → 授权"的映射放在作者服务端准入,而不是落在客户端可写的注册表。
- 消除单点分支(针对 F-4)
- 把判定结果用于派生后续数据(如异或密钥、输出解码表),而不是仅做一次
cmp/jnz;
- 采用多路分散判定 + 早期降级,使补丁必须同时改动多处。
--seal 双向化(针对 F-5)
- 让模式本身具备写入能力并做幂等/权限校验,或彻底移除该诊断入口(它泄露了内部哈希算法与失败标签)。
- 提高反向工程成本(针对 F-6 及整体)
- 引入反调试主动对抗(硬件断点检测、时序检测);
- 关键字符串/常量异或加密,避免明文泄露算法名(
ECDSA_P256、ECCPUBLICBLOB)与失败原因标签。
- 降低授权承担的风险
- 由于客户端离线校验原理上不可能绝对安全,建议叠加:授权有效期、在线激活/吊销、版本号绑定、签名者身份可追索性(每份授权单独签名,泄露后可定位)。
9. 附录
附录 A — 关键地址/偏移速查
| 项 |
VA |
RVA |
file offset |
start |
0x140002B70 |
0x2B70 |
0x1B70 |
CRT 启动 sub_1400029F0 |
0x1400029F0 |
0x29F0 |
0x19F0 |
主逻辑 sub_140001000 |
0x140001000 |
0x1000 |
0x400 |
机器指纹 sub_1400022E0 |
0x1400022E0 |
0x22E0 |
0x12E0 |
SHA-256 sub_140002480 |
0x140002480 |
0x2480 |
0x1480 |
读 .text sub_1400025B0 |
0x1400025B0 |
0x25B0 |
0x15B0 |
读封印 sub_140002190 |
0x140002190 |
0x2190 |
0x1190 |
决定性分支 jnz |
0x140001C97 |
0x1C97 |
0x1097 |
| ECDSA 验签调用 |
~0x140001E8x |
— |
— |
.data 完整性标记 |
0x140027000 |
0x27000 |
0x25000 |
.data 公钥密文(64B) |
0x140027011 |
0x27011 |
0x25011 |
.text 节数据 |
0x140001000 |
0x1000 |
0x400(长度 0x19600) |
附录 B — 关键伪代码清单
见 docs/NOTES.md 与 logs/ida/decompiled.c。核心三段:
<details><summary>1. 密钥流派生 + 公钥解密(main, VA ≈ 0x140001DA4..0x140001F00)</summary>
v84 = sub_1400025B0(&size); // .text raw, size = 0x19600
h = 0x14650FB0739D0383; n = 0;
for (i = 0; i < size && n < 64; ++i) {
h = 0x100000001B3 * (v84[i] ^ h);
if (i % (size >> 6) == 0) pbInput[n++] = HIBYTE(h);
}
v89 = xmmword_140027011 ^ pbInput[ 0..15];
v90 = xmmword_140027021 ^ pbInput[16..31];
v91 = xmmword_140027031 ^ pbInput[32..47];
v92 = xmmword_140027041 ^ pbInput[48..63];
*(QWORD*)pbInput = 0x2031534345; // "ECS1" + 0x20
*(__m128i*)&pbInput[ 8] = v89;
*(__m128i*)&pbInput[24] = v90;
*(__m128i*)&pbInput[40] = v91;
*(__m128i*)&pbInput[56] = v92;
BCryptImportKeyPair(hAlg, 0, L"ECCPUBLICBLOB", &hKey, pbInput, 0x48, 0);
v60 = BCryptVerifySignature(hKey, 0, pbHash, 0x20, pbSignature, 0x40, 0) == 0;
</details>
<details><summary>2. 机器指纹(sub_1400022E0)</summary>
RegGetValueA(HKLM, "SOFTWARE\\Microsoft\\Cryptography", "MachineGuid",
RRF_RT_REG_SZ, NULL, pvData, &cbGuid /*128*/);
GetWindowsDirectoryA(Buffer, 0x104);
RootPathName = Buffer[0];
strcpy(pcbData, ":\\");
GetVolumeInformationA(&RootPathName, 0, 0, &serial, 0, 0, 0, 0);
memcpy(pbInput, "CMX/Machine/v2|", 15);
v4 = strlen(pvData);
memcpy(pbInput + 15, pvData, v4);
pbInput[v4 + 15] = '|';
v5 = snprintf(pbInput + v4 + 16, "%08X", serial);
return sub_140002480(pbInput, v4 + v5 + 16, pbOutput); // SHA-256
</details>
<details><summary>3. 封印读取(sub_140002190)</summary>
if (!RegGetValueA(HKCU, "Software\\OfflineCrackme", "TextSeal",
RRF_RT_REG_BINARY, NULL, pvData, &cbData /*1024*/)) {
pDataIn = { pvData, cbData };
if (CryptUnprotectData(&pDataIn, NULL, NULL, NULL, NULL, 0, &pDataOut)) {
memcpy(v9, pDataOut.pbData, min(pDataOut.cbData, 127));
LocalFree(pDataOut.pbData);
v5 = strtoull(v9, NULL, 16);
*a1 = v5;
return v5 != 0;
}
}
return 0;
</details>
附录 C — 提取到的作者公钥
曲 线 : secp256r1 (NIST P-256)
X = 0x2EE69F7760C3811E173FD12A9D144445BDDE11806654F4B1EF5848BE216317F5
Y = 0x9D6D2FD3897BC6B00D70B0531CAB00CB91FAC9E6ADD5201C94D532F55C3EA6F4
raw X||Y (64 B, hex):
2ee69f7760c3811e173fd12a9d144445bdde11806654f4b1ef5848be216317f5
9d6d2fd3897bc6b00d70b0531cab00cb91fac9e6add5201c94d532f55c3ea6f4
私钥 : 不可得(ECDLP)
附录 D — 本机授权数据(实测样本)
MachineGuid = d863bef0-aed9-4a1f-82a5-a9a62318e7d5
C: 卷序列号 = 0x1A8455F9
机器码 = MX2-6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
TextSeal = 0x2d3117e8dd257598
签名消息 = CMX/Verify/v2|6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
z = SHA-256 = 2999451ce8d57c506ca81675e62ffc5cb042c292622c229c158518ca227a17bf
附录 E — 术语与缩写
| 缩写 |
说明 |
| IAT |
导入地址表 |
Raw r‖s / P1363 |
ECDSA 签名的一种定长编码(两个 32 字节大端整数拼接),区别于 DER/ASN.1 |
| DPAPI |
Windows 数据保护 API;CryptProtectData / CryptUnprotectData |
| BCRYPT_ECCKEY_BLOB |
bcrypt 的 ECC 公钥二进制结构(magic + cbKey + X + Y) |
| TextSeal |
本样本自定义名词:.text 段的自校验哈希值 |
| ECDLP |
椭圆曲线离散对数问题 |
报告结束。