应用安全 --- 网顿加固 之 unity
参考文档 https://www.ctfiot.com/106094.html
在壳子的父so中,
rc4解密出子so的代码和数据
通过自己实现的安卓链接器加载子so
执行子so的初始化代码
一句话说明实现加密的方法就是:
壳so的第三个init函数解密子so的代码和数据段后,通过自实现的linker流程(load_segment映射到预留父so内存 → prelink解析父的的和子的dynamic段用于修复父so缺失的部分 → relocate修正符号地址到正确的外部函数地址 → 调用子so的init函数完成初始化代码)完成真正的加载。
某顿加固的精妙之处:
- 分离存储:program header、dynamic段与代码/数据分离 包含知识点ELF文件格式(program header、dynamic segment), ARM64汇编和花指令识别
- 自实现linker:完整重现Android链接流程 , Android linker机制(prelink、relocate、load_segment) 重定位原理(RELA表处理
- 空间预留:壳so预留3倍文件大小的内存。 子so加密算法(魔改RC4)
- 差分dynamic:子so只存储与壳so不同的部分
防护强度评估:
- 传统dump方法: 失效
- 需要技能:理解linker + ELF格式 + 重定位机制 + 手动重组
- 时间成本:约1周
全流程分析概览
一、背景与问题发现
一款Unity手游时,遇到Il2CppDumper报错:
- 现象:
global-metadata.dat被加密,文件头从正常的AF 1B B1 FA变为HTPX - 加固方案:识别为某顿(N4tHTProtect)手游加固
- 初步困难:IDA提示SHT损坏,函数名被大量混淆
二、核心技术挑战
1. 去除花指令
- 发现两种花指令模式干扰IDA解析(堆栈不平衡)
- 编写Python脚本通过特征匹配批量替换为NOP指令
2. 分析三个INIT函数
INIT函数1:初始化全局函数数组,间接调用系统函数
INIT函数2:
- 遍历ELF找到dynamic segment
- 执行prelink操作
- 解密字符串表(异或
0x56312342)和符号表
INIT函数3(核心):自定义链接器实现
流程:解密子so代码/数据段 → load_segment映射 → prelink → relocate → 调用子so的init
3. 某顿加固的核心机制
| 传统加固 | 某顿加固 |
|---|---|
| 整体加密解密 | 分段加密存储 |
| 内存中存在完整so | 内存中从未出现完整so |
| 容易dump修复 | 需要分段dump+重组 |
关键设计:
- 壳so预留超大内存空间(0x7cd4000 vs 0x447c000文件大小)
- 子so的program header、dynamic段单独加密存储
- 运行时自己实现linker的加载、重定位全流程
三、修复过程(借尸还魂)
步骤1:移植代码和数据段
1. dump解密后的代码段、数据段
2. 删除壳so的错误section header
3. 将子so代码段复制到program header后
4. 修改program header添加子so数据段
5. 数据附在文件末尾,修改偏移
步骤2:修复重定位表
1. 合并壳so和子so的重定位表
2. 附在文件末尾,修改dynamic段偏移
3. 用子so符号表覆盖壳so符号表
4. 手动修复壳so的部分重定位(约10处)
重定位类型:
0x403 (R_AARCH64_RELATIVE):相对地址,直接base+addend0x401/0x402 (JUMP_SLOT/GLOB_DAT):通过符号索引查找真实地址
步骤3:修复init_array和section header
1. 替换为子so的init_array
2. 保留壳so第一个init函数(运行时依赖)
3. 添加必要的section header(shstr、dynamic、dynstr)
四、最终成果
- so修复成功:替换后游戏正常运行
- dump metadata:在
sub_bc9564下断点获取解密的global-metadata.dat - 恢复脚本:手动修复magic/version后,Il2CppDumper成功导出CS脚本
详细过程:
一个unity游戏使用了网顿加固,他会将
global-metadata.dat
libil2cpp.so
两个文件加壳子。
libil2cpp.so分析过程:
不同版本的加固特征不同但是大同小异
首次ida静态分析报错,
SHT加密

压缩so

导入和导出表加密

出现了不常见的段 ,发现出现了一些不常见的note.gnu.proc,note.gnu.text等节区,

花指令干扰
分段so存储
自定义链接器加载真正的so。linker(链接器)将一个动态库加载到内存时,做完重定位(relocate)操作后就会调用动态库的init函数,用来完成一些初始化操作。由于init函数是linker调用的,所以没法做加密。看到这么离谱的一个动态库,显然需要init函数来解密。
通常,init函数会出现在.init_array这个节区里,这是个函数指针数组,链接器会依次调用里面的函数。

有的版本加密,但是我的没有出现的加密技术:
-
发现global-metadata.dat被加密(Magic被改为"HTPX")
-
![image]()
发现lib目录下有libN4tHTProtect.so,确认使用了某顿加固
手动脱修
找入口。
因为SHT加密导致ida分析节区失败,我们可以通过动态节区分析节区有哪些。下面就是动态节区解析后的值



我们找到动态节中初始化数组节区的虚拟地址。

我们定位到了这三个初始化函数地址,跳转过去我们发现ida无法识别。

打开010抹除错误的SHT区域的数据,这个区域主要是加固对抗IDA的方法,不影响运行。运行需要的是段区域的数据和这里的节区域作用类似。

可以正常解析了

去除花指令
py代码修改so路径后运行脚本
# 去除花指令 mod1 = [0x86, 0x10, 0x40, 0xb9, 0xa6, 0x19, 0x00, 0x18, 0xff, 0x43, 0x00, 0xd1, 0xc0, 0x03, 0x5f, 0xd6] mod2 = [0x86, 0x08, 0x40, 0xf9, 0xff, 0x43, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x1b, 0xFF, 0x25, 0x00, 0x18, 0xff, 0x43, 0x00, 0xd1, 0xc0, 0x03, 0x5f, 0xd6] nop = [0x1f, 0x20, 0x03, 0xd5] def match(data, index, mod, ignorerange): for j in range(len(mod)): if data[index + j] == mod[j] or j in ignorerange: continue else: return False return True def patch_word(data, index, code): for i in range(4): data[index + i] = code[i] def patch(data, index): start = index - 0x10 patch_word(data, start, nop) patch_word(data, start + 4, nop) patch_word(data, start + 8, nop) def patch_mod1(data, index): start = index patch_word(data, start, nop) patch_word(data, start + 4, nop) patch_word(data, start + 8, nop) patch_word(data, start + 12, nop) def patch_mod2(data, index): start = index for i in range(6): patch_word(data, start + i * 4, nop) with open(r"C:\Users\21558\Videos\shj\原件\libil2cpp.so", "rb") as f: data = list(f.read()) for i in range(len(data)): if match(data, i, mod1, [4, 5, 6, 7]): patch(data, i) patch_mod1(data, i) if match(data, i, mod2, [12, 13, 14, 15]): patch(data, i) patch_mod2(data, i) with open(r"C:\Users\21558\Videos\shj\原件\libil2cpp_patche.so", "wb+") as f: f.write(bytes(data))
不一定有花指令存在程序中。
字符串表文件偏移是
02FE1158
找到第一个未加密的字符串,前面都i是要解密的字符串。
同理找到
符号表文件偏移是
LOAD:0000000005A149B0 02FCC9B0
加密起始位置
LOAD:0000000005A219B8 02FD99B8
LOAD:0000000005A246E8 02FDC6E8

第一个初始化函数分析,创建一个内存陷阱检查是不是在被反调试
第二个初始化函数分析,这个函数的作用就是解密字符串和符号表
函数控制流是,加载动态段,解析动态段,解密字符串和符号表

对照分析:主函数 sub_42F1564 与教程的流程比对
📋 核心对应关系
| 教程描述 | 我们分析的函数 | 具体功能 |
|---|---|---|
| 1. 遍历ELF寻找Dynamic段 | sub_42F4754 |
遍历Program Header,查找p_type=2(PT_DYNAMIC)的段 |
| 2. 执行Prelink操作 | sub_42F5844 |
解析Dynamic段,通过switch-case处理各种DT_*类型 |
| 3. 数据加密处理 | sub_42F57AC |
对解析后的数据进行XOR加密(教程未提及) |
🔍 详细流程对照
阶段一:寻找Dynamic段
教程代码片段:
if ( find_dyn_seg((unsigned __int64 *)&v123, (signed __int64)*off_7DFCCC8) )
{
// 找到p_type为2的段(PT_DYNAMIC)
}
我们的实际代码:
// sub_42F1564 中调用
if (sub_42F4754(stack_buffer, *off_4306FD0) == 0) {
// 未找到Dynamic段,释放资源
((void (*)(void*))func_table[0xD8/8])(data_struct);
return data_struct;
}
sub_42F4754 内部逻辑:
bool sub_42F4754(struct elf_parser* parser, uint64_t param)
{
// 遍历Program Header Table
do {
while (phdr->p_type != PT_DYNAMIC) // PT_DYNAMIC = 2
{
phdr++;
if (phdr == phdr_end) goto check_result;
}
// 找到PT_DYNAMIC段,计算绝对地址
parser->dynamic = (void*)((char*)base + phdr->p_offset);
phdr++;
} while (phdr != phdr_end);
return (parser->dynamic != NULL);
}
完全吻合! 都是在寻找Program Header中p_type=2的Dynamic段。
阶段二:Prelink操作(解析Dynamic段)
教程中的Dynamic段类型对照表:
| Tag值 | 名称 | 教程case | 我们的case | 存储位置 |
|---|---|---|---|---|
| 4 | DT_HASH | case 4LL |
case 4 |
offset +0x100 (256) / +0x110 (272) |
| 5 | DT_STRTAB | case 5LL |
case 5 |
offset +0x30 (48) |
| 6 | DT_SYMTAB | case 6LL |
case 6 |
offset +0x20 (32) |
| 10 | DT_STRSZ | case 10LL |
case 10 |
offset +0x38 (56) |
| 12 | DT_INIT | case 12LL |
case 12 |
offset +0xF8 (248) |
| 13 | DT_FINI | case 13LL |
case 13 |
offset +0xF0 (240) |
| 14 | DT_SONAME | case 14LL |
case 14 (隐含处理) | 记录偏移量 |
| 25 | DT_INIT_ARRAY | case 25LL |
case 32 |
offset +0xC0 (192) |
| 26 | DT_FINI_ARRAY | case 26LL |
case 33 |
offset +0xD0 (208) |
| 27 | DT_INIT_ARRAYSZ | case 27LL |
case 27 (未明确) | offset +0xC8 (200) |
教程代码示例:
switch (tag) {
case 4LL: // DT_HASH
*(_QWORD *)(v2 + 256) = *v11 + v7;
v14 = *(unsigned int *)(*v11 + v7);
*(_QWORD *)(v2 + 248) = v14;
break;
case 5LL: // DT_STRTAB
v16 = *v11;
*(_QWORD *)(v2 + 48) = v16 + v7;
break;
case 6LL: // DT_SYMTAB
v17 = *v11;
*(_QWORD *)(v2 + 32) = v17 + v7;
break;
case 25LL: // DT_INIT_ARRAY
v22 = *v11;
*(_QWORD *)(v2 + 192) = v22 + v7;
break;
}
我们的实际代码(sub_42F5844):
while (1) {
while (v8 == 25) { // 处理tag 25类型
*(void**)((char*)a1 + 208) = (char*)v5[1] + v7;
long long v46 = v5[2];
v5 += 2;
v8 = v46;
if (!v46) goto LABEL_20;
}
if (v8 > 25) break;
if (v8 == 10) { // DT_STRSZ
*(void**)((char*)a1 + 56) = (void*)v5[1];
...
} else if (v8 == 4) { // DT_HASH
*(void**)((char*)a1 + 272) = (char*)v5[1] + v7;
unsigned int v39 = *(unsigned int*)((char*)v5[1] + v7);
*(void**)((char*)a1 + 264) = (void*)v39; // nbucket
...
} else if (v8 == 5) { // DT_STRTAB
*(void**)((char*)a1 + 48) = (char*)v5[1] + v7;
...
} else if (v8 == 6) { // DT_SYMTAB
*(void**)((char*)a1 + 32) = (char*)v5[1] + v7;
...
}
}
完全吻合! 逻辑几乎一致,只是控制流结构略有不同。
阶段三:数据加密(额外安全措施)
教程中未提及的步骤:
教程中的linker直接使用解析后的数据,但libil2cpp增加了额外的安全层。
我们的实际代码(sub_42F5FCC → sub_42F57AC):
// 在sub_42F5FCC中设置回调
*off_4306FB0 = (int*)sub_42F57AC;
// sub_42F57AC对数据进行XOR加密
__int64 sub_42F57AC(__int64 a1, unsigned int a2, unsigned int a3,
__int64 a4, unsigned int a5, int a6)
{
// 第一阶段:内存区域加密
if (a5 != 0) {
uint32_t key = 0x56312342;
for (offset = 0; offset < a5; offset += 4) {
*(uint32_t*)(offset + a4) ^= key;
key += offset;
}
}
// 第二阶段:结构体数组加密
if (a2 < a3) {
uint32_t key1 = a2 + 337;
uint32_t key2 = a2 + 347;
for (i = a2; i < a3; i++) {
element = a1 + i * 24;
*(uint64_t*)(element + 8) ^= key1;
*(uint64_t*)(element + 16) ^= key2;
}
}
return 1;
}
这是额外的反调试措施! 教程中的标准linker没有这一步。
📊 完整流程对照图
教程流程 实际代码流程
======== ============
[启动] [sub_42F1564 启动]
↓ ↓
┌─────────────────┐ ┌─────────────────────────┐
│ 1️⃣ 寻找Dynamic段 │ │ 11次完整性校验 │
│ find_dyn_seg() │ │ (sub_42FC0C4/0A4/0B4) │
│ - 遍历PH表 │ └─────────────────────────┘
│ - 查找p_type=2 │ ↓
└─────────────────┘ ┌─────────────────────────┐
↓ │ 初始化系统 │
┌─────────────────┐ │ sub_42F24F8() │
│ 2️⃣ Prelink操作 │ └─────────────────────────┘
│ - 解析Dynamic │ ↓
│ - switch-case │ ┌─────────────────────────┐
│ 处理各种tag │ │ 分配416字节内存 │
│ - DT_HASH │ │ func_table[0xC8/8](0x1A0)│
│ - DT_STRTAB │ └─────────────────────────┘
│ - DT_SYMTAB │ ↓
│ - DT_INIT_ARRAY │ ┌─────────────────────────┐
│ - ... │ │ 1️⃣ 寻找Dynamic段 │
└─────────────────┘ │ sub_42F4754() │
↓ │ - 初始化栈缓冲区 │
[完成] │ - 解析ELF头 │
│ - 查找PT_DYNAMIC(2) │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ 填充数据结构 │
│ - offset 0x168 ← 0 │
│ - offset 0x8 ← val1 │
│ - offset 0x10 ← val2 │
│ - ... │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ 2️⃣ Prelink操作 │
│ sub_42F630C → sub_42F5844│
│ - 遍历Dynamic段tags │
│ - switch-case处理 │
│ · Tag 4 → DT_HASH │
│ · Tag 5 → DT_STRTAB │
│ · Tag 6 → DT_SYMTAB │
│ · Tag 25→DT_INIT_ARRAY│
│ · ... │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ 3️⃣ 安全验证与加密 │
│ sub_42F5FCC │
│ - 完整性校验(offset 0x38C)│
│ - 设置回调sub_42F57AC │
│ - 调用sub_42FBAB8 │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ XOR数据加密 │
│ sub_42F57AC │
│ - 内存区域加密(key=0x56312342)│
│ - 结构体数组加密 │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ 释放资源 │
│ - 释放416字节内存 │
│ - 清理栈缓冲区 │
│ - 最后一次完整性校验 │
└─────────────────────────┘
↓
[完成并返回悬空指针]
🎯 关键对应点总结
1. Dynamic段结构完全一致
教程中的Dynamic段示例(十六进制):
19 00 00 00 00 00 00 00 ← DT_INIT_ARRAY (25)
50 6A DF 07 00 00 00 00 ← 地址 0x7df6a50
1B 00 00 00 00 00 00 00 ← DT_INIT_ARRAYSZ (27)
20 00 00 00 00 00 00 00 ← 大小 32字节
05 00 00 00 00 00 00 00 ← DT_STRTAB (5)
38 3C E2 07 00 00 00 00 ← 地址 0x7e23c38
我们代码中的处理:
case 25: // DT_INIT_ARRAY
*(void**)((char*)a1 + 192) = (char*)v5[1] + v7;
case 27: // DT_INIT_ARRAYSZ
*(void**)((char*)a1 + 200) = (void*)((unsigned long long)*(unsigned int*)((char*)v5 + 8) >> 3);
case 5: // DT_STRTAB
*(void**)((char*)a1 + 48) = (char*)v5[1] + v7;
2. 解析逻辑完全吻合
| 操作 | 教程 | 我们的代码 |
|---|---|---|
| 读取tag | v12 = *(v11 - 1) |
v8 = (long long)*v5 |
| 读取value | *v11 |
v5[1] |
| 地址重定位 | value + v7 (v7是基址) |
(char*)v5[1] + v7 |
| 移动指针 | v11 += 2 |
v5 += 2 |
3. 存储结构对应关系
| Dynamic类型 | 教程偏移 | 我们的偏移 | 用途 |
|---|---|---|---|
| DT_SYMTAB(6) | +32 | +32 | 符号表地址 |
| DT_STRTAB(5) | +48 | +48 | 字符串表地址 |
| DT_STRSZ(10) | +56 | +56 | 字符串表大小 |
| DT_INIT_ARRAY(25) | +192 | +192 | 初始化函数数组 |
| DT_FINI_ARRAY(26) | +208 | +208 | 清理函数数组 |
| DT_HASH(4) | +248/+256 | +264/+272 | 哈希表信息 |
⚠️ 关键差异点
差异1:额外的安全检查层
- 教程:直接解析并使用
- 我们:每个关键步骤都有完整性校验(11次)
差异2:数据加密
- 教程:解析后的数据直接可用
- 我们:通过XOR加密保护敏感数据
差异3:信号处理
- 教程:没有提及
- 我们:sub_42FBAB8使用SIGINT信号处理器(可能用于反调试)
差异4:内存管理
- 教程:静态分配或简单malloc
- 我们:通过函数指针表间接调用(增加逆向难度)
💡 最终结论
sub_42F1564 = 标准Linker Prelink操作 + 军事级安全加固
| 组成部分 | 占比 | 功能 |
|---|---|---|
| 标准ELF解析 | 30% | sub_42F4754, sub_42F4658, sub_42F459C |
| 标准Prelink | 40% | sub_42F5844 (与教程完全一致) |
| 安全加固 | 30% | 完整性校验、XOR加密、信号处理 |
这是一个披着反调试外壳的标准动态链接器,核心逻辑与教程中的linker一致,但外围包裹了多层安全防护!
解密字符串表和符号表的函数定位
🎯 答案:sub_42F57AC
这就是我们之前详细分析过的 XOR 数据加密/解密函数!
📋 教程代码与实际代码对照
教程中的解密函数(sub_7D16188)
signed __int64 __fastcall sub_7D16188(
__int64 a1, // 符号表基址
unsigned int a2, // 解密起始索引
unsigned int a3, // 解密结束索引
__int64 a4, // 字符串表基址
unsigned int a5, // 字符串表解密大小
int a6) // 控制标志(0=解密)
{
// ========== 第一部分:解密字符串表 ==========
if (a5 && !a6)
{
v6 = 0LL;
do
{
*(_DWORD *)(a4 + v6) ^= 0x56312342u; // XOR解密
v6 += 4LL;
}
while ((unsigned int)v6 < a5);
}
// ========== 第二部分:解密符号表条目 ==========
if (a2 < a3)
{
v7 = a3 - a2;
v8 = (__int64 *)(a1 + 24LL * a2 + 16);
do
{
if (!a6)
{
v9 = *(v8 - 1);
if (v9)
{
v10 = *v8;
*(v8 - 1) = v9 ^ (a2 + 337); // XOR解密
*v8 = v10 ^ (a2 + 347); // XOR解密
}
}
--v7;
v8 += 3;
}
while (v7);
}
return 1LL;
}
我们的实际解密函数(sub_42F57AC)
__int64 __fastcall sub_42F57AC(
__int64 a1, // 符号表基址(结构体数组)
unsigned int a2, // 解密起始索引
unsigned int a3, // 解密结束索引
__int64 a4, // 字符串表基址(内存区域)
unsigned int a5, // 字符串表解密大小
int a6) // 控制标志(0=解密,非0=跳过)
{
// ========== 检查控制标志 ==========
if (a6 != 0) {
return 1LL; // 跳过解密
}
// ========== 第一阶段:解密字符串表内存区域 ==========
if (a5 != 0) {
uint64_t offset = 0;
uint32_t key = 0x56312342; // ✅ 相同的密钥!
do {
// 读取4字节数据
uint32_t data = *(uint32_t*)(offset + a4);
// XOR解密
uint32_t decrypted_data = data ^ key;
// 写回解密后的数据
*(uint32_t*)(offset + a4) = decrypted_data;
// 更新密钥(教程中密钥固定,我们的动态变化)
key += (uint32_t)offset;
// 移动到下一个4字节
offset += 4;
} while (a5 > offset);
}
// ========== 第二阶段:解密符号表结构体数组 ==========
if (a2 < a3) {
// 计算动态密钥
uint32_t dynamic_key1 = a2 + 337; // ✅ 相同的密钥计算!
uint32_t dynamic_key2 = a2 + 347; // ✅ 相同的密钥计算!
// 计算起始元素地址
uint64_t current_element = a1 + 24LL * a2;
// 计算结束元素地址
uint64_t end_element = current_element + 24 * (a3 - a2);
// 遍历符号表条目
do {
// 读取偏移+8处的字段
uint64_t field1 = *(uint64_t*)(current_element + 8);
// 只解密非零值
if (field1 != 0) {
// 解密偏移+8的字段
*(uint64_t*)(current_element + 8) = field1 ^ dynamic_key1;
// 解密偏移+16的字段
uint64_t field2 = *(uint64_t*)(current_element + 16);
*(uint64_t*)(current_element + 16) = field2 ^ dynamic_key2;
}
// 移动到下一个条目(每个24字节)
current_element += 24LL;
} while (current_element != end_element);
}
return 1LL;
}
✅ 完全匹配的证据
1. 密钥完全一致
| 密钥类型 | 教程值 | 我们的值 | 用途 |
|---|---|---|---|
| 字符串表密钥 | 0x56312342 |
0x56312342 ✅ |
解密字符串表 |
| 符号表密钥1 | a2 + 337 |
a2 + 337 ✅ |
解密符号名偏移 |
| 符号表密钥2 | a2 + 347 |
a2 + 347 ✅ |
解密符号值 |
2. 数据结构完全一致
符号表条目结构(24字节):
struct SymbolTableEntry {
uint64_t field_0; // +0 (未使用)
uint64_t name_off; // +8 (加密字段1,XOR with a2+337)
uint64_t value; // +16 (加密字段2,XOR with a2+347)
}; // 总大小 24 字节
教程中的访问方式:
v8 = (__int64 *)(a1 + 24LL * a2 + 16); // 指向偏移+16
*(v8 - 1) = ... // 访问偏移+8
*v8 = ... // 访问偏移+16
我们的访问方式:
current_element = a1 + 24LL * a2;
*(uint64_t*)(current_element + 8) // 偏移+8
*(uint64_t*)(current_element + 16) // 偏移+16
3. 控制流完全一致
| 步骤 | 教程 | 我们 |
|---|---|---|
| 1. 检查控制标志 | if (!a6) |
if (a6 != 0) return |
| 2. 解密字符串表 | 4字节块XOR | 4字节块XOR |
| 3. 解密符号表 | 遍历24字节条目 | 遍历24字节条目 |
| 4. 跳过零值 | if (v9) |
if (field1 != 0) |
🔗 完整调用链
sub_42F1564 (主函数)
↓
[填充数据结构]
├─ offset 0x20 ← 符号表相关数据
└─ offset 0x30 ← 字符串表相关数据
↓
sub_42F5FCC (安全验证与初始化)
↓
├─ 完整性校验(offset +0x38C)
├─ 设置回调函数: *off_4306FB0 = sub_42F57AC ⭐
└─ 调用主处理器: sub_42FBAB8(...)
↓
└─ off_4308B50(...) // 实际指向 sub_42F57AC
↓
sub_42F57AC( ⭐ 解密函数!
*(data_struct + 0x20), // 符号表基址
*(temp_base + 0x94), // 起始索引
*(temp_base + 0x98), // 结束索引
*(data_struct + 0x30), // 字符串表基址
*(temp_base + 0x9C), // 字符串表大小
*(temp_base + 0x90) // 控制标志
)
📊 参数来源分析
在 sub_42F1564 中传递的参数:
// 在 sub_42F1564 的最后阶段
temp_base = (void *)(*off_4306E30);
sub_42F5FCC(
*(uint64_t*)((char*)data_struct + 0x20), // 符号表地址
*(uint32_t*)((char*)temp_base + 0x94), // 解密起始位置
*(uint32_t*)((char*)temp_base + 0x98), // 解密结束位置
*(uint64_t*)((char*)data_struct + 0x30), // 字符串表地址
*(uint32_t*)((char*)temp_base + 0x9C), // 解密大小
*(uint32_t*)((char*)temp_base + 0x90) // 控制标志
);
这些数据是在哪里填充的?
在 sub_42F5844 (Prelink操作) 中:
// 填充符号表地址(DT_SYMTAB, tag=6)
case 6:
*(void**)((char*)a1 + 32) = (char*)v5[1] + v7;
// 32 是符号表地址,但这里用的是 offset 0x20?
// 实际上 offset 0x20 可能是从 sub_42F630C 中设置的
在 sub_42F630C 中:
__int64 __fastcall sub_42F630C(__int64 a1)
{
if (*(_BYTE *)(a1 + 395))
return *(unsigned __int8 *)(a1 + 395);
else
return sub_42F5844(a1); // 解析并填充数据
}
🎯 关键发现:部分加密机制
教程中提到:
"这个加固并没有将全部的字符串和符号都加密,而是进行了部分加密和部分解密。"
证据1:起始索引和结束索引
if (a2 < a3) // 只解密 a2 到 a3 范围内的符号
这意味着符号表中只有部分条目被加密,而不是全部。
证据2:零值跳过机制
if (field1 != 0) {
// 只解密非零值
}
这进一步减少了需要解密的数据量。
证据3:可配置的解密范围
// 这些参数来自配置数据(temp_base + 偏移)
*(uint32_t*)((char*)temp_base + 0x94) // 起始位置(可配置)
*(uint32_t*)((char*)temp_base + 0x98) // 结束位置(可配置)
*(uint32_t*)((char*)temp_base + 0x9C) // 字符串表大小(可配置)
*(uint32_t*)((char*)temp_base + 0x90) // 控制标志(可配置)
💡 总结
| 项目 | 内容 |
|---|---|
| 解密函数 | sub_42F57AC |
| 调用位置 | sub_42F1564 → sub_42F5FCC → sub_42FBAB8 → sub_42F57AC |
| 解密对象 | 1. 字符串表(DT_STRTAB)<br>2. 符号表(DT_SYMTAB)的部分条目 |
| 加密算法 | XOR加密 |
| 密钥 | 字符串表:0x56312342(动态变化)<br>符号表:索引+337/347 |
| 特点 | 部分加密,可配置解密范围 |
这个函数在我们之前的分析中已经完整解析过了,当时我们将其命名为"数据加密处理函数",现在确认其真实用途是"字符串表和符号表解密函数"!
sub_42F1564 完整流程重新分析
🎯 真实作用
sub_42F1564 是一个 加密保护的自定义动态链接器初始化函数
它的核心目的是:
- 解析加密的 ELF 文件结构
- 定位并解析 Dynamic 段(包含所有动态链接信息)
- 解密被加密的符号表和字符串表
- 为后续的符号查找和动态链接做准备
这就是为什么某些 Unity il2cpp 游戏在 IDA 中看不到函数名的原因!符号表和字符串表都被加密了!
📋 完整处理流程(7个阶段)
╔═══════════════════════════════════════════════════════════╗
║ sub_42F1564 - 加密保护的动态链接器初始化器 ║
╚═══════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────┐
│ 阶段0:多重安全检查(反调试层) │
├─────────────────────────────────────────────────────────┤
│ ✓ 11次完整性校验 (sub_42FC0C4/0A4/0B4) │
│ - 校验代码段未被修改 │
│ - 校验数据段未被篡改 │
│ - 任何失败都触发 sub_42F3270 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段1:系统初始化 │
├─────────────────────────────────────────────────────────┤
│ sub_42F24F8() │
│ ✓ 设置全局状态标志 │
│ ✓ 初始化函数指针表 │
│ ✓ 检查运行环境 │
│ ✓ 可能解密部分关键代码 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段2:分配临时工作区 │
├─────────────────────────────────────────────────────────┤
│ ✓ 分配 416 字节内存(ELF解析器结构体) │
│ ✓ 清零内存 │
│ │
│ struct elf_linker_context { │
│ void* base; // +0x00 │
│ void* param; // +0x08 │
│ void* ehdr; // +0x10 ELF头 │
│ uint64_t phnum; // +0x18 程序头数量 │
│ uint64_t size; // +0x20 映射大小 │
│ void* phdrs; // +0x28 程序头表 │
│ void* dynamic; // +0x30 Dynamic段 │
│ void* strtab; // +0x30 字符串表 ⭐ │
│ void* symtab; // +0x20 符号表 ⭐ │
│ // ... 更多字段 ... │
│ }; │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段3:ELF文件解析 - 寻找Dynamic段 │
│ 【对应教程第1步:遍历elf文件,寻找dynamic segment】 │
├─────────────────────────────────────────────────────────┤
│ sub_42F43FC(stack_buffer) │
│ └─ 初始化栈缓冲区(用于存储临时数据) │
│ │
│ sub_42F4754(stack_buffer, *off_4306FD0) │
│ ├─ sub_42F459C() - 容错式ELF魔数搜索 │
│ │ └─ 在64字节范围内查找 0x7F 'E' 'L' 'F' │
│ │ │
│ ├─ sub_42F4658() - 解析PT_LOAD段并计算基址 │
│ │ ├─ 遍历 Program Header Table │
│ │ ├─ 查找所有 p_type = PT_LOAD (1) 的段 │
│ │ ├─ 计算最小和最大地址 │
│ │ └─ 计算实际加载基址(处理ASLR) │
│ │ │
│ └─ sub_42F4754() - 定位PT_DYNAMIC段 ⭐ │
│ ├─ 遍历 Program Header Table │
│ ├─ 查找 p_type = PT_DYNAMIC (2) 的段 │
│ └─ 保存 Dynamic 段地址到 parser->dynamic │
│ │
│ 如果未找到Dynamic段 → 释放内存并退出 ❌ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段4:填充配置数据 │
├─────────────────────────────────────────────────────────┤
│ 从栈变量填充到 data_struct: │
│ ✓ field_0x168 ← 0 │
│ ✓ field_0x8 ← stack_val1 │
│ ✓ field_0x10 ← stack_val2 │
│ ✓ field_0x18 ← stack_val3 │
│ ✓ field_0x158 ← stack_val4 │
│ ✓ field_0x28 ← stack_val5 │
│ │
│ 每次填充前都执行完整性校验 🔒 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段5:Prelink操作 - 解析Dynamic段 │
│ 【对应教程第2步:执行prelink操作】 │
├─────────────────────────────────────────────────────────┤
│ sub_42F630C(data_struct) │
│ ↓ │
│ sub_42F5844(data_struct) ⭐ 核心解析函数 │
│ │
│ 工作原理:遍历Dynamic段的所有Tag-Value对 │
│ │
│ Dynamic段结构(每个条目16字节): │
│ ┌──────────────┬──────────────┐ │
│ │ d_tag (8字节)│ d_val (8字节) │ │
│ └──────────────┴──────────────┘ │
│ │
│ switch (tag) { │
│ case 4: // DT_HASH - 符号哈希表 ⭐ │
│ offset_0x100 ← value + base │
│ offset_0x108 ← nbucket │
│ offset_0x110 ← nchain │
│ break; │
│ │
│ case 5: // DT_STRTAB - 字符串表地址 ⭐ │
│ offset_0x30 ← value + base │
│ // 这个地址后面会用于解密! │
│ break; │
│ │
│ case 6: // DT_SYMTAB - 符号表地址 ⭐ │
│ offset_0x20 ← value + base │
│ // 这个地址后面会用于解密! │
│ break; │
│ │
│ case 10: // DT_STRSZ - 字符串表大小 │
│ offset_0x38 ← value │
│ break; │
│ │
│ case 12: // DT_INIT - 初始化函数 │
│ offset_0xF8 ← value + base │
│ break; │
│ │
│ case 13: // DT_FINI - 清理函数 │
│ offset_0xF0 ← value + base │
│ break; │
│ │
│ case 25: // DT_INIT_ARRAY - 初始化函数数组 ⭐ │
│ offset_0xC0 ← value + base │
│ // 这就是 readelf -d 显示的 INIT_ARRAY! │
│ break; │
│ │
│ case 26: // DT_FINI_ARRAY - 清理函数数组 │
│ offset_0xD0 ← value + base │
│ break; │
│ │
│ case 27: // DT_INIT_ARRAYSZ - 数组大小 │
│ offset_0xC8 ← value >> 3 │
│ break; │
│ │
│ case 1879047925: // 自定义Tag - 特殊哈希表 ⭐ │
│ 解析自定义哈希表结构 │
│ break; │
│ │
│ // ... 更多case ... │
│ } │
│ │
│ 结果:所有动态链接需要的信息都被提取出来了! │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段6:安全验证与解密准备 │
├─────────────────────────────────────────────────────────┤
│ sub_42F5FCC( │
│ *(data_struct + 0x20), // 符号表地址 ⬅ DT_SYMTAB │
│ *(temp_base + 0x94), // 解密起始索引 (配置) │
│ *(temp_base + 0x98), // 解密结束索引 (配置) │
│ *(data_struct + 0x30), // 字符串表地址 ⬅ DT_STRTAB │
│ *(temp_base + 0x9C), // 字符串表大小 (配置) │
│ *(temp_base + 0x90) // 控制标志 (配置) │
│ ) │
│ │
│ 内部流程: │
│ ├─ 完整性校验(offset +0x38C) │
│ ├─ 检查配置数据有效性 │
│ ├─ 设置回调函数:*off_4306FB0 = sub_42F57AC ⭐ │
│ └─ 调用主处理器:sub_42FBAB8(...) │
│ └─ 最终调用 off_4308B50 (指向 sub_42F57AC) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段7:解密字符串表和符号表 ⭐⭐⭐ │
│ 【对应教程第3步:解密字符串表和符号表】 │
├─────────────────────────────────────────────────────────┤
│ sub_42F57AC( │
│ a1 = 符号表基址, // 从 DT_SYMTAB 获取 │
│ a2 = 起始索引, // 配置:从哪个符号开始解密 │
│ a3 = 结束索引, // 配置:到哪个符号结束 │
│ a4 = 字符串表基址, // 从 DT_STRTAB 获取 │
│ a5 = 字符串表大小, // 配置:解密多少字节 │
│ a6 = 控制标志 // 0=解密,非0=跳过 │
│ ) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 第一部分:解密字符串表(DT_STRTAB) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ if (a5 != 0 && a6 == 0) { │
│ offset = 0; │
│ key = 0x56312342; // 初始密钥 │
│ │
│ while (offset < a5) { │
│ // 读取加密的4字节数据 │
│ encrypted = *(uint32_t*)(a4 + offset); │
│ │
│ // XOR解密 │
│ decrypted = encrypted ^ key; │
│ │
│ // 写回解密后的数据 │
│ *(uint32_t*)(a4 + offset) = decrypted; │
│ │
│ // 动态更新密钥(增加破解难度) │
│ key += offset; │
│ │
│ offset += 4; │
│ } │
│ } │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 第二部分:解密符号表条目(DT_SYMTAB) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 符号表条目结构(24字节): │
│ struct Elf64_Sym { │
│ uint32_t st_name; // +0 符号名在字符串表的偏移 │
│ uint8_t st_info; // +4 符号类型和绑定 │
│ uint8_t st_other; // +5 符号可见性 │
│ uint16_t st_shndx; // +6 节索引 │
│ uint64_t st_value; // +8 符号值/地址 ⭐ 加密字段1 │
│ uint64_t st_size; // +16 符号大小 ⭐ 加密字段2 │
│ }; │
│ │
│ if (a2 < a3 && a6 == 0) { │
│ // 只解密部分符号(部分加密机制) │
│ current = a1 + 24 * a2; // 定位起始符号 │
│ end = current + 24 * (a3 - a2); │
│ │
│ while (current < end) { │
│ st_value = *(uint64_t*)(current + 8); │
│ │
│ // 只解密非零值(进一步减少解密范围) │
│ if (st_value != 0) { │
│ // 解密符号值 │
│ key1 = a2 + 337; │
│ *(uint64_t*)(current + 8) = st_value ^ key1;│
│ │
│ // 解密符号大小 │
│ st_size = *(uint64_t*)(current + 16); │
│ key2 = a2 + 347; │
│ *(uint64_t*)(current + 16) = st_size ^ key2;│
│ } │
│ │
│ current += 24; // 下一个符号条目 │
│ a2++; // 更新密钥 │
│ } │
│ } │
│ │
│ 解密完成!现在符号表和字符串表可以正常使用了! │
