【AI】Binary Ninja 6.0.10601 (Personal) 许可机制分析
Binary Ninja 6.0.10601 (Personal) 许可机制分析
分析对象:
binaryninjacore.dll(Binary Ninja 6.0.10601 Personal,
分析工具:IDA Pro(ida-multi-mcp 实时反编译/反汇编)
⚠️ 本文仅用于安全研究与逆向学习。Binary Ninja 是 Vector 35 的商业软件,请支持正版。
- Binary Ninja 6.0.10601 (Personal) 许可机制分析
- 1. 总览
- 2. sub_181616F80 —— 核心许可验证函数
- 3. sub_181619FE0 —— 内置公钥加载器
- 4. sub_18161AB70 —— 设置许可 add-on
- 5. 标志位与导出 API 语义(实测反编译)
- 6. 攻击面与 keygen 设计
- 7. 简易方案:公钥指数单字节 patch(e=1)
- 8. 方案C:patch PK_Verifier(check_signature 恒真)
- 附录 A:公钥密文表原始数据(74 × DWORD,地址
0x181775A/0x18161A06/0x18161B055三处相同) - 附录 B:指令编码与 patch 偏移
- 附录 C:关键全局变量速查
- py
- 保存异常-补充说明
- ps
来源:
https://www.chinapyg.com/thread-169211-1-1.html
https://od.cloudsploit.top/tools/BinaryNinja/6.0.10601
1. 总览
| 函数 | 地址 | 大小 | 作用 |
|---|---|---|---|
sub_181616F80 |
0x181616F80 |
0x2971 |
核心许可验证函数(对应 5.1 文章的 check_lic_1810E63F0):加载许可文件 → JSON 解析 → RSA 验签 → serial 黑名单 → 安装 Personal 许可数据 |
sub_181619FE0 |
0x181619FE0 |
~0x36B |
内置公钥加载器:解码 XOR 混淆的 RSA-2048 公钥并构造 Botan 公钥对象 |
sub_18161AB70 |
0x18161AB70 |
0x169D |
设置许可 add-on(对应导出语义 "Set license add-ons"):用同一内置公钥逐个验证 add-on 签名 |
三者均被核心暴露给 UI 层的 32 位 RVA 函数指针表(0x18C9C5A20)引用:
0x18C9C5A2C -> RVA 0x1616F80 (sub_181616F80)
0x18C9C5AA4 -> RVA 0x1619FE0 (sub_181619FE0)
0x18C9C5B34 -> RVA 0x161AB70 (sub_18161AB70)
1.1 调用链(xrefs 实测)
sub_181616F80 的调用方(许可门禁):
| 调用点 | 调用方 |
|---|---|
0x180D4EF8E |
sub_180D4EF80(包装器:init → check_lic → 调用 personal hook) |
0x180D59F04 |
BNRegisterArchitecture(每次注册架构都会触发许可检查,失败抛异常) |
0x180D5A06E |
BNRegisterArchitectureExtension |
0x180DBEDD7 |
sub_180DBED40(核心初始化) |
0x180DD568B |
BNInitPlugins |
0x18161F9F9 |
BNInitUI(UI DLL 以魔数调用后触发) |
0x181E009E4 |
sub_181E00990 |
sub_18161AB70 的调用方:BNAreUpdatesAvailable(0x181E03F67,更新检查时设置 add-on)。
2. sub_181616F80 —— 核心许可验证函数
2.1 缓存标志与互斥锁
// 0x181616FC5
if ( byte_18C5C9789 ) // 全局"许可已验证"标志,置位后直接返回 1
return 1;
v2 = Mtx_lock((_Mtx_t)&unk_18C5C9710); // 0x181616FE2 全局互斥锁
byte_18C5C9789:许可已验证标志,只有 Personal 分支成功结束才会置 1(见 2.7)。byte_18C5C9788:0x48E 字节加密 blob 是否已解密(一次性)。
2.2 许可来源
// 0x181616FFC: if ( !qword_18C5C9890 ) // 全局许可文本对象尚未加载
if ( getenv("BN_LICENSE") ) // 0x181617009 / 0x181617021
{
v3 = getenv("BN_LICENSE"); // 优先级 1:环境变量 BN_LICENSE 的内容即许可 JSON
...
qword_18C5C9890 = v6; // 存入全局许可文本对象
goto LABEL_42;
}
v7 = (_QWORD *)sub_181619D20(v268); // 优先级 2:许可文件路径
...
if ( !(unsigned __int8)sub_1810EF310(v169) || (v8 = sub_1810EF2D0(v169), v9 = v8, v8 > 0x1000000) )
{ ... return 0; } // 打不开或大于 16MB → 返回 0(不算失败,仅未授权)
许可路径函数 sub_181619D20 的证据:
// 0x181619D48
v4[2] = 0xB; // 字符串长度 11
v4[0] = 0x2E65736E6563696CLL; // 小端 => "license."
v4[1] = 0x746164; // 小端 => "dat\0" => "license.dat"
v2 = sub_180DBE9D0(&v6); // 取用户数据目录(Windows 下为 %APPDATA%\Binary Ninja)
sub_180DC0AF0(a1, v2, v4); // 拼接路径 => %APPDATA%\Binary Ninja\license.dat
读取后有一个 UTF-8 清洗循环(0x1817171B0 起):把内容中所有 >= 0x80 的字节丢弃(先扫描到第一个高位字节,再压缩其后的 ASCII 字节)。因此许可文件必须是纯 ASCII。
2.3 JSON 解析与字段提取
// 0x1817295: 取 JSON 解析器单例,0x181732D 调用其 vtable+8 完成 parse
if ( !(*(unsigned __int8 (__fastcall **)(__int64, _QWORD, _QWORD, _BYTE *, __int128 *))
(*(_QWORD *)v22 + 8LL))(v22, 文本首, 文本尾, v185, &v247) )
goto LABEL_332; // 解析失败 => 抛 std::runtime_error
sub_180A82F00(v185, v174); // begin
sub_180A86830(v185, v186); // end
while ( !(unsigned __int8)sub_180A89590(v174, v186) ) // 遍历数组元素
{
v24 = sub_180A861F0(v174); // *it —— 每个许可对象
v25 = sub_180A7D190(v24, "product"); // 0x1817397 json_get
sub_180A82900(v25, Buf1); // to_string
... // email(0x18173B7) serial(0x18173D7)
... // created(0x18173F7) type(0x1817417)
v31 = sub_180A82410(v30); // count(0x1817437) to_int
... // data(0x1817457) signature(0x181748D)
if ( (unsigned __int8)sub_180A89780(v24, "expiresEpoch") ) // 可选字段
v172 = sub_180A82A80(v34); // to_int64
else
v172 = 0;
结论:许可文件根节点是 JSON 数组,可包含多个许可对象,逐个处理。(与官方 license.dat 格式一致)
字段清单:product / email / serial / created / type / count(int) / data(base64) / signature(base64) / 可选 expiresEpoch(int64)。
data 与 signature 经 sub_1810CBAF0 解码,该函数是自制 base64 解码器(实测反编译):
// 0x1810CBAF0 —— 支持 A-Z / a-z / 0-9;'+','-' => 62;'/','_' => 63(标准与 URL-safe 字母表通吃);'=' 被忽略
if ( (unsigned __int8)(*v6 - 0x41) <= 0x19u ) *(&v12 + v4) = v8 - 0x41; // 'A'-'Z'
if ( (unsigned __int8)(v8 - 0x61) <= 0x19u ) *(&v12 + v4) = v8 - 0x47; // 'a'-'z'
if ( (unsigned __int8)(v8 - 0x30) <= 9u ) *(&v12 + v4) = v8 + 4; // '0'-'9'
if ( ((v8 - 0x2B) & 0xFD) == 0 ) *(&v12 + v4) = 0x3E; // '+' 或 '-'
if ( v8 == 0x2F || v8 == 0x5F ) *(&v12 + v4) = 0x3F; // '/' 或 '_'
2.4 data 长度强校验
// 0x1817501
if ( v286[0] != 0x118 ) // data 解码后必须恰好 0x118 = 280 字节
{
sub_18116D9E0(v206, &unk_1877E1018);
throw (std::runtime_error *)v206;
}
// 随后 0x1817507-0x181757C 把 280 字节拷入局部缓冲 lic_data (v296[256] + v297 高地址紧邻 24 字节)
lic_data 布局:data[0:0x100](256 字节)+ data[0x100:0x118](24 字节)。
2.5 签名输入的构造(待签名缓冲区)
// 0x1817597: swprintf(String, 0x20, "%d", count); ← 宽字符,随后按字节 strlen 取第一个数字
// 0x18175A4-0x1817748: 构造 DataBuffer v255,各字段之间以 1 个 0x00 字节分隔:
BNAppendDataBufferContents_0(v255, product, len_product); sub_1810CB760(v255, 0); // 追加 '\0'
BNAppendDataBufferContents_0(v255, email, len_email); sub_1810CB760(v255, 0);
BNAppendDataBufferContents_0(v255, serial, len_serial); sub_1810CB760(v255, 0);
BNAppendDataBufferContents_0(v255, created, len_created); sub_1810CB760(v255, 0);
BNAppendDataBufferContents_0(v255, type, len_type); sub_1810CB760(v255, 0);
BNAppendDataBufferContents_0(v255, count_str, len_count_str); sub_1810CB760(v255, 0);
BNAppendDataBufferContents_0(v255, data_b64_str, len_data_str); // ★ data 的 base64 字符串原文!
即:
待签名内容 = product || 00 || email || 00 || serial || 00 || created || 00 || type || 00 || "%d"%count || 00 || data的base64字符串原文
2.6 内置公钥的解码与验签(关键!)
公钥密文以 74 个 mov dword ptr [reg+disp], imm32 立即数 形式内联在 .text(0x1817757–0x18179D3):
// 0x1817757: v45 = operator new(0x128);
v45 = (_DWORD *)operator new(0x128);
*v45 = 0xDCA5855; // 0x181775A
v45[1] = 0x26CDD755; // 0x1817760
v45[2] = 0xA9835C4F; // 0x1817767
... // 共 74 个 DWORD(v45[0]..v45[0x49])
v45[0x49] = 0x2FCBDB65; // 0x18179D3
随后逐字节 XOR 解码出 0x126=294 字节,并加载为 Botan 公钥对象:
// 0x18179F1-0x1817A6B
for ( j = 0; j < 0x126; ++j )
{
v47 = (v45[j >> 2] ^ 0x2FCBDA65u) >> (8 * (j & 3)); // 逐字节 XOR 0x2FCBDA65(小端取字节)
... 追加到字节向量 Block ...
}
BNFreeDataReferences_0(v45);
v48 = sub_1820CB950(Block); // 0x1817A7D = Botan X509::load_key(DER)
手工验证:0x0DCA5855 ^ 0x2FCBDA65 = 0x22018230,小端字节序即 30 82 01 22 —— 正是 DER SubjectPublicKeyInfo 的 SEQUENCE 头(RSA-2048,总长 0x126=294 字节,与 2.6 末尾 02 03 01 00 01(e=65537)吻合)。
验签调用(Botan 库):
// 0x1817B2E
strcpy((char *)v232, "EMSA3(SHA-256)");
sub_182005DD0(v184, v48, v232, 0, v265); // 0x1817B7E PK_Verifier(pubkey, "EMSA3(SHA-256)")
sub_18210AC80(v184, v255[2], v255[0]); // 0x1817C60 verifier.update(待签名缓冲区)
if ( !(unsigned __int8)sub_182075340(v184, v285[2], v285[0]) ) // 0x1817C84 verifier.check_signature(sig)
{
sub_18116D9E0(v207, &unk_1877E1018);
throw (std::runtime_error *)v207; // 验签失败 => 抛异常,许可检查中止
}
2.7 serial 黑名单、全局状态、Personal 分支
serial SHA-256 黑名单:
// 0x1818948: 创建 "SHA-256";0x1818A7F: update(serial);0x1818A95: hash_final
v111 = 0;
while ( v111 < v106 ) // v106 = dword_18C5C98A0(黑名单条目数),v107 = qword_18C5C9898(黑名单数据)
{
if ( !memcmp(v113, (const void *)(v107 + 0x20 * v111), 0x20) ) // SHA256(serial) 逐条比对
throw ...; // 命中黑名单 => 抛异常
++v111;
}
全局状态写入(每个数组元素验证通过后都执行):
sub_180A0A4D0(&qword_18C38B5B0, serial, len); // 0x1818AF3 存 serial
sub_180A0A4D0(&qword_18C38B5D0, email, len); // 0x1818B21 存 email
sub_180A0A4D0(&qword_18C38B5F0, product, len); // 0x1818B4F 存 product
sub_180A0A4D0(&qword_18C38B610, type, len); // 0x1818B7D 存 type
dword_18C5C9764 = v160; // 0x1818B89 存 count
qword_18C5C9780 = v172; // 0x1818B97 存 expiresEpoch
sub_180A0A4D0(&qword_18C38B630, signature, len); // 0x1818BC7 存 signature
// 0x1818BCC-0x1818C30: 0x118 字节 lic_data 存入全局 unk_18C5C9790(消费者 sub_181619EB0)
product 标志:
// 0x1818C58
if ( Size == 0xC && !memcmp(product, "Binary Ninja", 0xC) )
byte_18C5C978A = 1; // product 恰为 "Binary Ninja"(12 字节)→ 置位(粘性标志)
Personal 分支(v145 = (product == "Binary Ninja Personal"),0x1817CC2 处比较 0x15 字节):
// 0x1818C8C
if ( v145 ) // product == "Binary Ninja Personal"
{
if ( !byte_18C5C978A ) // 0x1818FBB:要求 valid_product 标志已置位
throw ...; // (GUI 下由 BNInitUI 提前置位;headless 下需数组前面
// 存在 product=="Binary Ninja" 的元素)
off_18C38B090[0] = sub_180DC13A0; // 0x1818FCF 插件加载 hook:真实 CorePluginInstance 初始化
// (LoadLibraryExA + CorePluginInit/CorePluginABIVersion,ABI=0xBB)
off_18C38B098 = sub_180DC1630; // 0x1818FDD 插件加载 hook:哑初始化(返回禁用状态的插件实例)
off_18C38B088[0] = sub_180D4EED0; // 0x1818FEB 容器 push_back 辅助 hook
if ( !byte_18C5C9788 ) // 0x1818FFC blob 解密仅执行一次
{
// 0x1819050: 创建 "RC4" Cipher;0x181917F: 分配 0x18 字节;0x18191A7: memmove(v138, Src, 0x18)
// Src = RC4(MD5(data[0:256]) 为密钥) 解密 data[0x100:0x118] 得到的 24 字节明文
// (MD5 计算:0x181867B 创建 "MD5",0x181874E update(lic_data, 0x100))
sub_1820FD980(v137, v138, 0x18); // 0x18191C0 cipher.set_key(24字节)
((void (__fastcall *)(...))(*v148)[5])(v148,
&unk_18C38B8D0, &unk_18C38B8D0, 0x48E); // 0x18191F8 RC4 原地解密 0x48E 字节 blob
byte_18C5C9788 = 1;
}
... // 合并 add-on 列表到全局向量 xmmword_18C5C9768(0xD0 字节/项)
byte_18C5C9789 = 1; // 0x18192B5 ★ 许可验证成功标志
...
return 1; // 0x18195ED
}
// 非 Personal 元素:清理后继续处理数组下一个元素;全部结束后:
return 0; // 0x181967B
关于 0x48E 字节 blob(
unk_18C38B8D0):其对 RC4 密钥(来自许可data字段)没有任何正确性校验,且实测对0x18C38B8D0..0x18C38B8D5E范围内地址做 xrefs 检查,除解密调用本身(0x18191EA/0x18191F1)外无任何其他引用——解密结果不参与任何判断。因此对自生成许可而言,data字段内容可为任意 280 字节(本文 keygen 填零)。
2.8 返回值语义
| 场景 | 结果 |
|---|---|
已验证(byte_18C5C9789) |
直接 return 1 |
| 无 BN_LICENSE 环境变量且 license.dat 打不开/超 16MB | return 0(未授权,不抛异常) |
| JSON 解析失败 / 验签失败 / data≠0x118 / serial 命中黑名单 / Personal 但无 valid_product 标志 | 抛 std::runtime_error(沿调用链传播,如 BNRegisterArchitecture 直接失败) |
| 数组中含合法 Personal 元素 | 安装许可数据,return 1 |
| 仅含合法非 Personal 元素 | 安装全局字段但 return 0(byte_18C5C9789 不置位) |
3. sub_181619FE0 —— 内置公钥加载器
独立版本的公钥解码函数(供函数指针表调用),逻辑与 2.6 完全一致:
_QWORD *__fastcall sub_181619FE0(_QWORD *a1)
{
v3 = (_DWORD *)sub_187664E50(0x128); // 0x18161A003 operator new(0x128)
*v3 = 0xDCA5855; // 0x18161A006 ← 74 个 mov 立即数开始
v3[1] = 0x26CDD755;
...
v3[0x49] = 0x2FCBDB65; // 0x18161A27F
do
{
v4 = (v3[v2 >> 2] ^ 0x2FCBDA65u) >> (8 * (v2 & 3)); // 0x18161A2BA 同样的 XOR 解码
... // 0x126 字节
} while ( v2 < 0x126 );
BNFreeDataReferences_0(v3);
*a1 = sub_1820CB950(&v7); // 0x18161A305 Botan X509::load_key
return a1;
}
其唯一引用是 RVA 表项 0x18C9C5AA4。
3.1 公钥密文在 .text 中共有 3 份拷贝(patch 时必须全部覆盖)
| # | 表起始(首条 mov) | 所在函数 |
|---|---|---|
| 1 | 0x181775A |
sub_181616F80(内联) |
| 2 | 0x18161A06 |
sub_181619FE0 |
| 3 | 0x18161B055 |
sub_18161AB70(内联) |
三处起始 16 字节完全相同(实测 get_bytes):
C7 00 55 58 CA 0D C7 40 04 55 D7 CD 26 C7 40 08 ...
└─ mov [rax],0x0DCA5855 ─┘└─ mov [rax+4],0x26CDD755 ─┘
13 字节锚点 C7 00 55 58 CA 0D C7 40 04 55 D7 CD 26 即可唯一定位全部 3 处。
4. sub_18161AB70 —— 设置许可 add-on
__int64 __fastcall sub_18161AB70(_QWORD *a1) // a1 = 指向 std::string 的 add-on JSON 文本
{
v5 = ...; // JSON 解析器单例
if ( !(*(v5->vtbl+8))(v5, a1_data, a1_end, v109, &err) ) // 0x18161AC4C 解析
goto 错误处理;
...
Mtx_lock(&unk_18C5C9710); // 0x18161AF5D
if ( byte_18C5C9789 ) // 0x18161AF79 ★要求主许可已验证
{
sub_180A0A4D0(Buf2, qword_18C38B5B0, ...); // 0x18161B00F 取当前主许可 serial
sub_180A0A4D0(v137, qword_18C38B630, ...); // 0x18161B03A 取当前主许可 signature
...
// 0x18161B052-0x18161B2CE:第三份公钥密文表 + XOR 解码
v108 = sub_1820CB950(Block); // 0x18161B36A 加载公钥
v25 = json_get(v109, "data"); // 0x18161B3F6 add-on 数组位于 "data" 字段
while (遍历数组)
{
v28 = *it; // 每个 add-on 对象
if ( !is_object(v28) || !is_string(json_get(v28,"signature")) ) throw;
// 0x18161B493:按 off_18781C770 表取 5 个字段,以 0x00 分隔拼接为待签名缓冲区
// off_18781C770 = { "id", "license_serial", "product", "created", "expiration" }
// (.rdata 注释实测:aId_9 / aLicenseSerial / aProduct / aCreated / aExpiration)
v66 = json_get(v56, "license_serial");
v70 = (len != 主serial长度) || memcmp(v66, 主serial); // 0x18161B5EA
if ( v70 ) throw; // add-on 必须属于当前许可
// 0x18161B704: "EMSA3(SHA-256)" → 0x18161B759 PK_Verifier(公钥)
// 0x18161B83E: update(5字段缓冲) → 0x18161B862 check_signature(addon签名),失败 throw
// 0x18161B8EB/0x18161B9E2: created/expiration 按 "%Y-%m-%d%ET%H:%M:%E*S%Ez" 解析
// 0x18161BAF2: created > expiration 则 throw
// 存入 0xD0 字节记录(id/license_serial/product/created/expiration/签名时间戳),v96 |= 4
}
// 0x18161BD06 之后:与全局主许可 serial/signature 一致性比对,去重合并入 xmmword_18C5C9768
}
}
失败路径(0x18161AD23 "Unknown error" / 0x18161AE43)会记录日志 "Could not set license add-ons: %s"。
add-on 签名输入:
id || 00 || license_serial || 00 || product || 00 || created || 00 || expiration(5 字段,\0 分隔,无末尾分隔符),RSA-2048/PKCS#1v1.5-SHA256,与主许可同一公钥。
5. 标志位与导出 API 语义(实测反编译)
| 导出函数 | 地址 | 实现 | 语义 |
|---|---|---|---|
BNIsLicenseValidated |
0x18161FA10 |
return byte_18C5C9789; |
仅当 Personal 许可安装成功才为 true |
BNIsUIEnabled |
0x18161FA50 |
return byte_18C5C978A; |
UI 允许标志 |
BNIsThisUIThread |
0x18161FA20 |
return byte_18C5C978A && dword_18C5C9760 == Thrd_id(); |
|
BNGetLicenseExpirationTime |
0x18161F800 |
return qword_18C5C9780; |
返回 expiresEpoch(0 = 永久) |
BNInitUI |
0x18161F9D0 |
见下 | UI DLL 启动入口 |
// BNInitUI —— binaryninjaui.dll 用魔数调用,随后核心才开始许可检查
char __fastcall BNInitUI(__int64 a1)
{
if ( a1 == 0xF9CE343C15708856uLL ) // 0x18161F9E1 魔数校验
{
byte_18C5C978A = 1; // 0x18161F9E3 UI 启用标志(GUI 运行时必然为 1)
dword_18C5C9760 = Thrd_id();
return sub_181616F80(); // 0x18161F9F9 触发许可验证
}
return 0x56;
}
BNRegisterArchitecture(0x180D59E20,节选):
sub_180D4DED0(v8);
sub_181616F80(); // 0x180D59F04 许可检查(失败异常向上抛)
off_18C38B088[0](); // 0x180D59F1A personal hook(Personal 许可下才被替换为真实实现)
6. 攻击面与 keygen 设计
6.1 机制小结
- 许可 = JSON 数组,每个元素含
product/email/serial/created/type/count/data/signature(data、signature为 base64)。 - 签名 = RSA-2048 / PKCS#1 v1.5 / SHA-256,签名输入 = 7 段字段以
\0连接(见 2.5)。 - 验签公钥以 XOR(
0x2FCBDA65)混淆,作为 74 个立即数硬编码 3 份于.text。 - Personal 许可要求:签名有效 +
data恰 280 字节 + serial 不在黑名单 +byte_18C5C978A已置位(GUI 自动满足 / headless 需数组含product=="Binary Ninja"元素)。 - 成功后置
byte_18C5C9789,BNIsLicenseValidated()返回 true,插件加载 hook 被启用。 - 无有效期强校验(
expiresEpoch仅存储与展示);created/expiration强校验只存在于 add-on。
6.2 keygen 思路(patch 公钥 + 自签名)
- 本地生成 RSA-2048 密钥对(e=65537)。
- 将自选公钥导出为 294 字节 DER SubjectPublicKeyInfo,逐字节 XOR
0x2FCBDA65后写回 3 份 公钥立即数表(只改 imm32,不改指令字节)。 - 用自选私钥对许可 JSON 两个元素签名:
- 元素 1:
product = "Binary Ninja"(置位byte_18C5C978A,兼容 headless); - 元素 2:
product = "Binary Ninja Personal"(触发 Personal 安装分支,置位byte_18C5C9789)。
- 元素 1:
- 生成
license.dat,写入%APPDATA%\Binary Ninja\license.dat(或用环境变量BN_LICENSE注入)。 - 用打过补丁的
binaryninjacore.dll替换原文件(脚本自动备份)。
对应实现见同目录 bn_patch_tool.py(单文件工具:CLI + GUI,公私密钥已内置)。
7. 简易方案:公钥指数单字节 patch(e=1)
"RSA 单字节 patch"——把公钥指数改为 e=1 后,
s^e mod N = s,"签名"就是 EMSA3 编码块本身,无需任何私钥。
7.1 前提验证:BN 6.0.10601 捆绑的 Botan 版本
binaryninjacore.dll 文件偏移 0x7BD9050 处实测版本字符串:
Botan 2.19.3 (unreleased, revision unknown, distribution unspecified)
Botan 2.19.3 的 RSA_PublicKey::init()(src/lib/pubkey/rsa/rsa.cpp @2.19.3):
void RSA_PublicKey::init(BigInt&& n, BigInt&& e) {
if(n.is_negative() || n.is_even() || e.is_negative() || e.is_even())
throw Decoding_Error("Invalid RSA public key parameters");
...
}
只拒绝负数与偶数指数——e=1(奇数)合法加载。(注意:Botan 3.x 已加入 e <= 1 拒绝检查,此方案强依赖 2.19.3 这一版本行为;BN 后续版本若升级 Botan 则失效。)
7.2 验签路径证据(Botan 2.19.3 源码)
PK_Ops::Verification_with_EMSA::is_valid_signature(pk_ops.cpp):RSA 的with_recovery()为 true,走verify_mr(sig)—— 返回s^e mod N的 BigInt 最小编码,再调m_emsa->verify(output, msg, max_input_bits()),其中 RSA 的max_input_bits() = n_bits - 1 = 2047。EMSA_PKCS1v15::verify(pk_pad/emsa_pkcs1/emsa_pkcs1.cpp):精确比对coded == emsa3_encoding(sha256(msg), 2047, hash_id, 19)。emsa3_encoding(2047):output_length = 255,P_LENGTH = 255-32-19-2 = 202,块 =[01, FF×202, 00, DigestInfo(19), hash(32)](255 字节)——正是标准 PKCS#1 v1.5 EM(00 01 FF×202 00 …)作为整数的最小编码。
因此伪造签名只需满足:s^1 mod N == int(00 01 FF×202 00 || DigestInfo || sha256(签名输入)),即 s = 该 EM 块本身。
7.3 单字节 patch 的定位
指数 INTEGER 位于 DER[0x121..0x125] = 02 03 01 00 01(e=65537)。将其内容首字节 DER[0x123](=291)01 → 00,内容变为 [00, 00, 01] = e=1(非最小编码,Botan 2.19.3 的 BigInt::binary_decode 不做最小化检查)。
DER[291] 落在公钥表第 0x48 号立即数的最高字节:
imm32: 0x2EC8D856 -> 0x2FC8D856 (LE 字节 56 D8 C8 2E -> 56 D8 C8 2F)
解码: DER[288..291] = 33 02 03 01 -> 33 02 03 00
3 份公钥表各改 1 字节,全文件共 3 字节。文件中的指令上下文(10 字节锚点,每处唯一):
C7 80 20 01 00 00 56 D8 C8 2E → C7 80 20 01 00 00 56 D8 C8 2F
└─ mov dword [rax+0x120], imm32 ─┘
7.4 简易方案许可生成
signature = base64( 00 01 || FF×202 || 00 || 3031300d060960864801650304020105000420 || sha256(签名输入) )
即 256 字节经典 PKCS#1 v1.5 EM 块直接作为"签名"(签名输入见 2.5 节——data 为 base64 原文)。已用 cryptography 库独立生成的签名做交叉验证:两者还原出的 EM 整数完全一致(DigestInfo 前缀与 202 个 0xFF 均正确)。
7.5 方案对比
| 方案A 简易(e=1) | 方案B 完整(整表替换) | |
|---|---|---|
| DLL 修改量 | 3 字节(每表 1 字节 ×3) | 3 表 × 74 立即数(~864 字节) |
| 私钥 | 不需要 | 需自选 RSA-2048 密钥对 |
| 依赖 | 仅标准库 | cryptography |
| 版本依赖 | 强依赖 Botan 2.19.3 无 e≤1 检查 | 任何版本均有效 |
| add-on 签名 | 同样用 EM 块直接签名 | 私钥签名 |
三方案均已实现并测试通过:单文件工具 bn_patch_tool.py(--selftest 全流程自检通过:orig → e1/custom/patched 状态机正确、license 回验通过;公私密钥内置脚本,无密钥文件)。
8. 方案C:patch PK_Verifier(check_signature 恒真)
8.1 目标函数身份(IDA 实测)
sub_182075340 = Botan(BN 内置 botan_all.cpp)的 PK_Verifier::check_signature。证据:
- 反编译内部断言字符串:
"check_signature"、"m_parts != 0 && m_part_size != 0"、"PK_Verifier: signature size invalid"、"PK_Verifier: signature is not the canonical DER encoding" - 源文件路径断言:
"C:\\jenkins\\workspace\\ja-stable-multibranch_stable_6.0\\build\\botan\\botan\\botan_all.cpp", 0x7A0B - 逻辑:
a1[2]为 Signature_Format 枚举(0=IEEE_1363 → 直接调 ops->verify;1=DER_SEQUENCE → 拆分/规范化 DER 后比较,"signature is not the canonical DER encoding");RSA+EMSA3 走前者,返回 bool,失败由调用方抛异常。
8.2 调用面(xrefs 实测,决定 patch 的覆盖范围与副作用)
| 调用点 | 所属 | 用途 |
|---|---|---|
0x181617C7D、0x18161818A |
sub_181616F80 |
主许可验签 ×2(主元素 + add-on) |
0x18161B85B |
sub_18161AB70 |
add-on 验签 |
0x181616B1B |
sub_1816167F0 |
另一个许可检查函数 |
0x180AF2F8B |
sub_180AF2AF0 |
应用级验签(同样构造 PK_Verifier) |
0x181FF2AA9、0x1820FFBDB、0x1820FFCC6、0x18210D0CD、0x18210D663、0x18210EB67 |
Botan 内部 | X.509 对象/证书签名检查 |
→ 单点 patch 即覆盖全部许可验证路径;副作用是 X.509 对象等检查也恒真(对破解无碍,无崩溃风险:函数改为立即返回,调用方只消费 bool,对象生命周期由调用方管理)。
8.3 patch 内容
函数序言(0x182075340,实测 16 字节):
48 89 5C 24 20 56 57 41 54 41 56 41 57 48 81 EC
mov [rsp+18h],rbx; push rsi/rdi/r12/r14/r15; sub rsp,1E0h
只需改前 3 字节(VA 0x182075340,文件偏移 0x2074740):
48 89 5C -> B0 01 C3 (mov al,1; ret —— 恒真)
调用方 sub_181616F80 处 if (!sub_182075340(...)) throw 永不触发,许可 JSON 的 signature 字段内容不再有意义(仍须是合法 base64)。
8.4 三方案对比
| A 简易(e=1) | B 完整(整表替换) | C patch PK_Verifier | |
|---|---|---|---|
| DLL 修改量 | 3 字节 ×3 处 | ~864 字节 ×3 处 | 3 字节 ×1 处 |
| 私钥 | 不需要 | 需要 | 不需要 |
| 许可 signature | EMSA3 块 | 真实签名 | 任意(工具填 256 零字节) |
| 版本依赖 | Botan 2.19.3 无 e≤1 检查 | 无 | 函数 VA/序言锚点(6.0.10601) |
| 覆盖范围 | 仅该 3 处验签 | 同左 | 全部 Botan check_signature(含 X.509) |
8.5 工具支持(单文件工具 bn_patch_tool.py,密钥内置)
python bn_patch_tool.py # GUI(默认):三方案可视化
python bn_patch_tool.py --selftest # 无界面自检:三方案 patch 副本 + license 回验
python bn_patch_tool.py --test-core # ★ 真实核心验证:ctypes 调用 BNInitUI 全链路测三方案
python bn_patch_tool.py --check-only # 检测公钥表(orig/e1/custom/unknown)与 PK_Verifier(orig/patched)
python bn_patch_tool.py --scheme A --dll X # 方案A(e=1 单字节 ×3)
python bn_patch_tool.py --scheme B --dll X # 方案B(整表替换,内置私钥签名)
python bn_patch_tool.py --scheme C --dll X # 方案C(PK_Verifier 恒真)
python bn_patch_tool.py --print-pubkey # 打印内置公钥与原厂公钥
- 实测:三方案 patch 回读校验均通过(orig → e1/custom/patched),序言/表锚点校验防止版本不符误写
- 自检产物输出到
test_out\(可用--selftest随时重新生成)
附录 A:公钥密文表原始数据(74 × DWORD,地址 0x181775A / 0x18161A06 / 0x18161B055 三处相同)
0x0DCA5855 0x26CDD755 0xA9835C4F 0x2ECAD792 0x2CCBDF64 0x2FC4DBE7 0x25CA5855 0x2ECA5867
0xB1A70C65 0xDC4D06AD 0x9779B2BE 0x7ECEBCDA 0xDBECDF06 0x14D2EB89 0xCEF5AE0C 0x9E608A83
0x4EA8FDC7 0xDD48D9D5 0x25A3BC58 0x9778C40D 0xBEB490B9 0xD9F93D34 0x4CA26DB1 0xA0E1C8B4
0x44B8433D 0x335AA1BB 0xCB9EF0FA 0x494E7048 0x918877AE 0x17E4C10D 0x6D089370 0xDA3AC399
0x7B8F08F6 0x82DCC711 0x1C3DFB97 0xC78676C0 0x19C92C3B 0x1932224D 0x36A0BBE9 0xEBC90D2C
0x328A877C 0x34E4C0B0 0x0D4883E1 0xC095D9FC 0xCCD5640D 0xF06F4818 0xBD935510 0x1A3D70AD
0x826CDCC1 0x2C74442A 0xF58FD48E 0xDF70812D 0x1633E7FD 0xC7C19845 0x13112E41 0xBAA20BAC
0x5E94B950 0xFDB26834 0x8D739E14 0xCD7DBFFF 0xD26411E9 0xF1F471BE 0xCE3B11E1 0x84E3949A
0xE6B5C300 0xE2B6D898 0xAFE32F1F 0x5AF9CEDA 0xADCCF51D 0x68F0DEA6 0x137DDE67 0x80B02625
0x2EC8D856 0x2FCBDB65
XOR 解码后(前 32 字节):30 82 01 22 30 0D 06 09 2A 86 48 86 F7 0D 01 01 01 05 00 03 82 01 0F 00 30 82 01 0A 02 82 01 01 —— 标准 RSA-2048 SubjectPublicKeyInfo(e=65537)。
附录 B:指令编码与 patch 偏移
表区指令形态(sub_181619FE0 / sub_181616F80 / sub_18161AB70 三处实测字节相同):
idx 0 : C7 00 imm32 (6 字节, imm@指令+2)
idx 1..0x1F : C7 40 disp8 imm32 (7 字节, imm@指令+3, disp8 = 4*idx)
idx 0x20..0x49: C7 80 disp32 imm32 (10 字节, imm@指令+6, disp32 = 4*idx)
表总长 = 6 + 0x1F×7 + 0x2A×10 = 643 = 0x283 字节。patch 只需改写 74 个 imm32 = LE32(DER[4i:4i+4]) ^ 0x2FCBDA65(DER 294 字节尾部补 2 字节 0 至 296)。
注意:表中 imm0/imm1 编码的是 DER 头
30 82 01 22 30 0D 06 09,对所有 RSA-2048 SPKI 公钥都相同——因此不能靠前两个立即数区分新旧公钥,必须整表解码后比对。
附录 C:关键全局变量速查
| 地址 | 含义 |
|---|---|
byte_18C5C9789 |
许可已验证(Personal 安装成功)标志 → BNIsLicenseValidated |
byte_18C5C978A |
valid_product / UI 启用标志(BNInitUI 或 product=="Binary Ninja" 置位) |
byte_18C5C9788 |
0x48E blob 已解密标志 |
qword_18C5C9890 |
全局许可文本对象 |
qword_18C5C9780 |
expiresEpoch → BNGetLicenseExpirationTime |
dword_18C5C9764 |
count |
qword_18C38B5B0/D0/F0/610/630 |
serial / email / product / type / signature |
unk_18C5C9790 |
lic_data(0x118 字节) |
xmmword_18C5C9768 |
add-on 记录向量(0xD0 字节/项) |
qword_18C5C9898 / dword_18C5C98A0 |
serial SHA-256 黑名单数据 / 条目数 |
unk_18C38B8D0 |
0x48E 字节 RC4 加密 blob(Personal 分支原地解密) |
off_18C38B088/090/098 |
personal 插件加载 hooks |
off_18781C770 |
add-on 字段名表 |
py
"""
Binary Ninja 6.0.10601 Personal 许可补丁工具(单文件版:CLI + GUI,密钥内置)
三种方案:
A 简易 : 公钥指数单字节 patch,e=65537 -> 1(依赖 Botan 2.19.3 行为),签名 = EMSA3 块,无需私钥
B 完整 : 整表替换为内置 RSA-2048 公钥,用内置私钥签名(需 cryptography)
C verifier: patch PK_Verifier::check_signature 恒真(单点 3 字节),签名内容随意
关于 data 字段(重要):仅通过验签还不够。核心 Personal 分支会用许可 data 字段派生 24 字节密钥,
RC4 原地解密 binaryninjacore.dll 内置的 0x48E 字节"激活表"(VA 0x18C38B8D0,583 个 u16 表项),
该表被词法/语法分析器使用。data 错误 => 表为乱码 => 建库/保存 .bndb 时异常退出(实测崩溃)。
本工具已按正版 data 的结构生成该字段(随机 256 字节前缀 + 其 MD5 派生密钥加密的原厂 24 字节密钥),
并在自检/部署核查中校验(派生密钥 == 原厂密钥、目标 DLL 激活表可正常解密)。
用法:
python bn_patch_tool.py 打开 GUI
python bn_patch_tool.py --selftest 无界面自检(对 DLL 副本执行三方案全流程)
python bn_patch_tool.py --check-only [--dll X] 检测公钥表 / PK_Verifier / 0x48E 激活表状态
python bn_patch_tool.py --scheme A --dll X [--out Y] 执行方案A并生成 license.dat
python bn_patch_tool.py --scheme B ... 方案B(需 cryptography)
python bn_patch_tool.py --scheme C ... 方案C
python bn_patch_tool.py --print-pubkey 打印原厂公钥与内置公钥
python bn_patch_tool.py --extract-data-key L.dat 从 license.dat 导出 24 字节激活表密钥
原理详见: 《BinaryNinja_6.0.10601_许可机制分析.md》;
data 字段与 0x48E 激活表见《BinaryNinja_6.0.10601_许可data字段与0x48E激活表分析.md》
"""
import argparse
import base64
import binascii
import hashlib
import json
import os
import queue
import secrets
import struct
import subprocess
import sys
import threading
XOR_KEY = 0x2FCBDA65
ORIG_DWORDS = [
0x0DCA5855, 0x26CDD755, 0xA9835C4F, 0x2ECAD792, 0x2CCBDF64, 0x2FC4DBE7,
0x25CA5855, 0x2ECA5867, 0xB1A70C65, 0xDC4D06AD, 0x9779B2BE, 0x7ECEBCDA,
0xDBECDF06, 0x14D2EB89, 0xCEF5AE0C, 0x9E608A83, 0x4EA8FDC7, 0xDD48D9D5,
0x25A3BC58, 0x9778C40D, 0xBEB490B9, 0xD9F93D34, 0x4CA26DB1, 0xA0E1C8B4,
0x44B8433D, 0x335AA1BB, 0xCB9EF0FA, 0x494E7048, 0x918877AE, 0x17E4C10D,
0x6D089370, 0xDA3AC399, 0x7B8F08F6, 0x82DCC711, 0x1C3DFB97, 0xC78676C0,
0x19C92C3B, 0x1932224D, 0x36A0BBE9, 0xEBC90D2C, 0x328A877C, 0x34E4C0B0,
0x0D4883E1, 0xC095D9FC, 0xCCD5640D, 0xF06F4818, 0xBD935510, 0x1A3D70AD,
0x826CDCC1, 0x2C74442A, 0xF58FD48E, 0xDF70812D, 0x1633E7FD, 0xC7C19845,
0x13112E41, 0xBAA20BAC, 0x5E94B950, 0xFDB26834, 0x8D739E14, 0xCD7DBFFF,
0xD26411E9, 0xF1F471BE, 0xCE3B11E1, 0x84E3949A, 0xE6B5C300, 0xE2B6D898,
0xAFE32F1F, 0x5AF9CEDA, 0xADCCF51D, 0x68F0DEA6, 0x137DDE67, 0x80B02625,
0x2EC8D856, 0x2FCBDB65,
]
N_DWORDS = len(ORIG_DWORDS)
DER_LEN = 0x126
DATA_LEN = 0x118
# ---- 许可 data 字段 / 0x48E 激活表 ----
# data 结构 = data[0:0x100](随机前缀)|| data[0x100:0x118](24 字节"激活表密钥"的 RC4 密文,
# 密钥 = MD5(data[0:0x100]))。核心 Personal 分支用解出的 24 字节密钥 RC4 原地解密
# binaryninjacore.dll 内的 0x48E 字节激活表(VA 0x18C38B8D0),该表被词法/语法分析器
# 以 583 个 u16 表项使用;密钥错误 => 表为乱码 => 建库/保存 .bndb 时崩溃。
ACTIVATION_TABLE_VA = 0x18C38B8D0
ACTIVATION_TABLE_LEN = 0x48E
# 原厂 24 字节激活表密钥:取自两份互不相关的正版 Personal 许可(serial/email/created 均不同),
# 由各自 data 字段按上述链路派生,结果完全一致 —— 该密钥与许可内容无关,是软件内置常量。
LICENSE_DATA_KEY = bytes.fromhex("9c2aaa09a4e2252b0ba125db1e1cd272207d97cca8446899")
# 6.0.10601 激活表正确解密后的 SHA-256(用于判断目标 DLL 与内置密钥是否配套)
ACTIVATION_TABLE_SHA256 = "efd6ef5f597dfd31de358fa886396d867042edeaa362730aebd46eafd135bcd6"
E1_BYTE_INDEX = 0x123
HASH_ID_SHA256 = bytes.fromhex("3031300d060960864801650304020105000420")
E1_PATCH_INFO = (0x48, 0x2EC8D856, 0x2FC8D856)
VERIFIER_VA = 0x182075340
VERIFIER_ORIG = bytes.fromhex("48895c242056574154415641574881ec")
VERIFIER_PATCH = b"\xb0\x01\xc3"
EMBEDDED_PRIVATE_KEY_PEM = """-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDn8+rIvK8SK+y
WJ1Z0tUnANm5vMonX5I3NL9yDsEflJVeX8Jp/BK1IBIA3kWYUJsyA3D4/Ikq/OUD
mMC8VAYSMBnBD9XCbjQIfAl8mEEcnThe7A+YjosPpbqLCClpez+Tzi3cOC+sljCc
fTnRVPf3QGO5nnjhFDUblsABWwwdIg91g1Ifu0hVJD9zW1tmZ+9vzWnKGA6a7KhL
eMveHPzgBICd5D2Mqe0lOxlMDgthfC+CIAkzijr/HdnbpEXEPZu7R5dzixSkz1UA
lXJQIsl1JUF9K3U6iFq/NUX1HBWu2BOPdlYEWf2aRR5FZ8AxQjTgTvJkCuvIci5z
fvIVD0pbAgMBAAECggEAEfXdOigmQxUlVGmAYNvTiBYy5zOnF8BVi04DebRTFNQu
dpCyJ8rl6M09+ehuWVKlPQ1F1QZSjajUWeUvZNsOB6N06d3LUWOO0WyZ7l+IMtlY
cJaZw/bqW5BqtRMVwkJ3jtlyKnLaJa1QXz28DjUP4xp9TXB4Rqn1Skgn7rfuP8pU
T/D7ojEoo9VUoowzQedfJy0LgVvHq09sMPwwzCvn86Ygoe+UgRf4632oVXpjdjmq
iGMYKEGGQrrOnUWnGFlX70kzg73TGO0oSwg4Votk/0bifXOLxCFs9olJBOQ+5py/
lR3rzbUmICIHji1wze5oAC77/fvfHIpWaVEndeT94QKBgQD/TEIxUCPazp+gxK93
t+/7lQGQbVcyMvT9+A4nDpTlk1LKhATY+frdodZj383L8KzBLLirHDRWhSxmVLPg
2FAq0fI8Am3gUCxdzGU2BO9XowLACK5BkUJMMOk6Nn/XPSVDzWYy10IH679M5j4J
eW0aUvMvO+psuRxNwviul+CQMQKBgQDEKYonx/EqLN/nZhhDPFVGadPyRvUmInxY
aD93/oPfqw7gF2VaE/YRyoGFFa5x387Cs0bBCdyoF7OIM36nH49RJTCl/C7TSyNR
ZMEW3PmvvUrOiMgzjB8UpD2DludN2hQ735x1OIpfkSjIYryrNv5rPLEPUM6MZuBT
Jz8iz77MSwKBgHcUsfdggyjnoguSUqpL8LNvwGgGqFpw7FeeIbPUGF2xRzfRNpWi
nIJOn7q8RLjwzRPZ8VVW9Y651Yjxz2l8XIBsp0mYdHnhy6AA2+DDkoRVM84w/l71
C3CQi+ksTJswGTa0Ewx53YHmU7fh2WP/8IUL1gTdNK579ZjL7cU6cHOxAoGAZCmx
2c5GXnq7ebSv/uT3xhSTnMqOswI47AuPHP5g5/y4g8RuhChdJYh5yjLf+FWHhfN0
1WzrtwkjKxDgQwXicFTFGnxCjLF5Pz5rOflrpUjKwdsmHuwVkryqjF6pjR8l5cyw
X0qXnWTJpkiEfgh+sNM0tdVVImwFb3BC1MuvaTECgYB9bDXR6BtgwwSjdXqb/HeN
CU/TwkeUT9xGsX2wIySViAFnfzi9UyyaFbks9WWU/+4X32swYWhM27ecqXXKYVjM
BNcjhHIRAp3n35R6vvAJDiegcm+4ZMWPB0Okl4kif/4u2n6ZPidmF0AjZRA4G0LZ
sqihLkeejHFvYYQfLp17kw==
-----END PRIVATE KEY-----"""
_cached_priv = None
def embedded_priv():
global _cached_priv
if _cached_priv is None:
from cryptography.hazmat.primitives import serialization
_cached_priv = serialization.load_pem_private_key(
EMBEDDED_PRIVATE_KEY_PEM.encode("ascii"), password=None)
return _cached_priv
def embedded_custom_der():
from cryptography.hazmat.primitives import serialization
return embedded_priv().public_key().public_bytes(
serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
def dwords_to_der(dwords):
raw = bytearray()
for d in dwords:
raw += struct.pack("<I", d ^ XOR_KEY)
return bytes(raw[:DER_LEN])
def der_to_dwords(der):
padded = der + b"\x00" * (4 * N_DWORDS - len(der))
return [int.from_bytes(padded[i * 4:i * 4 + 4], "little") ^ XOR_KEY
for i in range(N_DWORDS)]
def iter_table():
off = 0
for i in range(N_DWORDS):
if i == 0:
yield i, off, 2
off += 6
elif i < 0x20:
yield i, off, 3
off += 7
else:
yield i, off, 6
off += 10
def read_dwords(buf, site):
return [struct.unpack_from("<I", buf, site + off + imm)[0]
for _, off, imm in iter_table()]
def find_tables(data):
a = (b"\xc7\x00" + struct.pack("<I", ORIG_DWORDS[0])
+ b"\xc7\x40\x04" + struct.pack("<I", ORIG_DWORDS[1]))
sites = []
pos = 0
while True:
i = data.find(a, pos)
if i < 0:
break
sites.append(i)
pos = i + 1
return sites
def patch_site(data, site, new_dwords):
buf = bytearray(data)
for i, off, imm in iter_table():
p = site + off
if buf[p] != 0xC7 or (buf[p + 1] & 0x38) != 0:
raise RuntimeError(f"site {site:#x}: idx {i} 指令头异常")
if 1 <= i < 0x20 and buf[p + 2] != 4 * i:
raise RuntimeError(f"site {site:#x}: idx {i} disp8 异常")
if i >= 0x20 and struct.unpack_from("<I", buf, p + 2)[0] != 4 * i:
raise RuntimeError(f"site {site:#x}: idx {i} disp32 异常")
struct.pack_into("<I", buf, p + imm, new_dwords[i])
return bytes(buf)
def e1_dwords():
orig_der = dwords_to_der(ORIG_DWORDS)
if orig_der[0x121:0x126] != bytes.fromhex("0203010001"):
raise RuntimeError("原厂 DER 指数结构异常")
e1_der = orig_der[:E1_BYTE_INDEX] + b"\x00" + orig_der[E1_BYTE_INDEX + 1:]
dwords = der_to_dwords(e1_der)
diff = [(i, ORIG_DWORDS[i], dwords[i]) for i in range(N_DWORDS)
if ORIG_DWORDS[i] != dwords[i]]
if len(diff) != 1 or diff[0][0] != E1_PATCH_INFO[0] or diff[0][2] != E1_PATCH_INFO[2]:
raise RuntimeError(f"e=1 单字节 patch 表计算异常: {diff}")
return dwords, diff[0]
def _pe_sections(data):
pe = struct.unpack_from("<I", data, 0x3C)[0]
if data[pe:pe + 4] != b"PE\x00\x00":
return None, None, None
nsec = struct.unpack_from("<H", data, pe + 6)[0]
optsz = struct.unpack_from("<H", data, pe + 20)[0]
magic = struct.unpack_from("<H", data, pe + 24)[0]
base = (struct.unpack_from("<Q", data, pe + 24 + 24)[0] if magic == 0x20B
else struct.unpack_from("<I", data, pe + 24 + 28)[0])
secs = []
for s in range(nsec):
off = pe + 24 + optsz + s * 40
secs.append((struct.unpack_from("<I", data, off + 12)[0],
struct.unpack_from("<I", data, off + 16)[0],
struct.unpack_from("<I", data, off + 20)[0]))
return base, secs, (nsec, optsz)
def file_off_to_va(data, fo):
base, secs, _ = _pe_sections(data)
if base is None:
return None
for va, rawsz, rawptr in secs:
if rawptr <= fo < rawptr + max(rawsz, 1):
return base + va + (fo - rawptr)
return None
def va_to_file_off(data, va):
base, secs, _ = _pe_sections(data)
if base is None:
return None
rva = va - base
for sec_va, rawsz, rawptr in secs:
if sec_va <= rva < sec_va + max(rawsz, 1):
return rawptr + (rva - sec_va)
return None
def fmt_sites(data, sites):
out = []
for s in sites:
va = file_off_to_va(data, s)
out.append(f"{va:#x}" if va else f"file:{s:#x}")
return " ".join(out)
def find_verifier(data):
off = va_to_file_off(data, VERIFIER_VA)
if off is None or off + len(VERIFIER_ORIG) > len(data):
return None, "unknown"
if data[off:off + len(VERIFIER_ORIG)] == VERIFIER_ORIG:
return off, "orig"
if data[off:off + 3] == VERIFIER_PATCH:
return off, "patched"
return off, "unknown"
def patch_verifier(data, off):
buf = bytearray(data)
buf[off:off + 3] = VERIFIER_PATCH
return bytes(buf)
def classify_tables(data, custom_der=None):
orig_der = dwords_to_der(ORIG_DWORDS)
states = []
for s in find_tables(data):
der = dwords_to_der(read_dwords(data, s))
if der == orig_der:
states.append("orig")
elif (der[:E1_BYTE_INDEX] == orig_der[:E1_BYTE_INDEX]
and der[E1_BYTE_INDEX] == 0x00
and der[E1_BYTE_INDEX + 1:] == orig_der[E1_BYTE_INDEX + 1:]):
states.append("e1")
elif custom_der is not None and der == custom_der:
states.append("custom")
else:
states.append("unknown")
return states
def build_signature_input(lic):
out = b""
for field in ("product", "email", "serial", "created", "type"):
out += lic[field].encode("ascii") + b"\x00"
out += str(lic["count"]).encode("ascii") + b"\x00"
out += lic["data"].encode("ascii")
return out
def emsa3_pkcs1_v15(msg):
digest = hashlib.sha256(msg).digest()
return b"\x00\x01" + b"\xff" * 202 + b"\x00" + HASH_ID_SHA256 + digest
def rc4(key, data):
"""标准 RC4(与核心内 Botan RC4 同算法;加解密为同一操作)。"""
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) & 0xFF
s[i], s[j] = s[j], s[i]
out = bytearray()
i = j = 0
for b in data:
i = (i + 1) & 0xFF
j = (j + s[i]) & 0xFF
s[i], s[j] = s[j], s[i]
out.append(b ^ s[(s[i] + s[j]) & 0xFF])
return bytes(out)
def data_key_from_data(data_raw):
"""复刻核心的解密链:key24 = RC4(key=MD5(data[0:0x100]), data[0x100:0x118])。"""
if len(data_raw) != DATA_LEN:
raise ValueError(f"data 解码后应为 {DATA_LEN} 字节,实际 {len(data_raw)}")
return rc4(hashlib.md5(data_raw[:0x100]).digest(), data_raw[0x100:0x118])
def make_license_data():
"""生成 280 字节 data:随机 256 字节前缀 + 用其 MD5 派生密钥 RC4 加密的原厂 24 字节密钥。
正版许可的 data 结构与此完全一致(随机前缀 + 该前缀派生密钥加密的 24 字节密钥),
因此生成结果与正版等价;核心解出的密钥正确,0x48E 激活表可正常解密。
"""
prefix = secrets.token_bytes(0x100)
return prefix + rc4(hashlib.md5(prefix).digest(), LICENSE_DATA_KEY)
def activation_table_status(dll_data):
"""检查目标 DLL 的 0x48E 激活表能否被内置密钥正确解密。
返回 (是否与 6.0.10601 基准一致, 说明文本)。
"""
off = va_to_file_off(dll_data, ACTIVATION_TABLE_VA)
if off is None or off + ACTIVATION_TABLE_LEN > len(dll_data):
return None, f"未找到 0x48E 激活表(VA {ACTIVATION_TABLE_VA:#x},DLL 版本可能不符)"
plain = rc4(LICENSE_DATA_KEY, dll_data[off:off + ACTIVATION_TABLE_LEN])
digest = hashlib.sha256(plain).hexdigest()
if digest == ACTIVATION_TABLE_SHA256:
return True, f"内置密钥可正确解密(明文 sha256={digest[:16]}… 与 6.0.10601 基准一致)"
return False, (f"内置密钥解密结果与 6.0.10601 基准不一致"
f"(明文 sha256={digest[:16]}…)——该 DLL 的激活表/密钥不同版,"
"需用该版本正版许可的 data 重新确定密钥")
def check_license_data(data_raw, dll_data=None):
"""校验许可 data 字段是否可用(能否让 0x48E 激活表正确解密)。"""
key = data_key_from_data(data_raw)
if key != LICENSE_DATA_KEY:
return False, (f"派生密钥 {key.hex()} ≠ 原厂密钥 {LICENSE_DATA_KEY.hex()};"
"该 data 会把 0x48E 激活表解成乱码,建库/保存 .bndb 会崩溃")
if dll_data is not None:
same, msg = activation_table_status(dll_data)
if same is False:
return False, "派生密钥等于原厂密钥,但目标 DLL 的激活表与 6.0.10601 基准不符;" + msg
return True, "派生密钥等于原厂密钥(0x48E 激活表可正确解密)"
def make_license_common(email, serial, created, count, ltype, product, data_raw):
return {
"product": product,
"email": email,
"serial": serial,
"created": created,
"type": ltype,
"count": int(count),
"data": base64.b64encode(data_raw).decode("ascii"),
}
def make_licenses_simple(email, serial, created, count):
licenses = []
for product, ltype in (("Binary Ninja", "headless"),
("Binary Ninja Personal", "personal")):
lic = make_license_common(email, serial, created, count, ltype, product,
make_license_data())
lic["signature"] = base64.b64encode(emsa3_pkcs1_v15(
build_signature_input(lic))).decode("ascii")
licenses.append(lic)
return licenses
def make_licenses_full(email, serial, created, count):
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
priv = embedded_priv()
licenses = []
for product, ltype in (("Binary Ninja", "headless"),
("Binary Ninja Personal", "personal")):
lic = make_license_common(email, serial, created, count, ltype, product,
make_license_data())
lic["signature"] = base64.b64encode(priv.sign(
build_signature_input(lic),
padding.PKCS1v15(), hashes.SHA256())).decode("ascii")
licenses.append(lic)
return licenses
def make_licenses_verifier(email, serial, created, count):
sig = base64.b64encode(b"\x00" * 256).decode("ascii")
licenses = []
for product, ltype in (("Binary Ninja", "headless"),
("Binary Ninja Personal", "personal")):
lic = make_license_common(email, serial, created, count, ltype, product,
make_license_data())
lic["signature"] = sig
licenses.append(lic)
return licenses
def verify_licenses(licenses, scheme, dll_data=None):
for lic in licenses:
msg = build_signature_input(lic)
sig = base64.b64decode(lic["signature"])
if scheme == "simple":
if sig != emsa3_pkcs1_v15(msg):
raise RuntimeError("签名与 EMSA3 块不一致")
elif scheme == "verifier":
if sig != b"\x00" * 256:
raise RuntimeError("签名应为 256 零字节")
else:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
embedded_priv().public_key().verify(sig, msg, padding.PKCS1v15(), hashes.SHA256())
ok, why = check_license_data(base64.b64decode(lic["data"]), dll_data)
if not ok:
raise RuntimeError(f"[{lic['product']}] data 字段不可用: {why}")
def write_license(path, licenses):
lic_dir = os.path.dirname(os.path.abspath(path))
os.makedirs(lic_dir, exist_ok=True)
with open(path, "w", encoding="ascii") as f:
f.write(json.dumps(licenses, indent=1))
def validate_license_inputs(email, serial, created, count):
"""校验许可输入并返回规范化 count(GUI 与 CLI 共用,避免 int()/ASCII 编码类异常)。"""
try:
cnt = int(count)
except (TypeError, ValueError):
raise RuntimeError(f"count 必须是整数,当前为 {count!r}")
if cnt <= 0:
raise RuntimeError(f"count 必须为正整数,当前为 {cnt}")
for name, val in (("email", email), ("serial", serial), ("created", created)):
if not val:
raise RuntimeError(f"{name} 不能为空")
try:
str(val).encode("ascii")
except UnicodeEncodeError:
raise RuntimeError(f"{name} 必须是 ASCII 字符(核心会按 ASCII 清洗许可文件): {val!r}")
return cnt
def auto_detect_dll():
here = os.path.dirname(os.path.abspath(__file__))
for cand in (os.path.join(here, "binaryninjacore.dll"),
os.path.join(here, "binaryninja_win64_6.0.10601_personal",
"binaryninjacore.dll")):
if os.path.exists(cand):
return cand
return ""
def load_dll(path):
if not path:
raise RuntimeError("请先选择 binaryninjacore.dll 文件")
if not os.path.isfile(path):
raise RuntimeError(f"目标不是文件: {path}\n"
"请选择安装目录下的 binaryninjacore.dll(例如 "
"C:\\Program Files\\Vector35\\BinaryNinja\\binaryninjacore.dll)")
with open(path, "rb") as f:
return f.read()
def report_state(data, log=print):
sites = find_tables(data)
try:
custom_der = embedded_custom_der() # 用于识别方案B(整表替换)后的公钥状态
except Exception:
custom_der = None
states = classify_tables(data, custom_der)
voff, vstate = find_verifier(data)
log(f"[*] 公钥表命中: {len(sites)} 处 @ {fmt_sites(data, sites)}")
log(f"[*] 公钥表状态: {states}(orig=原厂 / e1=方案A / custom=方案B / patched 见下)")
if "unknown" in states and custom_der is None:
log("[!] 有表为 unknown 且未能加载内置公钥(缺 cryptography,无法判定方案B 状态)")
log(f"[*] PK_Verifier({VERIFIER_VA:#x}) 状态: {vstate}"
+ (f" @ 文件偏移 {voff:#x}" if voff is not None else ""))
ok, msg = activation_table_status(data)
log(f"[*] 许可 data 用 0x48E 激活表({ACTIVATION_TABLE_VA:#x}): {msg}"
+ ("" if ok is not False else " → license.dat 的 data 需由该版本正版许可导出"))
return sites, states, (voff, vstate)
def apply_patch(data, scheme):
voff, vstate = find_verifier(data)
if scheme == "verifier":
if vstate == "unknown":
raise RuntimeError("PK_Verifier 序言字节不匹配,拒绝 patch(DLL 版本可能不符)")
if vstate == "patched":
return data, "PK_Verifier 已 patch(跳过)"
return patch_verifier(data, voff), (f"PK_Verifier(check_signature @ {VERIFIER_VA:#x})"
"入口 48 89 5C -> B0 01 C3(3 字节)")
sites = find_tables(data)
if not sites:
raise RuntimeError("未找到公钥表锚点(3 处),DLL 可能不是 6.0.10601 或已被深度修改")
if scheme == "simple":
target, diff = e1_dwords()
desc = (f"e=1 单字节 patch: dword[{diff[0]:#x}] {diff[1]:#010x} -> {diff[2]:#010x},"
"每表 1 字节")
elif scheme == "full":
target = der_to_dwords(embedded_custom_der())
desc = "整表替换为内置公钥"
else:
raise RuntimeError(f"未知方案: {scheme}")
for s in sites:
data = patch_site(data, s, target)
return data, desc
def pipeline(dll, scheme, args, log=print):
data = load_dll(dll)
log(f"[*] 目标: {dll} ({len(data)} 字节)")
report_state(data, log)
patched, desc = apply_patch(data, scheme)
log(f"[*] 应用: {desc}")
if args.in_place:
bak = dll + ".bak"
with open(bak, "wb") as f:
f.write(data)
out = dll
log(f"[+] 原文件已备份: {bak}")
else:
out = args.out or dll + ".patched"
out_dir = os.path.dirname(os.path.abspath(out))
os.makedirs(out_dir, exist_ok=True)
with open(out, "wb") as f:
f.write(patched)
log(f"[+] 已写出: {out}")
report_state(patched, log)
serial = args.serial or secrets.token_hex(16)
email = args.email or "user@local.host"
created = args.created or "2026-01-01T00:00:00+00:00"
kwargs = dict(email=email, serial=serial, created=created,
count=validate_license_inputs(email, serial, created, args.count or 1))
makers = {"simple": make_licenses_simple, "full": make_licenses_full,
"verifier": make_licenses_verifier}
licenses = makers[scheme](**kwargs)
verify_licenses(licenses, scheme, data)
lic_path = args.license_out or os.path.join(
os.path.dirname(os.path.abspath(__file__)), "license.dat")
write_license(lic_path, licenses)
log(f"[+] 已生成许可: {lic_path} (serial={serial})")
log(f"[=] 完成。部署步骤:")
letter = {"simple": "A", "full": "B", "verifier": "C"}.get(scheme, "A")
log(f" 1) 用 {out} 替换安装目录的 binaryninjacore.dll(原文件请先备份;若用了 --in-place 已自动备份)")
log(f" 2) 把 {lic_path} 放到 %APPDATA%\\Binary Ninja\\license.dat")
log(f" 3) 重启 Binary Ninja(激活表仅在进程启动时解密一次,必须重启才生效)")
log(f" 4) 用 --verify-deploy --scheme {letter} 可核查部署状态")
_CORE_PROBE_CHILD = r'''
import binascii
import ctypes, os, struct, sys
mode = sys.argv[1]
dll_path = os.path.abspath(sys.argv[2])
dll = ctypes.CDLL(dll_path)
h = dll._handle
data = open(dll_path, "rb").read()
pe = struct.unpack_from("<I", data, 0x3C)[0]
pref = struct.unpack_from("<Q", data, pe + 24 + 24)[0]
if mode == "loadkey":
der = binascii.unhexlify(sys.argv[3])
addr = h + (0x1820CB950 - pref)
proto = ctypes.WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)
buf = ctypes.create_string_buffer(der, len(der))
buf_addr = ctypes.addressof(buf)
vec = (ctypes.c_void_p * 3)(buf_addr, buf_addr + len(der), buf_addr + len(der))
fn = ctypes.cast(addr, proto)
pk = fn(ctypes.cast(vec, ctypes.c_void_p))
print("RESULT OK pubkey=%s" % hex(pk or 0))
elif mode == "initui":
fn = ctypes.WINFUNCTYPE(ctypes.c_char, ctypes.c_uint64)
f = ctypes.cast(h + (0x18161F9D0 - pref), fn)
ret = f(0xF9CE343C15708856)
v = ctypes.WINFUNCTYPE(ctypes.c_bool)
ok = ctypes.cast(h + (0x18161FA10 - pref), v)()
print("RESULT BNInitUI=%r validated=%s" % (ret, ok))
'''
def _probe_child(dll_path, mode, *extra, env=None):
envd = os.environ.copy()
if env:
envd.update(env)
script = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"test_out", "_child_probe.py")
os.makedirs(os.path.dirname(script), exist_ok=True)
with open(script, "w", encoding="ascii") as f:
f.write(_CORE_PROBE_CHILD)
cmd = [sys.executable, script, mode, dll_path] + [str(e) for e in extra]
r = subprocess.run(cmd, capture_output=True, text=True, env=envd, timeout=180)
out = (r.stdout or "").strip()
err = (r.stderr or "").strip()
return r.returncode, out, err, r.returncode != 0
def run_core_test(dll_path):
print("== 核心探针1: X509::load_key 对公钥 DER 变体的接受性(真实核心) ==")
orig_der = dwords_to_der(ORIG_DWORDS)
variants = [("orig-e65537", orig_der),
("e1-nonminimal", orig_der[:E1_BYTE_INDEX] + b"\x00" + orig_der[E1_BYTE_INDEX + 1:]),
("custom-full", embedded_custom_der())]
for name, der in variants:
rc, out, err, crashed = _probe_child(dll_path, "loadkey", binascii.hexlify(der).decode())
status = "CRASH(被拒)" if crashed else out
print(f" {name}: rc={rc} {status}")
if crashed and err:
print(" stderr tail:", err[-200:])
print("== 核心探针2: BNInitUI 全链路(BN_LICENSE 注入,license 与方案配对) ==")
out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_out")
os.makedirs(out_dir, exist_ok=True)
base_data = load_dll(dll_path)
scheme_license = {
"A": make_licenses_simple, "B": make_licenses_full, "C": make_licenses_verifier}
for scheme in ("A", "B", "C"):
pdll = os.path.join(out_dir, f"coretest_{scheme}.dll")
patched, _ = apply_patch(base_data, {"A": "simple", "B": "full", "C": "verifier"}[scheme])
with open(pdll, "wb") as f:
f.write(patched)
lic_json = json.dumps(scheme_license[scheme](
email="user@local.host", serial=secrets.token_hex(16),
created="2026-01-01T00:00:00+00:00", count=1))
rc, out, err, crashed = _probe_child(pdll, "initui", env={"BN_LICENSE": lic_json})
status = "CRASH(验证失败/异常)" if crashed else out
ok = ("validated=True" in out)
print(f" 方案{scheme}: rc={rc} {status} => {'通过' if ok else '未通过'}")
if crashed and err:
print(" stderr tail:", err[-300:])
print("[=] 真实核心测试完成")
return 0
def default_license_path():
user_dir = os.environ.get("BN_USER_DIRECTORY")
if user_dir:
return os.path.join(user_dir, "license.dat")
appdata = os.environ.get("APPDATA", "")
return os.path.join(appdata, "Binary Ninja", "license.dat")
def verify_deploy(dll, license_file, scheme="A", log=print):
# 统一接受 "A/B/C" 与 "simple/full/verifier" 两种写法(此前签名判定分支用 "verifier" 与
# 调用方传入的 "C" 不匹配,导致方案C 被误判为"签名不匹配")
scheme = {"simple": "A", "full": "B", "verifier": "C"}.get(scheme, scheme)
if scheme not in ("A", "B", "C"):
raise RuntimeError(f"未知方案: {scheme}")
ok = True
data = load_dll(dll)
log(f"[*] 部署 DLL: {dll}")
sites = find_tables(data)
states = classify_tables(data)
voff, vstate = find_verifier(data)
log(f"[*] 公钥表状态: {states} PK_Verifier: {vstate}")
tstate, tmsg = activation_table_status(data)
log(f"[*] 0x48E 激活表({ACTIVATION_TABLE_VA:#x}): {tmsg}")
if scheme == "A" and (not sites or states != ["e1"] * len(sites)):
log("[!] 部署的 DLL 不是方案A patch 状态(期望 ['e1',...])——请重新 patch 并部署")
ok = False
if scheme == "C" and vstate != "patched":
log("[!] 部署的 DLL 不是方案C patch 状态(PK_Verifier 应为 patched)")
ok = False
for env_name in ("BN_USER_DIRECTORY", "BN_LICENSE"):
v = os.environ.get(env_name)
if v:
log(f"[!] 环境变量 {env_name} 已设置: {v}"
+ ("(★ 它优先于 %APPDATA% 的 license.dat!)" if env_name == "BN_USER_DIRECTORY"
else "(★ 它优先于 license.dat 文件,注意是否为旧许可)"))
if not license_file:
license_file = default_license_path()
log(f"[*] license 文件: {license_file} 存在={os.path.isfile(license_file)}")
if not os.path.isfile(license_file):
log("[!] license 文件不存在——核心将视为未授权")
ok = False
return ok
with open(license_file, "r", encoding="ascii", errors="replace") as f:
text = f.read()
try:
arr = json.loads(text)
if not isinstance(arr, list) or not arr:
raise ValueError("根节点不是非空数组")
except Exception as exc:
log(f"[!] license 文件解析失败: {exc}")
ok = False
return ok
log(f"[*] license 元素数: {len(arr)}")
for lic in arr:
product = lic.get("product", "?")
try:
if len(base64.b64decode(lic.get("data", ""))) != DATA_LEN:
raise ValueError("data 解码长度 != 0x118")
msg = build_signature_input(lic)
sig = base64.b64decode(lic["signature"])
if scheme == "A":
expect = emsa3_pkcs1_v15(msg)
verdict = "匹配(当前工具格式)" if sig == expect else "不匹配(旧格式或被改动)"
elif scheme == "C":
verdict = "匹配" if sig == b"\x00" * 256 else "不匹配(应为 256 零字节)"
else:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
try:
embedded_priv().public_key().verify(sig, msg, padding.PKCS1v15(), hashes.SHA256())
verdict = "匹配(内置私钥签名)"
except Exception:
verdict = "不匹配(旧格式或被改动)"
log(f" [{product}] serial={lic.get('serial','-')} 签名: {verdict}")
if "不匹配" in verdict:
log(" → 请用当前版本工具重新生成 license.dat(旧版工具签名的输入格式有误)")
ok = False
dok, dmsg = check_license_data(base64.b64decode(lic.get("data", "")), data)
log(f" [{product}] data: {dmsg}")
if not dok:
log(" → 该 data 会让核心内置的 0x48E 激活表解成乱码,"
"表现为建库/保存 .bndb 时异常退出(崩溃);请用当前版本工具重新生成")
ok = False
except Exception as exc:
log(f" [{product}] 检查失败: {exc}")
ok = False
log("[*] 真实核心 BNInitUI 全链路(部署 DLL + 部署 license 内容)…")
rc, out, err, crashed = _probe_child(dll, "initui", env={"BN_LICENSE": text})
if crashed:
log(f"[!] 核心异常退出 rc={rc}: {err[-200:]}")
ok = False
else:
log(f"[*] {out}")
if "validated=True" in out:
log("[=] 部署状态验证通过:该 DLL + 该 license 在真实核心上有效")
else:
log("[!] 部署组合在真实核心上无效(结合上方各项定位差异)")
ok = False
return ok
def run_selftest(dll_path):
out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_out")
os.makedirs(out_dir, exist_ok=True)
from cryptography.hazmat.primitives import serialization
priv = embedded_priv()
print("[*] 内置密钥解析 OK, 公钥指纹:",
hashlib.sha256(priv.public_key().public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo)).hexdigest()[:16])
data = load_dll(dll_path)
print(f"[*] 目标: {dll_path} ({len(data)} 字节)")
import re
m = re.search(rb"Botan \d+\.\d+\.\d+[^\x00]{0,40}", data)
print("[*] Botan 版本字符串:", m.group().decode() if m else "未找到")
sites, states, _ = report_state(data)
if states != ["orig"] * len(sites):
print("[!] 原 DLL 公钥表状态非 orig,自检中止")
return 2
serial = secrets.token_hex(16)
plans = (("simple", "binaryninjacore.dll.e1", "license_e1.dat"),
("full", "binaryninjacore.dll.full", "license_full.dat"),
("verifier", "binaryninjacore.dll.verifier", "license_verifier.dat"))
for scheme, dll_name, lic_name in plans:
patched, desc = apply_patch(data, scheme)
out_dll = os.path.join(out_dir, dll_name)
with open(out_dll, "wb") as f:
f.write(patched)
patched_sites = find_tables(patched)
states2 = classify_tables(patched, embedded_custom_der())
voff2, vstate2 = find_verifier(patched)
print(f"[+] {scheme}: {desc}")
print(f" -> {out_dll} 表状态: {states2} PK_Verifier: {vstate2}")
if scheme == "simple" and states2 != ["e1"] * len(patched_sites):
raise RuntimeError("方案A patch 后状态校验失败")
if scheme == "full" and states2 != ["custom"] * len(patched_sites):
raise RuntimeError("方案B patch 后状态校验失败")
if scheme == "verifier" and vstate2 != "patched":
raise RuntimeError("方案C patch 后状态校验失败")
licenses = (make_licenses_simple(email="user@local.host", serial=serial,
created="2026-01-01T00:00:00+00:00", count=1)
if scheme == "simple" else
(make_licenses_full(email="user@local.host", serial=serial,
created="2026-01-01T00:00:00+00:00", count=1)
if scheme == "full" else
make_licenses_verifier(email="user@local.host", serial=serial,
created="2026-01-01T00:00:00+00:00", count=1)))
verify_licenses(licenses, scheme, data)
lic_path = os.path.join(out_dir, lic_name)
write_license(lic_path, licenses)
print(f" -> 许可已生成并回验: {lic_path}"
"(含 data 字段:派生密钥 == 原厂密钥,激活表可正确解密)")
print("[=] 三方案自检全部通过")
return 0
def cli_main(argv):
ap = argparse.ArgumentParser()
ap.add_argument("--selftest", action="store_true")
ap.add_argument("--test-core", action="store_true",
help="真实核心验证:BNInitUI 全链路测试三方案")
ap.add_argument("--check-only", action="store_true")
ap.add_argument("--print-pubkey", action="store_true")
ap.add_argument("--extract-data-key", default=None, metavar="LICENSE.DAT",
help="从 license.dat 导出 24 字节激活表密钥(用于新版本重新确定密钥)")
ap.add_argument("--scheme", choices=("A", "B", "C", "simple", "full", "verifier"),
default=None)
ap.add_argument("--verify-deploy", action="store_true",
help="部署状态自证:DLL patch 状态 + license 签名新旧 + 真实核心 BNInitUI")
ap.add_argument("--license-file", default=None,
help="部署的 license.dat 路径(默认按核心读取规则定位)")
ap.add_argument("--dll", default=None)
ap.add_argument("--out", default=None)
ap.add_argument("--license-out", default=None)
ap.add_argument("--in-place", action="store_true")
ap.add_argument("--email", default="user@local.host")
ap.add_argument("--serial", default=None)
ap.add_argument("--created", default="2026-01-01T00:00:00+00:00")
ap.add_argument("--count", default="1")
args = ap.parse_args(argv)
here = os.path.dirname(os.path.abspath(__file__))
if args.print_pubkey:
from cryptography.hazmat.primitives import serialization
print("== 内置公钥(方案B 自签名用) ==")
print(embedded_priv().public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo).decode())
print("== 原厂公钥(XOR 0x2FCBDA65 解码自 .text 立即数表) ==")
print(serialization.load_der_public_key(dwords_to_der(ORIG_DWORDS))
.public_bytes(serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo).decode())
return 0
if args.extract_data_key:
with open(args.extract_data_key, "r", encoding="ascii", errors="replace") as f:
arr = json.load(f)
for lic in arr:
try:
key = data_key_from_data(base64.b64decode(lic.get("data", "")))
except Exception as exc:
print(f" [{lic.get('product', '?')}] data 无效: {exc}")
continue
print(f" [{lic.get('product', '?')}] 24 字节激活密钥: {key.hex()}"
+ ("(== 内置原厂密钥)" if key == LICENSE_DATA_KEY else "(≠ 内置密钥)"))
dll = args.dll or auto_detect_dll()
if dll:
print(f"[*] 目标 DLL: {dll}")
print(f"[*] 0x48E 激活表: {activation_table_status(load_dll(dll))[1]}")
return 0
dll = args.dll or auto_detect_dll()
if not dll:
print("未找到 binaryninjacore.dll,请用 --dll 指定")
return 1
try:
if args.selftest:
return run_selftest(dll)
if args.test_core:
return run_core_test(dll)
if args.verify_deploy:
sch = {"A": "A", "B": "B", "C": "C", "simple": "A",
"full": "B", "verifier": "C"}.get(args.scheme or "A", "A")
ok = verify_deploy(dll, args.license_file, sch)
return 0 if ok else 1
data = load_dll(dll)
if args.check_only:
report_state(data)
return 0
scheme = (args.scheme or "A")
scheme = {"A": "simple", "B": "full", "C": "verifier"}.get(scheme, scheme)
pipeline(dll, scheme, args)
except PermissionError as exc:
print(f"[!] 权限不足或文件被占用: {exc}")
print(" 请更换输出路径(如桌面/文档),以管理员身份运行,或先关闭 Binary Ninja")
return 1
except RuntimeError as exc:
print(f"[!] {exc}")
return 1
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(f"[!] 文件/数据错误: {exc}")
return 1
return 0
def gui():
import tkinter as tk
from tkinter import filedialog, scrolledtext, ttk
logq = queue.Queue()
root = tk.Tk()
root.title("Binary Ninja 6.0.10601 许可补丁工具(三方案,密钥内置)")
root.minsize(780, 640)
frm_file = ttk.LabelFrame(root, text="目标文件")
frm_file.pack(fill="x", padx=8, pady=6)
var_dll = tk.StringVar(value=auto_detect_dll())
var_out = tk.StringVar()
var_inplace = tk.BooleanVar(value=False)
ttk.Label(frm_file, text="binaryninjacore.dll:").grid(row=0, column=0, sticky="e", padx=4, pady=3)
ttk.Entry(frm_file, textvariable=var_dll, width=72).grid(row=0, column=1, sticky="we", padx=4)
def pick_dll():
p = filedialog.askopenfilename(title="选择 binaryninjacore.dll",
filetypes=[("DLL", "*.dll"), ("全部", "*.*")])
if p:
var_dll.set(p)
ttk.Button(frm_file, text="浏览…", command=pick_dll).grid(row=0, column=2, padx=4)
ttk.Label(frm_file, text="输出 DLL:").grid(row=1, column=0, sticky="e", padx=4, pady=3)
ttk.Entry(frm_file, textvariable=var_out, width=72).grid(row=1, column=1, sticky="we", padx=4)
def pick_out():
p = filedialog.asksaveasfilename(title="输出 patched DLL", defaultextension=".dll")
if p:
var_out.set(p)
ttk.Button(frm_file, text="浏览…", command=pick_out).grid(row=1, column=2, padx=4)
ttk.Checkbutton(frm_file, text="直接改写原文件(自动 .bak 备份)",
variable=var_inplace).grid(row=2, column=1, sticky="w", padx=4)
frm_file.columnconfigure(1, weight=1)
frm_scheme = ttk.LabelFrame(root, text="Patch 方案(公私密钥已内置,方案B 无需密钥文件)")
frm_scheme.pack(fill="x", padx=8, pady=6)
var_scheme = tk.StringVar(value="A")
ttk.Radiobutton(frm_scheme, text="方案A 简易:公钥指数单字节 patch(e=65537 → 1),"
"签名 = EMSA3 块,共 3 字节 ×3 处",
value="A", variable=var_scheme).pack(anchor="w", padx=6, pady=2)
ttk.Radiobutton(frm_scheme, text="方案B 完整:整表替换为内置 RSA-2048 公钥,"
"用内置私钥签名(~864 字节 ×3 处)",
value="B", variable=var_scheme).pack(anchor="w", padx=6, pady=2)
ttk.Radiobutton(frm_scheme, text="方案C:patch PK_Verifier(check_signature 恒真,"
"单点 3 字节,签名内容随意,也使 X.509 等检查恒真)",
value="C", variable=var_scheme).pack(anchor="w", padx=6, pady=2)
frm_lic = ttk.LabelFrame(root, text="许可信息")
frm_lic.pack(fill="x", padx=8, pady=6)
var_email = tk.StringVar(value="user@local.host")
var_serial = tk.StringVar(value=secrets.token_hex(16))
var_created = tk.StringVar(value="2026-01-01T00:00:00+00:00")
var_count = tk.StringVar(value="1")
var_licout = tk.StringVar()
for r, (label, var) in enumerate((("Email", var_email), ("Serial", var_serial),
("Created", var_created), ("Count", var_count),
("license.dat 输出", var_licout))):
ttk.Label(frm_lic, text=label + ":").grid(row=r, column=0, sticky="e", padx=4, pady=2)
ttk.Entry(frm_lic, textvariable=var, width=72).grid(row=r, column=1, sticky="we", padx=4)
frm_lic.columnconfigure(1, weight=1)
def rand_serial():
var_serial.set(secrets.token_hex(16))
def pick_licout():
p = filedialog.asksaveasfilename(title="保存 license.dat", initialfile="license.dat")
if p:
var_licout.set(p)
ttk.Button(frm_lic, text="随机", command=rand_serial).grid(row=1, column=2, sticky="w", padx=2)
ttk.Button(frm_lic, text="浏览…", command=pick_licout).grid(row=4, column=2, padx=4)
frm_btn = ttk.Frame(root)
frm_btn.pack(fill="x", padx=8, pady=6)
progress = ttk.Progressbar(frm_btn, mode="indeterminate", length=120)
txt = scrolledtext.ScrolledText(root, height=15, state="disabled", font=("Consolas", 9))
txt.pack(fill="both", expand=True, padx=8, pady=6)
def log(msg):
logq.put(msg)
def poll():
try:
while True:
line = logq.get_nowait()
txt.configure(state="normal")
txt.insert("end", line + "\n")
txt.see("end")
txt.configure(state="disabled")
except queue.Empty:
pass
root.after(120, poll)
def run_bg(fn):
def worker():
try:
fn()
except PermissionError as exc:
log(f"[!] 权限不足或文件被占用: {exc}")
log(" 请更换输出路径(如桌面/文档),以管理员身份运行本工具,"
"或先关闭正在运行的 Binary Ninja")
except Exception as exc:
log(f"[!] 出错: {exc}")
finally:
progress.stop()
for w in frm_btn.winfo_children():
if isinstance(w, ttk.Button):
w.configure(state="normal")
for w in frm_btn.winfo_children():
if isinstance(w, ttk.Button):
w.configure(state="disabled")
progress.pack(side="right", padx=6)
progress.start(12)
threading.Thread(target=worker, daemon=True).start()
def license_kwargs():
"""GUI 许可输入统一校验(与 CLI 同一套规则)。"""
email = var_email.get().strip() or "user@local.host"
serial = var_serial.get().strip() or secrets.token_hex(16)
created = var_created.get().strip() or "2026-01-01T00:00:00+00:00"
count = validate_license_inputs(email, serial, created, var_count.get().strip() or "1")
return dict(email=email, serial=serial, created=created, count=count)
def try_load_target_dll():
"""可选的 DLL(用于核对 0x48E 激活表);未指定或不可读时返回 None。"""
p = var_dll.get().strip()
if not p or not os.path.isfile(p):
return None
return load_dll(p)
def act_detect():
def job():
data = load_dll(var_dll.get().strip())
report_state(data, log)
run_bg(job)
def act_patch():
def job():
data = load_dll(var_dll.get().strip())
scheme = {"A": "simple", "B": "full", "C": "verifier"}[var_scheme.get()]
patched, desc = apply_patch(data, scheme)
log(f"[*] 应用: {desc}")
if var_inplace.get():
bak = var_dll.get().strip() + ".bak"
with open(bak, "wb") as f:
f.write(data)
out = var_dll.get().strip()
log(f"[+] 原文件已备份: {bak}")
else:
out = var_out.get().strip() or (var_dll.get().strip() + ".patched")
out_dir = os.path.dirname(os.path.abspath(out))
os.makedirs(out_dir, exist_ok=True)
with open(out, "wb") as f:
f.write(patched)
log(f"[+] 已写出: {out}")
report_state(patched, log)
run_bg(job)
def act_license():
def job():
scheme = {"A": "simple", "B": "full", "C": "verifier"}[var_scheme.get()]
kwargs = license_kwargs()
makers = {"simple": make_licenses_simple, "full": make_licenses_full,
"verifier": make_licenses_verifier}
licenses = makers[scheme](**kwargs)
dll_data = try_load_target_dll()
verify_licenses(licenses, scheme, dll_data)
log("[+] 许可签名与 data 字段回验通过"
+ ("(已核对目标 DLL 的 0x48E 激活表)" if dll_data is not None
else "(未指定/读不到 DLL,未核对目标激活表)"))
path = var_licout.get().strip() or os.path.join(
os.path.dirname(os.path.abspath(__file__)), "license.dat")
write_license(path, licenses)
log(f"[+] 已写出: {path} (serial={kwargs['serial']})")
log(f"[*] 部署: 把 {path} 复制为 %APPDATA%\\Binary Ninja\\license.dat 后重启 Binary Ninja")
run_bg(job)
def act_all():
def job():
data = load_dll(var_dll.get().strip())
report_state(data, log)
scheme = {"A": "simple", "B": "full", "C": "verifier"}[var_scheme.get()]
patched, desc = apply_patch(data, scheme)
log(f"[*] 应用: {desc}")
if var_inplace.get():
bak = var_dll.get().strip() + ".bak"
with open(bak, "wb") as f:
f.write(data)
out = var_dll.get().strip()
log(f"[+] 原文件已备份: {bak}")
else:
out = var_out.get().strip() or (var_dll.get().strip() + ".patched")
out_dir = os.path.dirname(os.path.abspath(out))
os.makedirs(out_dir, exist_ok=True)
with open(out, "wb") as f:
f.write(patched)
log(f"[+] 已写出: {out}")
report_state(patched, log)
kwargs = license_kwargs()
makers = {"simple": make_licenses_simple, "full": make_licenses_full,
"verifier": make_licenses_verifier}
licenses = makers[scheme](**kwargs)
verify_licenses(licenses, scheme, data)
log("[+] 许可签名与 data 字段回验通过(激活表可正确解密)")
path = var_licout.get().strip() or os.path.join(
os.path.dirname(os.path.abspath(__file__)), "license.dat")
write_license(path, licenses)
log(f"[+] 已写出: {path} (serial={kwargs['serial']})")
log("[=] 一键流程完成。部署步骤:")
log(f" 1) 用 {out} 替换安装目录的 binaryninjacore.dll"
+ ("(本次为直接改写原文件,已生成 .bak 备份)" if var_inplace.get()
else "(原文件请先备份)"))
log(f" 2) 把 {path} 复制为 %APPDATA%\\Binary Ninja\\license.dat")
log(" 3) 重启 Binary Ninja(激活表仅在进程启动时解密一次,必须重启才生效)")
log(" 4) 之后可点『部署核查』验证部署状态")
run_bg(job)
ttk.Button(frm_btn, text="检测状态", command=act_detect).pack(side="left", padx=4)
ttk.Button(frm_btn, text="执行 Patch", command=act_patch).pack(side="left", padx=4)
ttk.Button(frm_btn, text="生成 license.dat", command=act_license).pack(side="left", padx=4)
ttk.Button(frm_btn, text="★ 一键完成", command=act_all).pack(side="left", padx=10)
def act_verify_deploy():
def job():
sch = {"A": "A", "B": "B", "C": "C"}[var_scheme.get()]
licf = var_licout.get().strip() or None
ok = verify_deploy(var_dll.get().strip(), licf, sch, log=log)
log("[=] 部署核查结论: " + ("通过" if ok else "存在问题(见上)"))
run_bg(job)
ttk.Button(frm_btn, text="部署核查", command=act_verify_deploy).pack(side="left", padx=4)
log("[*] 就绪。公私密钥已内置(方案B 无需密钥文件)。")
log("[*] 选择 DLL 后可『检测状态』或直接『一键完成』;方案A/C 仅标准库,方案B 需 cryptography。")
log("[*] 生成的 license.dat 已带正确的 data 字段(可正确解密核心 0x48E 激活表),"
"否则建库/保存 .bndb 会异常退出。")
log("[*] 部署后必须重启 Binary Ninja 才生效(激活表仅在进程启动时解密一次)。")
root.after(120, poll)
root.mainloop()
def main(argv):
if not argv:
gui()
return 0
if argv[0] in ("-h", "--help"):
print(__doc__)
return 0
return cli_main(argv)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
保存异常-补充说明
保存/创建 .bndb 数据库时 Binary Ninja 异常退出:
1. 结论摘要
- 许可
data字段不只是被签名覆盖的数据:它派生出 24 字节密钥,用于 RC4 解密核心内置的 0x48E 字节"激活表"(VA0x18C38B8D0,583 个 u16)。 - 该激活表被核心实际使用:建库/保存
.bndb(BNCreateDatabase)时必然读取它;data错误 → 表为乱码 → 进程异常退出(堆损坏0xC0000374或访问违例),GUI 下表现为生成的.bndb为 0 字节。 - 旧结论"无任何引用"是误判:该表所有访问都编译为
[__ImageBase + idx*2 + RVA]形式(disp32 是 RVA,不是重定位后的绝对地址),IDA 不会为其建立交叉引用。 - 派生密钥是与许可内容无关的软件内置常量:因此可自造与之等价的
data。
2. 问题现象
保存/创建 .bndb 数据库时 Binary Ninja 异常退出:
| 场景 | 表现 |
|---|---|
| GUI | 保存出的 .bndb 为 0 字节,进程消失 |
headless(BinaryView.create_database()) |
进程以 0xC0000374(STATUS_HEAP_CORRUPTION,堆损坏)退出;或抛出 OSError: exception: access violation reading …(BNCreateDatabase 内触发访问违例) |
触发条件:许可 data 字段(旧工具产物为 280 零字节)无法正确解密激活表。
3. 激活表结构
| 项目 | 实测值 |
|---|---|
| 长度 | 0x48E = 583 个 u16 |
| 索引范围 | 0 .. 0x246(与 583 项一一对应) |
| 明文值域 | min = 1254 (0x4E6)、max = 1663 (0x67F) |
| 众数 | 1254 出现 334 次(其余为散布的较大值) |
| 明文 SHA-256 | efd6ef5f597dfd31de358fa886396d867042edeaa362730aebd46eafd135bcd6 |
与同簇其他表并行使用(读表逻辑,来自 sub_181B43570):
i = 当前 token(来自输入流)
if (i <= 0x246):
v10 = unk_1878430E0[i] # 明文表(RVA 0x78430E0,同为 583×u16),提供基址
v11 = v10 + <辅助值>
if (word_187841E10[v11] == <辅助值>): i = word_187840CD0[v11]
else: 沿 word_1878438C0 链继续探测;
探测失败时回落到激活表: i = unk_18C38B8D0[i] # ★ 本表
...
if (i < 0x4E9) 结束;if (i > 0x4E5) 走特殊分支(报 "near %T: syntax error" / "incomplete input")
同簇相关表:unk_1878430E0(RVA 0x78430E0)、word_187841E10(0x7841E10)、word_187840CD0(0x7840CD0)、word_187840D9C(0x7840D9C)、word_1878438C0(0x78438C0)、byte_187843E20。
unk_1878430E0与激活表明文是两组不同数据(583 项中 582 项取值不同,且前者含负值0xFFFB…),不是同一表的"明文/密文"两态。
4. data → 24 字节密钥 → 激活表
派生链路(地址为 6.0.10601 实测):
data[0:0x100] # 256 字节随机前缀
key16 = MD5(data[0:0x100]) # "MD5" 名串 @0x18781C758;MD5 update(lic_data,0x100) @0x18161874E
k24 = RC4(key16, data[0x100:0x118]) # "RC4" 名串 @0x18780AC24;建 cipher @0x1816187E5;set_key(24B) @0x1816191C0
明文 = RC4(k24, <0x48E 密文 @ VA 0x18C38B8D0>) # 原地解密 @0x1816191F8(len=0x48E),随后置 byte_18C5C9788 = 1
k24 = 9c2aaa09a4e2252b0ba125db1e1cd272207d97cca8446899
即 k24 是与许可内容无关的软件内置常量;data 只是"用随机前缀把该常量封装起来"的容器。因此可自造与正版等价的 data:
data = 随机 256 字节 || RC4(MD5(随机 256 字节), k24)
data 本体被签名覆盖,方案A(e=1)下可自行产生匹配签名,故自造 data 完全可用。
ps


浙公网安备 33010602011771号