GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 网顿加固 之 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函数完成初始化代码)完成真正的加载。

某顿加固的精妙之处:

  1. 分离存储:program header、dynamic段与代码/数据分离 包含知识点ELF文件格式(program header、dynamic segment), ARM64汇编和花指令识别
  2. 自实现linker:完整重现Android链接流程 , Android linker机制(prelink、relocate、load_segment) 重定位原理(RELA表处理
  3. 空间预留:壳so预留3倍文件大小的内存。 子so加密算法(魔改RC4)
  4. 差分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:移植代码和数据段

text
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+addend
  • 0x401/0x402 (JUMP_SLOT/GLOB_DAT):通过符号索引查找真实地址

步骤3:修复init_array和section header

1. 替换为子so的init_array
2. 保留壳so第一个init函数(运行时依赖)
3. 添加必要的section header(shstr、dynamic、dynstr)

四、最终成果

  1. so修复成功:替换后游戏正常运行
  2. dump metadata:在sub_bc9564下断点获取解密的global-metadata.dat
  3. 恢复脚本:手动修复magic/version后,Il2CppDumper成功导出CS脚本

 

详细过程:

 

一个unity游戏使用了网顿加固,他会将

 

 global-metadata.dat

 

libil2cpp.so

 

两个文件加壳子。

 

 

 libil2cpp.so分析过程:

 

不同版本的加固特征不同但是大同小异

首次ida静态分析报错,

SHT加密

image

压缩so

image

 导入和导出表加密

image

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

image

 

花指令干扰

分段so存储

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

image

有的版本加密,但是我的没有出现的加密技术:

  • 发现global-metadata.dat被加密(Magic被改为"HTPX")

  • image

    发现lib目录下有libN4tHTProtect.so,确认使用了某顿加固

 

 

手动脱修

找入口。

 因为SHT加密导致ida分析节区失败,我们可以通过动态节区分析节区有哪些。下面就是动态节区解析后的值

image

image

image

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

 image

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

image

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

image

 可以正常解析了

image

 

 去除花指令

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

 

 

image

 

 

第一个初始化函数分析,创建一个内存陷阱检查是不是在被反调试

 

第二个初始化函数分析,这个函数的作用就是解密字符串和符号表

函数控制流是,加载动态段,解析动态段,解密字符串和符号表

 

image

 

 

 

 

对照分析:主函数 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段

教程代码片段:

C
if ( find_dyn_seg((unsigned __int64 *)&v123, (signed __int64)*off_7DFCCC8) )
{
  // 找到p_type为2的段(PT_DYNAMIC)
}

我们的实际代码:

C
// sub_42F1564 中调用
if (sub_42F4754(stack_buffer, *off_4306FD0) == 0) {
    // 未找到Dynamic段,释放资源
    ((void (*)(void*))func_table[0xD8/8])(data_struct);
    return data_struct;
}

sub_42F4754 内部逻辑:

C
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)

教程代码示例:

C
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):

C
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):

C
// 在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没有这一步。


📊 完整流程对照图

text
教程流程                           实际代码流程
========                           ============

[启动]                             [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段示例(十六进制):

text
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

我们代码中的处理:

C
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)

C
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)

C
__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字节):

C
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 字节

教程中的访问方式:

C
v8 = (__int64 *)(a1 + 24LL * a2 + 16);  // 指向偏移+16
*(v8 - 1) = ...  // 访问偏移+8
*v8 = ...        // 访问偏移+16

我们的访问方式:

C
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)

🔗 完整调用链

text
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 中传递的参数:

C
// 在 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操作) 中:

C
// 填充符号表地址(DT_SYMTAB, tag=6)
case 6:
    *(void**)((char*)a1 + 32) = (char*)v5[1] + v7;
    // 32 是符号表地址,但这里用的是 offset 0x20?

// 实际上 offset 0x20 可能是从 sub_42F630C 中设置的

在 sub_42F630C 中:

C
__int64 __fastcall sub_42F630C(__int64 a1)
{
    if (*(_BYTE *)(a1 + 395))
        return *(unsigned __int8 *)(a1 + 395);
    else
        return sub_42F5844(a1);  // 解析并填充数据
}

🎯 关键发现:部分加密机制

教程中提到:

"这个加固并没有将全部的字符串和符号都加密,而是进行了部分加密和部分解密。"

证据1:起始索引和结束索引

C
if (a2 < a3)  // 只解密 a2 到 a3 范围内的符号

这意味着符号表中只有部分条目被加密,而不是全部。

证据2:零值跳过机制

C
if (field1 != 0) {
    // 只解密非零值
}

这进一步减少了需要解密的数据量。

证据3:可配置的解密范围

C
// 这些参数来自配置数据(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 是一个 加密保护的自定义动态链接器初始化函数

它的核心目的是:

  1. 解析加密的 ELF 文件结构
  2. 定位并解析 Dynamic 段(包含所有动态链接信息)
  3. 解密被加密的符号表和字符串表
  4. 为后续的符号查找和动态链接做准备

这就是为什么某些 Unity il2cpp 游戏在 IDA 中看不到函数名的原因!符号表和字符串表都被加密了!


📋 完整处理流程(7个阶段)

text
╔═══════════════════════════════════════════════════════════╗
║  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++;           // 更新密钥                     │
│     }                                                   │
│ }                                                       │
│                                                         │
│ 解密完成!现在符号表和字符串表可以正常使用了!           │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 阶段8:清理临时数据并返回                                │
├─────────────────────────────────────────────────────────┤
│ ✓ 释放 416 字节临时内存                                  │
│ ✓ 清理栈缓冲区(sub_42F44F0 → sub_42F4478)             │
│ ✓ 最后一次完整性校验                                     │
│ ✓ 返回悬空指针(仅作为成功标识)                         │
└─────────────────────────────────────────────────────────┘
     [完成]

🎯 真实返回数据

虽然返回的是悬空指针,但真正的数据都在全局变量中:

类型位置内容
Dynamic段信息 off_4306E30 所有动态链接需要的地址和大小
符号表(已解密) *(off_4306E30 + 0x20) 部分符号的 st_value 和 st_size 已解密
字符串表(已解密) *(off_4306E30 + 0x30) 部分字符串已解密
哈希表 *(off_4306E30 + 0x100) 用于快速符号查找
INIT_ARRAY *(off_4306E30 + 0xC0) 初始化函数数组地址
回调函数 off_4306FB0 指向 sub_42F57AC(可重复加密/解密)
状态标志 byte_43085D0 初始化完成标志

💡 为什么需要这个函数?

问题:IDA 打开某些 Unity 游戏看不到函数名

原因:符号表和字符串表在 ELF 文件中是加密存储的!

text
正常的 libil2cpp.so:
┌──────────────┐
│ ELF Header   │
├──────────────┤
│ .text (代码) │
├──────────────┤
│ .symtab      │ ← 明文符号表
│  func1       │   IDA可以直接读取
│  func2       │
├──────────────┤
│ .strtab      │ ← 明文字符串表
│  "func1"     │   IDA可以直接读取
│  "func2"     │
└──────────────┘

加密的 libil2cpp.so:
┌──────────────┐
│ ELF Header   │
├──────────────┤
│ .text (代码) │
├──────────────┤
│ .symtab      │ ← 加密的符号表!
│  0xDEADBEEF  │   IDA无法识别
│  0xCAFEBABE  │
├──────────────┤
│ .strtab      │ ← 加密的字符串表!
│  "@#$%^&*"   │   IDA无法识别
│  "!@#$%^"    │
└──────────────┘
    ↓ 运行时
sub_42F1564() 解密
┌──────────────┐
│ .symtab      │ ← 已解密!
│  func1       │   程序可以正常使用
│  func2       │
├──────────────┤
│ .strtab      │ ← 已解密!
│  "func1"     │   程序可以正常使用
│  "func2"     │
└──────────────┘

部分加密机制

为什么不全部加密?

  1. 性能考虑:解密全部符号会很慢
  2. 兼容性:某些系统符号不能加密
  3. 隐蔽性:部分加密更不容易被发现

加密范围由配置决定:

C
// 这些参数决定了哪些符号被加密
*(temp_base + 0x94)  // 起始索引:从第几个符号开始加密
*(temp_base + 0x98)  // 结束索引:到第几个符号结束
*(temp_base + 0x9C)  // 字符串表:加密多少字节

📊 与教程的完全对应

教程步骤对应函数功能
1. 寻找Dynamic段 sub_42F4754 遍历Program Header,查找PT_DYNAMIC
2. Prelink操作 sub_42F5844 解析Dynamic段,提取所有动态链接信息
3. 解密字符串表和符号表 sub_42F57AC XOR解密符号表和字符串表

🔑 关键发现

  1. 这是一个自定义的动态链接器初始化器,不依赖系统 linker
  2. 符号表和字符串表在文件中是加密的,运行时动态解密
  3. 采用部分加密机制,只加密关键符号,提高效率
  4. 包含11次完整性校验,任何篡改都会被检测
  5. 使用悬空指针作为返回值,真正的数据在全局变量中
  6. 解密后的数据可以用于后续的符号查找和动态链接

这就是 Unity il2cpp 反逆向保护的核心机制之一!

 

 

sub_42F1564 函数控制流中每个函数的通俗解释

🎭 整体比喻

把整个流程想象成"一个绝密文件的安全解封过程"


📚 函数详解(按调用顺序)

🏁 主函数:sub_42F1564

text
角色:总指挥官
比喻:负责整个绝密文件解封流程的总负责人

工作内容:

  1. 派出11组安检队伍检查现场
  2. 协调各部门按顺序工作
  3. 确保每个步骤都安全完成
  4. 最后验收并撤离现场

🔐 第一组:安全检查团队

1. sub_42FC0C4(模乘运算)

text
角色:数学密码专家
比喻:用特殊算法生成"验证指纹"的第一步

通俗解释:

  • 接收两个数字,进行复杂的乘法运算
  • 就像用两把钥匙的齿纹相乘,生成一个新的图案
  • 这个图案用来验证"钥匙有没有被人偷换过"

代码逻辑:

C
result = (a % 4096) * (b % 4096)

2. sub_42FC0A4(加法运算)

text
角色:数学密码专家的助手
比喻:用加法混淆验证指纹

通俗解释:

  • 把两个数字用特殊方法加在一起
  • 就像把两个拼图碎片组合成新图案
  • 让别人无法从结果倒推原始数据

代码逻辑:

C
result = ~(~a - b)  // 等价于 a + b,但更隐蔽

3. sub_42FC0B4(减法运算)

text
角色:数学密码专家的另一个助手
比喻:用减法再次混淆验证指纹

通俗解释:

  • 把两个数字用特殊方法相减
  • 和加法配合使用,生成复杂的验证码
  • 增加破解难度

代码逻辑:

C
result = ~(b + ~a)  // 等价于 a - b,但更隐蔽

4. sub_42F3270(错误处理)

text
角色:警报系统
比喻:发现异常时拉响警报的红色按钮

通俗解释:

  • 这是一个"空函数"(什么都不做)
  • 就像一个假的红色按钮,按了也没反应
  • 但它的存在本身就是一种混淆手段
  • 真正的错误处理可能在别处,或者直接让程序崩溃

为什么是空函数?

  • 反调试技巧:让逆向工程师以为这里会处理错误
  • 实际上:可能通过其他方式终止程序(如异常、信号等)

🏗️ 第二组:系统初始化团队

5. sub_42F24F8(系统初始化)

text
角色:环境准备员
比喻:打开保险柜前先检查房间,确保没人偷看

通俗解释:

  • 检查全局状态标志 byte_43085D0
  • 如果是第一次运行,做完整初始化
  • 如果已经初始化过,走快速通道
  • 设置函数指针表,准备工具箱

实际工作:

  1. 循环安检:反复检查内存中的关键数据
  2. 等待信号:等待某个全局变量变成特定值
  3. 触发解密:满足条件后调用 sub_42F1F78 解密代码段

比喻故事:

text
就像开保险柜前,保安要:
1. 先在外围巡逻11圈(完整性检查)
2. 通过对讲机确认没有可疑人员(检查全局标志)
3. 等待总部发来开锁密码(等待全局变量)
4. 收到密码后才能开始解锁(调用解密函数)

6. sub_42F1F78(代码段解密)

text
角色:密码破译员
比喻:用密钥解开一段加密的文字

通俗解释:

  • 接收一段加密的内存区域
  • 用 XOR 算法进行解密
  • 解密后的内存可能包含可执行代码
  • 使用 NEON SIMD 指令加速(批量处理16字节)

工作流程:

  1. 修改内存权限:把内存改成"可读写"(用 mprotect)
  2. 执行解密:用密钥 XOR 每个字节
  3. 刷新缓存:清除CPU缓存,让新代码生效(DC CVAU, IC IVAU)
  4. 恢复权限:把内存改回"可读可执行"

比喻故事:

text
就像解密一份绝密文件:
1. 先把文件从保险柜拿出来(设置内存可写)
2. 用解密器逐字解密(XOR运算)
3. 用风扇吹干墨水(刷新CPU缓存)
4. 把文件放回防护罩里(设置内存只读可执行)

🧰 第三组:内存管理团队

7. 函数表中的内存管理函数

text
角色:仓库管理员
比喻:负责申请场地、清理场地、归还场地

7a. func_table[0xC8/8](内存分配)

text
真实函数:malloc
通俗解释:租一个416平方米的临时仓库

7b. func_table[0xA8/8](内存清零)

text
真实函数:memset
通俗解释:把仓库打扫得一尘不染

7c. func_table[0xD8/8](内存释放)

text
真实函数:free
通俗解释:用完后把仓库退租

为什么要通过函数表间接调用?

  • 混淆技术:让逆向工程师难以直接看出是 malloc/free
  • 灵活性:可以在运行时替换这些函数
  • 反调试:增加断点调试的难度

📦 第四组:栈缓冲区管理团队

8. sub_42F43FC(栈缓冲区初始化)

text
角色:临时记录本初始化员
比喻:准备一个干净的笔记本记录临时数据

通俗解释:

  • 在栈上分配一块内存(大小约 0xF4 = 244 字节)
  • 把这块内存全部清零
  • 用于临时存储 ELF 解析过程中的数据

代码逻辑:

C
memset(buffer, 0, 244);  // 清零所有字段

比喻:

text
就像准备一个新笔记本:
- 打开到空白页
- 用橡皮擦把所有痕迹擦干净
- 准备记录新的信息

9. sub_42F44F0(栈缓冲区清理)

text
角色:临时记录本销毁员
比喻:用完笔记本后用碎纸机销毁

通俗解释:

  • 清理栈缓冲区中的敏感数据
  • 释放可能分配的资源
  • 确保不留痕迹

工作流程:

  1. 调用 sub_42F4478 预处理
  2. 检查特定偏移的状态
  3. 如果有需要释放的资源,调用释放函数

10. sub_42F4478(栈缓冲区预处理)

text
角色:临时记录本检查员
比喻:销毁前先检查有没有需要特殊处理的部分

通俗解释:

  • 检查缓冲区的多个字段
  • 根据状态标志决定清理方式
  • 调用不同的资源释放函数

代码逻辑:

C
if (buffer->field_0x88 != NULL && buffer->field_0x98 != 0) {
    // 释放资源方式1
}
if (buffer->field_0x90 != NULL) {
    // 释放资源方式2
}

🔍 第五组:ELF文件解析团队

11. sub_42F4754(寻找Dynamic段)

text
角色:档案馆管理员
比喻:在一堆文件夹中找到标记为"Dynamic"的那一个

通俗解释:

  • 遍历 ELF 文件的 Program Header Table(程序头表)
  • 查找类型为 PT_DYNAMIC (值为2) 的段
  • 这个段包含所有动态链接需要的信息

工作流程:

  1. 初始化栈缓冲区(调用 sub_42F43FC)
  2. 验证 ELF 文件格式(调用 sub_42F459C)
  3. 解析 PT_LOAD 段并计算基址(调用 sub_42F4658)
  4. 遍历 Program Header 查找 PT_DYNAMIC

比喻故事:

text
就像在图书馆找书:
1. 先拿一个购物车(初始化缓冲区)
2. 检查这是不是图书馆(验证ELF魔数)
3. 查看图书馆地图(解析PT_LOAD段)
4. 找到标记为"目录"的书架(定位PT_DYNAMIC)

12. sub_42F459C(容错式ELF魔数搜索)

text
角色:身份验证员
比喻:检查这个文件是不是真的 ELF 文件

通俗解释:

  • 在开头 64 字节范围内搜索 ELF 魔数 0x7F 'E' 'L' 'F'
  • 支持两种魔数格式(可能是大端/小端)
  • 容错设计:允许文件头有小幅偏移

为什么需要容错?

  • 非对齐加载:内存注入时可能不是从文件开头加载
  • 反调试:故意偏移几个字节来迷惑分析工具
  • 损坏恢复:文件头前面有几个字节的垃圾数据

比喻故事:

text
就像检查护照:
1. 正常护照封面在第一页
2. 但如果封面被撕掉了几页纸
3. 我们会翻前64页来找封面
4. 找到了就认为这是有效护照

代码逻辑:

C
for (offset = 0; offset < 64; offset++) {
    if (memcmp(ptr + offset, "\x7FELF", 4) == 0) {
        return true;  // 找到了!
    }
}
return false;  // 64字节内都没找到

13. sub_42F4658(PT_LOAD段解析与基址计算)

text
角色:房产测量员
比喻:测量房子占地面积,计算房子的实际位置

通俗解释:

  • 遍历所有 PT_LOAD 类型的段(可加载段)
  • 记录最小地址和最大地址
  • 计算文件在内存中的实际基址
  • 处理 ASLR(地址空间布局随机化)

工作流程:

  1. 遍历 Program Header Table
  2. 找到所有 p_type = PT_LOAD 的条目
  3. 记录每个段的起始地址和结束地址
  4. 计算总的地址范围
  5. 获取系统页面大小(通常4096字节)
  6. 进行页面对齐
  7. 计算实际基址:base = param - aligned_min_offset

比喻故事:

text
就像测量一栋房子:
1. 房子有多个房间(PT_LOAD段)
2. 记录最西边的墙(最小地址)
3. 记录最东边的墙(最大地址)
4. 计算房子总宽度(地址范围)
5. 按照土地规划对齐边界(页面对齐)
6. 计算房子在地图上的坐标(实际基址)

关键参数:

C
// 输入
parser->param   // 模块实际加载地址

// 输出
parser->base    // 计算出的实际基址
parser->size    // 映射区域总大小

14. sub_42F4070(内存区域边界计算)

text
角色:土地测量员
比喻:测量一块土地的边界,并按规划对齐

通俗解释:

  • 遍历 PT_LOAD 段,找出最小和最大地址
  • 获取系统页面大小
  • 将边界对齐到页面边界
  • 返回对齐后的地址范围

为什么需要页面对齐?

  • 内存管理单元(MMU)要求:操作系统以页为单位管理内存
  • 内存保护:权限设置必须按页对齐
  • 性能优化:页面对齐的内存访问更快

代码逻辑:

C
// 获取系统页面大小(通常4096)
page_size = sysconf(_SC_PAGESIZE);

// 向下对齐最小地址
aligned_min = min_address & (-page_size);

// 向上对齐最大地址
aligned_max = (max_address + page_size - 1) & (-page_size);

比喻:

text
就像划分土地:
1. 原始边界:100米~250米
2. 但规划要求每块地必须是50米的倍数
3. 所以调整为:100米~300米
4. 这样才符合城市规划要求

📖 第六组:Dynamic段解析团队

15. sub_42F630C(数据结构初始化)

text
角色:任务分发员
比喻:检查任务是否已完成,未完成则启动主任务

通俗解释:

  • 检查偏移 +0x18F (395) 的标志位
  • 如果已经初始化过,直接返回
  • 如果没初始化,调用 sub_42F5844 执行解析

代码逻辑:

C
if (*(byte*)(a1 + 395) != 0) {
    return 1;  // 已经完成
} else {
    return sub_42F5844(a1);  // 执行解析
}

比喻:

text
就像检查作业:
1. 老师问:"作业做完了吗?"
2. 如果做完了,直接交上来
3. 如果没做,现在开始做

16. sub_42F5844(Prelink操作 - 解析Dynamic段)⭐

text
角色:档案解析专家
比喻:打开"目录"文件夹,把每一项信息记录下来

通俗解释:

  • 这是整个流程最核心的函数之一
  • 遍历 Dynamic 段的所有条目
  • 每个条目是一个 Tag-Value 对
  • 根据 Tag 类型,将 Value 存储到不同位置

Dynamic段结构:

C
struct Elf64_Dyn {
    int64_t  d_tag;   // 类型标识(如 DT_STRTAB = 5)
    uint64_t d_val;   // 值(如字符串表的地址)
};

处理的关键Tag:

Tag值名称存储位置用途
1 DT_NEEDED 计数器+1 依赖的动态库
4 DT_HASH +0x100 符号哈希表
5 DT_STRTAB +0x30 字符串表地址 ⭐
6 DT_SYMTAB +0x20 符号表地址 ⭐
10 DT_STRSZ +0x38 字符串表大小
12 DT_INIT +0xF8 初始化函数
13 DT_FINI +0xF0 清理函数
25 DT_INIT_ARRAY +0xC0 初始化函数数组
26 DT_FINI_ARRAY +0xD0 清理函数数组
27 DT_INIT_ARRAYSZ +0xC8 数组大小

工作流程:

C
while (tag != DT_NULL) {
    switch (tag) {
        case 4:  // DT_HASH
            *(a1 + 0x100) = value + base;
            break;
        case 5:  // DT_STRTAB
            *(a1 + 0x30) = value + base;
            break;
        case 6:  // DT_SYMTAB
            *(a1 + 0x20) = value + base;
            break;
        // ... 更多case ...
    }
}

比喻故事:

text
就像整理一个目录:
1. 打开目录文件
2. 看到第一项:"字符串表在第30页"
3. 记录:字符串表 → 第30页
4. 看到第二项:"符号表在第20页"
5. 记录:符号表 → 第20页
6. 继续处理所有条目...
7. 完成后,就知道所有重要信息在哪里了

🔐 第七组:安全验证与解密团队

17. sub_42F5FCC(安全验证与初始化)

text
角色:解密流程协调员
比喻:确认所有安全检查通过后,授权执行解密

通俗解释:

  • 执行最后一轮完整性检查
  • 检查配置数据是否有效
  • 设置解密回调函数
  • 调用主处理器执行解密

工作流程:

  1. 完整性校验(offset +0x38C)
  2. 检查配置:*(off_4306E30 + 0x04) 是否非零
  3. 设置回调:*off_4306FB0 = sub_42F57AC
  4. 调用处理器:sub_42FBAB8(...)

比喻故事:

text
就像银行金库解锁流程:
1. 保安再次确认所有监控正常(完整性校验)
2. 确认解锁权限卡有效(检查配置)
3. 设置解锁程序到电脑上(设置回调函数)
4. 按下解锁按钮(调用主处理器)

18. sub_42FBA38(设置字节标志)

text
角色:开关操作员
比喻:打开或关闭一个开关

通俗解释:

  • 超级简单的函数
  • 就是把一个字节设置为特定值

代码逻辑:

C
*result = a2;  // 就这一行!
return result;

比喻:

text
就像打开电灯开关:
1. 接到命令:"把开关设为开"
2. 咔嚓一下,打开
3. 完成!

19. sub_42FBAB8(主处理器)

text
角色:信号调度员
比喻:决定用普通方式还是特殊方式执行任务

通俗解释:

  • 检查标志位决定执行方式
  • 方式1:直接调用函数
  • 方式2:通过信号处理器调用(反调试技巧)

代码逻辑:

C
if (*a1 == 0) {
    // 方式1:直接调用
    return off_4308B50(a2, a3, a4, a5, a6, a7, a8);
} else {
    // 方式2:通过信号处理
    byte_4308B60 = 1;
    sub_42FBA40();  // 设置信号处理器
    // 保存参数
    qword_4308B10 = a2;
    qword_4308B18 = a3;
    // ...
    raise(SIGINT);  // 触发信号
    // 信号处理器会调用 off_4308B50
    sigaction(SIGINT, &old_handler, NULL);  // 恢复
    return qword_4308B58;
}

为什么要用信号?

  • 反调试:让调试器难以跟踪
  • 混淆:增加代码复杂度
  • 保护:关键操作通过信号间接执行

比喻故事:

text
就像传递秘密信息:
1. 普通方式:直接打电话告诉他
2. 特殊方式:
   - 先把信息写在纸条上
   - 放进保险柜
   - 通过无线电发信号
   - 对方收到信号后去开保险柜
   - 这样就算有人窃听无线电也没用

20. sub_42FBA40(信号处理器设置)

text
角色:信号接收器安装员
比喻:安装一个特殊的"报警器"

通俗解释:

  • 设置 SIGINT 信号的处理函数
  • 当程序收到 Ctrl+C 信号时,不会退出
  • 而是执行我们自定义的函数

代码逻辑:

C
struct sigaction new_handler;
memset(&new_handler, 0, sizeof(new_handler));
new_handler.sa_handler = sub_42FB9C8;  // 自定义处理函数
sigaction(SIGINT, &new_handler, &old_handler);  // 安装

比喻:

text
就像改装门铃:
1. 原来按门铃 → "叮咚"响
2. 现在改装成 → 按门铃 → 播放一段密码
3. 只有知道密码的人才知道这是门铃

21. sub_42FB9C8(信号处理回调)

text
角色:信号响应员
比喻:听到门铃响后执行特定任务

通俗解释:

  • 当 SIGINT 信号被触发时执行
  • 检查全局标志是否允许执行
  • 如果允许,调用 off_4308B50(指向 sub_42F57AC)
  • 将结果保存到 qword_4308B58

代码逻辑:

C
if (signum == SIGINT) {
    if (off_4308B50 && byte_4308B60) {
        qword_4308B58 = off_4308B50(
            qword_4308B10,  // 之前保存的参数
            qword_4308B18,
            // ...
        );
    }
}

比喻:

text
就像密室逃脱的机关:
1. 听到特定音乐(SIGINT信号)
2. 检查钥匙是否在位置上(byte_4308B60)
3. 如果在,打开暗门(调用解密函数)
4. 记录暗门后面的宝藏位置(保存结果)

22. nullsub_2(空函数)

text
角色:假装工作的员工
比喻:一个什么都不做的占位符

通俗解释:

  • 函数体是空的
  • 调用它什么都不会发生
  • 可能用于混淆或占位

代码逻辑:

C
void nullsub_2(void) {
    return;  // 直接返回,啥也不做
}

为什么存在?

  • 预留接口:以后可能会用
  • 混淆技术:让逆向工程师浪费时间分析
  • 占位符:保持代码结构一致

23. sub_42F57AC(解密字符串表和符号表)⭐⭐⭐

text
角色:密码破译专家
比喻:用密钥解开加密的书籍目录

通俗解释:

  • 这是最关键的函数!
  • 解密两样东西:
    1. 字符串表(所有函数名、变量名等文字)
    2. 符号表(函数地址、变量地址等信息)

解密算法:XOR(异或)

第一部分:解密字符串表

C
// 字符串表在文件中是这样的:
加密前:"il2cpp_init"
加密后:乱码 "K#@$%^&*"

// 解密过程:
key = 0x56312342;
for (offset = 0; offset < string_table_size; offset += 4) {
    data = *(uint32_t*)(string_table + offset);
    data = data ^ key;  // XOR解密
    *(uint32_t*)(string_table + offset) = data;
    key += offset;  // 动态更新密钥
}

// 解密后:
"il2cpp_init"  // 恢复正常!

第二部分:解密符号表

C
// 符号表条目结构(24字节)
struct Symbol {
    uint32_t name_offset;  // +0  名字在字符串表的位置(未加密)
    uint32_t info;         // +4  符号类型(未加密)
    uint64_t address;      // +8  函数地址(加密了!)⭐
    uint64_t size;         // +16 函数大小(加密了!)⭐
};

// 只解密部分符号(start_index 到 end_index)
for (i = start_index; i < end_index; i++) {
    symbol = &symbol_table[i];
    
    if (symbol->address != 0) {
        // 解密地址
        key1 = i + 337;
        symbol->address ^= key1;
        
        // 解密大小
        key2 = i + 347;
        symbol->size ^= key2;
    }
}

比喻故事:

text
就像解密一本密码书:

原始书籍:
┌──────────────────┐
│ 目录(符号表)    │
│ 第1章:@#$%      │ ← 加密的地址
│ 第2章:*&^%      │ ← 加密的地址
├──────────────────┤
│ 内容(字符串表)  │
│ #$%^&*()_+      │ ← 加密的文字
│ !@#$%^&*()      │ ← 加密的文字
└──────────────────┘

用密钥解密后:
┌──────────────────┐
│ 目录(符号表)    │
│ 第1章:第10页    │ ← 解密后的地址
│ 第2章:第20页    │ ← 解密后的地址
├──────────────────┤
│ 内容(字符串表)  │
│ il2cpp_init     │ ← 解密后的文字
│ UnityEngine     │ ← 解密后的文字
└──────────────────┘

现在可以正常使用了!

24. sub_42FF920(缓存刷新)

text
角色:CPU缓存清理员
比喻:清空电脑的临时缓存,让新数据生效

通俗解释:

  • 解密后的数据还在CPU缓存中
  • 必须清除缓存,让CPU重新从内存读取
  • 使用ARM架构的特殊指令

使用的指令:

  1. DC CVAU:清理数据缓存(把缓存写回内存)
  2. IC IVAU:使无效化指令缓存(让CPU重新读取)
  3. DSB ISH:数据同步屏障(等待所有操作完成)
  4. ISB:指令同步屏障(刷新CPU流水线)

代码逻辑:

C
// 获取缓存行大小(通常64字节)
cache_line_size = get_cache_line_size();

// 清理数据缓存
for (addr = start; addr < end; addr += cache_line_size) {
    DC_CVAU(addr);  // 清理这一行缓存
}
DSB_ISH();  // 等待完成

// 使无效化指令缓存
for (addr = start; addr < end; addr += cache_line_size) {
    IC_IVAU(addr);  // 使无效化这一行缓存
}
DSB_ISH();  // 等待完成
ISB();      // 刷新流水线

比喻故事:

text
就像更新电脑软件:
1. 下载了新版本(解密数据)
2. 但电脑还在用旧版本(CPU缓存里是旧数据)
3. 必须清除缓存(DC CVAU)
4. 让电脑重新加载新版本(IC IVAU)
5. 等所有操作完成(DSB ISH)
6. 重启程序(ISB)
7. 现在新版本生效了!

🧹 第八组:内存权限管理团队

25. sub_42F3258(内存权限设置)

text
角色:内存权限管理员
比喻:给房间上锁或开锁

通俗解释:

  • 修改内存区域的访问权限
  • 权限类型:
    • 3 = 可读+可写(PROT_READ | PROT_WRITE)
    • 5 = 可读+可执行(PROT_READ | PROT_EXEC)

使用场景:

  1. 解密前:设置为"可读可写",允许修改数据
  2. 解密后:设置为"可读可执行",防止被修改

系统调用:

C
mprotect(address, size, protection);

代码逻辑:

C
// 解密前
sub_42F3258(memory, size, 3);  // 设置为可读写
// 执行解密
decrypt_data(memory);
// 解密后
sub_42F3258(memory, size, 5);  // 设置为可读可执行

比喻故事:

text
就像银行金库:
1. 要存钱时 → 打开金库门(可读写)
2. 存完钱后 → 锁上金库门(只读可执行)
3. 这样钱就安全了,别人无法修改

26. sysconf(获取系统配置)

text
角色:系统信息查询员
比喻:问操作系统要一些配置信息

通俗解释:

  • 查询系统配置信息
  • 常用参数:
    • sysconf(39) = _SC_PAGESIZE = 获取内存页面大小(通常4096字节)

使用场景:

  • 计算页面对齐的地址
  • 分配内存时需要知道页面大小

代码示例:

C
long page_size = sysconf(_SC_PAGESIZE);  // 通常返回4096

27. sub_42F83F0(代码完整性检查 - 读取/proc/self/maps)

text
角色:自我检查员
比喻:照镜子检查自己有没有被人动过手脚

通俗解释:

  • 读取 /proc/self/maps 文件
  • 这个文件包含程序所有内存映射信息
  • 查找包含当前函数的内存段
  • 返回该段对应的文件路径

工作流程:

  1. 解密路径字符串:"/proc/self/maps"(用XOR解密)
  2. 打开文件
  3. 逐行读取
  4. 解析每一行:起始地址-结束地址 权限 偏移 ... 路径
  5. 检查当前函数地址是否在 [起始地址, 结束地址) 范围内
  6. 如果匹配,返回路径字符串

为什么要这样做?

  • 反调试:检测代码是否被注入
  • 完整性验证:确认代码运行在正确的模块中
  • 反篡改:检测内存映射是否异常

比喻故事:

text
就像检查身份证:
1. 打开户口本(/proc/self/maps)
2. 查看自己的信息(当前函数地址)
3. 确认自己住在哪里(对应的文件路径)
4. 如果发现自己的地址在别人家里 → 说明被绑架了!
5. 如果地址正常 → 一切安全

📊 完整调用链总结

text
sub_42F1564 (总指挥官)
  ├─ [安全检查] sub_42FC0C4/0A4/0B4 (数学密码专家组)
  │   └─ sub_42F3270 (警报系统)
  ├─ [初始化] sub_42F24F8 (环境准备员)
  │   └─ sub_42F1F78 (代码段解密)
  │       ├─ sub_42F3258 (内存权限管理)
  │       ├─ XOR解密算法
  │       └─ sub_42FF920 (缓存刷新)
  ├─ [内存管理] 函数表调用
  │   ├─ malloc (租仓库)
  │   ├─ memset (打扫仓库)
  │   └─ free (退仓库)
  ├─ [ELF解析] sub_42F4754 (档案馆管理员)
  │   ├─ sub_42F43FC (准备笔记本)
  │   ├─ sub_42F459C (检查ELF魔数)
  │   ├─ sub_42F4658 (测量房子)
  │   │   ├─ sub_42F4070 (土地测量)
  │   │   └─ sysconf (查询页面大小)
  │   └─ 查找PT_DYNAMIC段
  ├─ [Prelink] sub_42F630C (任务分发)
  │   └─ sub_42F5844 (档案解析专家) ⭐
  │       └─ 解析所有Dynamic条目
  ├─ [解密] sub_42F5FCC (解密协调员)
  │   ├─ sub_42FBA38 (开关操作)
  │   ├─ sub_42FBAB8 (信号调度)
  │   │   ├─ sub_42FBA40 (信号处理器设置)
  │   │   └─ sub_42FB9C8 (信号处理回调)
  │   ├─ nullsub_2 (假装工作)
  │   └─ sub_42F57AC (密码破译专家) ⭐⭐⭐
  │       ├─ 解密字符串表 (XOR: 0x56312342)
  │       └─ 解密符号表 (XOR: index+337/347)
  └─ [清理] sub_42F44F0 (销毁笔记本)
      └─ sub_42F4478 (笔记本检查)

🎯 用一个完整故事串起所有函数

text
🏛️ 绝密档案解封流程

第一天早上(安全检查):
总指挥官(sub_42F1564)召集11组密码专家(sub_42FC0C4/0A4/0B4)
检查保险柜的封条有没有被撕开(完整性校验)
如果发现封条被动过,立即拉响警报(sub_42F3270)

第一天下午(环境准备):
环境准备员(sub_42F24F8)到现场检查
如果是第一次来,就把整个房间检查一遍
如果之前来过,就走快速通道
准备好工具箱(函数表)

第二天(场地准备):
仓库管理员租了一个416平米的临时仓库(malloc)
用拖把把仓库打扫得一尘不染(memset)
准备一个干净笔记本记录信息(sub_42F43FC)

第三天(寻找档案):
档案馆管理员(sub_42F4754)开始工作:
1. 先检查这是不是档案馆(sub_42F459C检查ELF魔数)
2. 测量档案馆的大小(sub_42F4658解析PT_LOAD)
3. 找到标记为"目录"的柜子(查找PT_DYNAMIC)

第四天(解析目录):
任务分发员(sub_42F630C)检查任务清单
档案解析专家(sub_42F5844)打开"目录"柜子:
1. "字符串档案在30号柜"→ 记录
2. "符号档案在20号柜"→ 记录
3. "初始化档案在192号柜"→ 记录
4. 把所有信息都记录下来

第五天(解密准备):
解密协调员(sub_42F5FCC)做最后检查:
1. 再次确认所有安全措施正常
2. 把解密程序安装到电脑上(设置sub_42F57AC回调)
3. 通过信号调度员(sub_42FBAB8)发出解密指令

第六天(解密行动)⭐:
密码破译专家(sub_42F57AC)开始工作:

上午:解密字符串档案
1. 拿出密钥:0x56312342
2. 打开30号柜(字符串表)
3. 逐页用密钥解密:"@#$%^" → "il2cpp_init"
4. 所有字符串恢复正常

下午:解密符号档案
1. 打开20号柜(符号表)
2. 只解密第50个到第100个档案(部分解密)
3. 用特殊密钥(索引+337/347)解密每个档案
4. 档案地址和大小恢复正常

傍晚:清理缓存
缓存清理员(sub_42FF920)工作:
1. 清空所有CPU缓存(DC CVAU)
2. 刷新指令缓存(IC IVAU)
3. 等待所有操作完成(DSB ISH, ISB)
4. 现在解密后的数据生效了!

第七天(善后工作):
1. 把临时仓库退租(free)
2. 销毁临时笔记本(sub_42F44F0)
3. 检查没有遗漏任何痕迹(sub_42F4478)
4. 总指挥官确认任务完成

结果:
虽然总指挥官只返回了一个"已销毁"的笔记本地址(悬空指针)
但实际上,所有重要信息都已经记录在全局档案柜里了:
- 符号表已解密 ✅
- 字符串表已解密 ✅
- 所有动态链接信息已就绪 ✅
- 程序可以正常运行了 ✅

💡 总结

这个函数就像一个007特工执行任务:

  1. 多重身份验证(11次完整性检查)
  2. 准备秘密工具(分配内存、初始化缓冲区)
  3. 寻找情报(解析ELF文件,定位Dynamic段)
  4. 破译密码(Prelink操作,解析动态链接信息)
  5. 解密文件(解密字符串表和符号表)⭐ 最关键
  6. 销毁证据(清理临时数据)
  7. 假装什么都没发生(返回悬空指针)

但实际上,绝密档案已经被成功解密并存放在安全的地方了!

 

 

 

 

第三个INIT函数详解:自定义Linker实现

第三个INIT函数是整个加固方案的核心,它完整实现了Android Linker的加载流程。

image

 

 进入第三个init函数,首先解密了一小段代码,执行完又加密回去,功能是初始化一些全局变量,这里就不展开讲了。

核心是进入了自定义的链接器函数:

image

具体怎么看出来的是需要结合android linker部分的源码来研究,这里直接说最终的结论。已经在图中标出了各个函数的作用。

image

 

 "像拼图一样:先解密碎片(decrypt),塞进框架(load),对照答案纸修正连接(relocate),最后按启动键(init)。"

 

 

一、整体流程概览

第三个INIT函数是整个加固方案的核心,它完整实现了Android Linker的加载流程。

1.1 完整流程图

text
第三个INIT函数入口
[1] 初始化soinfo结构体
[2] 解密子so代码段和数据段 (魔改RC4)
[3] load_segment - 将解密数据映射到内存
[4] 第一次prelink - 解析壳so的dynamic段
[5] 第二次prelink - 解析子so的dynamic段
[6] relocate - 执行重定位
[7] call_init - 调用子so的初始化函数
子so成功加载到内存,可以正常执行

二、步骤详解

步骤1:初始化soinfo结构体

1.1 什么是soinfo?

soinfo是Android Linker中描述动态库的核心数据结构,包含了库的所有元数据。

C
// Android Linker中的soinfo结构(简化版)
struct soinfo {
    // 基础信息
    const char* name;              // +0x00  库名称
    void* base;                    // +0x08  加载基址
    Elf64_Phdr* phdr;             // +0x10  Program Header地址
    size_t phnum;                 // +0x18  Program Header数量
    
    Elf64_Dyn* dynamic;           // +0x20  Dynamic段地址
    
    // 从dynamic段解析出的信息
    const char* strtab;           // +0x30  字符串表
    size_t strtab_size;           // +0x38  字符串表大小
    Elf64_Sym* symtab;            // +0x40  符号表
    
    // 重定位信息
    Elf64_Rela* plt_rela;         // +0x50  PLT重定位表
    size_t plt_rela_count;        // +0x58  PLT重定位数量
    Elf64_Rela* rela;             // +0x60  普通重定位表
    size_t rela_count;            // +0x68  重定位数量
    
    // 初始化和终结函数
    void** init_array;            // +0xC0  初始化函数数组
    size_t init_array_count;      // +0xC8  初始化函数数量
    void** fini_array;            // +0xD0  终结函数数组
    size_t fini_array_count;      // +0xD8  终结函数数量
    
    // ... 其他字段
};

1.2 代码分析

C
// 伪代码还原
void* third_init() {
    // 分配soinfo结构体内存
    v31 = malloc_wrapper(soinfo_size);
    
    // 清零
    memset(v31, 0, soinfo_size);
    
    // 存储到v6寄存器(后续会一直使用)
    v6 = v31;  // v6就是soinfo指针
    
    *(_QWORD *)(v6 + 128) = v32;  // 初始化某些字段
}

步骤2:解密子so的代码段和数据段

2.1 加密存储位置

子so的代码和数据在壳so中是分开加密存储的:

text
壳so文件结构:
├── ELF Header
├── Program Headers
├── 壳so代码段(加密的子so代码混在其中)
├── 壳so数据段
│   ├── ...
│   ├── 加密的子so代码段 ← v15指向这里
│   ├── 加密的子so数据段 ← v25指向这里
│   └── 子so的Program Header(加密)
└── ...

2.2 解密代码段

C
// 代码段解密
v23 = decrypt0(
    v15,                          // 加密数据源地址
    *(_DWORD *)(v4 + 116),       // 加密数据大小
    v11,                          // 解密后的目标地址
    *(_DWORD *)(v4 + 116),       // 解密大小
    (__int64)v22                 // 密钥/上下文
);

// 检查是否解密成功
if ( !*(_DWORD *)(v4 + 116) )
    goto LABEL_48;  // 解密失败

// 将解密后的信息记录到soinfo
*(_QWORD *)(v6 + 136) = v11;              // 代码段地址
*(_QWORD *)(v6 + 152) = *(_DWORD *)(v4 + 116);  // 代码段大小

// 计算下一段的起始地址(需要对齐)
v24 = *(_DWORD *)(v4 + 116) + v15;
if ( v24 & 7 )
    v24 += 4LL;  // 8字节对齐
*(_QWORD *)(v6 + 176) = v24;

2.3 解密数据段

C
// 找到Program Header的位置
// v5是壳so的ELF Header地址
// 公式:phdr地址 = ehdr + phdr_offset + phdr_size * phdr_count
v25 = v5 + 56LL * *(unsigned __int16 *)(v5 + 56) + 64;

// 为解密后的数据段分配内存
v26 = malloc_wrapper(*(_DWORD *)(v4 + 112));

// 解密数据段
decrypt0(
    v25,                          // 加密的数据段源地址
    *(_DWORD *)(v4 + 112),       // 数据段大小
    v26,                          // 解密目标地址
    *(_DWORD *)(v4 + 112),       // 大小
    v28                           // 密钥
);

2.4 解密算法:魔改RC4

C
// decrypt0函数的核心逻辑(伪代码)
void decrypt0(void* src, size_t src_len, void* dst, size_t dst_len, rc4_ctx* ctx) {
    // 标准RC4的KSA和PRGA过程
    unsigned char S[256];
    unsigned char key[32];
    
    // 初始化S盒(魔改点1:初始化方式不同)
    for (int i = 0; i < 256; i++) {
        S[i] = i ^ 0xAA;  // 不是标准的S[i]=i
    }
    
    // KSA - 密钥调度算法(魔改点2:交换逻辑)
    int j = 0;
    for (int i = 0; i < 256; i++) {
        j = (j + S[i] + key[i % keylen] + i) & 0xFF;  // 额外加了i
        swap(S[i], S[j]);
    }
    
    // PRGA - 伪随机生成算法
    int i = 0, j = 0;
    for (size_t k = 0; k < src_len; k++) {
        i = (i + 1) & 0xFF;
        j = (j + S[i]) & 0xFF;
        swap(S[i], S[j]);
        
        unsigned char keystream = S[(S[i] + S[j]) & 0xFF];
        dst[k] = src[k] ^ keystream;
    }
}

步骤3:load_segment - 自定义内存映射

3.1 为什么不用mmap?

传统linker的做法:

C
// 标准做法
void* addr = mmap(
    vaddr,           // 目标虚拟地址
    memsz,          // 大小
    PROT_READ | PROT_EXEC,
    MAP_PRIVATE | MAP_FIXED,
    fd,             // 文件描述符
    offset          // 文件偏移
);

某顿的做法:

  • 壳so在加载时已经预留了超大内存空间
  • 直接使用mprotect修改权限 + memcpy复制数据
  • 不需要mmap,避免在/proc/[pid]/maps中暴露子so的映射

3.2 预留内存空间的证据

C
// 壳so的第一个LOAD段
struct program_table_entry64_t {
    p_type   = PT_LOAD (1)
    p_flags  = PF_Read_Exec (5)        // 可读可执行
    p_offset = 0x0
    p_vaddr  = 0x0
    p_paddr  = 0x0
    p_filesz = 0x447C000               // 文件中只有这么大
    p_memsz  = 0x7CD4000               // 内存中申请了这么大!
    p_align  = 0x1000
};

// 多申请的空间 = 0x7CD4000 - 0x447C000 = 0x3858000 (约56MB)
// 这就是用来存放解密后的子so代码和数据的!

3.3 自定义load_segment实现

C
// 伪代码还原
bool custom_load_segment(soinfo* si, Elf64_Phdr* phdr, void* decrypted_data) {
    // 1. 计算需要映射的地址范围
    uint64_t seg_start = phdr->p_vaddr;
    uint64_t seg_end = seg_start + phdr->p_memsz;
    
    // 2. 页对齐
    uint64_t seg_page_start = PAGE_START(seg_start);  // 向下对齐到4KB
    uint64_t seg_page_end = PAGE_END(seg_end);        // 向上对齐到4KB
    
    uint64_t seg_page_size = seg_page_end - seg_page_start;
    
    // 3. 修改内存权限为RWX(临时)
    if (mprotect(
            (void*)(base + seg_page_start),
            seg_page_size,
            PROT_READ | PROT_WRITE | PROT_EXEC
        ) == -1) {
        return false;
    }
    
    // 4. 用0xBB填充整个区域(调试标记)
    memset(
        (void*)(base + seg_page_start),
        0xBB,
        seg_page_size
    );
    
    // 5. 复制解密后的数据
    if (phdr->p_filesz > 0) {
        memcpy(
            (void*)(base + seg_start),
            decrypted_data,
            phdr->p_filesz
        );
    }
    
    // 6. BSS段(.bss)处理:将剩余部分清零
    uint64_t data_end = seg_start + phdr->p_filesz;
    if ((phdr->p_flags & PF_W) && (data_end & 0xFFF)) {
        // 如果是可写段且不是页对齐,清零到下一页
        memset(
            (void*)(base + data_end),
            0,
            PAGE_SIZE - (data_end & 0xFFF)
        );
    }
    
    // 7. 如果memsz > filesz,需要额外映射匿名页
    uint64_t aligned_data_end = PAGE_END(data_end);
    if (seg_page_end > aligned_data_end) {
        // 映射额外的匿名页(用于.bss段)
        void* gap = mmap(
            (void*)(base + aligned_data_end),
            seg_page_end - aligned_data_end,
            PROT_READ | PROT_WRITE,
            MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
            -1,
            0
        );
        
        if (gap == MAP_FAILED) {
            return false;
        }
        
        // 清零
        memset(gap, 0, seg_page_end - aligned_data_end);
    }
    
    // 8. 恢复正确的权限
    int prot = 0;
    if (phdr->p_flags & PF_R) prot |= PROT_READ;
    if (phdr->p_flags & PF_W) prot |= PROT_WRITE;
    if (phdr->p_flags & PF_X) prot |= PROT_EXEC;
    
    if (mprotect(
            (void*)(base + seg_page_start),
            seg_page_size,
            prot
        ) == -1) {
        return false;
    }
    
    return true;
}

3.4 实际代码对应关系

C
// 原文中的代码片段标注

v22 = mprotect(
    v14 & 0xFFFFFFFFFFFFF000LL,  // 页对齐的起始地址
    v17,                          // 大小
    *v9 | 3u                      // 权限:默认+读写
);  // [步骤3]

sub_7D0FC10(v14 & 0xFFFFFFFFFFFFF000LL, 0xBB, v17);  // [步骤4] 填充0xBB

if ( v35 )
    memcpy(v14, v3 + v34, v35);  // [步骤5] 复制解密数据

// [步骤6] 清零到页边界
if ( v25 & 2 && v26 & 0xFFF )
    memset(v26, 0LL, 4096 - (v26 & 0xFFF));

// [步骤7] 处理额外的匿名页
v27 = (v26 + 4095) & 0xFFFFFFFFFFFFF000LL;
if ( v36 > v27 ) {
    mmap(v27, v36 - v27, 3LL, 50LL, -1, 0LL);
    memset(v27, 0LL, v36 - v27);
}

// [步骤8] 恢复正确权限
mprotect(
    v38,
    (*v28 & 2 | (*v24 >> 2) & 1) & 0xFFFFFFFF | 4 * (*v24 & 1)
);  // 根据phdr的p_flags计算权限

步骤4:第一次prelink - 解析壳so的dynamic段

4.1 什么是prelink?

prelink的作用是解析dynamic段的各种标签,将信息填充到soinfo结构体中。

4.2 Dynamic段结构

C
// Dynamic段是一个Elf64_Dyn数组,以DT_NULL结尾
typedef struct {
    int64_t d_tag;   // 标签类型
    union {
        uint64_t d_val;  // 整数值
        uint64_t d_ptr;  // 指针值
    } d_un;
} Elf64_Dyn;

// 示例:壳so的dynamic段
Elf64_Dyn dynamic[] = {
    { DT_NEEDED, .d_val = 0x0000013C },      // 依赖库名在strtab的偏移
    { DT_NEEDED, .d_val = 0x00000146 },
    { DT_INIT_ARRAY, .d_ptr = 0x7DF6A50 },   // 初始化函数数组
    { DT_INIT_ARRAYSZ, .d_val = 0x20 },      // 大小32字节(4个函数)
    { DT_FINI_ARRAY, .d_ptr = 0x7DF6A70 },
    { DT_FINI_ARRAYSZ, .d_val = 0x20 },
    { DT_STRTAB, .d_ptr = 0x7E23C38 },       // 字符串表
    { DT_SYMTAB, .d_ptr = 0x7E01990 },       // 符号表
    { DT_STRSZ, .d_val = 0x1A480 },          // 字符串表大小
    { DT_SYMENT, .d_val = 0x18 },            // 符号表条目大小(24字节)
    { DT_RELA, .d_ptr = 0x7CFF4E8 },         // 重定位表
    { DT_RELASZ, .d_val = 0xD260 },          // 重定位表大小
    { DT_RELAENT, .d_val = 0x18 },           // 重定位条目大小(24字节)
    { DT_HASH, .d_ptr = 0x7E1B730 },         // Hash表
    { DT_PLTGOT, .d_ptr = 0x7DFBA88 },       // PLT/GOT
    { DT_PLTRELSZ, .d_val = 0x25C8 },        // PLT重定位大小
    { DT_JMPREL, .d_ptr = 0x7D0C748 },       // PLT重定位表
    { DT_NULL, .d_val = 0 }                  // 结束标记
};

4.3 prelink实现

C
// 伪代码还原
bool prelink(soinfo* si) {
    Elf64_Dyn* d = si->dynamic;
    void* load_bias = si->base;
    
    // 遍历dynamic段
    for (; d->d_tag != DT_NULL; ++d) {
        switch (d->d_tag) {
            case DT_HASH:
                // 解析Hash表(用于符号查找)
                si->nbucket = ((uint32_t*)(load_bias + d->d_un.d_ptr))[0];
                si->nchain = ((uint32_t*)(load_bias + d->d_un.d_ptr))[1];
                si->bucket = (uint32_t*)(load_bias + d->d_un.d_ptr + 8);
                si->chain = si->bucket + si->nbucket;
                break;
            
            case DT_STRTAB:
                si->strtab = (const char*)(load_bias + d->d_un.d_ptr);
                break;
            
            case DT_STRSZ:
                si->strtab_size = d->d_un.d_val;
                break;
            
            case DT_SYMTAB:
                si->symtab = (Elf64_Sym*)(load_bias + d->d_un.d_ptr);
                break;
            
            case DT_SYMENT:
                if (d->d_un.d_val != sizeof(Elf64_Sym)) {
                    return false;  // 错误:符号表条目大小不对
                }
                break;
            
            case DT_RELA:
                si->rela = (Elf64_Rela*)(load_bias + d->d_un.d_ptr);
                break;
            
            case DT_RELASZ:
                si->rela_size = d->d_un.d_val;
                si->rela_count = d->d_un.d_val / sizeof(Elf64_Rela);
                break;
            
            case DT_PLTGOT:
                si->plt_got = (void**)(load_bias + d->d_un.d_ptr);
                break;
            
            case DT_PLTRELSZ:
                si->plt_rela_size = d->d_un.d_val;
                break;
            
            case DT_JMPREL:
                si->plt_rela = (Elf64_Rela*)(load_bias + d->d_un.d_ptr);
                si->plt_rela_count = si->plt_rela_size / sizeof(Elf64_Rela);
                break;
            
            case DT_INIT_ARRAY:
                si->init_array = (void**)(load_bias + d->d_un.d_ptr);
                break;
            
            case DT_INIT_ARRAYSZ:
                si->init_array_count = d->d_un.d_val / sizeof(void*);
                break;
            
            case DT_FINI_ARRAY:
                si->fini_array = (void**)(load_bias + d->d_un.d_ptr);
                break;
            
            case DT_FINI_ARRAYSZ:
                si->fini_array_count = d->d_un.d_val / sizeof(void*);
                break;
            
            // ... 其他类型
        }
    }
    
    return true;
}

4.4 为什么要先解析壳so的dynamic?

C
// 原因1:壳so和子so共享字符串表和符号表
// 第一次prelink后得到:
si->strtab = 0x7E23C38;   // 壳so的字符串表(已解密)
si->symtab = 0x7E01990;   // 壳so的符号表(已解密)

// 这些表包含了:
// - 壳so自己的符号
// - 系统库的符号(libc.so、libm.so等)
// - 子so的部分符号(被加密后存储)

步骤5:第二次prelink - 解析子so的dynamic段

5.1 子so的dynamic段特点

text
子so的dynamic段非常精简,只包含:
- DT_INIT_ARRAY / DT_INIT_ARRAYSZ
- DT_FINI_ARRAY / DT_FINI_ARRAYSZ
- DT_VERSYM / DT_VERDEF(版本信息)

原因:
- 符号表、字符串表、重定位表都使用壳so的(已共享)
- 只需要修正init/fini函数指针

5.2 子so的dynamic段内容

C
// 从dump出的数据解析
00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  // DT_NEEDED
3C A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00  // DT_NEEDED
46 A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00  // DT_NEEDED
4E A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00  // DT_NEEDED
57 A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00  // DT_NEEDED
5F A4 01 00 00 00 00 00  19 00 00 00 00 00 00 00  // 关键!
D0 B4 48 04 00 00 00 00  1B 00 00 00 00 00 00 00  // DT_INIT_ARRAY (0x19)
48 00 00 00 00 00 00 00  1A 00 00 00 00 00 00 00  // DT_INIT_ARRAYSZ (0x1B) = 0x48 = 72字节 = 9个函数
18 B5 48 04 00 00 00 00  1C 00 00 00 00 00 00 00  // DT_FINI_ARRAY (0x1A)
10 00 00 00 00 00 00 00  FE FF FF 6F 00 00 00 00  // DT_FINI_ARRAYSZ (0x1C) = 0x10 = 16字节 = 2个函数
C8 E1 02 00 00 00 00 00  FF FF FF 6F 00 00 00 00  // DT_VERSYM (0x6FFFFFFE)
03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  // DT_VERDEF (0x6FFFFFFC)
                                                    // DT_NULL

5.3 第二次prelink的作用

C
// 第二次prelink会:
// 1. 用子so的init_array覆盖壳so的
si->init_array = (void**)(child_base + 0x448B4D0);  // 子so的9个init函数
si->init_array_count = 9;

// 2. 用子so的fini_array覆盖壳so的
si->fini_array = (void**)(child_base + 0x448B518);  // 子so的2个fini函数
si->fini_array_count = 2;

// 3. 其他字段保持不变(继续使用壳so的)
// si->strtab, si->symtab, si->rela 等都不变

步骤6:relocate - 执行重定位

6.1 什么是重定位?

重定位是将符号引用解析为实际地址的过程。

例子:

C
// 子so中的代码调用了libc的printf
extern int printf(const char* fmt, ...);

void test() {
    printf("Hello\n");  // 编译时printf地址未知
}

// 编译后生成:
00B0D580: BL printf   // 实际指令:BL 0x0(地址待重定位)

// 链接器需要:
// 1. 在符号表中找到printf这个符号
// 2. 在libc.so中找到printf的实际地址(如0x7F12345678)
// 3. 将指令修改为:BL 0x7F12345678

6.2 重定位表结构

C
// 每个重定位条目
typedef struct {
    uint64_t r_offset;  // 需要修改的位置(相对于base的偏移)
    uint64_t r_info;    // [高32位:符号索引 | 低32位:重定位类型]
    int64_t  r_addend;  // 附加值
} Elf64_Rela;

// 示例:
Elf64_Rela rela[] = {
    // RELATIVE类型(相对重定位)
    {
        .r_offset = 0x448B4D0,
        .r_info   = 0x0000000000000403,  // 类型=0x403, 符号索引=0
        .r_addend = 0xB0D5DC
    },
    
    // JUMP_SLOT类型(PLT重定位)
    {
        .r_offset = 0x7DFBAE8,
        .r_info   = 0x0000005100000402,  // 类型=0x402, 符号索引=0x51
        .r_addend = 0
    }
};

6.3 重定位类型对应关系

C
// ARM64重定位类型(十六进制 → 十进制)
#define R_AARCH64_JUMP_SLOT   1026  // 0x402 - PLT跳转
#define R_AARCH64_GLOB_DAT    1025  // 0x401 - 全局数据
#define R_AARCH64_RELATIVE    1027  // 0x403 - 相对重定位
#define R_AARCH64_ABS64       1024  // 0x404 - 绝对地址
#define R_AARCH64_TLS_TPREL64 1030  // 0x406 - TLS相对偏移
#define R_AARCH64_IRELATIVE   1032  // 0x408 - 间接相对

6.4 relocate实现

C
// 伪代码还原(对应原文的switch case)
bool relocate(soinfo* si) {
    Elf64_Rela* rela = si->rela;
    size_t rela_count = si->rela_count;
    void* base = si->base;
    Elf64_Sym* symtab = si->symtab;
    const char* strtab = si->strtab;
    
    // 遍历重定位表
    for (size_t i = 0; i < rela_count; i++) {
        uint32_t type = rela[i].r_info & 0xFFFFFFFF;
        uint32_t sym_idx = rela[i].r_info >> 32;
        void* reloc_addr = base + rela[i].r_offset;
        
        switch (type) {
            case 0x403:  // R_AARCH64_RELATIVE(最常见)
                // 相对重定位:不需要查符号表
                if (sym_idx != 0) {
                    // 有符号索引的情况
                    goto LABEL_37;  // 错误处理
                }
                
                // 公式:*reloc_addr = base + addend
                *(uint64_t*)reloc_addr = (uint64_t)base + rela[i].r_addend;
                break;
            
            case 0x402:  // R_AARCH64_GLOB_DAT
            case 0x401:  // R_AARCH64_JUMP_SLOT
                // PLT/GOT重定位:需要查符号表
                {
                    // 1. 从符号表获取符号
                    Elf64_Sym* sym = &symtab[sym_idx];
                    const char* sym_name = strtab + sym->st_name;
                    
                    // 2. 查找符号地址
                    void* sym_addr = NULL;
                    
                    if (sym->st_shndx != SHN_UNDEF) {
                        // 符号在当前so中定义
                        sym_addr = base + sym->st_value;
                    } else {
                        // 符号在依赖库中,需要搜索
                        sym_addr = lookup_symbol(sym_name, si->deps);
                    }
                    
                    if (!sym_addr) {
                        // 符号未找到
                        return false;
                    }
                    
                    // 3. 写入地址
                    *(uint64_t*)reloc_addr = (uint64_t)sym_addr + rela[i].r_addend;
                }
                break;
            
            case 0x404:  // R_AARCH64_ABS64
            case 0x405:  // R_AARCH64_ABS32
                // 绝对地址重定位
                {
                    Elf64_Sym* sym = &symtab[sym_idx];
                    void* sym_addr = base + sym->st_value;
                    
                    if (type == 0x404) {
                        *(uint64_t*)reloc_addr = (uint64_t)sym_addr + rela[i].r_addend;
                    } else {
                        *(uint32_t*)reloc_addr = (uint32_t)((uint64_t)sym_addr + rela[i].r_addend);
                    }
                }
                break;
            
            case 0x406:  // R_AARCH64_TLS_TPREL64
            case 0x407:  // R_AARCH64_TLS_DTPREL64
                // TLS重定位
                goto LABEL_31;
            
            case 0x408:  // R_AARCH64_IRELATIVE
                // 间接相对重定位(GNU扩展)
                goto LABEL_27;
            
            case 257:    // 0x101 - 某种自定义类型
                goto LABEL_24;
            
            case 260:    // 0x104 - 另一种自定义类型
                {
                    // 复杂计算(可能是加固特有的)
                    uint64_t v21 = *(rela + 2) +  // r_addend
                                   base - 
                                   *(rela - 2) +   // 前一个条目的某个字段
                                   some_offset;
                    *(uint64_t*)reloc_addr = v21;
                }
                break;
            
            default:
                // 未知类型
                goto LABEL_33;
        }
    }
    
    return true;
}

6.5 符号查找过程

C
// lookup_symbol的实现(简化版)
void* lookup_symbol(const char* name, soinfo** deps) {
    // 1. 先在当前so中查找
    Elf64_Sym* sym = find_symbol_in_so(name, current_so);
    if (sym && sym->st_shndx != SHN_UNDEF) {
        return current_so->base + sym->st_value;
    }
    
    // 2. 在依赖库中查找
    for (soinfo** dep = deps; *dep; dep++) {
        sym = find_symbol_in_so(name, *dep);
        if (sym && sym->st_shndx != SHN_UNDEF) {
            return (*dep)->base + sym->st_value;
        }
    }
    
    // 3. 未找到
    return NULL;
}

// Hash表加速的符号查找
Elf64_Sym* find_symbol_in_so(const char* name, soinfo* si) {
    // 计算符号名的hash
    uint32_t hash = elf_hash(name);
    
    // 在Hash桶中查找
    uint32_t n = si->bucket[hash % si->nbucket];
    
    while (n != 0) {
        Elf64_Sym* sym = &si->symtab[n];
        const char* sym_name = si->strtab + sym->st_name;
        
        if (strcmp(name, sym_name) == 0) {
            return sym;  // 找到
        }
        
        n = si->chain[n];  // 链表下一项
    }
    
    return NULL;  // 未找到
}

步骤7:call_init - 调用子so的初始化函数

7.1 init_array的作用

C
// .init_array段包含一个函数指针数组
// linker在重定位完成后会依次调用这些函数

// 子so的init_array(已重定位后)
void* init_array[] = {
    0x72376B0D5DC,  // sub_B0D5DC
    0x72376B0D964,  // sub_B0D964
    0x72376B0D9F0,  // sub_B0D9F0
    0x72376B0DF70,  // sub_B0DF70
    0x72376B0E08C,  // sub_B0E08C
    0x72376B0E16C,  // sub_B0E16C
    0x72376B0E1E4,  // sub_B0E1E4
    0x72376B0E320,  // sub_B0E320
    0x72376B0E354,  // sub_B0E354
    0x72377D440DC,  // 壳so的第一个init(保留)
    NULL
};

7.2 调用过程

C
// 对应原文的代码
v13 = *(_QWORD *)(v6 + 200);  // init_array_count(应该是偏移0xC8)
if ( (_DWORD)v13 )
{
    v14 = (unsigned int)v13;
    do
    {
        // 检查函数指针有效性
        if ( (unsigned __int64)*v12 + 1 >= 2 )
            v8 = (*v12)(v8);  // 调用init函数
        
        ++v14;
        ++v12;  // 下一个函数指针
    }
    while ( v14 );
}

还原为可读代码:

C
// call_init实现
void call_init(soinfo* si) {
    void** init_array = si->init_array;
    size_t count = si->init_array_count;
    
    for (size_t i = 0; i < count; i++) {
        void* func = init_array[i];
        
        // 检查函数指针有效性
        // (func + 1 >= 2) 等价于 (func != NULL && func != -1)
        if ((uintptr_t)func + 1 >= 2) {
            // 调用init函数(通常签名为 void (*init)(void))
            ((void(*)(void))func)();
        }
    }
}

7.3 为什么要检查 *v12 + 1 >= 2

C
// 这是一个巧妙的NULL和-1检查
uintptr_t ptr = (uintptr_t)func;

// 情况1:func == NULL (0x0)
ptr + 1 = 0x1 < 2  → 不调用 ✓

// 情况2:func == -1 (0xFFFFFFFFFFFFFFFF)
ptr + 1 = 0x0 < 2  → 不调用 ✓

// 情况3:func == 正常地址 (如 0x72376B0D5DC)
ptr + 1 = 0x72376B0D5DD >= 2  → 调用 ✓

// 相当于:
if (func != NULL && func != (void*)-1) {
    func();
}

三、完整流程图(带数据流)

text
┌─────────────────────────────────────────────────────────┐
│ 第三个INIT函数开始                                        │
└───────────────────┬─────────────────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ [1] malloc(soinfo_size) → v6              │
    │     memset(v6, 0, soinfo_size)            │
    └───────────────┬───────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ [2] 解密子so代码段                         │
    │     decrypt_rc4(encrypted_code, v11)       │
    │     *(v6+136) = v11  // 记录到soinfo       │
    │                                            │
    │     解密子so数据段                         │
    │     decrypt_rc4(encrypted_data, v26)       │
    └───────────────┬───────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ [3] load_segment                           │
    │     for each PT_LOAD phdr:                 │
    │       mprotect(seg, RWX)                   │
    │       memset(seg, 0xBB)  ← 填充标记        │
    │       memcpy(seg, decrypted_data)          │
    │       mprotect(seg, correct_prot)          │
    └───────────────┬───────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ [4] 第一次prelink(壳so的dynamic)         │
    │     parse_dynamic(shell_dynamic)           │
    │     → strtab = 0x7E23C38                   │
    │     → symtab = 0x7E01990                   │
    │     → rela = 0x7CFF4E8                     │
    └───────────────┬───────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ [5] 第二次prelink(子so的dynamic)         │
    │     parse_dynamic(child_dynamic)           │
    │     → init_array = 0x448B4D0 (9个函数)     │
    │     → fini_array = 0x448B518 (2个函数)     │
    │     (其他字段保持壳so的)                 │
    └───────────────┬───────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ [6] relocate                               │
    │     for each rela entry:                   │
    │       switch (type) {                      │
    │         case 0x403: // RELATIVE            │
    │           *addr = base + addend            │
    │         case 0x402: // JUMP_SLOT           │
    │           sym = lookup_symbol(...)         │
    │           *addr = sym + addend             │
    │         ...                                │
    │       }                                    │
    └───────────────┬───────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ [7] call_init                              │
    │     for i in 0..init_array_count:          │
    │       if init_array[i] is valid:           │
    │         init_array[i]()  ← 执行初始化     │
    └───────────────┬───────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 子so成功加载,JNI_OnLoad等函数可以正常调用               │
└─────────────────────────────────────────────────────────┘

四、关键技术点总结

1. 安全设计亮点

技术点传统加壳某顿加固安全提升
代码存储 整体加密 分段加密,分散存储 ⭐⭐⭐⭐⭐
内存映射 mmap直接映射 mprotect+memcpy间接映射 ⭐⭐⭐⭐
元数据 完整保留 Program Header分离存储 ⭐⭐⭐⭐⭐
符号表 独立 与壳so共享(融合) ⭐⭐⭐
重定位 标准流程 自定义linker实现 ⭐⭐⭐⭐
dump防护 内存中完整 从未完整出现 ⭐⭐⭐⭐⭐

2. 为什么难以dump?

text
问题1:无法定位子so的边界
└─ 代码和数据混在壳so的内存空间中
└─ 没有独立的/proc/[pid]/maps条目

问题2:缺少关键元数据
├─ Program Header分离存储(解密后才构建)
├─ Dynamic段信息不完整(diff格式)
└─ Section Header完全没有

问题3:时间窗口极短
└─ 解密 → 映射 → 重定位 → 执行
└─ 解密后的数据立即被使用,无法dump完整状态

问题4:符号表融合
└─ 子so符号混在壳so符号表中
└─ 单纯dump内存无法区分

3. 作者的脱壳策略

text
突破点:在relocate之前dump
├─ 此时代码和数据已解密
├─ soinfo结构体包含所有元数据
├─ Program Header已经构建
└─ 符号表和重定位表都在内存中

修复策略:"借尸还魂"
├─ 以壳so为基础
├─ 移植子so的代码和数据段
├─ 合并重定位表
├─ 覆盖符号表
└─ 手动修复Section Header

五、实战要点

如何在IDA中定位第三个INIT函数?

Python
# 1. 找到init_array
init_array_addr = 0x7DF6A50  # 从readelf -d获取

# 2. 查看第三个函数指针
third_init_ptr = Qword(init_array_addr + 16)  # 第三个(8*2偏移)

# 3. 跳转并分析
Jump(third_init_ptr)

如何识别关键函数?

C
// 识别malloc_wrapper
// 特征:调用malloc后立即memset清零
v31 = sub_XXXXXX(0x338);
sub_YYYYYY(v31, 0, 0x338);  // memset
→ sub_XXXXXX 就是 malloc_wrapper

// 识别decrypt函数
// 特征:
// - 参数多(源地址、大小、目标地址等)
// - 有循环结构
// - 有异或操作
// - 调用后数据变化明显

// 识别mprotect
// 特征:三个参数 (addr, size, prot)
sub_ZZZZZZ(aligned_addr, size, 7);  // 7 = RWX
→ sub_ZZZZZZ 就是 mprotect包装

// 识别relocate
// 特征:大switch case,case值为0x401, 0x402, 0x403等

如何dump关键数据?

Python
# Frida脚本框架
Interceptor.attach(relocate_addr, {
    onEnter: function(args) {
        var soinfo = args[0];
        
        // 读取soinfo各字段
        var base = soinfo.add(0x08).readPointer();
        var symtab = soinfo.add(0x40).readPointer();
        var rela = soinfo.add(0x60).readPointer();
        // ...
        
        // dump到文件
        dump_memory(seg1_addr, seg1_size, "dump_seg1");
        dump_memory(symtab, symtab_size, "dump_sym");
        // ...
    }
});

这就是第三个INIT函数的完整工作流程!它是整个加固方案的核心,完整实现了一个自定义的动态链接器。

 

 

 

 

 

 

其核心思路是 “借尸还魂”——即利用被加固的“壳so”的框架,将解密后的真实“子so”的代码、数据及关键信息移植进去,并修复运行环境,最终得到一个可正常加载和运行的、已脱壳的so文件。

以下是整个流程的核心步骤总结:

一、 核心思路:借壳还魂

  • 目标:无法直接从内存中dump完整的解密so,因为壳自己实现了linker,子so的代码和数据是分段解密、分开存储的。

  • 方法:不直接重建子so,而是改造壳so。将解密后的子so代码段、数据段等信息“移植”到壳so的文件结构中,并修复所有使其能正常运行的关键数据。

二、 关键修复步骤

1. 移植代码段和数据段

  • 操作:

    1. Dump解密数据:使用IDA脚本从内存中dump出已解密的子so代码段和数据段。

    2. 移植代码段:将子so的代码段数据覆盖到壳so的对应加密段位置。

    3. 移植数据段:

      • 修改壳so的Program Header,删除一个非必要的段(如GNU_RELRO),将其替换为子so数据段的头信息。

      • 由于文件空间紧张,将子so的数据段内容附加到文件末尾,并通过修改对应Program Header中的file offset字段来正确指向它。

    4. 修复Section Header:修正ELF头中Section Header的偏移量,使其指向文件末尾的新位置,避免IDA等工具解析错误。

2. 修复符号表与重定位表(最复杂环节)

  • 问题:直接移植后,函数调用无法解析,因为重定位信息指向的是子so的符号表,而壳so有自己的符号表。

  • 操作:

    1. 合并重定位表:将壳so和子so的重定位表合并成一个新的大表。

    2. 替换符号表:用子so的符号表覆盖壳so的符号表,以确保重定位索引正确。

    3. 更新文件结构:

      • 将合并后的新重定位表附加到文件末尾。

      • 修改Dynamic Segment中关于重定位表位置和大小等信息。

      • 再次调整数据段和Section Header的文件布局。

    4. 手动修复冲突:由于覆盖了符号表,壳so自身的部分重定位会失效,需要手动修复少量(约10处)重定位项。

3. 修复初始化数组(init_array)

  • 操作:

    1. 将子so的init_array地址写入Dynamic Segment,确保子so的初始化函数能执行。

    2. 将壳so的第一个初始化函数添加到子so的init_array中,因为壳的某些功能依赖于它。

4. 添加正确的Section Header以供Linker加载

  • 操作:Android系统的linker需要特定的Section Header来正确加载SO。为确保兼容性,移植了必要的Section Header,主要包括:

    • .shstrtab (节区名称字符串表)

    • .dynamic

    • .dynstr (动态链接字符串表)

三、 成果与验证

  • 将修复后的so文件替换原文件,游戏可正常运行。

  • 通过调试,在内存中dump出被某顿加密的global-metadata.dat文件(IL2CPP的关键文件),并修复其文件头后,成功使用Il2CppDumper导出C#脚本,完成了对游戏的完全脱壳。

四、 技术总结与特点

  • 某顿加固的先进性:与传统整体加壳不同,它通过自实现linker、分段加解密、数据分离存储,使得内存中从未出现完整子so,极大增加了脱壳难度。

  • 脱壳技术的复杂性:此“借尸还魂”方案需要对ELF文件格式、Android linker的加载机制、重定位过程有极其深入的理解。

  • 技术价值:该过程不仅是一次成功的逆向实践,更是一次对底层链接、加载和文件格式知识的深度巩固和应用。

重要声明:本文内容旨在技术学习和交流,严禁用于任何非法活动。

 

1.dump so

2.从原版复制修复24字节的elf头

3.修复so,使用工具

4.修复elf字段

 

 自动化脱壳

Il2CppDumper  对加固的无效

https://github.com/vfsfitvnm/frida-il2cpp-bridge

 https://github.com/yukiarrr/Il2cppSpy/releases

安装node稳定版本

安装python3.10以上,同时安装好frida和pip,同时在py安装命令下复制一个python.exe为python3.exe支持frida-il2cpp-bridge会使用python3命令

 

 详细流程

不编写c#和最小代码比较,再最小化修改后比较il2cpp

加固前后比对差异

但是加固要3000元/7天目前有可能困难

 

 

IDA动态调试和Dump加密SO文件完整教程

一、环境准备

1. 所需工具

- IDA Pro 7.5+(带Android调试器)
- 已Root的Android设备或模拟器
- ADB工具
- 目标APK
- android_server(IDA自带的调试服务端)

2. 配置IDA调试环境

# 1. 找到IDA安装目录下的android_server
# Windows: C:\Program Files\IDA 7.x\dbgsrv\android_server
# 或 android_server64(64位)

# 2. 推送到设备
adb push android_server /data/local/tmp/
adb shell chmod 755 /data/local/tmp/android_server

# 3. 启动调试服务
adb shell
su
cd /data/local/tmp
./android_server -p23946

# 4. 端口转发
adb forward tcp:23946 tcp:23946

二、动态调试步骤

方法1:附加到运行中的进程

Step 1: 启动目标APP

# 先启动APP
adb shell am start -n 包名/主Activity
# 例如:adb shell am start -n com.example.game/.MainActivity

Step 2: IDA附加进程

  1. 打开IDA Pro → Debugger → Attach → Remote ARM Linux/Android debugger
  2. 配置连接
    • Hostname: localhost
    • Port: 23946
  3. 选择进程:找到目标APP的进程
  4. 附加成功后,程序会暂停

Step 3: 定位目标SO文件

# IDA中查看已加载的模块
# View → Open Subviews → Segments
# 或使用快捷键 Ctrl+S

# 在IDA Python控制台执行:
import idaapi
import idc

# 列出所有加载的模块
for module in idautils.Modules():
    print(f"Base: {hex(module.base)}, Size: {hex(module.size)}, Name: {module.name}")

方法2:从启动开始调试

Step 1: IDA配置启动调试

  1. Debugger → Process options
  2. 设置:
    Application: /system/bin/app_process64  # 或 app_process32Parameters: -Djava.class.path=/data/local/tmp/android_server /system/binInput file: (留空)Directory: /data/local/tmpHostname: localhostPort: 23946
    

Step 2: 使用命令行启动

# 更简单的方法:使用am start配合等待
adb shell am start -D -n 包名/.MainActivity
# -D 参数会让APP启动后等待调试器连接

# 然后在IDA中附加到这个进程

三、定位解密点

1. 通过init_array断点

根据文章内容,关键解密发生在init函数中:

# IDA Python脚本
import idaapi
import idc

# 1. 找到.init_array位置
# 方法1:通过segment查找
for seg in idautils.Segments():
    seg_name = idc.get_segm_name(seg)
    if "init_array" in seg_name:
        print(f"Found init_array at: {hex(seg)}")
        
# 方法2:通过dynamic段查找
# 查找DT_INIT_ARRAY (type=25)
def find_init_array():
    # 假设dynamic段在0x某地址
    dynamic_addr = 0x7df6a50  # 从readelf或IDA中获取
    
    # 读取并解析
    ea = dynamic_addr
    while True:
        tag = idc.get_qword(ea)
        val = idc.get_qword(ea + 8)
        
        if tag == 0:  # DT_NULL
            break
        elif tag == 25:  # DT_INIT_ARRAY
            print(f"INIT_ARRAY at: {hex(val)}")
            return val
        elif tag == 27:  # DT_INIT_ARRAYSZ
            print(f"INIT_ARRAY size: {hex(val)}")
            
        ea += 16
    
    return None

init_array_addr = find_init_array()

2. 在init函数下断点

# 读取init_array中的函数地址
init_array_addr = 0x7df6a50  # 替换为实际地址

# 读取第一个init函数
init_func1 = idc.get_qword(init_array_addr)
print(f"First init function: {hex(init_func1)}")

# 添加断点
idc.add_bpt(init_func1)
print(f"Breakpoint added at {hex(init_func1)}")

# 如果有多个init函数
for i in range(3):  # 假设有3个init函数
    func_addr = idc.get_qword(init_array_addr + i * 8)
    idc.add_bpt(func_addr)
    print(f"Breakpoint {i+1} added at {hex(func_addr)}")

3. 通过函数名定位

# 搜索特定函数
def find_function(pattern):
    """搜索包含特定字符串的函数"""
    for func_ea in idautils.Functions():
        func_name = idc.get_func_name(func_ea)
        if pattern in func_name:
            print(f"Found: {func_name} at {hex(func_ea)}")
            return func_ea
    return None

# 例如搜索JNI_OnLoad
jni_onload = find_function("JNI_OnLoad")
if jni_onload:
    idc.add_bpt(jni_onload)

四、Dump内存数据

1. 手动Dump指定区域

import idaapi
import idc

def dump_memory(start_addr, size, output_file):
    """
    Dump指定内存区域到文件
    
    Args:
        start_addr: 起始地址
        size: 要dump的大小
        output_file: 输出文件路径
    """
    try:
        # 读取内存
        data = idaapi.dbg_read_memory(start_addr, size)
        
        if data is None:
            print(f"Failed to read memory at {hex(start_addr)}")
            return False
        
        # 写入文件
        with open(output_file, 'wb') as f:
            f.write(data)
        
        print(f"Successfully dumped {hex(size)} bytes from {hex(start_addr)}")
        print(f"Saved to: {output_file}")
        return True
        
    except Exception as e:
        print(f"Error: {e}")
        return False

# 使用示例
# 1. Dump代码段(从segment信息中获取地址和大小)
code_base = 0x70000000  # 替换为实际地址
code_size = 0x447c000
dump_memory(code_base, code_size, "C:/dump/code_segment.bin")

# 2. Dump数据段
data_base = 0x74480000  # 替换为实际地址
data_size = 0x100000
dump_memory(data_base, data_size, "C:/dump/data_segment.bin")

2. Dump整个SO模块

def dump_module(module_name, output_file):
    """
    Dump整个模块到文件
    
    Args:
        module_name: 模块名(如 libil2cpp.so)
        output_file: 输出文件路径
    """
    import idautils
    
    # 查找模块
    for module in idautils.Modules():
        if module_name in module.name:
            print(f"Found module: {module.name}")
            print(f"Base: {hex(module.base)}")
            print(f"Size: {hex(module.size)}")
            
            # Dump整个模块
            return dump_memory(module.base, module.size, output_file)
    
    print(f"Module {module_name} not found")
    return False

# 使用
dump_module("libil2cpp.so", "C:/dump/libil2cpp_dumped.so")

3. 监控并Dump解密后的数据

class DecryptMonitor:
    """监控解密过程并自动dump"""
    
    def __init__(self):
        self.decrypt_base = None
        self.decrypt_size = None
        
    def on_decrypt_complete(self, base_addr, size):
        """解密完成后的回调"""
        print(f"Decryption detected at {hex(base_addr)}, size: {hex(size)}")
        
        # 立即dump
        output_file = f"C:/dump/decrypted_{hex(base_addr)}.bin"
        dump_memory(base_addr, size, output_file)
        
        # 可以进一步分析
        self.analyze_decrypted_data(base_addr, size)
    
    def analyze_decrypted_data(self, addr, size):
        """分析解密后的数据"""
        # 检查ELF魔数
        magic = idaapi.dbg_read_memory(addr, 4)
        if magic and magic[:4] == b'\x7fELF':
            print("Detected ELF file!")
            
        # 搜索特定字符串
        data = idaapi.dbg_read_memory(addr, min(size, 0x10000))
        if b"JNI_OnLoad" in data:
            print("Found JNI_OnLoad in decrypted data!")

monitor = DecryptMonitor()

4. 在关键函数处Dump

根据文章,第三个init函数会解密子so:

# 在解密函数返回前设置断点并dump
def setup_decrypt_dump():
    """在解密完成后dump数据"""
    
    # 假设找到了解密函数
    decrypt_func = 0x12345678  # 替换为实际地址
    
    # 在函数返回前的地址下断点
    # 可以通过F5反编译查看,在return语句对应的地址下断点
    
    # 或者用脚本自动找到RET指令
    func_end = idc.find_func_end(decrypt_func)
    
    # 回溯找RET
    ea = func_end - 4
    while ea > decrypt_func:
        if idc.print_insn_mnem(ea) == "RET":
            print(f"Found RET at {hex(ea)}")
            idc.add_bpt(ea)
            break
        ea -= 4

# 设置条件断点,当执行到这里时自动dump
def breakpoint_callback(ea):
    """断点回调函数"""
    print(f"Hit breakpoint at {hex(ea)}")
    
    # 根据寄存器获取解密后的地址
    # ARM64: X0通常是返回值或第一个参数
    decrypted_addr = idc.get_reg_value("X0")
    size = idc.get_reg_value("X1")
    
    if decrypted_addr and size:
        dump_memory(decrypted_addr, size, f"C:/dump/auto_dump_{hex(ea)}.bin")
    
    return 0  # 继续执行

# 注册断点回调
idc.set_bpt_cond(breakpoint_addr, "breakpoint_callback(here)")

五、实战案例:按文章步骤Dump

场景:Dump子SO的代码段和数据段

class IL2CPPDumper:
    """专门用于dump IL2CPP加固的工具"""
    
    def __init__(self, module_name="libil2cpp.so"):
        self.module_name = module_name
        self.module_base = self.find_module_base()
        
    def find_module_base(self):
        """找到模块基址"""
        import idautils
        for module in idautils.Modules():
            if self.module_name in module.name:
                print(f"Module base: {hex(module.base)}")
                return module.base
        return None
    
    def dump_after_third_init(self):
        """在第三个init函数执行后dump"""
        
        # 1. 找到init_array
        init_array = self.find_init_array()
        if not init_array:
            print("Failed to find init_array")
            return
        
        # 2. 获取第三个init函数
        third_init = idc.get_qword(init_array + 2 * 8)
        print(f"Third init function: {hex(third_init)}")
        
        # 3. 找到解密完成的位置(通过阅读代码确定)
        # 假设在解密完成后会调用某个函数
        # 可以通过F5查看代码逻辑
        
        # 4. 在合适的位置下断点
        # 这里需要根据实际分析确定
        breakpoint_addr = third_init + 0x1234  # 示例偏移
        idc.add_bpt(breakpoint_addr)
        
        print("Breakpoint set, continue execution (F9)")
        print("When hit, run: dumper.do_dump()")
    
    def do_dump(self):
        """执行dump操作"""
        
        # 从文章中可以知道,解密后的数据在特定位置
        # 需要在调试时查看寄存器或变量确定地址
        
        # 示例:假设X20寄存器指向解密后的代码段基址
        code_segment_base = idc.get_reg_value("X20")
        code_segment_size = idc.get_reg_value("X21")
        
        if code_segment_base and code_segment_size:
            dump_memory(code_segment_base, code_segment_size, 
                       "C:/dump/child_so_code.bin")
        
        # 数据段同理
        data_segment_base = idc.get_reg_value("X22")
        data_segment_size = idc.get_reg_value("X23")
        
        if data_segment_base and data_segment_size:
            dump_memory(data_segment_base, data_segment_size,
                       "C:/dump/child_so_data.bin")
        
        # Dump program header
        ph_addr = idc.get_reg_value("X24")
        if ph_addr:
            # Program header表大小 = entry_size * entry_count
            dump_memory(ph_addr, 0x38 * 7, "C:/dump/program_headers.bin")
        
        print("Dump completed!")
    
    def dump_dynamic_info(self):
        """Dump dynamic段信息"""
        # 通过查找DT_DYNAMIC
        for seg_ea in idautils.Segments():
            seg_name = idc.get_segm_name(seg_ea)
            if "dynamic" in seg_name.lower():
                seg_end = idc.get_segm_end(seg_ea)
                size = seg_end - seg_ea
                dump_memory(seg_ea, size, "C:/dump/dynamic_segment.bin")
                print(f"Dynamic segment dumped from {hex(seg_ea)}")
                break

# 使用流程
dumper = IL2CPPDumper()
dumper.dump_after_third_init()
# ... 在断点触发后 ...
dumper.do_dump()

六、解析Dump出的数据

1. 解析ELF Header

import struct

def parse_elf_header(filename):
    """解析ELF文件头"""
    with open(filename, 'rb') as f:
        data = f.read(64)  # ELF header是64字节(64位)或52字节(32位)
        
        # 检查魔数
        if data[:4] != b'\x7fELF':
            print("Not a valid ELF file!")
            return None
        
        # 解析基本信息
        ei_class = data[4]  # 1=32位, 2=64位
        ei_data = data[5]   # 1=小端, 2=大端
        
        if ei_class == 2:  # 64位
            # 解析ELF64 header
            e_type, e_machine = struct.unpack('<HH', data[16:20])
            e_version = struct.unpack('<I', data[20:24])[0]
            e_entry = struct.unpack('<Q', data[24:32])[0]
            e_phoff = struct.unpack('<Q', data[32:40])[0]
            e_shoff = struct.unpack('<Q', data[40:48])[0]
            e_flags = struct.unpack('<I', data[48:52])[0]
            e_ehsize = struct.unpack('<H', data[52:54])[0]
            e_phentsize = struct.unpack('<H', data[54:56])[0]
            e_phnum = struct.unpack('<H', data[56:58])[0]
            e_shentsize = struct.unpack('<H', data[58:60])[0]
            e_shnum = struct.unpack('<H', data[60:62])[0]
            e_shstrndx = struct.unpack('<H', data[62:64])[0]
            
            print(f"ELF Type: {e_type}")
            print(f"Machine: {e_machine} (ARM64=0xB7)")
            print(f"Entry point: {hex(e_entry)}")
            print(f"Program header offset: {hex(e_phoff)}")
            print(f"Program header count: {e_phnum}")
            print(f"Section header offset: {hex(e_shoff)}")
            print(f"Section header count: {e_shnum}")
            
            return {
                'phoff': e_phoff,
                'phnum': e_phnum,
                'phentsize': e_phentsize,
                'shoff': e_shoff,
                'shnum': e_shnum,
                'shentsize': e_shentsize,
            }

parse_elf_header("C:/dump/libil2cpp_dumped.so")

2. 解析Program Headers

def parse_program_headers(filename, phoff, phnum, phentsize=56):
    """解析程序头表"""
    with open(filename, 'rb') as f:
        f.seek(phoff)
        
        PT_TYPES = {
            0: "PT_NULL",
            1: "PT_LOAD",
            2: "PT_DYNAMIC",
            3: "PT_INTERP",
            4: "PT_NOTE",
            6: "PT_PHDR",
        }
        
        for i in range(phnum):
            ph_data = f.read(phentsize)
            
            # 解析64位program header
            p_type = struct.unpack('<I', ph_data[0:4])[0]
            p_flags = struct.unpack('<I', ph_data[4:8])[0]
            p_offset = struct.unpack('<Q', ph_data[8:16])[0]
            p_vaddr = struct.unpack('<Q', ph_data[16:24])[0]
            p_paddr = struct.unpack('<Q', ph_data[24:32])[0]
            p_filesz = struct.unpack('<Q', ph_data[32:40])[0]
            p_memsz = struct.unpack('<Q', ph_data[40:48])[0]
            p_align = struct.unpack('<Q', ph_data[48:56])[0]
            
            type_name = PT_TYPES.get(p_type, f"UNKNOWN({p_type})")
            flags = ""
            if p_flags & 4: flags += "R"
            if p_flags & 2: flags += "W"
            if p_flags & 1: flags += "X"
            
            print(f"\nProgram Header {i}:")
            print(f"  Type: {type_name}")
            print(f"  Flags: {flags}")
            print(f"  Offset: {hex(p_offset)}")
            print(f"  VAddr: {hex(p_vaddr)}")
            print(f"  FileSz: {hex(p_filesz)}")
            print(f"  MemSz: {hex(p_memsz)}")

# 使用
header_info = parse_elf_header("C:/dump/libil2cpp_dumped.so")
if header_info:
    parse_program_headers("C:/dump/libil2cpp_dumped.so", 
                         header_info['phoff'],
                         header_info['phnum'])

3. 查找和解析Dynamic段

def find_and_parse_dynamic(filename):
    """查找并解析dynamic段"""
    
    # 先找到dynamic段的位置
    header_info = parse_elf_header(filename)
    
    with open(filename, 'rb') as f:
        # 读取program headers找DYNAMIC
        f.seek(header_info['phoff'])
        
        for i in range(header_info['phnum']):
            ph_data = f.read(header_info['phentsize'])
            p_type = struct.unpack('<I', ph_data[0:4])[0]
            
            if p_type == 2:  # PT_DYNAMIC
                p_offset = struct.unpack('<Q', ph_data[8:16])[0]
                p_filesz = struct.unpack('<Q', ph_data[32:40])[0]
                
                print(f"Found DYNAMIC segment at offset {hex(p_offset)}")
                
                # 读取dynamic段
                f.seek(p_offset)
                dynamic_data = f.read(p_filesz)
                
                # 解析dynamic entries
                DT_TYPES = {
                    0: "DT_NULL",
                    1: "DT_NEEDED",
                    5: "DT_STRTAB",
                    6: "DT_SYMTAB",
                    7: "DT_RELA",
                    25: "DT_INIT_ARRAY",
                    27: "DT_INIT_ARRAYSZ",
                    # 更多类型...
                }
                
                offset = 0
                while offset < len(dynamic_data):
                    d_tag = struct.unpack('<Q', dynamic_data[offset:offset+8])[0]
                    d_val = struct.unpack('<Q', dynamic_data[offset+8:offset+16])[0]
                    
                    if d_tag == 0:  # DT_NULL
                        break
                    
                    tag_name = DT_TYPES.get(d_tag, f"UNKNOWN({d_tag})")
                    print(f"  {tag_name}: {hex(d_val)}")
                    
                    offset += 16
                
                break

find_and_parse_dynamic("C:/dump/libil2cpp_dumped.so")

七、完整调试流程示例

# 完整的调试和dump流程脚本

class CompleteDebugger:
    def __init__(self, package_name, so_name="libil2cpp.so"):
        self.package_name = package_name
        self.so_name = so_name
        self.dump_dir = "C:/apk_dump/"
        
        import os
        if not os.path.exists(self.dump_dir):
            os.makedirs(self.dump_dir)
    
    def step1_attach_and_wait_load(self):
        """步骤1:附加进程并等待SO加载"""
        print("[Step 1] Attaching to process...")
        print("Please manually attach in IDA: Debugger -> Attach")
        print(f"Look for process: {self.package_name}")
        input("Press Enter after attached...")
        
        # 等待SO加载
        print("Waiting for SO to load...")
        while True:
            for module in idautils.Modules():
                if self.so_name in module.name:
                    print(f"[+] {self.so_name} loaded at {hex(module.base)}")
                    self.so_base = module.base
                    return True
            time.sleep(0.5)
    
    def step2_find_init_array(self):
        """步骤2:找到init_array"""
        print("\n[Step 2] Finding init_array...")
        
        # 使用readelf的方式:解析dynamic段
        # ... (使用前面的代码)
        
        # 或者直接在IDA中搜索
        init_array_addr = idc.get_name_ea_simple(".init_array")
        if init_array_addr != BADADDR:
            print(f"[+] Found init_array at {hex(init_array_addr)}")
            self.init_array = init_array_addr
            return True
        
        print("[-] init_array not found, need manual analysis")
        return False
    
    def step3_break_on_decrypt(self):
        """步骤3:在解密函数处下断点"""
        print("\n[Step 3] Setting breakpoints on decrypt functions...")
        
        # 在三个init函数处下断点
        for i in range(3):
            func_addr = idc.get_qword(self.init_array + i * 8)
            idc.add_bpt(func_addr)
            print(f"[+] Breakpoint {i+1} at {hex(func_addr)}")
        
        print("Press F9 to continue, breakpoints will be hit")
    
    def step4_dump_decrypted(self):
        """步骤4:解密完成后dump"""
        print("\n[Step 4] Dumping decrypted data...")
        print("Execute this when third init function completes")
        
        # 根据寄存器或全局变量获取地址
        # 这需要根据实际调试情况调整
        
        # 示例:dump整个模块
        for module in idautils.Modules():
            if self.so_name in module.name:
                output_file = f"{self.dump_dir}{self.so_name}_full_dump.bin"
                dump_memory(module.base, module.size, output_file)
                print(f"[+] Dumped to {output_file}")
                break
        
        # 还需要dump其他关键数据
        print("[!] Remember to also dump:")
        print("  - Program headers")
        print("  - Dynamic segment")
        print("  - Symbol table")
        print("  - Relocation table")
    
    def run(self):
        """运行完整流程"""
        self.step1_attach_and_wait_load()
        self.step2_find_init_array()
        self.step3_break_on_decrypt()
        input("\nPress Enter when ready to dump...")
        self.step4_dump_decrypted()
        print("\n[+] Debug and dump process completed!")

# 使用
debugger = CompleteDebugger("com.example.game")
debugger.run()

八、常见问题和技巧

1. 找不到解密后的数据位置

# 使用内存搜索
def search_memory_pattern(pattern):
    """在所有可读内存中搜索特征"""
    for seg_ea in idautils.Segments():
        seg_start = seg_ea
        seg_end = idc.get_segm_end(seg_ea)
        
        ea = seg_start
        while ea < seg_end:
            data = idaapi.dbg_read_memory(ea, 0x1000)
            if data and pattern in data:
                offset = data.find(pattern)
                print(f"Found at {hex(ea + offset)}")
            ea += 0x1000

# 搜索ELF头
search_memory_pattern(b'\x7fELF')

# 搜索JNI_OnLoad字符串
search_memory_pattern(b'JNI_OnLoad')

2. 监控内存变化

def monitor_memory_change(addr, size, interval=0.1):
    """监控内存区域的变化"""
    import time
    
    previous = idaapi.dbg_read_memory(addr, size)
    
    while True:
        current = idaapi.dbg_read_memory(addr, size)
        
        if current != previous:
            print(f"Memory changed at {hex(addr)}!")
            # 找出变化的位置
            for i in range(size):
                if current[i] != previous[i]:
                    print(f"  Offset {hex(i)}: {hex(previous[i])} -> {hex(current[i])}")
            
            previous = current
        
        time.sleep(interval)

3. 自动化脚本执行

# 在IDA启动时自动加载脚本
# File -> Script file -> 选择你的Python脚本

# 或者使用IDC脚本在特定断点自动执行
static breakpoint_handler(addr)
{
    auto reg_value;
    
    Message("Hit breakpoint at %x\n", addr);
    
    // 获取寄存器
    reg_value = get_reg_value("X0");
    Message("X0 = %x\n", reg_value);
    
    // 调用Python函数dump数据
    exec_python("dump_memory(0x%x, 0x1000, 'auto_dump.bin')" % reg_value);
    
    return 0; // 继续执行
}

这个完整的教程涵盖了从环境配置到实际dump和解析的全过程。关键是要理解整个加固和解密的流程,然后在关键节点进行断点和数据捕获。

 

 

 

 

示例

29天      mono型

ICEY艾希

钢铁部队

孤星大冒险

 https://www.yun88.com/product/4641.html

倩女幽魂   无顿 unity

摩尔庄园

 

il2型   加固HTPX和so加固
创造与魔法   

奥比岛

 

 

 

 

 

 

 

 

 

 

 


前言
前一段时间在逆向某游戏Y,被其丧心病狂的ollvm混淆折磨的欲仙欲死。游戏Y以牺牲性能为代价做安全保护的精神实在令人佩服,截图如图:

m_strcpy(v36, v38);
v39.n64_u64[0] = vdup_n_s32(v25).n64_u64[0];
v40 = (_QWORD *)(v114 + 40 * v24);
v41 = v25 ^ *((_DWORD *)v40 + 2) ^ 0x6FC1FC65;

if (v41 == -1) {
v42 = 0x2C44BE802BC13BE0LL;
} else {
v42 = qword_11A096A0 + 0x58LL * v41 + 0x2C44BE802BC13BE0LL;
}

v37[10] = v42;

*((_DWORD *)v37 + 0xD) = ((*((_DWORD *)v40 + 8) - 0x7C47B523) ^ v25) + 1104302448;
*((_DWORD *)v37 + 0xE) = (v25 ^ *((_DWORD *)v40 + 9) ^ 0x59DE33C6) + 3230933333;
*((_DWORD *)v37 + 4) = v25 ^ (*((_DWORD *)v40 + 4) - 1324464800) ^ 0x59279255;
*((_DWORD *)v37 + 0xC) = (v25 ^ *((_DWORD *)v40 + 5) ^ 0x5C32F766) + 1735669998;

v37[9] = vrev64_s32(
veor_s8(
veor_s8(v39, vadd_s32((int32x2_t)v40, (int32x2_t)0xA50EB293AA69A96ELL)),
(int8x8_t)0x1E704F5B30B8C466LL
).n64_u64[0]
);

*((_DWORD *)v37 + 8) = (v25 ^ *((_DWORD *)v40 + 6) ^ 0x2F721445) + 2105366011;
*(_DWORD *)v37 = (v25 ^ *((_DWORD *)v40 + 7) ^ 0x7D0C1E1D) + 941875987;

v43 = 0x43LL;
v44 = v37 + 8;
v45 = &off_11788B28;
v46 = (const char *)(*v30 ^ 0x4E15E9C45FEEA39LL);

do {
v47 = *v45;
if (!(unsigned int)m_strcmp(v46, (const char *)(*v45)[8])) {
*v44 = (char *)v47 + 0x4660EA4B5553B8E7LL;
}
++v45;
--v43;
} while (v43);

 

竟然对所有常数都进行了加密!

于是恼羞成怒打开另外一款小众的手游,想破解练练手,打开后发现是Unity的,上Il2cppdumper伺候:
Initializing metadata...
System.IO.InvalidDataException: ERROR: Metadata file supplied is not valid metadata file.
at I12CppDumper.Metadata.ctor(Stream stream) 位置 C:\projects\I12cppdumperMain\I12CppDumper\Metadata.cs:行号 53
at I12CppDumper.Program.Init(String il2cppPath, String metadataPath, Metadata& metadata, I12Cpp& il2cpp) 位置 C:\projects\il2cppdumper\Il2CppDumper\Program.cs:行号 126
at Il2CppDumper.Program.Main(String[] args) 位置 C:\projects\il2cppdumper\Il2CppDumper\Program.cs:行号 100
Press any key to exit...
这是一个 Il2CppDumper 工具的错误信息,主要内容是:

错误类型: System.IO.InvalidDataException
错误信息: 提供的 Metadata 文件不是有效的 metadata 文件
错误堆栈: 显示了错误发生在 Metadata 构造函数、Program.Init 方法和 Program.Main 方法中的具体行号

这个错误通常表示你提供给 Il2CppDumper 的 metadata 文件已损坏或格式不正确。

直接报错。

打开global-metadata.dat看看,发现被加密了,正常的MAgic 是 AF 1B B1 FA,被换上了奇怪的Magic:HTPX

文件名: global-metadata.dat
十六进制数据(部分关键行):
0000h: 48 54 50 58 48 91 11 01 3B 19 9E B2 C0 98 CF 1C
0010h: 02 16 39 74 2E B3 D4 B3 D7 A0 E1 D4 3D 6 Q6 19
0020h: E5 67 1F C0 BD 6E 0E A7 CF 67 08 A2 F3 19 AA A4
0030h: 30 6C 34 80 2E 34 E3 7E 0E 62 79 97 37 B6 8D 2B
0040h: EB 9F 20 58 89 E2 94 6F 19 D0 1C 6E EF 15 3F 1B
0050h: 76 2A 91 9C 83 40 76 D0 20 0B 58 DA B9 1A 93 FB
0060h: 66 EB C6 A0 A2 6E DC 58 EC AE 17 2E 7C 3E E7 21
0070h: EA 95 C3 Q9 52 41 2F 54 D6 A9 Q0 D4 10 A3 88 6B
0080h: 73 1A 40 87 56 95 B3 27 2D B3 73 C1 B4 8D 7E E9
0090h: 01 FB 97 10 9E AC A7 51 D2 21 70 62 4C 0D 6C 1D
...
右侧ASCII显示区域(红框部分)显示:
H T P X H . . .
这个文件是 Il2Cpp 游戏引擎的 global-metadata.dat 文件,文件头包含了特定的魔数标识。从十六进制数据来看:

起始字节为 48 54 50 58(对应ASCII为 "HTPX" 或类似)
这是 Il2CppDumper 工具需要处理的元数据文件

如果这个文件导致了之前的错误,可能是因为:

文件已损坏或不完整
文件版本与 Il2CppDumper 工具不兼容
文件被加密或混淆处理过

打开apk包,看到lib目录下有libNetHTProtect.so字样。原来是某顿手游加固:

根据图片中的文件列表内容,我识别出以下OCR文字:
libmain.so 2023/1/6 10:55 SO 文件 11 KB
libNetHTProtect.so 2023/1/6 10:55 SO 文件 3,794 KB
libnetmobsec-4.4.5.so 2023/1/6 10:55 SO 文件 1,249 KB
这是一个文件列表,显示了三个 .so 文件(Linux/Android共享库文件):

libmain.so - 11 KB
libNetHTProtect.so - 3,794 KB(红色箭头指向此文件)
libnetmobsec-4.4.5.so - 1,249 KB

所有文件的修改日期都是 2023年1月6日 10:55。
其中 libNetHTProtect.so 和 libnetmobsec-4.4.5.so 看起来像是网络保护/安全相关的库文件,这可能与之前 Il2Cpp metadata 文件的加密或保护有关。

抱着试一试的心情,想看看他的保护是怎么做的,但是一不小心就搞了一周。由于内容太过精彩,于是想总结成文章供大家学习赏析,这就是这篇文章的来源。

 


寻找Init_Array


使用ida加载libil2cpp.so,ida直接给出提示:
Warning

⚠️ The SHT is corrupt and has some overlapping sections that will not be fully processed.
Try loading sections manually.

[OK]
翻译:
警告

SHT(节头表)已损坏,并且有一些重叠的节区无法完全处理。
请尝试手动加载节区。

[确定]
这个警告提示表明:

SHT (Section Header Table) 节头表损坏
存在重叠的节区(sections)
文件无法被完全处理
建议手动加载节区

显示section header有问题。直接点ok进去,发现一大堆奇怪的函数名:

 



好的,这张图片展示的是一个反汇编工具(如 IDA Pro)中的函数列表视图。

最显著的特点是,这些函数名称(Function name)都经过了高度混淆,使用了大量无规律的字母、数字和符号组合,这是一种常见的代码保护技术,旨在增加逆向分析的难度。

以下是图片中显示的完整内容:

| Function name | Segment | Start |
| :--- | :--- | :--- |
| `f` _R_2_R__PB__FE_1WP____n0_F_2_GP___X... | .note.gnu... | 00000000 |
| `f` _O_52Sn5._BB__DT__J_7_F1_._R_2_R__PB_... | .note.gnu... | 00000000 |
| `f` X_p_A____71Pn__OD3_PX____X_p_A___D$0F... | .note.gnu... | 00000000 |
| `f` J_d_SA__OP_1_V36_W__DBV__O_52Sn5._BB_... | .note.gnu... | 00000000 |
| `f` _O_52Sn5._BB__J__6B_5__B_8F1_._R_2_R_... | .note.gnu... | 00000000 |
| `f` J_d_SA__OP_1_V36_A70F__BJ_d_SA__OP_1... | .note.gnu... | 00000000 |
| `f` _R_2_R__PB__FE__FE___Gn00L___B_3BJ_... | .note.gnu... | 00000000 |
| `f` _R_2_R__PB__FE__ME30EP5_P1_._R_2_R... | .note.gnu... | 00000000 |
| `f` J_d_SA__OP_1_V36_W__OU_BJ_d_SA__OP_1... | .note.gnu... | 00000000 |
| `f` _O_52Sn5._BB__DT__FG3_WBV__O_52Sn5._BB_... | .note.gnu... | 00000000 |
| `f` _O_52Sn5._BB__KP__SP$_MEV__O_52Sn5._BB_... | .note.gnu... | 00000000 |
| `f` J_d_SA__OP_1_W$__Nn_._R_2_E_2F1_._R_2... | .note.gnu... | 00000000 |
| `f` X_p_A____71Pn_1_B____71Pn9$_X_p_A_... | .note.gnu... | 00000000 |
| `f` X_p_A____GT2_BC$__Zn5._BB__DT_BJ_d_S... | .note.gnu... | 00000000 |
| `f` _R_2_P$0BH._F_16K1_._R_2_P$0BH__FF... | .note.gnu... | 00000000 |
| `f` X_p_A__BC$__Zn1_Wn4_WT._F_16K1_._R_2... | .note.gnu... | 00000000 |
| `f` X_p_A__QT__OG3_JR7.01_._R_2_C__WX... | .note.gnu... | 00000000 |
| `f` X_p_A__B____1_._R_2_P__LR76J_8_DC7_V... | .note.gnu... | 00000000 |
| `f` J_d_SA_7MY7_G_3__T._FA__L_V__O_52Sn... | .note.gnu... | 00000000 |
| `f` J_d_SA__L_7_Mn1_W1_._R_2_U9_BX8_DT_... | .note.gnu... | 00000000 |
| `f` _R_2_B36_E3_Sn2__Q1_._R_2_B_7WU95M1... | .note.gnu... | 00000000 |
| `f` _O_52Sn__Wn5_N_7_G___Fn70DD__ME_BJ_d... | .note.gnu... | 00000000 |
| `f` X_p_A__PT____8$JV__JCV__O_52Sn__Wn5_M... | .note.gnu... | 00000000 |
| `f` _O_52Sn__WY9__V36_A70B__O_52Sn__WY9... | .note.gnu... | 00000000 |
| `f` _O_52Sn__WY9__X__J__6B_5__X_p_A__N_8... | .note.gnu... | 00000000 |
| `f` J_d_SA___FE__Gn_1_V3_FC___X_p_A__NT... | .note.gnu... | 00000000 |
| `f` _R_2___36K_2_DT__LS___EV__O_52Sn__WY... | .note.gnu... | 00000000 |
| `f` _O_52Sn__WY9__V36__7_F1_._R_2__36K_2... | .note.gnu... | 00000000 |
| `f` _R_2___36K_2_DT___QT_7Q__6ZA3BJ_d_SA... | .note.gnu... | 00000000 |
| `f` _O_52Sn___JE__OX__MT_1_R7._D__WX9__T8... | .note.gnu... | 00000000 |
| `f` _O_52Sn9$EB36__0_BC$__Zn4_V_21_X8_BC$... | .note.gnu... | 00000000 |
| `f` J_d_SA___n1_Wn___BA_1JK3BJ_d_SA___n1... | .note.gnu... | 00000000 |
| `f` X_p_A__DR_$LC3___Y__FP_BJ_d_SA___n1_W... | .note.gnu... | 00000000 |
| `f` X_p_A__DR__Pn___C3_F___01_._R_2_V5_P... | .note.gnu... | 00000000 |
| `f` J_d_SA___n1_Wn___n__NT__10X5____BJ_d... | .note.gnu... | 00000000 |
| `f` _O_52Sn1__X__GX__A_3__X_p_A__DR__Pn... | .note.gnu... | 00000000 |
| `f` _R_2_V5_GX___A_3BJ_d_SA___n3_BS___X... | .note.gnu... | 00000000 |
| `f` J_d_SA_$JT___B__WX5_PT__UP_7F1_._R_2... | .note.gnu... | 00000000 |
| `f` _R_2_W__OU_1WP___n1_Wn___OD3BJ_d_SA... | .note.gnu... | 00000000 |
| `f` X_p_A__EX3._Gn__Wn___OD3BJ_d_SA_$JT... | .note.gnu... | 00000000 |
| `f` X_p_A__EX3._Gn1_Wn___OD3BJ_d_SA_$JT... | .note.gnu... | 00000000 |
| `f` J_d_SA_6ZA3_DT___WH___X_p_A__WH___X... | .note.gnu... | 00000000 |
| `f` X_p_A__LG30QX2___B____Z__BR_6QP5__X_p... | .note.gnu... | 00000000 |
| `f` _O_52Sn__9T7__V36_W$__NT__W1____R_2_F... | .note.gnu... | 00000000 |


查看segement信息,发现出现了一些不常见的note.gnu.proc,note.gnu.text等节区,这显然不正常。

Name Start End R
LOAD 0000000000000000 00000000000001C8 R
.note.gnu.proc 00000000000001C8 0000000003A82D00 R
.eh_frame_hdr 0000000003A82D00 0000000003C737B4 R
.note.gnu.proc 0000000003C737B4 0000000003C737B8 R
.eh_frame 0000000003C737B8 00000000043E9C48 R
.gcc_except_table 00000000043E9C48 000000000447B438 R
.note.gnu.proc 000000000447B438 000000000447C000 R
LOAD 000000000447C000 0000000007CD4000 R
.note.gnu.ident 0000000007CD4000 0000000007CD4008 R
LOAD 0000000007CD4008 0000000007CD4030 R
.text 0000000007CD4030 0000000007CD4040 R
.note.gnu.text 0000000007CD4040 0000000007DE64F8 R
.note.gnu.content 0000000007DF6A50 0000000007E01990 R
LOAD 0000000007E01990 0000000007E3E258 R
.data 0000000007E3E258 000000000891D178 R
extern 000000000891D178 00000000089216C0 ?

我们知道,linker(链接器)将一个动态库加载到内存时,做完重定位(relocate)操作后就会调用动态库的init函数,用来完成一些初始化操作。由于init函数是linker调用的,所以没法做加密。看到这么离谱的一个动态库,显然需要init函数来解密。

通常,init函数会出现在.init_array这个节区里,这是个函数指针数组,链接器会依次调用里面的函数。

 

LOAD 0000000000000000 000000000390558 R . . . L mempage 01 pub... DATA 64 00 12
.gcc_except_table 000000000390558 0000000003BDE6C R . . . L dword 06 pub... CONST 64 00 12
LOAD 0000000003BDE6C 0000000003BDE70 R . . . L mempage 01 pub... DATA 64 00 12
.rodata 0000000003BDE70 0000000006A0133 R . . . L para 07 pub... CONST 64 00 12
LOAD 0000000006A0133 0000000006A0134 R . . . L mempage 01 pub... DATA 64 00 12
protodesc_cold 0000000006A0134 0000000006A381C R . . . L dword 08 pub... CONST 64 00 12
.eh_frame_hdr 0000000006A381C 000000000754EB0 R . . . L dword 09 pub... CONST 64 00 12
.eh_frame 000000000754EB0 00000000A3240C R . . . L qword 0A pub... CONST 64 00 12
.text 00000000A33420 000000003704940 R . X . L 32byte 0B pub... CODE 64 00 12
.plt 000000003704940 0000000037064E0 R . X . L para 0C pub... CODE 64 00 12
.data.rel.ro 0000000037074E0 0000000037FA6A8 R W . . L para 0D pub... DATA 64 00 12
.fini_array 0000000037FA6A8 0000000037FA6B8 R W . . L qword 0E pub... DATA 64 00 12
.init_array 0000000037FA6B8 0000000037FB4D0 R W . . L qword 0F pub... DATA 64 00 12
LOAD 0000000037FB4D0 0000000037FB6E0 R W . . L mempage 03 pub... DATA 64 00 12
.got 0000000037FB6E0 0000000037FF208 R W . . L qword 10 pub... DATA 64 00 12
.got.plt 0000000037FF208 0000000037FFE0 R W . . L qword 11 pub... DATA 64 00 12
.data 000000003801000 000000003971398 R W . . L 64byte 12 pub... DATA 64 00 12
LOAD 000000003971398 0000000039713C0 R W . . L mempage 04 pub... DATA 64 00 12
.bss 0000000039713C0 000000003C75CF8 R W . . L 64byte 13 pub... BSS 64 00 12
extern 000000003C75CF8 000000003C76A38 ? ? ? . L qword 14 pub... 64 00 14

但是我们的动态库没有.init_array这个节,linker去哪里找初始化函数?

我们可以使用readelf工具来查找:使用readelf -d 操作:

 

Tag Type Name/Value
0x0000000000000004 (HASH) 0x7e1b730
0x0000000000000001 (NEEDED) Shared library: [liblog.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so]
0x0000000000000019 (INIT_ARRAY) 0x7df6a50
0x000000000000001b (INIT_ARRAYSZ) 32 (bytes)
0x000000000000001a (FINI_ARRAY) 0x7df6a70
0x000000000000001c (FINI_ARRAYSZ) 32 (bytes)
0x0000000000000005 (STRTAB) 0x7e23c38
0x0000000000000006 (SYMTAB) 0x7e01990
0x000000000000000a (STRSZ) 107648 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x7dfba88
0x0000000000000002 (PLTRELSZ) 9672 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x7d0c748
0x0000000000000007 (RELA) 0x7cff4e8
0x0000000000000008 (RELASZ) 53856 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x0000000000000018 (BIND_NOW)
0x000000006ffffffb (FLAGS_1) Flags: NOW
0x000000006fffffff9 (RELACOUNT) 515
0x000000000000000e (SONAME) Library soname: [libil2cpp.so]
0x0000000000000000 (NULL) 0x0

发现INIT_ARRAY在地0x7df6a50这个位置。

跳过去看看:

; ELF Initialization Function Table
; ===========================================================================

; Segment type: Pure data
AREA .note.gnu.content, DATA
; ORG 0x7DF6A50
off_7DF6A50 DCQ dword_700DC ; DATA XREF: LOAD:000000000000C01o
; LOAD:0000000000001A01o
DCQ dword_708EC
DCQ dword_70FE4
DCQ 0

确实,上面的蓝字写了这里是ELF Initialization Function Table,我们点进第一个地址看看:

0700D8 DCB 0x6D, 0x49, 0x76, 0x81
0700DC dword_700DC DCD 0x62A41895 ; DATA XREF: .note.gnu.content:off_7DF6A50↓o
0700E0 DCQ 0x10BFF7CE82F0E4A0, 0x1D7427739E29C155, 0x859D9C9FF8C66758
0700E0 DCQ 0x5A7E66E490AE410F, 0x4D56A7DCC02BEDD, 0x33595BD822088223
0700E0 DCQ 0x2DA30F609656EF66, 0x95FE14CC776799BE, 0x4559EC7EB95EDAEB
0700E0 DCQ 0x1FD249AD67D96AFB, 0xA1BF62372C0E052A, 0x243C34D8A9AD9AAE
0700E0 DCQ 0x2CC26A1013E15B81, 0x1CDDA1214D0D7F64, 0x77AF24BE0AC9BA82
0700E0 DCQ 0xC4888B28ACE7F26F, 0x7C404C9F1A6F38FF, 0x9BE4322B16E66E2E
0700E0 DCQ 0xF1D770A036FA9CB0, 0x627C10DEBD6C6852, 0x1B56BDCBAE6B05CE
0700E0 DCQ 0xE448AE3516C12503, 0x5D6078B034B74DB7, 0x7FB63BA895080E6C
0700E0 DCQ 0x86F52EF98DCD6624, 0xEE422F257CB5CCFA, 0xCF0A03418B652418
0700E0 DCQ 0x76231FCCE33632, 0xD614FB06D7F4AE11, 0xBCBF780FC1DAACB4
0700E0 DCQ 0x2224DA41A2268021, 0xF9E28FA57CA41E11, 0x548CDD07D24A14F
0700E0 DCQ 0x39F411D592FEAC80, 0x81E4C6A2DD118477, 0xAE306B153DE7CAB3
0700E0 DCQ 0xCFFBFC61914FA1AC, 0x9C96E59F27827FE0, 0x85485321EC725F19
0700E0 DCQ 0x3988F0C9C325C856, 0x7176C36E9B24F730, 0x5303096124018731
0700E0 DCQ 0x42817D36B5461EDB, 0xB0EA639593C03490, 0x4B6EBAC18BE09CC9
0700E0 DCQ 0x5ABF6E69E94D442C, 0xA1D58B2B5773C03A, 0xF7FCF4DE30014B65
0700E0 DCQ 0x3D4C6962B08B4658, 0x6079D89D10B590C4, 0x777ED0E2FC900F56
0700E0 DCQ 0xE2E5C5DE7DA7D083, 0x8017CE83E3067955, 0xB977FEC776CF4EC4
0700E0 DCQ 0xD12AFBFE659BCE3B, 0xF1C116FF7892813D, 0x5A6291118E0BEF05
0700E0 DCQ 0xA8E0E81924128983, 0x301818A004139F0B, 0xC77ACDE769C67133
0700E0 DCQ 0x1C680BDEDDBA79EB, 0xACD85D9BD0128B31, 0x81136D95FEE6D714
0700E0 DCQ 0x3A659959E33B78C3, 0x370FBA60ED1BF385, 0x121C0464FBA91800
0700E0 DCQ 0x49536D3FE8B8C71, 0xCAFC6AD8F19BCA15, 0x8185B4C64098386C
0700E0 DCQ 0x38194A8238E975, 0x9F93D40ECD116D0B, 0x590285670C6D7012
0700E0 DCQ 0x6724F943E433B7B3, 0x8FD072761DF9157, 0xF2FCE94D9C2683AB


发现是一堆无意义的数据,ida也无法将其解析为汇编指令。这不科学!

因为这是链接器第一个调用的函数,如果这个函数都是加密的,那在链接阶段就会报错。所以我们合理怀疑初始化函数位置找错了。

其实之所以会搞错,是因为错误的section header干扰了ida的解析。这里有一个技巧,因为ida在解析动态库的时候不需要section信息,只需要segement信息ida也可以解析。所以我们直接将原来的section header全部抹去。

使用010Editor打开libil2cpp.so,运行ELF模板,直接跳转到section header的起始位置。

将所有数据用0覆盖:

界面分为上下两个部分:

上半部分:Hex/十六进制视图 这部分以十六进制形式显示了文件的原始数据。

- 左侧是内存地址,从 50B:5260h 到 50B:5370h 。
- 中间是对应地址的十六进制数据。可以看到从地址 50B:5260h 到 50B:5370h 的所有数据都是 00 。
- 从地址 50B:52B0h 到 50B:52E0h 的数据块被蓝色高亮选中。
```
50B:5260h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5270h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5280h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5290h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:52A0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:52B0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:52C0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:52D0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:52E0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:52F0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5300h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5310h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5320h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5330h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5340h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5350h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5360h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50B:5370h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
```
下半部分:模板结果 (Template Result) 这部分显示了 ELF.bt 模板解析二进制数据后的结构化视图。

- 模板名称 : 模板结果 - ELF.bt
- 结构 :
- struct section_header_table (节头表结构体)
- struct section_table_entry64_t section_table_element[0]
- struct section_table_entry64_t section_table_element[1]
- struct section_table_entry64_t section_table_element[2]
- ... (一直到 [10] )
- 列 :
- 名称 (Name) : 显示结构体成员的名称。
- 值 (Value) : 显示对应成员的值。在这里,所有可见的 section_table_element 的值都是 SHN_UNDEF 。 SHN_UNDEF 是 ELF 文件格式中的一个特殊值,表示一个未定义的节区索引。
总结 :
这张图片展示了对一个 ELF 文件的分析。分析结果显示,文件的节头表(Section Header Table)中至少有11个条目(从索引0到10),但这些条目都是未定义的( SHN_UNDEF ),并且它们在文件中的原始数据全部为零。这可能意味着节头表被擦除或故意设置为空。


然后重新打开ida,再次跳转的0x7df6a50这个位置,发现ida已经正常解析INIT函数的地址了:

; ELF Initialization Function Table
; ===========================================================================

; Segment type: Pure data
AREA LOAD, DATA, ALIGN=12
; ORG 0x7DF6A50
off_7DF6A50 DCQ unk_7D440DC ; DATA XREF: LOAD:000000000000C01o
; LOAD:0000000000001A01o
DCQ unk_7D448EC
DCQ unk_7D44FE4
DCB 0
DCB 0
DCB 0
DCB 0
DCB 0
DCB 0


并且代码也被成功的解析了出来:

07D440DC F4 4F BE A9 STP X20, X19, [SP,#-0x10+var_10]!
07D440E0 FD 7B 01 A9 STP X29, X30, [SP,#0x10+var_s0]
07D440E4 FD 43 00 91 ADD X29, SP, #0x10
07D440E8 00 67 80 52 MOV W0, #0x338
07D440EC 01 30 FF 97 BL sub_7D100F0
07D440F0 F3 03 00 AA MOV X19, X0
07D440F4 44 D0 FF 97 BL sub_7D38204
07D440F8 C8 05 00 F0 ADRP X8, #off_7DFFE78@PAGE
07D440FC 00 09 80 52 MOV W0, #0x48
07D44100 01 09 80 52 MOV W1, #0x48
07D44104 13 3D 07 F9 STR X19, [X8,#off_7DFFE78@PAGEOFF]
07D44108 47 60 FF 97 BL sub_7D1C224
07D4410C 01 09 80 52 MOV W1, #0x48
07D44110 3D 60 FF 97 BL sub_7D1C204
07D44114 A1 01 80 52 MOV W1, #0xD
07D44118 F3 03 00 AA MOV X19, X0
07D4411C 3A 60 FF 97 BL sub_7D1C204


我们愉快的按下F5,ida又报错了:

这张图片展示的是一个反编译工具(很可能是 IDA Pro)弹出的警告对话框。

以下是图片中警告框的完整内容:

标题 : Warning (警告)

内容 :
Decompilation failure:
7D4418C: positive sp value has been found

Please refer to the manual to find appropriate actions

(一个 "OK" 按钮)

堆栈不平衡,ida发现了错误的栈指针。这通常是因为代码中有花指令的缘故,我们要考虑去除花指令了。



去除花指令

 

我们看看出错位置附近的代码:

7D4415C TBNZ W8, #0, loc_7D4416C
7D44160 BL sub_7D459BC
7D44164 ADD SP, SP, #0x20
7D44168 B loc_7D44184
7D4416C ; ---------------------------------------------------------------------------
7D4416C
7D4416C loc_7D4416C ; CODE XREF: sub_7D440DC+80↑j
7D4416C LDR X6, [X4,#0x10]
7D44170 SUB SP, SP, #0x10
7D44174 MADD W0, W0, W0, W0
7D44178 LDR W4, =0x78902345
7D4417C SUB SP, SP, #0x10
7D44180 RET
7D44184 ; ---------------------------------------------------------------------------
7D44184
7D44184 loc_7D44184 ; CODE XREF: sub_7D440DC+8C↑j
7D44184 LDP X29, X30, [SP,#-0x10+arg_10]
7D44188 LDP X20, X19, [SP-0x10+arg_0],#0x20
7D4418C B loc_7D18EE8
7D4418C ; End of function sub_7D440DC


从0x7D4415C开始,如果w8不为0就会跳转的0x7d4416c,然后给栈寄存器sp减了0x20然后ret。这显然错了!

因为函数的一开始并没有给sp寄存器加0x20,这里减去0x20再返回一定会堆栈不平衡。所以有理由怀疑,这里就是花指令,用来干扰ida解析的。

但是如果w8为0,会先执行sub_7d459bc这个函数,然后给栈寄存器加0x20,再跳过错误的部分,去0x7d44184。我们看看sub_7d459bc这个函数:

sub_7D459BC           ; CODE XREF: LOAD:0000000007D1842C1
                  ; LOAD:0000000007D185C01p ...
      SUB     SP, SP, #0x20
      RET
; End of function sub_7D459BC


是给sp减去0x20,那这样就没问题。执行完后再加上0x20,栈是平衡的。
所以我们确信,中间的ret部分就是花指令。

观察后发现,这样的花指令在代码中有几十处,并且有两种模式,我们肯定不能手动Patch了。

模式1:

loc_7D4416C                                    ; CODE XREF: sub_7D440DC+80↑j

86 08 40 F9        LDR             X6, [X4,#0x10]

FF 43 00 D1        SUB             SP, SP, #0x10

00 00 00 1B        MADD            W0, W0, W0, W0

C4 00 00 18        LDR             W4, =0x78902345

FF 43 00 D1        SUB             SP, SP, #0x10

C0 03 5F D6        RET

模式2:

76   __int64 v73; // [xsp+488h] [xbp-28h]

77   __int64 v74; // [xsp+490h] [xbp-20h]

78   __int64 v75; // [xsp+498h] [xbp-18h]

79

80   v41 = (int *)sub_7D100F0(824LL);

81   sub_7D38204();

82   off_7DFFE78 = v41;

83   v42 = sub_7D1C224(72LL, 72LL);

84   v43 = sub_7D1C204(v42, 72LL);

85   v44 = sub_7D1C204(v43, 13LL);

86   v45 = sub_7D1C214(v43, 31LL);

87   v46 = sub_7D1C224(v44, v45);

88   v47 = sub_7D1C204(v43, 7LL);

89   sub_7D1C204(v46, v47);

90   v73 = v39;

91   v74 = v38;

92   v75 = v37;

93   v0 = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));

94   v72 = *(_QWORD *)(v0 + 40);

95   v1 = off_7DFCC68;

96   v2 = sub_7D1C224(off_7DFCC68, off_7DFCC68);

97   v3 = sub_7D1C204(v2, v1);

98   v4 = sub_7D1C204(v3, 13LL);

99   v5 = sub_7D1C214(v3, 31LL);

100  v6 = sub_7D1C224(v4, v5);

101  v7 = sub_7D1C204(v3, 7LL);

102  sub_7D1C204(v6, v7);

103  BYTE2(v63) = aCPFs6wzln[2] ^ 0x11;

104  LOBYTE(v63) = aCPFs6wzln[0] ^ 0xF;

105  BYTE1(v63) = aCPFs6wzln[1] ^ 0x10;

106  BYTE3(v63) = aCPFs6wzln[3] ^ 0x12;

107  BYTE4(v63) = aCPFs6wzln[4] ^ 0x13;

108  BYTE5(v63) = aCPFs6wzln[5] ^ 0x14;

109  BYTE6(v63) = aCPFs6wzln[6] ^ 0x15;

110  HIBYTE(v63) = aCPFs6wzln[7] ^ 0x16;

111  v64 = aCPFs6wzln[8] ^ 0x17;

112  v65 = aCPFs6wzln[9] ^ 0x18;

113  v66 = aCPFs6wzln[10] ^ 0x19;

114  v67 = aCPFs6wzln[11] ^ 0x1A;

115  v68 = aCPFs6wzln[12] ^ 0x1B;

116  v69 = aCPFs6wzln[13] ^ 0x1C;

117  v70 = aCPFs6wzln[14] ^ 0x1D;

118  v71 = 0;

119  v8 = off_7DFCC68;

120  v9 = sub_7D1C224(off_7DFCC68, off_7DFCC68);

121  v10 = sub_7D1C214(v9, v8);

122  v11 = v10;

123  v12 = sub_7D1C224(v10, v10);

124  v13 = sub_7D1C204(v12, v11);

125  v14 = sub_7D1C214(v13, 17LL);

126  v15 = sub_7D1C204(v13, 41LL);

我们可以根据这两种模式的机器码的特点,在原so中进行匹配,匹配后换上arm64下nop指令的机器码:0x1f2003d5就可以了。

去花指令脚本如下:
#去除花指令mod1 = [0x86,0x10,0x40,0xb9,0xa6,0x19,0x0,0x18,0xff,0x43,0x0,0xd1,0xc0,0x3,0x5f,0xd6]mod2 = [0x86,0x8,0x40,0xf9,0xff,0x43,0x0,0xd1,0x0,0x0,0x0,0x1b,0xFF,0x25,0x0,0x18,0xff,0x43,0x0,0xd1,0xc0,0x3,0x5f,0xd6]nop = [0x1f,0x20,0x3,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 patchWord(data,index,code): for i in range(4): data[index+i] = code[i] def patch(data,index): start = index - 0x10 patchWord(data,start,nop) patchWord(data,start+4,nop) patchWord(data,start+8,nop) def patch_mod1(data,index): start = index patchWord(data,start,nop) patchWord(data,start+4,nop) patchWord(data,start+8,nop) patchWord(data,start+12,nop) def patch_mod2(data,index): start = index for i in range(6): patchWord(data,start+i*4,nop) with open("f:\test\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("f:\test\libil2cpp_patch.so",'wb+') as f: f.write(bytes(data))

非常简单的读文件,匹配,写文件的操作。

去除完花指令,我们就可以愉快的F5了:

 76   __int64 v73; // [xsp+488h] [xbp-28h]

77   __int64 v74; // [xsp+490h] [xbp-20h]

78   __int64 v75; // [xsp+498h] [xbp-18h]

79

80   v41 = (int *)sub_7D100F0(824LL);

81   sub_7D38204();

82   off_7DFFE78 = v41;

83   v42 = sub_7D1C224(72LL, 72LL);

84   v43 = sub_7D1C204(v42, 72LL);

85   v44 = sub_7D1C204(v43, 13LL);

86   v45 = sub_7D1C214(v43, 31LL);

87   v46 = sub_7D1C224(v44, v45);

88   v47 = sub_7D1C204(v43, 7LL);

89   sub_7D1C204(v46, v47);

90   v73 = v39;

91   v74 = v38;

92   v75 = v37;

93   v0 = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));

94   v72 = *(_QWORD *)(v0 + 40);

95   v1 = off_7DFCC68;

96   v2 = sub_7D1C224(off_7DFCC68, off_7DFCC68);

97   v3 = sub_7D1C204(v2, v1);

98   v4 = sub_7D1C204(v3, 13LL);

99   v5 = sub_7D1C214(v3, 31LL);

100  v6 = sub_7D1C224(v4, v5);

101  v7 = sub_7D1C204(v3, 7LL);

102  sub_7D1C204(v6, v7);

103  BYTE2(v63) = aCPFs6wzln[2] ^ 0x11;

104  LOBYTE(v63) = aCPFs6wzln[0] ^ 0xF;

105  BYTE1(v63) = aCPFs6wzln[1] ^ 0x10;

106  BYTE3(v63) = aCPFs6wzln[3] ^ 0x12;

107  BYTE4(v63) = aCPFs6wzln[4] ^ 0x13;

108  BYTE5(v63) = aCPFs6wzln[5] ^ 0x14;

109  BYTE6(v63) = aCPFs6wzln[6] ^ 0x15;

110  HIBYTE(v63) = aCPFs6wzln[7] ^ 0x16;

111  v64 = aCPFs6wzln[8] ^ 0x17;

112  v65 = aCPFs6wzln[9] ^ 0x18;

113  v66 = aCPFs6wzln[10] ^ 0x19;

114  v67 = aCPFs6wzln[11] ^ 0x1A;

115  v68 = aCPFs6wzln[12] ^ 0x1B;

116  v69 = aCPFs6wzln[13] ^ 0x1C;

117  v70 = aCPFs6wzln[14] ^ 0x1D;

118  v71 = 0;

119  v8 = off_7DFCC68;

120  v9 = sub_7D1C224(off_7DFCC68, off_7DFCC68);

121  v10 = sub_7D1C214(v9, v8);

122  v11 = v10;

123  v12 = sub_7D1C224(v10, v10);

124  v13 = sub_7D1C204(v12, v11);

125  v14 = sub_7D1C214(v13, 17LL);

126  v15 = sub_7D1C204(v13, 41LL);

去除后的汇编代码变成这个样子:

9007D4414C                 MOV             X1, X0

9007D44150                 MOV             X0, X20

9007D44154                 BL              sub_7D1C204

9007D44158                 MOV             X8, X0

9007D4415C                 NOP

9007D44160                 NOP

9007D44164                 NOP

9007D44168                 B               loc_7D44184

9007D4416C ; -----------------------------------------------------------------------

9007D4416C                 NOP

9007D44170                 NOP

9007D44174                 NOP

9007D44178                 NOP

9007D4417C                 NOP

9007D44180                 NOP

9007D44184

9007D44184 loc_7D44184                            ; CODE XREF: sub_7D440DC+8C↑j

9007D44184                 LDP             X29, X30, [SP,#0x10+var_s0]

9007D44188                 LDP             X20, X19, [SP+0x10+var_10],#0x20

9007D4418C                 B               loc_7D18EE8

9007D4418C ; End of function sub_7D440DC

9007D4418C

中间混淆的部分全部去掉,直接跳转到正确位置。

接下来就看看三个init函数都做了些什么。



分析init函数


1、第一个init函数

第一个init函数主要进行了一个初始化操作:

v41 = (int *)m_malloc_wrap(0x338LL);

init_function_list((__int64)v41);

off_7DFFE78 = v41;

v42 = junk_code0(72LL, 72LL);

v43 = junk_code1(v42, 72LL);

v44 = junk_code1(v43, 13LL);

v45 = junk_code3(v43, 31LL);

v46 = junk_code0(v44, v45);

v47 = junk_code1(v43, 7LL);

junk_code1(v46, v47);

v73 = v39;

v74 = v38;

v75 = v37;

v0 = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));

v72 = *(_QWORD *)(v0 + 40);

v1 = off_7DFCC68;

v2 = junk_code0(off_7DFCC68, off_7DFCC68);

v3 = junk_code1(v2, v1);

v4 = junk_code1(v3, 13LL);

v5 = junk_code3(v3, 31LL);

v6 = junk_code0(v4, v5);

v7 = junk_code1(v3, 7LL);

junk_code1(v6, v7);

BYTE2(v63) = aCPFs6wzln[2] ^ 0x11;

LOBYTE(v63) = aCPFs6wzln[0] ^ 0xF;

BYTE1(v63) = aCPFs6wzln[1] ^ 0x10;

BYTE3(v63) = aCPFs6wzln[3] ^ 0x12;

BYTE4(v63) = aCPFs6wzln[4] ^ 0x13;

BYTE5(v63) = aCPFs6wzln[5] ^ 0x14;

BYTE6(v63) = aCPFs6wzln[6] ^ 0x15;

HIBYTE(v63) = aCPFs6wzln[7] ^ 0x16;

v64 = aCPFs6wzln[8] ^ 0x17;

v65 = aCPFs6wzln[9] ^ 0x18;

v66 = aCPFs6wzln[10] ^ 0x19;

v67 = aCPFs6wzln[11] ^ 0x1A;

将一些常用的系统函数如fopen,mmap,memcpy等赋值给一个全局的函数数组,后面通过这个函数数组来间接调用基本的系统函数。显然这种脱裤子放*的做法是为了增加代码阅读难度,提升安全性:

v1 = m_select;

v2 = m_raise;

v3 = off_7DFC978;

v4 = m_inet_ntoa;

v5 = off_7DFCD38;

v6 = off_7DFC750;

v7 = off_7DFCF48;

v8 = off_7DFCA78;

v9 = off_7DFC930;

v10 = off_7DFCC50;

v11 = off_7DFC980;

v12 = off_7DFCF70;

v13 = off_7DFC790;

v14 = off_7DFCD10;

v15 = off_7DFC858;

v16 = off_7DFC7A0;

v17 = off_7DFCE80;

v18 = off_7DFCFD0;

v19 = off_7DFC850;

v20 = off_7DFC8A0;

v21 = off_7DFCD30;

v22 = off_7DFCE70;

v23 = off_7DFCDE8;

v24 = off_7DFC910;

*(_QWORD *)(a1 + 504) = off_7DFCE40;

v25 = off_7DFCC78;

*(_QWORD *)(a1 + 16) = v2;

*(_QWORD *)(a1 + 24) = v3;

v26 = off_7DFCD88;

*(_QWORD *)(a1 + 480) = v4;

*(_QWORD *)&v27 = v9;

*((_QWORD *)&v27 + 1) = v10;

*(__int64 (__fastcall **)(*))(a1 + 464) = off_7DFCCD0[0];

*(_QWORD *)(a1 + 472) = v5;

v28 = off_7DFCEF8;

*(_QWORD *)(a1 + 96) = v7;

*(_QWORD *)(a1 + 104) = v8;

v29 = off_7DFCE38;

v30 = off_7DFCE48;

*(_QWORD *)(a1 + 128) = v25;

*(_QWORD *)(a1 + 136) = v11;

v31 = off_7DFCFE0;

*(_QWORD *)(a1 + 144) = v12;

*(_QWORD *)(a1 + 152) = v13;

v32 = off_7DFCC68;

*(_QWORD *)(a1 + 160) = v14;

2、第二个init函数

第二个init函数主要做了三件事:

1.遍历elf文件,寻找dynmic segement

v19 = junk_code1(v17, v18);

v20 = junk_code2(v16, 9LL);

junk_code2(v19, v20);

sub_7D14AC8((__int64)&v123);

if ( find_dyn_seg((unsigned __int64 *)&v123, (signed __int64)*off_7DFCCC8) )

{

  v11[43] = 0LL;

  v21 = junk_code1(159LL, 159LL);

  v22 = junk_code0(v21, 159LL);

  v23 = junk_code0(v22, 13LL);

  v24 = junk_code2(v22, 31LL);

  v25 = junk_code1(v23, v24);

  v26 = junk_code0(v22, 7LL);

  if ( !(junk_code0(v25, v26) & 1) )

具体逻辑就是遍历program header,寻找p_type为2的段。

名称                                                    值

> struct program_table_entry64_t program_table_element[1]    (R_X) Loadable Segment

> struct program_table_entry64_t program_table_element[2]    (RW_) Loadable Segment

v struct program_table_entry64_t program_table_element[3]    (RW_) Dynamic Segment

    enum p_type64_e p_type                                   PT_DYNAMIC (2h)

    enum p_flags64_e p_flags                                 PF_Read_Write (6h)

    Elf64_Off p_offset_FROM_FILE_BEGIN                       45D60B8h

    Elf64_Addr p_vaddr_VIRTUAL_ADDRESS                       0x00000000007E3E0B8

    Elf64_Addr p_paddr_PHYSICAL_ADDRESS                      0x00000000007DFB878

    Elf64_Xword p_filesz_SEGMENT_FILE_LENGTH                 1A0h

    Elf64_Xword p_memsz_SEGMENT_RAM_LENGTH                   1A0h

    Elf64_Xword p_align                                      8h

> char p_data[416]                                           □

> struct program_table_entry64_t program_table_element[4]    (R__) GCC .eh_frame_hdr Segment

> struct program_table_entry64_t program_table_element[5]    (RW_) GNU Stack (executability)

> struct program_table_entry64_t program_table_element[6]    (RW_) Loadable Segment

> struct section_header_table

为什么要找Dynamic 段?我们转到Dynamic段看看:

 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00

30 B7 E1 07 00 00 00 00 01 00 00 00 00 00 00 00

3C A4 01 00 00 00 00 00 01 00 00 00 00 00 00 00

46 A4 01 00 00 00 00 00 01 00 00 00 00 00 00 00

4E A4 01 00 00 00 00 00 01 00 00 00 00 00 00 00

57 A4 01 00 00 00 00 00 01 00 00 00 00 00 00 00

5F A4 01 00 00 00 00 00 19 00 00 00 00 00 00 00

50 6A DF 07 00 00 00 00 1B 00 00 00 00 00 00 00

20 00 00 00 00 00 00 00 1A 00 00 00 00 00 00 00

70 6A DF 07 00 00 00 00 1C 00 00 00 00 00 00 00

20 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00

38 3C E2 07 00 00 00 00 06 00 00 00 00 00 00 00

90 19 E0 07 00 00 00 00 0A 00 00 00 00 00 00 00

80 A4 01 00 00 00 00 00 0B 00 00 00 00 00 00 00

18 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00

88 BA DF 07 00 00 00 00 02 00 00 00 00 00 00 00

C8 25 00 00 00 00 00 00 14 00 00 00 00 00 00 00

07 00 00 00 00 00 00 00 17 00 00 00 00 00 00 00

48 C7 D0 07 00 00 00 00 07 00 00 00 00 00 00 00

E8 F4 CF 07 00 00 00 00 08 00 00 00 00 00 00 00

60 D2 00 00 00 00 00 00 09 00 00 00 00 00 00 00

18 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 FB FF FF 6F 00 00 00 00

原来,dynamic段保存了所有动态链接需要的信息。注意到0x19,这个正是INIT_ARRAY的编码,而后面的0x7df6a50则正是初始化函数在内存中的位置。

其实readelf -d的工作原理也是遍历这个dynamic段。

 Tag                    Type              Name/Value

0x0000000000000004    (HASH)            0x7e1b730

0x0000000000000001    (NEEDED)          Shared library: [liblog.so]

0x0000000000000001    (NEEDED)          Shared library: [libm.so]

0x0000000000000001    (NEEDED)          Shared library: [libdl.so]

0x0000000000000001    (NEEDED)          Shared library: [libc.so]

0x0000000000000001    (NEEDED)          Shared library: [libstdc++.so]

0x0000000000000019    (INIT_ARRAY)      0x7df6a50

0x000000000000001b    (INIT_ARRAYSZ)    32 (bytes)

0x000000000000001a    (FINI_ARRAY)      0x7df6a70

0x000000000000001c    (FINI_ARRAYSZ)    32 (bytes)

0x0000000000000005    (STRTAB)          0x7e23c38

0x0000000000000006    (SYMTAB)          0x7e01990

0x000000000000000a    (STRSZ)           107648 (bytes)

0x000000000000000b    (SYMENT)          24 (bytes)

0x0000000000000003    (PLTGOT)          0x7dfba88

0x0000000000000002    (PLTRELSZ)        9672 (bytes)

0x0000000000000014    (PLTREL)          RELA

0x0000000000000017    (JMPREL)          0x7d0c748

0x0000000000000007    (RELA)            0x7cff4e8

0x0000000000000008    (RELASZ)          53856 (bytes)

0x0000000000000009    (RELAENT)         24 (bytes)

0x0000000000000018    (BIND_NOW)

0x000000006ffffffb    (FLAGS_1)         Flags: NOW

0x000000006ffffff9    (RELACOUNT)       515

0x000000000000000e    (SONAME)          Library soname: [libil2cpp.so]

0x0000000000000000    (NULL)            0x0

可以看到所有动态信息与文件中的数据对应。同时,ida之所以不需要section信息也能解析,也是因为有了dynamic段就足够的缘故。

这里附一份动态段type表:

 Here's the OCR text transcription of the table:

Figure 5-10: Dynamic Array Tags, d_tag

NameValued_unExecutableShared Object
DT_NULL 0 ignored mandatory mandatory
DT_NEEDED 1 d_val optional optional
DT_PLTRELSZ 2 d_val optional optional
DT_PLTGOT 3 d_ptr optional optional
DT_HASH 4 d_ptr mandatory mandatory
DT_STRTAB 5 d_ptr mandatory mandatory
DT_SYMTAB 6 d_ptr mandatory mandatory
DT_RELA 7 d_ptr mandatory optional
DT_RELASZ 8 d_val mandatory optional
DT_RELAENT 9 d_val mandatory optional
DT_STRSZ 10 d_val mandatory mandatory
DT_SYMENT 11 d_val mandatory mandatory
DT_INIT 12 d_ptr optional optional
DT_FINI 13 d_ptr optional optional
DT_SONAME 14 d_val ignored optional
DT_RPATH* 15 d_val optional ignored
DT_SYMBOLIC* 16 ignored ignored optional
DT_REL 17 d_ptr mandatory optional
DT_RELSZ 18 d_val mandatory optional
DT_RELENT 19 d_val mandatory optional
DT_PLTREL 20 d_val optional optional
DT_DEBUG 21 d_ptr optional ignored
DT_TEXTREL* 22 ignored optional optional
DT_JMPREL 23 d_ptr optional optional
DT_BIND_NOW* 24 ignored optional optional
DT_INIT_ARRAY 25 d_ptr optional optional
DT_FINI_ARRAY 26 d_ptr optional optional
DT_INIT_ARRAYSZ 27 d_val optional optional
DT_FINI_ARRAYSZ 28 d_val optional optional
DT_RUNPATH 29 d_val optional optional
DT_FLAGS 30 d_val optional optional
DT_ENCODING 32 unspecified unspecified unspecified
DT_PREINIT_ARRAY 32 d_ptr optional ignored

这张表中,我们可以清楚的看到,有字符串表DT_STRTAB(5),有符号表DT_SYMTAB(6),有重定位表DT_RELA(7),大家可以对照这张表,将readelf的输出,和动态信息在文件中二进制数据结合在一起看,会更加清楚。

2.执行prelink操作
prelink是android linker源码里进行的一个操作。这个操作的本质就是解析dynamic段的各种数据,将他们分门别类的存储起来,供后面使用。

libil2cpp.so中自己实现了prelink操作,令人毛骨悚然,看看代码:

 v11 = (__int64 *)(_QWORD *)(v2 + 40) + 8LL);

while ( 2 )

{

  v12 = *(v11 - 1);

  result = 0LL;

  switch ( 0LL )

  {

    case 0LL:

      if ( v9 )

      {

        v31 = *(_QWORD *)(v2 + 48);

        if ( v31 )

          *(_QWORD *)v2 = v31 + v9;

      }

      v32 = *(_QWORD *)(v2 + 344);

      if ( !v32 )

        goto LABEL_58;

      *(_QWORD *)v2 = *(_QWORD *)v32;

      *(_DWORD *)(v32 + 340) = v8;

      v33 = off_7DFC988;

      *(_QWORD *)(*(*(_QWORD *)(v2 + 344) + 368LL) = (*(__int64 (__fastcall **)(signed __int64))((char *)&qword_B8

                                                      + off_7DFC988))(144LL * *(unsigned

      v34 = (*(__int64 (__fastcall *)(_QWORD, _QWORD, signed __int64))((char *)&qword_B0 + v33))(

             *(_QWORD *)(*(_QWORD *)(v2 + 344) + 368LL),

             0LL,

             144LL * *(unsigned int *)(*(_QWORD *)(v2 + 344) + 340LL));

      if ( !*(_QWORD *)(*(_QWORD *)(v2 + 344) + 368LL) )

        goto LABEL_53;

      v35 = 0;

      v36 = (_QWORD *)(*(_QWORD *)(v2 + 40) + 8LL);

      break;

    case 1LL:

      ++v8;

      v11 += 2;

      continue;

    case 2LL:

    case 3LL:

    case 7LL:

    case 8LL:

    case 9LL:

    case 11LL:

    case 15LL:

    case 16LL:

    case 17LL:

    case 18LL:

    case 19LL:

    case 20LL:

    case 21LL:

    case 23LL:

    case 24LL:

 

 

 

 case 30LL:

case 31LL:

  goto LABEL_13;

case 4LL:

  *(_QWORD *)(v2 + 256) = *v11 + v7;

  v14 = *(unsigned int *)(*v11 + v7);

  *(_QWORD *)(v2 + 248) = v14;

  *(_QWORD *)(v2 + 264) = *(unsigned int *)(*v11 + v7 + 4);

  *(_QWORD *)(v2 + 272) = *v11 + v7 + 4;

  *(_QWORD *)(v2 + 256) = v10 + *v11;

  v15 = *v11;

  v11 += 2;

  *(_BYTE *)(v2 + 376) = 1;

  *(_QWORD *)(v2 + 272) = v10 + v15 + 4 * v14;

  continue;

case 5LL:

  v16 = *v11;

  v11 += 2;

  *(_QWORD *)(v2 + 48) = v16 + v7;

  continue;

case 6LL:

  v17 = *v11;

  v11 += 2;

  *(_QWORD *)(v2 + 32) = v17 + v7;

  continue;

case 10LL:

  v18 = *v11;

  v11 += 2;

  *(_QWORD *)(v2 + 56) = v18;

  continue;

case 12LL:

  v19 = *v11;

  v11 += 2;

  *(_QWORD *)(v2 + 224) = v19 + v7;

  continue;

case 13LL:

  v20 = *v11;

  v11 += 2;

  *(_QWORD *)(v2 + 232) = v20 + v7;

  continue;

case 14LL:

  v21 = *(_DWORD *)v11;

  v11 += 2;

  v9 = v21;

  continue;

case 22LL:

  goto LABEL_59;

case 25LL:

  v22 = *v11;

  v11 += 2;

  *(_QWORD *)(v2 + 192) = v22 + v7;

  continue;

case 26LL:

  v23 = *v11;

  v11 += 2;

  *(_QWORD *)(v2 + 208) = v23 + v7;

  continue;

case 27LL:

  v24 = *v11;

  v11 += 2;

  *(_QWORD *)(v2 + 200) = (unsigned int)v24 >> 3;

  continue;

就是通过一个大的switch case去解析各项数据。

而andorid源码里是这样的:

 xref: /bionic/linker/linker.cpp

 

Home | History | Annotate | Line# | Navigate | Download                    Search  only in

 

3082    // and the relative order of DT_NEEDED elements, entries may appear in any order."

3083    //

3084    // source: http://www.sco.com/developers/gabi/1998-04-29/ch5.dynamic.html

3085    uint32_t needed_count = 0;

3086    for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {

3087      DEBUG("d = %p, d[0](tag) = %p d[1](val) = %p",

3088            d, reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));

3089      switch (d->d_tag) {

3090        case DT_SONAME:

3091          // this is parsed after we have strtab initialized (see below).

3092          break;

3093

3094        case DT_HASH:

3095          nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];

3096          nchain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];

3097          bucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8);

3098          chain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8 + nbucket_ * 4);

3099          break;

3100

3101        case DT_GNU_HASH:

3102          gnu_nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];

3103          // skip symndx

3104          gnu_maskwords_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[2];

3105          gnu_shift2_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[3];

3106

3107          gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr)*>(load_bias + d->d_un.d_ptr + 16);

3108          gnu_bucket_ = reinterpret_cast<uint32_t*>(gnu_bloom_filter_ + gnu_maskwords_);

3109          // amend chain for symdx = header[1]

3110          gnu_chain_ = gnu_bucket_ + gnu_nbucket_ -

3111                       reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];

3112

3113          if (!powerof2(gnu_maskwords_)) {

3114            DL_ERR("invalid maskwords for gnu_hash = 0x%x, in \"%s\" expecting power to two",

3115                   gnu_maskwords_, get_realpath());

3116            return false;

3117          }

3118          --gnu_maskwords_;

3119

3120          flags_ |= FLAG_GNU_HASH;

3121          break;

3122

3123        case DT_STRTAB:

3124          strtab_ = reinterpret_cast<const char*>(load_bias + d->d_un.d_ptr);

3125          break;

3126

3127        case DT_STRSZ:

3128          strtab_size_ = d->d_un.d_val;

3129          break;

3130

3131        case DT_SYMTAB:

3132          symtab_ = reinterpret_cast<ElfW(Sym)*>(load_bias + d->d_un.d_ptr);

3133          break;

3134

3135        case DT_SYMENT:

3136          if (d->d_un.d_val != sizeof(ElfW(Sym))) {

3137            DL_ERR("invalid DT_SYMENT: %zd in \"%s\"",

3138                   static_cast<size_t>(d->d_un.d_val), get_realpath());

可以看到几乎是一样的。但是如果不熟悉那些type的具体数值,如init_array是0x19,即10进制的25,则很难在ida中搞懂他是在做什么。

3.解密字符串表和符号表

执行完prelink操作后,就拿到了字符串表和符号表在内存中的偏移和大小。然后对其进行了解密操作:

sub_7D16188(

    v11[4],

    *(_DWORD *)(v9 + 0x80),

    *(_DWORD *)(v9 + 0x84),

    v11[6],

    *(_DWORD *)(v9 + 0x88),

    *(_DWORD *)(v9 + 0x7C));

其中,v11[4]是符号表的偏移,v11[6]是字符串表的偏移。而每个偏移后面跟随的是解密起始位置和总长度。

这个加固并没有将全部的字符串和符号都加密,而是进行了部分加密和部分解密。

1 signed __int64 __fastcall sub_7D16188(__int64 a1, unsigned int a2, unsigned int a3, __int64

2 {

3   __int64 v6; // x8

4   int v7; // w10

5   __int64 *v8; // x11

6   __int64 v9; // x12

7   __int64 v10; // x13

8

9   if ( a5 && !a6 )

10  {

11    v6 = 0LL;

12    do

13    {

14      *(_DWORD *)(a4 + v6) ^= 0x56312342u;

15      v6 += 4LL;

16    }

17    while ( (unsigned int)v6 < a5 );

18  }

19  if ( a2 < a3 )

20  {

21    v7 = a3 - a2;

22    v8 = (__int64 *)(a1 + 24LL * a2 + 16);

23    do

24    {

25      if ( !a6 )

26      {

27        v9 = *(v8 - 1);

28        if ( v9 )

29        {

30          v10 = *v8;

31          *(v8 - 1) = v9 ^ (a2 + 337);

32          *v8 = v10 ^ (a2 + 347);

33        }

34      }

35      --v7;

36      v8 += 3;

37    }

38    while ( v7 );

39  }

40  return 1LL;

41}

可以看到,字符串表的解密方式是按DWord进行异或0x56312342,而符号表的解密是与解密长度本身有关。

我们可以找到文件中字符串表和符号表的偏移,然后手动用脚本进行解密,解密完后再将数据覆盖回去:
def getInt32(data,offset): return data[offset]|data[offset+1]<<8|data[offset+2]<<16|data[offset+3]<<24 def getInt64(data,offset): return getInt32(data,offset)|getInt32(data,offset+4)<<32 def putInt64(data,offset,value): for i in range(8): data[offset+i]=value&0xff value = value >> 8 def decodeStrTable(): start = 0x45BBC38 encryptLen = 0x19c98//4 key = [0x42,0x23,0x31,0x56] with open("f:\test\libil2cpp.so",'rb') as f: data = list(f.read()) for i in range(encryptLen): for j in range(4): data[start+4*i+j]=data[start+4*i+j]^key[j] with open("f:\test\libil2cpp_str.so","wb") as f: f.write(bytes(data)) def decodeSymTable(): start = 0x4599990 _from = 0x8a8 _to = 0x113c with open("f:\test\libil2cpp_str.so", 'rb') as f: data = list(f.read()) while _from<_to: offset = start + 0x18*_from val0 = getInt64(data,offset+8)^((0x8a8+0x151)&0xffffffff) putInt64(data,offset+8,val0) val0 = getInt64(data,offset+0x10)^((0x8a8+0x15b)&0xffffffff) putInt64(data,offset+0x10,val0) _from = _from+1 with open("f:\test\libil2cpp_str_sym.so","wb") as f: f.write(bytes(data)) decodeStrTable()decodeSymTable()

覆盖回去后,重新用ida加载so,我们看到函数名已经正常了:

 Functions window

 

Function name                              Segment    Start

il2cpp_set_temp_dir                       LOAD       00000000000B9

il2cpp_set_memory_callbacks               LOAD       00000000000B9

il2cpp_resolve_icall                      LOAD       00000000000B9

il2cpp_array_get_byte_length              LOAD       00000000000B9

il2cpp_array_new_specific                 LOAD       00000000000B9

il2cpp_array_new_full                     LOAD       00000000000B9

il2cpp_array_element_size                 LOAD       00000000000B9

il2cpp_class_from_system_type             LOAD       00000000000B9

il2cpp_class_is_generic                   LOAD       00000000000B9

il2cpp_class_is_inflated                  LOAD       00000000000B9

il2cpp_class_has_parent                   LOAD       00000000000B9

il2cpp_class_get_events                   LOAD       00000000000B9

il2cpp_class_get_interfaces               LOAD       00000000000B9

il2cpp_class_get_properties               LOAD       00000000000B9

il2cpp_class_get_field_from_name          LOAD       00000000000B9

il2cpp_class_num_fields                   LOAD       00000000000B9

il2cpp_class_is_valuetype                 LOAD       00000000000B9

il2cpp_class_get_flags                    LOAD       00000000000B9

il2cpp_class_is_abstract                  LOAD       00000000000B9

il2cpp_class_from_type                    LOAD       00000000000B9

il2cpp_class_is_enum                      LOAD       00000000000B9

il2cpp_class_get_data_size                LOAD       00000000000B9

il2cpp_get_exception_argument_null        LOAD       00000000000B9

il2cpp_field_get_name                     LOAD       00000000000B9

il2cpp_field_get_offset                   LOAD       00000000000B9

il2cpp_field_get_value                    LOAD       00000000000B9

il2cpp_field_get_value_object             LOAD       00000000000B9

il2cpp_field_set_value_object             LOAD       00000000000B9

il2cpp_field_static_set_value             LOAD       00000000000B9

il2cpp_gc_collect                         LOAD       00000000000B9

il2cpp_gc_is_disabled                     LOAD       00000000000B9

il2cpp_gc_set_max_time_slice_ns           LOAD       00000000000B9

il2cpp_gc_set_max_time_slice_ns           LOAD       00000000000B9

il2cpp_gc_get_used_size                   LOAD       00000000000B9

il2cpp_gc_get_heap_size                   LOAD       00000000000B9

il2cpp_stop_gc_world                      LOAD       00000000000B9

il2cpp_start_gc_world                     LOAD       00000000000B9

il2cpp_gchandle_get_target                LOAD       00000000000B9

il2cpp_gc_set_external_allocation_tr...   LOAD       00000000000B9

il2cpp_gchandle_free                      LOAD       00000000000B9

il2cpp_unity_liveness_calculation_fr...   LOAD       00000000000B9

il2cpp_method_get_return_type             LOAD       00000000000B9

il2cpp_method_get_class                   LOAD       00000000000B9

il2cpp_method_get_declaring_type          LOAD       00000000000B9

il2cpp_method_get_token                   LOAD       00000000000B9

il2cpp_profiler_install_gc                LOAD       00000000000B9

il2cpp_profiler_install_fileio            LOAD       00000000000B9

il2cpp_profiler_install_thread            LOAD       00000000000B9

il2cpp_property_get_set_method            LOAD       00000000000B9

il2cpp_object_get_size                    LOAD       00000000000B9

il2cpp_object_get_virtual_method          LOAD       00000000000B9

il2cpp_object_unbox                       LOAD       00000000000B9

il2cpp_monitor_enter                      LOAD       00000000000B9

il2cpp_monitor_try_enter                  LOAD       00000000000B9

il2cpp_monitor_pulse                      LOAD       00000000000B9

il2cpp_monitor_try_wait                   LOAD       00000000000B9

il2cpp_runtime_class_init                 LOAD       00000000000B9

il2cpp_runtime_object_init                LOAD       00000000000B9

il2cpp_runtime_object_init_exception      LOAD       00000000000B9

il2cpp_string_length                      LOAD       00000000000B9

il2cpp_string_new                         LOAD       00000000000B9

但是,这时候还只是壳在运行,真正的原始的so还没有出来。真正的so要出来,在第三个init函数里。

 


自定义链接与重定位加载真正的so——第三个INIT函数


进入第三个init函数,首先解密了一小段代码,执行完又加密回去,功能是初始化一些全局变量,这里就不展开讲了。

核心是进入了自定义的链接器函数:

v23 = decrypt0(v15, *(_DWORD *)(v4 + 116), v11, *(_DWORD *)(v4 + 116), (__int64)v22);

if ( !*(unsigned *)(v4 + 116) )

    goto LABEL_48;

*(_QWORD *)(v6 + 136) = v11;

*(_QWORD *)(v6 + 152) = *(unsigned int *)(v4 + 116);

v24 = *(unsigned int *)(v4 + 116) + v15;

if ( v24 & 7 )

    v24 += 4LL;

*(_QWORD *)(v6 + 176) = v24;

v25 = v5 + 56LL * *(unsigned __int16 *)(v5 + 56) + 64;

v26 = (int8x16_t *)(*(__int64 (__fastcall *)(_QWORD))((char *)&qword_B8 + v7))(*(unsigned int *)(v4 + 112));

if ( (unsigned int)v27 & 1 ? v49 : (__int64 *)((char *)&v47 + 1));

decrypt0(v25, *(_DWORD *)(v4 + 112), v26, *(_DWORD *)(v4 + 112), v28);

if ( (unsigned int)v44 + 108) & 1 )

{

    v30 = HIDWORD(v27->n128_u64[0]);

    v31 = *(__int64 (**)(void))((char *)&qword_B8 + v7);

    v44 = LODWORD(v27->n128_u64[0]);

    v32 = v31();

    *(_QWORD *)(v6 + 128) = v32;

    v33 = build_reel_v32, 8, 14, (__int64)&v27->n128_i64[1], v30);

    v34 = *(void (__fastcall **)(int8x16_t *))((char *)off_C0 + v7);

    if ( v33 )

    {

        v34(*(int8x16_t **)(v6 + 128));

        v23 = (*(__int64 (__fastcall **)(int8x16_t *))((char *)off_C0 + v7))(v27);

        goto LABEL_48;

    }

    v34(v27);

    v29 = v44;

}

else

具体怎么看出来的是需要结合android linker部分的源码来研究,这里直接说最终的结论。已经在图中标出了各个函数的作用。

首先是初始化了soinfo,(先malloc一段内存,然后清零)

接下来解密了子so的代码段和数据段:

 v23 = decrypt0(v15, *(_DWORD *)(v4 + 116), v11, *(_DWORD *)(v4 + 116), (__int64)v22);

if ( !*(unsigned *)(v4 + 116) )

    goto LABEL_48;

*(_QWORD *)(v6 + 136) = v11;

*(_QWORD *)(v6 + 152) = *(unsigned int *)(v4 + 116);

v24 = *(unsigned int *)(v4 + 116) + v15;

if ( v24 & 7 )

    v24 += 4LL;

*(_QWORD *)(v6 + 176) = v24;

v25 = v5 + 56LL * *(unsigned __int16 *)(v5 + 56) + 64;

v26 = (int8x16_t *)(*(__int64 (__fastcall *)(_QWORD))((char *)&qword_B8 + v7))(*(unsigned int *)(v4 + 112));

if ( (unsigned int)v27 & 1 ? v49 : (__int64 *)((char *)&v47 + 1));

decrypt0(v25, *(_DWORD *)(v4 + 112), v26, *(_DWORD *)(v4 + 112), v28);

if ( (unsigned int)v44 + 108) & 1 )

{

    v30 = HIDWORD(v27->n128_u64[0]);

    v31 = *(__int64 (**)(void))((char *)&qword_B8 + v7);

    v44 = LODWORD(v27->n128_u64[0]);

    v32 = v31();

    *(_QWORD *)(v6 + 128) = v32;

    v33 = build_reel_v32, 8, 14, (__int64)&v27->n128_i64[1], v30);

    v34 = *(void (__fastcall **)(int8x16_t *))((char *)off_C0 + v7);

    if ( v33 )

    {

        v34(*(int8x16_t **)(v6 + 128));

        v23 = (*(__int64 (__fastcall **)(int8x16_t *))((char *)off_C0 + v7))(v27);

        goto LABEL_48;

    }

    v34(v27);

    v29 = v44;

}

else

解密算法是魔改的rc4。

解密之后,也解密出了子so的program header。然后根据子so的program header,将解密后的代码和数据通过load_segement操作映射到libil2cpp.so的内存空间。

子so的program header 是解密后存在别处的,在使用时单独调用。这样做可以防止无脑dump整个内存,因为dump下来没有正确的program header信息,子so的program header:

 00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00

3C A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00

46 A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00

4E A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00

57 A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00

5F A4 01 00 00 00 00 00  19 00 00 00 00 00 00 00

D0 B4 48 04 00 00 00 00  1B 00 00 00 00 00 00 00

48 00 00 00 00 00 00 00  1A 00 00 00 00 00 00 00

18 B5 48 04 00 00 00 00  1C 00 00 00 00 00 00 00

10 00 00 00 00 00 00 00  FE FF FF 6F 00 00 00 00

C8 E1 02 00 00 00 00 00  FF FF FF 6F 00 00 00 00

03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

load_segement也是android linker加载动态库的操作,核心步骤是:

1、遍历program header,找到PT_TYPE为1的段。(上面说过,type为2的是dynamic段,而type为1 的段是loadable段,即需要加载到内存中的部分)!

 > struct program_table_entry64_t program_table_element[0]      (R_X) Loadable Segment

> struct program_table_entry64_t program_table_element[1]      (R_X) Loadable Segment

v struct program_table_entry64_t program_table_element[2]      (RW_) Loadable Segment

    enum p_type64_e p_type                                     PT_LOAD (1h)

    enum p_flags64_e p_flags                                   PF_Read_Write (6h)

    Elf64_Off p_offset_FROM_FILE_BEGIN                         458EA50h

    Elf64_Addr p_vaddr_VIRTUAL_ADDRESS                         0x00000000007DF6A50

    Elf64_Addr p_paddr_PHYSICAL_ADDRESS                        0x00000000007DF6A50

    Elf64_Xword p_filesz_SEGMENT_FILE_LENGTH                   1610AD8h

    Elf64_Xword p_memsz_SEGMENT_RAM_LENGTH                     1610AD8h

    Elf64_Xword p_align                                        1000h

> char p_data[23136984]


数据段和代码段通常都是loadable的。而dynamic段通常包含在数据段中,只是单独用type=2指明出来,方便链接器快速定位。

2.通过mmap将文件中数据映射到内存,同时修改内存的访问权限。

3.进行页对齐操作,将多映射的内存用0填充。

对于某顿的加固,他自己实现的操作有些不一样,具体是:

1.计算出需要映射的地址的偏移和大小,通过mprotect函数将内存权限修改为可读可写可执行。

2.使用0xBB填充需要映射的内存。

 00000072375FFFF0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................

00000072376DC000  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC010  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC020  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC030  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC040  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC050  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC060  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC070  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC080  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC090  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC0A0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC0B0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC0C0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC0D0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC0E0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC0F0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC100  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC110  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC120  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC130  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC140  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC150  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC160  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC170  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC180  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC190  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC1A0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC1B0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC1C0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC1D0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

00000072376DC1E0  BB BB BB BB BB BB BB BB  BB BB BB BB BB BB BB BB  ................

3.使用memcpy将解密出来的数据和代码复制到对应内存。

4.将多余的内存用0填充。

5.根据segement的读写权限,再次调用,mprotect,修改为对应的权限。

91      v17 = v37;

92    }

93    v22 = (*(__int64 (__fastcall **)(unsigned __int64, unsigned __int64, _QWORD))((char *)&qword_10 + off_7DFC988))(

94        v14 & 0xFFFFFFFFFFFF00LL,

95        v17,

96        *v9 | 3u);                          // 用Hmprotect修改页属性,默认U写权限

97    sub_7D0FC10(v14 & 0xFFFFFFFFFFFF00BLL, 0x8BBLL, v17);// 用0x8BB填充

98    if ( v22 == -1 )

99      return 0LL;

100   v8 = v17;

101   if ( v35 )

102     (*(_void (__fastcall **)(__int64, __int64, __int64))((char *)&off_88 + off_7DFC988))(v14, v3 + v34, v35);// 使用memcpy将解密后的数据复制到这

103     v25 = v17 + 56 + v10;

104   v25 = *(_BYTE *)(v23 + 4);

105   v24 = (_DWORD *)(v23 + 4);

106   v26 = v35 + v14;

107   if ( v25 & 2 && v26 & 0xFFF )

108     (*(_void (__fastcall **)(__int64, _QWORD, __int64))((char *)&qword_80 + off_7DFC988))// 使用Hmemset将多余的内存用0填充

109       v26,

110       0LL,

111       4096 - (v26 & 0xFFF));

112   v27 = (v26 + 4095) & 0xFFFFFFFFFFFFF000LL;

113   if ( v36 > v27 )

114   {

115     v28 = v3;

116     v29 = off_7DFC988;

117     (*(_void (__fastcall **)(unsigned __int64, unsigned __int64, signed __int64, signed __int64, signed __int64, _QWORD))off_7DFC988)(

118       v27,

119       v36 - v27,

120       3LL,

121       50LL,

122       0xFFFFFFFFFLL,

123       0LL);

124   v30 = *(void (__fastcall **)(unsigned __int64, _QWORD, unsigned __int64))((char *)&qword_80 + v29);

125   v3 = v28;

126   v1 = v35;

127   v30(v27, 0LL, v36 - v27);

128   }

129   if ( (*(unsigned int (__fastcall **)(unsigned __int64, unsigned __int64, _QWORD))((char *)&qword_10 + off_7DFC988))()// 再次使用Hmprotect,将页权限还原回页头指定

130   {

131     v38,

132     (*v28 & 2 | (*v24 >> 2) & 1) & 0xFFFFFFFF | 4 * (*v24 & 1)) == -1 )

133     return 0LL;

134   v7 = v1[3];

135   v9 = off_7DFC820[0];

等等,没有调用mmap,那么往哪里映射?

其实,在壳so加载的时候,已经映射了足够的内存空间:

struct program_table_entry64_t program_table_element[0]     (R X) Loadable Segment

    enum p_type64_e p_type                                   PT_LOAD (1h)

    enum p_flags64_e p_flags                                 PF_Read_Exec (5h)

    Elf64_Off p_offset_FROM_FILE_BEGIN                       0h

    Elf64_Addr p_vaddr_VIRTUAL_ADDRESS                       0x0000000000000000

    Elf64_Addr p_paddr_PHYSICAL_ADDRESS                      0x0000000000000000

    Elf64_Xword p_filesz_SEGMENT_FILE_LENGTH                 447C000h

    Elf64_Xword p_memsz_SEGMENT_RAM_LENGTH                   7CD4000h

    Elf64_Xword p_align                                      1000h

看看第一段。第一段其实是原始的so的代码,文件大小只有0x447c000,但是居然在内存中要了0x7cd4000的空间。这么大的空间就是为了后面把解密的代码和数据全部复制过来用的。

由于这部分数据是加密的,没什么卵用,所以直接将解密后的数据覆盖过来挺好。某顿的壳就是这么做的。

load_segement之后,接着根据壳so的dynamic信息,执行了一次prelink操作,这次操作的目的是获取到字符串表等信息。

(由于壳so和子so是共享了部分信息,所以有必要获取一下壳so的各种数据信息)

接着,根据解密后子so的dynamic段信息,再次执行prelink。
为什么再次执行prelink?

因为壳so和子so的初始化函数肯定不一样,要把子so成功加载进内存,显然要执行子so的init函数。所以这次prelink要把init_array修改为子so的init_array,方便后面执行。子so的dynamic 段数据如下:

00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00

3C A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00

46 A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00

4E A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00

57 A4 01 00 00 00 00 00  01 00 00 00 00 00 00 00

5F A4 01 00 00 00 00 00  19 00 00 00 00 00 00 00

D0 B4 48 04 00 00 00 00  1B 00 00 00 00 00 00 00

48 00 00 00 00 00 00 00  1A 00 00 00 00 00 00 00

18 B5 48 04 00 00 00 00  1C 00 00 00 00 00 00 00

10 00 00 00 00 00 00 00  FE FF FF 6F 00 00 00 00

C8 E1 02 00 00 00 00 00  FF FF FF 6F 00 00 00 00

03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

可以看到,子so的dynamic信息很少。只有init_array,fini_array的信息。
其实,这个动态信息是壳so的dynamic 和子so的dynamic diff出来的信息。对于子so,大部分信息已经包含在壳的信息里了,只有少部分需要修正。

第二次prelink后,子so的数据已经全部加载到内存中了,但是还没有进行重定位,无法执行。

需要执行重定位(relocate)操作。

*(_QWORD *)(v6 + 32) = v29;
*(_QWORD *)(v6 + 48) = v30;
*(_QWORD *)(v6 + 144) = v31;
*(_QWORD *)(v6 + 160) = v32;
*(_QWORD *)(v6 + 64) = v33;
*(_QWORD *)(v6 + 72) = v34;
if ( !(prelink(v6) & 1) )
  goto LABEL_36;
sub_7D16A74(v6);
v8 = decrypt_something(v6, v35);
if ( *(_BYTE *)(v6 + 336) & 1 )
  goto LABEL_9;
v8 = relocate(v6);
if ( !(v8 & 1) )

 

具体的,也是仿照android 源码,根据重定位符号类型,对符号地址做修正:

 

switch ( (_DWORD)v8 )
{
  case 0x401:
  case 0x402:
    goto LABEL_28;
  case 0x403:
    if ( v11 )
      goto LABEL_37;
    v2 = v4[2];
    goto LABEL_29;
  case 0x404:
  case 0x405:
    goto LABEL_33;
  case 0x406:
  case 0x407:
    goto LABEL_31;
  case 0x408:
    goto LABEL_27;
  default:
    if ( (_DWORD)v8 == 257 )
      goto LABEL_24;
    if ( (_DWORD)v8 != 260 )
      goto LABEL_33;
    v21 = *v7 + v29 - *(v7 - 2) + *(_QWORD *)(v9 + v10);
    break;
}

 

其中,0x401,0x402是pltgot类型的重定位,0x403是相对距离调用类型的重定位。需要对这些数字很敏感,才能看出这是在做重定位。这就需要各位仔细阅读andorid linker部分的源码了,我也是在做这次逆向过程中读了好几遍,才基本搞懂的。
根据图片中的代码,我识别出以下OCR文字:
c/* Dynamic relocations */
#define R_AARCH64_COPY 1024
#define R_AARCH64_GLOB_DAT 1025 /*%
#define R_AARCH64_JUMP_SLOT 1026 /*%
#define R_AARCH64_RELATIVE 1027 /*%
#define R_AARCH64_TLS_TPREL64 1030
#define R_AARCH64_TLS_DTPREL32 1031
#define R_AARCH64_IRELATIVE 1032
ARM64 (AArch64) 重定位类型定义:
这些是ARM64架构的ELF重定位类型常量:
重定位类型值说明R_AARCH64_COPY1024复制重定位(用于共享对象中的数据)R_AARCH64_GLOB_DAT1025全局数据重定位(对应前面的R_GENERIC_GLOB_DAT)R_AARCH64_JUMP_SLOT1026PLT跳转槽重定位(对应R_GENERIC_JUMP_SLOT)R_AARCH64_RELATIVE1027相对重定位(对应R_GENERIC_RELATIVE)R_AARCH64_TLS_TPREL641030TLS线程局部存储重定位R_AARCH64_TLS_DTPREL321031TLS动态线程局部存储重定位R_AARCH64_IRELATIVE1032间接相对重定位(对应R_GENERIC_IRELATIVE
上图是android 源码里关于重定位类型的定义0x401对应10进制的1025,依次类推。

根据图片中的代码,我识别出以下OCR文字:
cswitch (type) {
case R_GENERIC_JUMP_SLOT:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO JMP_SLOT %16p <- %16p %s\n",
reinterpret_cast<void*>(reloc),
reinterpret_cast<void*>(sym_addr + addend), sym_name);

*reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
break;

case R_GENERIC_GLOB_DAT:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO GLOB_DAT %16p <- %16p %s\n",
reinterpret_cast<void*>(reloc),
reinterpret_cast<void*>(sym_addr + addend), sym_name);
*reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
break;

case R_GENERIC_RELATIVE:
count_relocation(kRelocRelative);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO RELATIVE %16p <- %16p\n",
reinterpret_cast<void*>(reloc),
reinterpret_cast<void*>(load_bias + addend));
*reinterpret_cast<ElfW(Addr)*>(reloc) = (load_bias + addend);
break;

case R_GENERIC_IRELATIVE:
count_relocation(kRelocRelative);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO IRELATIVE %16p <- %16p\n",
reinterpret_cast<void*>(reloc),
reinterpret_cast<void*>(load_bias + addend));

android 源码中重定位部分,也是根据重定位类型做switch case循环。(只不过为了跨平台,把宏又全部重新定义为了R_GENERIC类型)

执行完重定位后,调用了子so的init函数:
根据图片中的代码,我识别出以下OCR文字:
cv13 = *(_QWORD *)(v6 + 200);
if ( (_DWORD)v13 )
{
v14 = (unsigned int)v13;
do
{
if ( (unsigned __int64)*v12 + 1 >= 2 )
v8 = (*v12)(v8);
++v14;
++v12;
}
while ( v14 );
}
代码分析(红框部分):
cif ( (unsigned __int64)*v12 + 1 >= 2 )
v8 = (*v12)(v8);
到这里,子so就被正确的加载进了内存。

 


修复——借尸还魂


6.1移植代码段和数据段


我们将所有解密的数据先在内存中dump下来,idadump脚本:
import idaapidata = idaapi.dbg_read_memory(start, len)fp = open('filename', 'wb')fp.write(data)fp.close()

子so有两个segement(代码段与数据段),同时,子so的program header,dynamic段信息,符号表,重定位表是存在别处的,我们都要dump下来(或者截屏保存,如果数据不多的话):
根据图片中的文件列表,我识别出以下OCR文字:
dum_relo
dum_seg1
dum_seg2
dum_sym
libil2cpp.so

然后,我们对已经解密过字符串表和符号表的壳so做修改。(因为很多数据是共享的,不能只组装一个子so,所以我们要借壳so的尸,来还子so的魂)
首先将解密后的第一段复制到program header后面(我们保留壳so的program header。

因为壳的很多东西还要映射,后面我们在壳so的program header上做修改,把子so的移植进去)

壳so的第一段,可以看到数据是加密的。

根据图片中的十六进制数据,我识别出以下OCR文字:
01C0h: 01 00 00 00 00 00 00 00 EF A7 4E D3 CE 11 E0 D6
01D0h: AF 4B 3B 0C D2 63 F9 82 2A E0 27 1C 32 45 F5 FF . K ; . . c . . * . ' . 2 E . .
01E0h: D5 F5 0A 4A 55 A3 A0 9F FE 4A C5 85 05 FF F6 9F . . . J U . . . . J . . . . . .
01F0h: 5D 85 C5 05 0A 9F 9F 54 F5 F7 76 DF DF D1 72 73 ] . . . . . . T . . v . . . r s
0200h: F7 F2 5F F5 5A DF 5F 38 EF AE 29 3F 14 03 2C B0 . . _ . Z . _ 8 . . ) ? . . , .
0210h: 13 35 8E 50 E3 F7 97 E3 F5 34 66 2B F1 FD 4C D8 . 5 . P . . . . . 4 f + . . L
0220h: 3C D2 FA 3E 8E 45 D5 68 9A EA ED 96 03 F5 BD 17 . . . > . E . . . . . . . . . .
0230h: 0F 76 F2 DA FB 55 A7 16 D6 B5 CE B2 A9 EE F3 6B . v . . . U . . . . . . . . . β .
0240h: B1 8A 35 06 5C B6 D2 7B 3E 24 BF 52 7A FD A3 D1 . . 5 . \ . . { > $ . R z . . .
0250h: B5 52 9C C7 BF 36 0B ED 86 EC D9 27 58 24 AC 78 v R . . . 6 . . . . . ' X $ . x
0260h: DF CB B2 7F 02 F5 18 E7 8B B0 25 22 0D 1E 73 F4 . . . . . . . . . % " . . . s
0270h: 6F 88 23 22 F3 5C EF E4 DB F4 AC 0D 19 4B 6 E7 . . # " . \ . . . . . . . K . .
0280h: D3 CC 01 CC 99 71 A5 D8 A4 BA 87 DE B4 A8 79 47 . . . . . q . . . . . . . . y G
0290h: B7 F4 DB C7 75 77 5E 84 60 2D 72 63 6E 76 22 2B . . . . u w ^ . ` - r c n v " +
02A0h: 23 72 C1 83 40 95 E3 D4 2F FE 6B 45 66 75 74 94 # r . . @ . . . / . k E f u t .
02B0h: E2 50 3C E9 A1 2F C8 E7 76 3E 6D 9C FC 3E 65 2B . P < . . / . . v > m . . > e +
02C0h: 5E CA B8 39 2E BC 99 F2 24 8A 2C 83 18 B2 0F AD ^ . . 9 . . . . $ . , . . . . .
02D0h: D8 73 B9 51 D8 89 33 CF 2D AB BF 2D 45 02 D4 B3 . . . Q . . 3 . - . . - E . . .
02E0h: 6A 04 D4 AA A3 59 AF 71 4C E8 AE 5F 1F F9 5C AE j . . . . Y . q L . . _ . . \ .
02F0h: 59 D6 9D E6 84 3F 1C 4A FE C5 24 52 30 C9 F8 21 Y . . . . ? . J . . $ R 0 . . !
0300h: 47 E7 53 87 DC 78 AB EB 79 FA E2 36 76 F0 CA 7B G . . . . x . . y . . 6 v . . {
0310h: AA 0F E0 20 CF 03 B2 79 A9 E0 21 B0 AB A0 E0 D7 . . . . . . . y . . ! . . . . .
0320h: 2F 88 6A 47 EC 36 F2 90 01 B1 E6 D0 5C 9E AF 70 . . j G . 6 . . . . . . \ . . p

复制后:
01A0h: 50 6A DF 07 00 00 00 00 50 6A DF 07 00 00 00 00 P j
01B0h: B0 65 00 00 00 00 00 00 B0 65 00 00 00 00 00 00 . e .
01C0h: 01 00 00 00 00 00 00 00 04 00 00 00 14 00 00 00 . . .
01D0h: 03 00 00 00 47 4E 55 00 34 5C A5 91 6D D1 DC 00 . . .
01E0h: C4 01 99 02 33 3C DB D7 28 DB 64 F3 00 00 00 00 . . .
01F0h: 05 08 00 00 B5 09 00 00 01 00 00 00 00 00 00 00 . . .
0200h: 00 00 00 00 00 00 00 00 15 06 00 00 00 00 00 00
0210h: B1 00 00 00 C6 00 00 00 00 00 00 00 82 05 00 00
0220h: 00 00 00 00 F0 00 00 00 00 00 00 00 00 00 00 00
0230h: 00 00 00 00 5E 00 00 00 00 00 00 00 00 00 00 00
0240h: C1 00 00 00 20 01 00 00 2E 05 00 00 87 04 00 00
0250h: ED 00 00 00 00 00 00 00 1B 01 00 00 F8 04 00 00
0260h: 00 00 00 00 78 03 00 00 96 00 00 00 48 05 00 00
0270h: 00 00 00 00 76 01 00 00 00 00 00 00 04 03 00 00
第一段其实是加密过的子so的代码段。

接下来需要将子so的数据段弄进来。但是program header里没有关于子so数据段的映射信息。我们需要先修改program header。

根据图片中的程序头表(Program Header Table)信息,我识别出以下OCR文字:
▼ struct program_header_table
> struct program_table_entry64_t program_table_element[0] (R_X) Loadable Segment
> struct program_table_entry64_t program_table_element[1] (R_X) Loadable Segment
> struct program_table_entry64_t program_table_element[2] (RW_) Loadable Segment
> struct program_table_entry64_t program_table_element[3] (RW_) Dynamic Segment
> struct program_table_entry64_t program_table_element[4] (R__) GCC_eh_frame_hdr Segment
> struct program_table_entry64_t program_table_element[5] (RW_) GNU Stack (executability)
> struct program_table_entry64_t program_table_element[6] (RW_) Loadable Segment ←
struct section_header_table
程序头表段类型说明:
索引权限段类型说明[0]R_XLoadable Segment只读可执行代码段[1]R_XLoadable Segment只读可执行代码段[2]RW_Loadable Segment可读写数据段[3]RW_Dynamic Segment动态链接信息段[4]R__GCC_eh_frame_hdr异常处理帧头[5]RW_GNU Stack栈段(可执行性标记)[6]RW_Loadable Segment可读写数据段(红色箭头指向)
权限标识:

R = Read(可读)
W = Write(可写)
X = Execute(可执行)

红色箭头指向的第6个段是一个可读写的可加载段,这通常是数据段或BSS段,可能包含Il2Cpp运行时需要的全局变量和动态分配的数据。

壳so有七个program header,期中,最后一个是GNU Read-only After Relocation。里面保留的主要是pltgot表信息。在重定位后提示链接器,这里不要再动了。

我们可以把这段删掉,换成子so数据段对应的header信息:

直接复制过来,这时候第七个program header也变成loadable 的了:

然后我们将子so的数据段复制过来,复制到哪?

由于在文件中,数据十分紧密,如果随意覆盖,可能会破话其他部分的数据,会出问题。

所以我们将子so的数据段数据附在文件的末尾,然后通过修改program header里面的 file offset字段来完成修改。

将子so的代码和数据都移植进去后,我们打开ida看看:

Please confirm

⚠️ Binary data 76338216 is incorrect, maximum possible value is 13646880. Do you want to continue with the new value?

[Yes] [No] [Cancel]
翻译:
请确认

二进制数据 76338216 不正确,最大可能值为 13646880。您要继续使用新值吗?

[是] [否] [取消]

直接报错。

是因为我们在把子so的数据段往文件末尾添加时,忘记修改section header的偏移了。错误的section header会干扰ida的分析。

于是我们调整section header的偏移。在文件的末尾再加上一个空的section header,然后修改elf 头中关于section header offset的信息:

55D:1030h: FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00 00 . . . .
55D:1040h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . . . .
55D:1050h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . . . .
55D:1060h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . . . .
55D:1070h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . . . .
55D:1080h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . . . .
55D:1090h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . . . .
55D:10A0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . . . .
55D:10B0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . . . .

根据图片中的ELF文件头结构信息,我识别出以下OCR文字:
▼ struct elf_header
> struct e_ident t e_ident
enum e_type64_e e_type ET_DYN (3h)
enum e_machine64_e e_machine B7h
enum e_version64_e e_version EV_CURRENT (1h)
Elf64_Addr e_entry START_ADDRESS 0x00000000000B0D580
Elf64_Off e_phoff_PROGRAM_HEADER_OFFSET_IN_FILE 40h
Elf64_Off e_shoff_SECTION_HEADER_OFFSET_IN_FILE 55D1040h ←
Elf32_Word e_flags 0h
Elf64_Half e_ehsize_ELF_HEADER_SIZE 40h
关键信息解析:

e_type: ET_DYN (3h) - 动态共享对象(.so文件)
e_machine: B7h (0xB7 = 183) - AArch64 (ARM 64位架构)
e_version: EV_CURRENT (1h) - 当前ELF版本
e_entry: 0x00000000000B0D580 - 程序入口地址
e_phoff: 40h (64字节) - 程序头表偏移
e_shoff: 55D1040h (红色箭头指向) - 节头表偏移位于文件的 0x55D1040 位置
e_flags: 0h - 处理器特定标志
e_ehsize: 40h (64字节) - ELF文件头大小

重点:红色箭头指向的 e_shoff = 55D1040h 表示**节头表(Section Header Table)**在文件中的偏移位置,这与之前看到的"SHT损坏"警告直接相关。这个偏移量指向了存储所有段(section)信息的表格位置。

再次打开ida,我们兴奋的发现,熟悉的JNI_ONLoad出现了:

根据图片中的ARM64汇编代码,我识别出以下OCR文字:
assembly EXPORT JNI_OnLoad
JNI_OnLoad ; DATA XREF: LOAD:000000007E0F0104↓o

var_10 = -0x10
var_s0 = 0

STR X19, [SP,#-0x10+var_10]!
STP X29, X30, [SP,#0x10+var_s0]
ADD X29, SP, #0x10
ADRP X1, #aIl2cpp@PAGE ; "IL2CPP"
ADRP X2, #aJniOnload_0@PAGE ; "JNI_OnLoad"
MOV X19, X0
ADD X1, X1, #aIl2cpp@PAGEOFF ; "IL2CPP"
ADD X2, X2, #aJniOnload_0@PAGEOFF ; "JNI_OnLoad"
MOV W0, #4
BL sub_B0BCA0
LDP X29, X30, [SP,#0x10+var_s0]
ADRP X10, #sub_B9C01C@PAGE
ADRP X8, #qword_49ADAF0@PAGE
ADRP X9, #qword_49ADB10@PAGE
ADD X10, X10, #sub_B9C01C@PAGEOFF
MOV W0, #6
MOVK W0, #1,LSL#16
STR X19, [X8,#qword_49ADAF0@PAGEOFF]
STR X10, [X9,#qword_49ADB10@PAGEOFF]
LDR X19, [SP+0x10+var_10],#0x20
RET
; End of function JNI_OnLoad
关键信息分析:

函数名: JNI_OnLoad - 这是JNI库加载时的入口函数
字符串引用:

"IL2CPP" - Il2Cpp引擎标识
"JNI_OnLoad" - 函数名字符串


重要调用:

sub_B0BCA0 - 可能是日志或初始化函数
sub_B9C01C - 存储到全局变量,可能是Il2Cpp的核心初始化函数


返回值: W0 = 0x00010006 (JNI版本号 1.6)
全局变量:

qword_49ADAF0 - 存储 JNI JavaVM 指针
qword_49ADB10 - 存储初始化函数指针

 

这是Il2Cpp通过JNI与Java层交互的关键入口点,负责初始化Il2Cpp运行时环境。

并且有关于IL2CPP的信息。但是点进sub_b0bca0后发现,函数是空的:

根据图片中的IDA Pro反汇编代码,我识别出以下OCR文字:
assembly0 ; ================ S U B R O U T I N E ========================================
0
0
0 sub_B0BCA0 ; CODE XREF: JNI_OnLoad+24↓p
0 ; sub_B9C01C+17C↓p ...
0 ADRP X16, #off_467CB28@PAGE
4 LDR X17, [X16,#off_467CB28@PAGEOFF]
8 ADD X16, X16, #off_467CB28@PAGEOFF
C BR X17 ; sub_B0B360
C ; End of function sub_B0BCA0
C
0
0 ; ================ S U B R O U T I N E ========================================
0
0
0 sub_B0BCB0 ; CODE XREF: std::__ndk1::moneypunct_byname<char
0 ; std::__ndk1::moneypunct_byname<char,false>::...
0 ADRP X16, #off_467CB30@PAGE
4 LDR X17, [X16,#off_467CB30@PAGEOFF]
8 ADD X16, X16, #off_467CB30@PAGEOFF
C BR X17 ; sub_B0B360
C ; End of function sub_B0BCB0
C
0
0 ; ================ S U B R O U T I N E ========================================
0
0
0 sub_B0BCC0 ; CODE XREF: std::__ndk1::basic_istream<char,st
0 ADRP X16, #off_467CB38@PAGE
4 LDR X17, [X16,#off_467CB38@PAGEOFF]
8 ADD X16, X16, #off_467CB38@PAGEOFF
C BR X17 ; sub_B0B360
C ; End of function sub_B0BCC0
C
0
0 ; ================ S U B R O U T I N E ========================================
0
0
0 sub_B0BCD0 ; CODE XREF: il2cpp_init+1C↓p
0 ; il2cpp_init_utf16+34↓p ...
0 ADRP X16, #off_467CB40@PAGE
4 LDR X17, [X16,#off_467CB40@PAGEOFF]
8 ADD X16, X16, #off_467CB40@PAGEOFF
C BR X17 ; sub_B0B360
C ; End of function sub_B0BCD0
代码分析:
这些是 ARM64 架构的函数跳转桩(stub functions),也称为 PLT (Procedure Linkage Table) 条目。每个函数都执行相同的模式:

ADRP: 加载页地址到 X16
LDR: 从全局偏移表(GOT)加载实际函数地址到 X17
ADD: 计算完整地址
BR X17: 跳转到实际函数

关键函数:

sub_B0BCA0: 被 JNI_OnLoad 调用
sub_B0BCB0: C++ STL moneypunct_byname 相关
sub_B0BCC0: C++ STL basic_istream 相关
sub_B0BCD0: Il2Cpp 初始化函数 (il2cpp_init)

这些跳转桩是动态链接的关键部分,它们通过GOT表间接调用实际函数,支持运行时地址重定位。


看这个样子像是子so的plt表。但是函数都是sub_b0b360。原来,我们没有修复子so的重定位信息,导致ida找不到这些符号的真实地址。因为,子so的重定位表还在我们手里呢?

6.2修复符号表与重定位表


由于子so与壳so都需要重定位,所以我们可以把子so和壳so的重定位表合在一起。但是这样一来,壳so数据段空间不够了。(文件中数据很紧密的)而且如果直接将子so的重定位表覆盖在壳so重定位表的后面,谁知道会破坏什么数据。

所以我们需要做的操作是:
1.将壳so的重定位表copy一份出来。

2.将子so的重定位表与壳so的重定位表合成一个大的重定位表。

3.将新的重定位表附在壳数据段的末尾。(此时我们覆盖已经了子so的数据段数据,因为壳so的数据段末尾基本在原文件的末尾)

4.修改program header中壳so的数据段中,file_size字段(多出了子so的重定位表,需要一起映射进去)

5.修改dynamic 段中关于重定位表偏移和大小的信息(因为我们把重定位表附在了文件末尾,原来的已经不用了,并且大小变了)

dynamic 段中关于重定位表的信息(7,8为重定位表相关,可以参考前面的那张dynamic type表):

00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00
30 B7 E1 07 00 00 00 00 01 00 00 00 00 00 00 00
3C A4 01 00 00 00 00 00 01 00 00 00 00 00 00 00
46 A4 01 00 00 00 00 00 01 00 00 00 00 00 00 00
4E A4 01 00 00 00 00 00 01 00 00 00 00 00 00 00
57 A4 01 00 00 00 00 00 01 00 00 00 00 00 00 00
5F A4 01 00 00 00 00 00 19 00 00 00 00 00 00 00
50 6A DF 07 00 00 00 00 1B 00 00 00 00 00 00 00
20 00 00 00 00 00 00 00 1A 00 00 00 00 00 00 00
70 6A DF 07 00 00 00 00 1C 00 00 00 00 00 00 00
20 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00
38 3C E2 07 00 00 00 00 06 00 00 00 00 00 00 00
90 19 E0 07 00 00 00 00 0A 00 00 00 00 00 00 00
80 A4 01 00 00 00 00 00 0B 00 00 00 00 00 00 00
18 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00
88 BA DF 07 00 00 00 00 02 00 00 00 00 00 00 00
C8 25 00 00 00 00 00 00 14 00 00 00 00 00 00 00
07 00 00 00 00 00 00 00 17 00 00 00 00 00 00 00
48 C7 D0 07 00 00 00 00 07 00 00 00 00 00 00 00 ← 红框
E8 F4 CF 07 00 00 00 00 08 00 00 00 00 00 00 00 ← 红框
60 D2 00 00 00 00 00 00 09 00 00 00 00 00 00 00 ← 红框
18 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 FB FF FF 6F 00 00 00 00
01 00 00 00 00 00 00 00 F9 FF FF 6F 00 00 00 00
03 02 00 00 00 00 00 00 0E 00 00 00 00 00 00 00

修改后:

C8 25 00 00 00 00 00 00 14 00 00 00 00 00 00 00
07 00 00 00 00 00 00 00 17 00 00 00 00 00 00 00
48 C7 D0 07 00 00 00 00 07 00 00 00 00 00 00 00
78 D1 91 08 00 00 00 00 08 00 00 00 00 00 00 00
B0 A3 AE 00 00 00 00 00 09 00 00 00 00 00 00 00
18 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 FB FF FF 6F 00 00 00 00

6.由于我们覆盖了子so的数据段。所以我们需要重新把子so的数据段附在文件末尾,然后修改该段对应的program header中偏移信息,然后附加上新的section header,然后再次修改elf头中section header的偏移。
这样,子so的重定位表就加进去了。

但是没完。重定位中主要有两种,一种是pltgot表的重定位,一种是相对距离调用的重定位。

相对距离调用的重定位比较简单,linker会直接用elf文件 的base 加上重定位信息中的addend信息作为内存中真实的地址。

根据图片中的十六进制编辑器内容,我识别出以下OCR文字:
0 1 2 3 4 5 6 7 8 9 A B C D E F 01
D0 B4 48 04 00 00 00 00 03 04 00 00 00 00 00 00
DC D5 B0 00 00 00 00 00 D8 B4 48 04 00 00 00 00
03 04 00 00 00 00 00 00 64 D9 B0 00 00 00 00 00
E0 B4 48 04 00 00 00 00 03 04 00 00 00 00 00 00
F0 D9 B0 00 00 00 00 00 E8 B4 48 04 00 00 00 00
03 04 00 00 00 00 00 00 70 DF B0 00 00 00 00 00
F0 B4 48 04 00 00 00 00 03 04 00 00 00 00 00 00
8C E0 B0 00 00 00 00 00 F8 B4 48 04 00 00 00 00
数据结构分析(红框标注的部分):
这看起来是一个结构体数组,每个条目包含:

前8字节:地址/指针(如 D0 B4 48 04 00 00 00 00)
中间8字节:标志/类型信息(如 03 04 00 00 00 00 00 00)
后8字节:另一个地址/指针(如 00 00 00 00 00 00 00 00)

或者每16字节为一组:

字节0-7: 第一个指针(如 04 48 B4 D0 = 0x04B48D0,小端序)
字节8-15: 大小或标志 + 第二个指针

例如上图中红框内是一个重定位信息。

重定位表的数据结构是这样的:
typedef struct elf64_rela { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend;} Elf64_Rela;

第一个字段是符号的偏移(重定位前),第二个是重定位类型(0x403,0x402之类),第三个是附加信息。

对于红框中的数据来说,他的意思是在0x448b4d0这个位置的符号,是0x403(相对距离)类型的重定位信息。addend是0xb0d5dc。
假设我们的so文件加载的基地址是0x70000000。那么在重定位的过程中,linker会做这样的操作:
*(0x70000000+0x448b4d0)=0x700000000+0xb0d5dc

这样就完成了重定位。但是pltgot表的重定位比较特殊:
根据图片中的十六进制数据,我识别出以下OCR文字:
E8 BA DE 07 00 00 00 00 02 04 00 00 51 00 00 00
00 00 00 00 00 00 00 00 F0 BA DF 07 00 00 00 00

上面是一个pltgot类型的重定位,可以看到符号的地址是0x7dfbae8,但是没有addend,反而在02 04后面有一个0x51。

其实,这个0x51是符号表的符号索引。

linker会根据这个索引,先在符号表中找到对应的符号,然后获取符号名。
通过符号名分别在依赖库和自身的符号表里找这个符号。

然后将找到的符号的真实地址直接用来重定位。

某顿在做重定位时,用的是子so的符号表而不是壳so的符号表。由于符号表只能有一张,如果我们直接按照重定位表那样融合,索引一定会乱掉。所以我们只能用子so的符号表去覆盖壳so的符号表。

这样一来,壳so的重定位会有一部分乱掉。我们手动修复一下,运气好的话不会太多,10处左右。

修复完符号表和重定位信息后,我们看到了之前JNI_onload里的函数:
04448B518 ; ELF Termination Function Table
04448B518 DCQ j_nullsub_1211
04448B520 DCQ sub_B0E74C
04448B528 DCB 0
关键信息:

地址 04448B518: ELF 终止函数表(ELF Termination Function Table)
j_nullsub_1211: 一个空函数跳转(null subroutine),通常是占位符或已被优化掉的函数
sub_B0E74C: 另一个终止函数

红色箭头指向 j_nullsub_1211,这表明这个位置可能是一个重要的hook点或需要特别注意的函数指针。在Il2Cpp的上下文中,这可能与程序的清理或反调试机制有关。

原来是在打log。

6.3修复init_array
记得要把init_array修复为子so的init_array。具体过程就不在赘述了。

6.4将壳so的第一个init_array添加
因为某顿的保护在子so运行过程中会用到之前初始化过的全局函数数组。所以要把壳so的第一个函数的init_array函数加到子so的init_array里。

具体的:
1.修改dynamic 段init_array_size部分,加一个函数。

2.对应的需要对finit_array_size部分减一个函数,同时将finit_array的偏移向后移动一个指针的距离(空出来的部分给添加的init函数)

3.找到壳so 第一个init函数的重定位信息,将其复制给新添加的init函数对应的重定位表里。

如图,是原来fini_array的样子:

04448B518 ; ELF Termination Function Table
04448B518 DCQ j_nullsub_1211
04448B520 DCQ sub_B0E74C
04448B528 DCB 0
关键信息:

地址 04448B518: ELF 终止函数表(ELF Termination Function Table)
j_nullsub_1211: 一个空函数跳转(null subroutine),通常是占位符或已被优化掉的函数
sub_B0E74C: 另一个终止函数

红色箭头指向 j_nullsub_1211,这表明这个位置可能是一个重要的hook点或需要特别注意的函数指针。在Il2Cpp的上下文中,这可能与程序的清理或反调试机制有关。

把第一个函数替换后:

根据图片中的IDA Pro反汇编代码,我识别出以下OCR文字:
assembly90447B4CF
90448B4D0 ; ELF Initialization Function Table
90448B4D0 ; ===============================================================================
90448B4D0
90448B4D0 ; Segment type: Pure data
90448B4D0 AREA LOAD, DATA, ALIGN=0
90448B4D0 ; ORG 0x4488D0
90448B4D0 off_4488D0 DCQ sub_B0D5DC ; DATA XREF: LOAD:00000000000001A0↑o
90448B4D8 DCQ sub_B0D964
90448B4E0 DCQ sub_B0D9F0
90448B4E8 DCQ sub_B0DF70
90448B4F0 DCQ sub_B0E08C
90448B4F8 DCQ sub_B0E16C
90448B500 DCQ sub_B0E1E4
90448B508 DCQ sub_B0E320
90448B510 DCQ sub_B0E354
90448B518 DCQ sub_7D440DC
90448B520 ; ELF Termination Function Table
90448B520 DCQ sub_B0E74C
90448B528 DCB 0
90448B529 DCB 0
90448B52A DCB 0
90448B52B DCB 0
90448B52C DCB 0
90448B52D DCB 0
90448B52E DCB 0
90448B52F DCB 0
90448B530 ; public Il2CppExceptionWrapper
90448B530 ; `typeinfo for'Il2CppExceptionWrapper
90448B530 _ZTI22Il2CppExceptionWrapper DCQ _ZTVN10__cxxabiv17__class_type_infoE+0x10
90448B530 ; DATA XREF: sub_BC5C80+1C↑o
90448B530 ; sub_BC5C80+20↑o ...
90448B530 ; reference to RTTI's type class
90448B538 DCQ _ZTS22Il2CppExceptionWrapper ; reference to type's name
90448B540 DCB 8 ; OFF64 SEGDEF [LOAD,48CD408]
90448B541 DCB 0xD4
90448B542 DCB 0x8C
关键信息:

ELF初始化函数表 (Initialization Function Table):包含多个初始化函数指针
ELF终止函数表 (Termination Function Table):包含清理函数
重要函数指针:

sub_7D440DC(红色箭头指向)
各种 sub_B0xxxx 函数


Il2CppExceptionWrapper 类型信息:这是 Il2Cpp 异常处理包装器的 RTTI 信息

这些是 Il2Cpp 运行时初始化和类型系统的核心数据结构。

可以到fini 函数少了一个,而init函数多了一个!这个函数正是壳so的第一个init函数。

6.5添加正确的section header
对于ida来说,没有正常的section header是可以正常解析的。但是对于系统linker来说,没有section header会无法正确加载so。所以我们需要移植一个正确的section header。

android linker需要的section header信息其实不多,主要由三个:

1.shstr,2.dynamic,3.dynstr
具体过程就不再赘述了,大家可以根据加载时候的报错信息,结合andorid源码来针对性的修改:
c10 signed int *v9; // x12
11 unsigned int v10; // w13
12 signed int v11; // w13
13 __int64 v12; // x13
14 __int64 v13; // x13
15 __int64 v14; // x13
16
17 v2 = a1;
18 v3 = a2;
19 result = sub_BC9564("global-metadata.dat");
20 qword_49AE628 = result;
21 if ( result )
22 {
23 qword_49AE630 = result;
24 v5 = (unsigned __int64)*(signed int *)(result + 172) * (unsigned __int128)0xCCCCCCCCCCCCCCCDLL >> 64;
25 *v2 = v5 >> 5;
26 dword_49AE638 = v5 >> 5;
27 *v3 = *(_DWORD *)(result + 180) >> 6;
28 qword_49AE640 = sub_C1D0A8((signed __int64)(v5 << 27) >> 32, 24LL);
29 qword_49AE648 = sub_C1D0A8(*(signed int *)(qword_49AE620 + 48), 8LL);
30 qword_49AE650 = sub_C1D0A8(
31 (unsigned __int64)((unsigned __int64)*(signed int *)(qword_49AE630 + 164)
32 * (unsigned __int128)0x2E8BA2E8BA2E8BA3uLL >> 64) >> 4,
33 8LL);
34 qword_49AE658 = sub_C1D0A8((unsigned __int64)*(signed int *)(qword_49AE630 + 52) >> 5, 8LL);
35 v6 = sub_C1D0A8(*(signed int *)(qword_49AE620 + 64), 8LL);
36 v7 = qword_49AE620;
37 qword_49AE660 = v6;
38 result = 1LL;
39 if ( *(_DWORD *)(qword_49AE620 + 48) >= 1 )
40 {
41 v8 = 0LL;
42 while ( 1 )
43 {
44 v9 = *(signed int **)(*(_QWORD *)(v7 + 56) + 8 * v8);
45 v10 = *((unsigned __int8 *)v9 + 10);
46 if ( v10 <= 0x1E )
这段代码显示了 Il2Cpp 加载 global-metadata.dat 文件的逆向工程代码,关键点包括:

第19行:通过 sub_BC9564 函数加载 metadata 文件
第24-27行:从 metadata 偏移 172 和 180 处读取数据并进行计算
第28-35行:为各种数据结构分配内存
第39-46行:循环处理 metadata 中的元素

三个section header就可以了(第一个需要为空)



起飞


接下来将我们修复的so替换app里面的so,发现游戏可以正常运行:)
但是global-metadata还是加密的。

没关系,我们已经把il2cpp扒光了,可以直接定位到加载global-metadata的位置:
10 signed int *v9; // x12
11 unsigned int v10; // w13
12 signed int v11; // w13
13 __int64 v12; // x13
14 __int64 v13; // x13
15 __int64 v14; // x13
16
17 v2 = a1;
18 v3 = a2;
19 result = sub_BC9564("global-metadata.dat");
20 qword_49AE628 = result;
21 if ( result )
22 {
23 qword_49AE630 = result;
24 v5 = (unsigned __int64)*(signed int *)(result + 172) * (unsigned __int128)0xCCCCCCCCCCCCCCCDLL >> 64;
25 *v2 = v5 >> 5;
26 dword_49AE638 = v5 >> 5;
27 *v3 = *(_DWORD *)(result + 180) >> 6;
28 qword_49AE640 = sub_C1D0A8((signed __int64)(v5 << 27) >> 32, 24LL);
29 qword_49AE648 = sub_C1D0A8(*(signed int *)(qword_49AE620 + 48), 8LL);
30 qword_49AE650 = sub_C1D0A8(
31 (unsigned __int64)((unsigned __int64)*(signed int *)(qword_49AE630 + 164)
32 * (unsigned __int128)0x2E8BA2E8BA2E8BA3uLL >> 64) >> 4,
33 8LL);
34 qword_49AE658 = sub_C1D0A8((unsigned __int64)*(signed int *)(qword_49AE630 + 52) >> 5, 8LL);
35 v6 = sub_C1D0A8(*(signed int *)(qword_49AE620 + 64), 8LL);
36 v7 = qword_49AE620;
37 qword_49AE660 = v6;
38 result = 1LL;
39 if ( *(_DWORD *)(qword_49AE620 + 48) >= 1 )
40 {
41 v8 = 0LL;
42 while ( 1 )
43 {
44 v9 = *(signed int **)(*(_QWORD *)(v7 + 56) + 8 * v8);
45 v10 = *((unsigned __int8 *)v9 + 10);
46 if ( v10 <= 0x1E )
这段代码是 IDA Pro 反汇编代码,主要功能是:

第19行: 调用 sub_BC9564 函数加载 "global-metadata.dat" 文件
第24-27行: 从加载的 metadata 中提取和计算各种偏移量
第28-37行: 调用 sub_C1D0A8 函数为各种数据结构分配内存
第39-46行: 遍历处理 metadata 中的数据

这是 Il2Cpp 游戏引擎加载和解析 global-metadata.dat 文件的核心代码逻辑。

在sub_bc9564下断点,执行完后直接内存中dump出global-metadata就可以了。

不过某顿把global-metadata的magic和version信息抹去了。这两个信息在运行时是不用的,但是il2cppdumper解析需要。我们手动修复就好:

根据图片中的十六进制编辑器内容,我识别出以下OCR文字:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000h: AF 1B B1 F1 1B 00 00 00 00 01 00 00 D8 DC 02 00
0010h: D8 DD 02 00 0C 41 08 00 E4 1E 0B 00 00 11 2B 00
0020h: E4 2F 36 00 B8 05 00 00 9C 35 36 00 48 AE 0E 00
0030h: E4 E3 44 00 40 76 5A 00 24 5A 9F 00 D0 73 01 00
0040h: F4 CD A0 00 F8 74 05 00 EC 42 A6 00 38 10 06 00
0050h: 24 53 AC 00 B0 10 03 00 D4 63 AF 00 40 7A 15 00
0060h: 14 DE C4 00 0C C9 15 00 20 A7 DA 00 C0 70 00 00
0070h: E0 17 DB 00 64 05 00 00 44 1D DB 00 90 50 00 00
0080h: D4 6D DB 00 3C 59 00 00 10 C7 DB 00 EC 31 00 00
这个十六进制数据看起来是:

文件头部数据结构
可能是 Il2Cpp 相关的数据段
包含了各种偏移地址和指针信息
每行16字节,按列(0-F)显示

从数据特征来看,这可能是 dump 出来的 Il2Cpp 结构体数据或者是 DLL 文件的某个关键段。RetryClaude can make mistakes. Please double-check responses. Sonnet 4.5

上il2cppdumper:

Initializing metadata...
Metadata Version: 27
Initializing il2cpp file...
Applying relocations...
WARNING: find JNI_OnLoad
ERROR: This file may be protected.
Il2Cpp Version: 27
Detected this may be a dump file.
Input il2cpp dump address or input 0 to force continue:
0
Searching...
CodeRegistration : 0
MetadataRegistration : 45d4c88
ERROR: No symbol is detected
ERROR: Can't use auto mode to process file, try manual mode.
Input CodeRegistration: 45d4280
Input MetadataRegistration: 45d4c88
Change il2cpp version to: 27.1
Dumping...
Done!
Generate struct...
Done!
Generate dummy dll...
Done!
Press any key to exit...

成功dump出cs脚本。简单搜索了一个叫get_hp的函数,如果把这个函数修改了,后果不堪设想。

当然,作为社会主义好青年,我们不会做这种事滴!至此,某顿手游加固的脱壳与修复,就全部完成了。

 


后记


某顿手游加固不同于传统的加固方式。传统的加固方式是直接整体加密,解密,执行。内存dump下来很好修复。

但是某顿自己实现了linker中加载,重定位的全部代码,将子so的数据全部加密,并且分开存储,使得内存中从来没有出现过完整的子so的影子,无法整体dump,只能分段dump,然后重组,安全强度确实高了不止一个数量级。

本来只是想逆向一个普通游戏泄愤,没想到入了某顿加固的坑。不过整个实验的过程中,看雪前辈关于android linker的文章我都仔细读了,andorid linker相关的源码也是翻了个遍,之前模糊的链接与重定位,elf文件格式相关的知识也在脑海里明晰了起来,也算是收获颇丰。

以上内容仅供学习技术,交流,严禁用于违法的活动!

 

*(_QWORD *)(v6 + 32) = v29; *(_QWORD *)(v6 + 48) = v30; *(_QWORD *)(v6 + 144) = v31; *(_QWORD *)(v6 + 160) = v32; *(_QWORD *)(v6 + 64) = v33; *(_QWORD *)(v6 + 72) = v34; if ( !(prelink(v6) & 1) ) goto LABEL_36; sub_7D16A74(v6); v8 = decrypt_something(v6, v35); if ( *(_BYTE *)(v6 + 336) & 1 ) goto LABEL_9; v8 = relocate(v6); if ( !(v8 & 1) )

 

 

 

在壳so基础上移植子so数据的详细操作流程

前置准备

需要的文件/数据:

  1. 已处理的壳so:
    • 已去除花指令
    • 已解密字符串表和符号表
  2. 从内存dump的子so数据:
    • dump_seg1:子so代码段
    • dump_seg2:子so数据段
    • dump_sym:子so符号表
    • dump_relo:子so重定位表
  3. 记录的子so结构信息:
    • Program Header(可截图或导出)
    • Dynamic段信息

详细操作流程

步骤1:移植子so代码段到壳so

1.1 确定插入位置

text
使用010 Editor打开壳so
定位到Program Header Table结束位置
记录偏移地址(例如:0x1C0)

1.2 插入代码段数据

Bash
# 原壳so在0x1C0位置的数据(加密状态):
01C0h: 01 00 00 00 00 00 00 00 EF A7 4E D3 CE 11 E0 D6
01D0h: AF 4B 3B 0C D2 63 F9 82 2A E0 27 1C 32 45 F5 FF
...

# 用dump_seg1的数据覆盖(解密后的代码段):
01C0h: 01 00 00 00 00 00 00 00 04 00 00 00 14 00 00 00
01D0h: 03 00 00 00 47 4E 55 00 34 5C A5 91 6D D1 DC 00
...

操作步骤:

  1. 在010 Editor中选中从0x1C0开始的区域
  2. 打开dump_seg1文件
  3. 全选 → 复制
  4. 返回壳so,粘贴覆盖

步骤2:修改Program Header添加数据段映射信息

2.1 定位壳so的Program Header Table

C
// 使用ELF模板查看
struct program_header_table {
    program_table_element[0]  // (R_X) 代码段1
    program_table_element[1]  // (R_X) 代码段2  
    program_table_element[2]  // (RW_) 壳数据段
    program_table_element[3]  // (RW_) Dynamic
    program_table_element[4]  // (R__) eh_frame_hdr
    program_table_element[5]  // (RW_) Stack
    program_table_element[6]  // (RW_) RELRO段 ← 要替换这个
}

2.2 替换最后一个Program Header

C
// 原来的壳so Program Header[6] (GNU_RELRO类型)
// 替换为子so数据段的Program Header

// 子so数据段的Program Header示例:
struct program_table_entry64_t {
    p_type   = PT_LOAD (1)        // Loadable段
    p_flags  = PF_Read_Write (6)  // 可读写
    p_offset = 0x458EA50           // 文件偏移(待修改)
    p_vaddr  = 0x7DF6A50           // 虚拟地址
    p_paddr  = 0x7DF6A50           // 物理地址
    p_filesz = 0x1610AD8           // 文件中大小
    p_memsz  = 0x1610AD8           // 内存中大小
    p_align  = 0x1000              // 对齐
}

关键修改:

  • p_type从GNU_RELRO改为PT_LOAD (0x00000001)
  • p_flags改为可读写 (0x00000006)
  • 其他字段先按子so的值填写(p_offset后续会调整)

步骤3:将子so数据段附加到文件末尾

3.1 为什么要附加到末尾?

text
原因:文件中数据紧密排列,直接覆盖会破坏其他数据
解决:将新数据附加到文件末尾,通过修改偏移字段关联

3.2 执行附加操作

Bash
# 1. 记录壳so当前文件大小
例如:0x55D0000

# 2. 在010 Editor中:
- 光标移动到文件末尾
- 插入 → 插入字节 → 选择dump_seg2的大小
- 将dump_seg2的内容粘贴进去

# 3. 记录新的偏移地址
新偏移 = 0x55D0000(就是原文件大小)

3.3 修改Program Header[6]的p_offset

C
// 在Program Header[6]中修改:
p_offset = 0x55D0000  // 指向刚才附加的位置

步骤4:合并和修复重定位表

4.1 提取壳so原有重定位表

Bash
# 查看Dynamic段找到重定位表位置
DT_RELA    = 0x7CFF4E8  # 重定位表虚拟地址
DT_RELASZ  = 0xD260     # 重定位表大小(53856字节)

# 转换为文件偏移并dump
文件偏移 = 虚拟地址 - load_bias
使用dd命令或010 Editor导出

4.2 合并重定位表

Python
# 合并脚本示例
import struct

def merge_relo_tables(shell_relo_path, child_relo_path, output_path):
    with open(shell_relo_path, 'rb') as f:
        shell_relo = f.read()
    
    with open(child_relo_path, 'rb') as f:
        child_relo = f.read()
    
    # 直接拼接
    merged = shell_relo + child_relo
    
    with open(output_path, 'wb') as f:
        f.write(merged)
    
    print(f"合并后大小: {len(merged)} 字节")

# 执行合并
merge_relo_tables('shell_relo.bin', 'dump_relo', 'merged_relo.bin')

4.3 将合并后的重定位表附加到文件

Bash
# 记录当前文件末尾偏移(已包含子so数据段)
当前末尾 = 0x55D0000 + dump_seg2_size

# 将merged_relo.bin附加到此位置
新重定位表偏移 = 当前末尾

4.4 修改壳so数据段大小

C
// 在Program Header[2](壳数据段)中修改:
p_filesz += merged_relo.bin的大小
p_memsz  += merged_relo.bin的大小

4.5 修改Dynamic段中重定位表信息

C
// 找到Dynamic段(通常在文件偏移0x45D60B8附近)
// 修改以下项:

// DT_RELA (Type = 7):重定位表地址
原值: E8 F4 CF 07 00 00 00 00  // 0x7CFF4E8
新值: 78 D1 91 08 00 00 00 00  // 新的虚拟地址

// DT_RELASZ (Type = 8):重定位表大小  
原值: 60 D2 00 00 00 00 00 00  // 0xD260
新值: B0 A3 AE 00 00 00 00 00  // 新的大小(壳+子so)

// DT_RELAENT (Type = 9)保持不变:0x18(每个条目24字节)

关键公式:

text
新虚拟地址 = load_bias + (新文件偏移)
新大小 = 壳重定位表大小 + 子so重定位表大小

步骤5:替换符号表

5.1 为什么要替换而不是合并?

text
问题:重定位表中的符号索引指向符号表
     如果合并符号表,索引会错位
     
解决:用子so符号表完全覆盖壳so符号表
     代价是壳so的部分重定位失效(需手动修复)

5.2 定位壳so符号表

C
// 从Dynamic段找到符号表信息
DT_SYMTAB  = 0x7E01990  // 符号表地址
DT_SYMENT  = 0x18       // 每个符号24字节

// 计算符号表范围(通常到字符串表开始)
DT_STRTAB  = 0x7E23C38  // 字符串表地址
符号表大小 = 0x7E23C38 - 0x7E01990 = 0x222A8

5.3 用子so符号表覆盖

Bash
# 计算文件偏移
符号表文件偏移 = 0x7E01990 - load_bias

# 在010 Editor中:
1. 定位到符号表文件偏移
2. 选中整个符号表区域
3. 用dump_sym的内容覆盖

5.4 手动修复壳so重定位(重要!)

C
// 覆盖后,部分壳so的重定位符号找不到了
// 需要手动修复这些重定位项

// 示例:修复__stack_chk_guard
1. 在IDA中搜索错误的重定位引用
2. 在子so符号表中找到正确的符号索引
3. 修改重定位表中的r_info字段

// 重定位条目结构:
struct Elf64_Rela {
    uint64_t r_offset;  // 需要重定位的位置
    uint64_t r_info;    // [高32位:符号索引 | 低32位:类型]
    int64_t  r_addend;  // 附加值
};

// 修改示例:
原r_info: 00 00 00 51 00 00 04 02  // 符号索引0x51, 类型0x402
新r_info: 00 00 00 3A 00 00 04 02  // 符号索引0x3A, 类型0x402

常见需要修复的符号:

  • __stack_chk_guard
  • __stack_chk_fail
  • abort
  • __cxa_atexit

步骤6:修复INIT_ARRAY和FINI_ARRAY

6.1 理解目标

text
目的:将子so的初始化函数替换进来
      同时保留壳so第一个init函数(维护全局函数数组)
      
策略:从FINI_ARRAY"借"一个位置给新的init函数

6.2 修改Dynamic段

C
// 原配置:
DT_INIT_ARRAY     = 0x7DF6A50  // init数组地址
DT_INIT_ARRAYSZ   = 0x20       // 4个函数(4*8=32字节)
DT_FINI_ARRAY     = 0x7DF6A70  // fini数组地址
DT_FINI_ARRAYSZ   = 0x20       // 4个函数

// 新配置:
DT_INIT_ARRAY     = 0x7DF6A50  // 不变
DT_INIT_ARRAYSZ   = 0x48       // 增加到9个函数(9*8=72字节)
DT_FINI_ARRAY     = 0x7DF6A98  // 向后移动(0x70+0x8*9)
DT_FINI_ARRAYSZ   = 0x10       // 减少到2个函数

6.3 在文件中修改init_array

C
// 找到init_array在文件中的位置
文件偏移 = 0x7DF6A50 - load_bias

// 原内容(壳so的3个init函数):
07DF6A50: D0 B4 48 04 00 00 00 00  // 壳init函数1(要保留)
07DF6A58: xx xx xx xx 00 00 00 00  // 壳init函数2(要删除)
07DF6A60: xx xx xx xx 00 00 00 00  // 壳init函数3(要删除)
07DF6A68: 00 00 00 00 00 00 00 00  // 结束标记

// 新内容(子so的8个init + 壳的1个):
07DF6A50: DC D5 B0 00 00 00 00 00  // 子so init1
07DF6A58: 64 D9 B0 00 00 00 00 00  // 子so init2
07DF6A60: F0 D9 B0 00 00 00 00 00  // 子so init3
07DF6A68: 70 DF B0 00 00 00 00 00  // 子so init4
07DF6A70: 8C E0 B0 00 00 00 00 00  // 子so init5
07DF6A78: 6C E1 B0 00 00 00 00 00  // 子so init6
07DF6A80: E4 E1 B0 00 00 00 00 00  // 子so init7
07DF6A88: 20 E3 B0 00 00 00 00 00  // 子so init8
07DF6A90: DC 40 D4 07 00 00 00 00  // 壳init函数1(保留!)
07DF6A98: 00 00 00 00 00 00 00 00  // fini_array开始

6.4 添加壳init函数的重定位

C
// 在合并后的重定位表末尾添加:
struct Elf64_Rela {
    r_offset = 0x7DF6A90,      // 上面保留的壳init位置
    r_info   = 0x0000040300000000,  // 类型0x403(RELATIVE)
    r_addend = 0x7D440DC       // 壳init函数实际地址
};

// 对应的16进制:
90 6A DF 07 00 00 00 00  03 04 00 00 00 00 00 00
DC 40 D4 07 00 00 00 00

步骤7:重建Section Header

7.1 为什么需要Section Header?

text
IDA分析:不需要Section Header(仅需Segment)
Linker加载:必须要以下3个Section
    1. .shstrtab  - Section名称字符串表
    2. .dynamic   - Dynamic段
    3. .dynstr    - 符号名称字符串表

7.2 创建最小Section Header Table

C
// 在文件末尾添加4个Section Header条目

// [0] 必须为空(SHN_UNDEF)
struct section_header_entry {
    sh_name      = 0,
    sh_type      = SHT_NULL (0),
    sh_flags     = 0,
    sh_addr      = 0,
    sh_offset    = 0,
    sh_size      = 0,
    sh_link      = 0,
    sh_info      = 0,
    sh_addralign = 0,
    sh_entsize   = 0
};

// [1] .shstrtab(Section名字符串表)
struct section_header_entry {
    sh_name      = 1,              // 在shstrtab中的偏移
    sh_type      = SHT_STRTAB (3),
    sh_flags     = 0,
    sh_addr      = 0,
    sh_offset    = <shstrtab在文件中的偏移>,
    sh_size      = <shstrtab大小>,
    sh_link      = 0,
    sh_info      = 0,
    sh_addralign = 1,
    sh_entsize   = 0
};

// [2] .dynamic
struct section_header_entry {
    sh_name      = 11,             // ".dynamic"在shstrtab���的偏移
    sh_type      = SHT_DYNAMIC (6),
    sh_flags     = SHF_WRITE | SHF_ALLOC (3),
    sh_addr      = 0x7E3E0B8,      // Dynamic段虚拟地址
    sh_offset    = <Dynamic段文件偏移>,
    sh_size      = 0x1A0,          // Dynamic段大小
    sh_link      = 3,              // 指向.dynstr
    sh_info      = 0,
    sh_addralign = 8,
    sh_entsize   = 16
};

// [3] .dynstr(符号名字符串表)
struct section_header_entry {
    sh_name      = 20,             // ".dynstr"在shstrtab中的偏移
    sh_type      = SHT_STRTAB (3),
    sh_flags     = SHF_ALLOC (2),
    sh_addr      = 0x7E23C38,      // 字符串表虚拟地址
    sh_offset    = <字符串表文件偏移>,
    sh_size      = 0x1A480,        // 字符串表大小
    sh_link      = 0,
    sh_info      = 0,
    sh_addralign = 1,
    sh_entsize   = 0
};

7.3 创建.shstrtab内容

C
// 在Section Header Table之前插入字符串表
char shstrtab[] = {
    0x00,                           // 偏移0:空字符串
    '.', 's', 'h', 's', 't', 'r', 't', 'a', 'b', 0x00,  // 偏移1
    '.', 'd', 'y', 'n', 'a', 'm', 'i', 'c', 0x00,       // 偏移11
    '.', 'd', 'y', 'n', 's', 't', 'r', 0x00             // 偏移20
};

// 十六进制:
00 2E 73 68 73 74 72 74 61 62 00 2E 64 79 6E 61
6D 69 63 00 2E 64 79 6E 73 74 72 00

7.4 修改ELF Header

C
// 在ELF文件头部修改:
e_shoff      = <Section Header Table在文件中的偏移>,  
e_shentsize  = 64,        // 每个Section Header 64字节
e_shnum      = 4,         // 共4个Section Header
e_shstrndx   = 1          // .shstrtab是第1个section

完整布局:

text
[文件末尾前]
... 数据 ...
[.shstrtab数据]  ← 记录偏移A
00 2E 73 68 73 74 72 74 61 62 00 ...
[Section Header Table]  ← 记录偏移B
[SHN_UNDEF]     (64字节)
[.shstrtab]     (64字节, sh_offset=A)
[.dynamic]      (64字节)
[.dynstr]       (64字节)
[文件结束]

[ELF Header修改]
e_shoff = B

步骤8:再次附加子so数据段

8.1 为什么又要附加?

text
问题:之前在步骤4合并重定位表时,已经覆盖了之前附加的子so数据段
      需要重新附加

原因:数据紧密,修改重定位表后空间被占用

8.2 操作流程

Bash
# 1. 记录当前文件末尾偏移
当前末尾 = Section Header Table结束位置

# 2. 附加dump_seg2
在010 Editor中将dump_seg2再次附加到文件末尾
新偏移 = 当前末尾

# 3. 修改Program Header[6]
p_offset = 新偏移

# 4. 重新附加shstrtab和Section Header
因为又增加了数据,需要:
- 将shstrtab和Section Header移到新的文件末尾
- 再次修改ELF Header中的e_shoff

验证清单

使用readelf验证

Bash
# 1. 检查Program Headers
readelf -l fixed.so
# 应该看到:
# - LOAD段数量正确
# - 数据段的File offset和VirtAddr合理

# 2. 检查Dynamic段
readelf -d fixed.so
# 检查:
# - DT_INIT_ARRAY地址和大小
# - DT_RELA地址和大小
# - DT_SYMTAB、DT_STRTAB地址

# 3. 检查Section Headers
readelf -S fixed.so
# 应该看到:
# [0] NULL
# [1] .shstrtab
# [2] .dynamic
# [3] .dynstr

使用IDA验证

Bash
# 1. 用IDA加载fixed.so
# 应该:
# - 没有警告错误
# - 函数名正确显示(il2cpp_init等)
# - JNI_OnLoad可以F5反编译

# 2. 检查INIT函数
# 跳转到init_array地址
# 应该看到9个函数指针(8个子so + 1个壳)

# 3. 检查重定位
# 查看.got.plt段
# 函数指针应该正确解析为函数名

运行时验证

Bash
# 1. 替换APK中的so文件
# 2. 重新签名APK
# 3. 安装运行

# 如果成功:
# - 游戏正常启动
# - 没有崩溃
# - Logcat没有linker错误

常见错误及排查

错误1:linker报错"cannot locate symbol"

text
原因:符号表索引错误或符号缺失
排查:
1. 检查重定位表的r_info字段
2. 用readelf -s查看符号表
3. 手动修正符号索引

错误2:段错误/SIGSEGV

text
原因:重定位地址错误
排查:
1. 检查Program Header的p_offset和p_vaddr
2. 计算load_bias是否正确
3. 用IDA查看.got.plt段的值

错误3:IDA显示"Binary data incorrect"

text
原因:Section Header的sh_offset超出文件范围
排查:
1. 检查e_shoff是否指向正确位置
2. 检查每个Section Header的sh_offset
3. 确保文件末尾有足够空间

错误4:init函数未执行

text
原因:DT_INIT_ARRAY配置错误
排查:
1. 检查Dynamic段的INIT_ARRAY和INIT_ARRAYSZ
2. 确认init_array的重定位信息正确
3. 用gdb断点在_start查看linker行为

工具和脚本汇总

Python合并脚本

Python
#!/usr/bin/env python3
import struct

def merge_files(file1, file2, output):
    """合并两个二进制文件"""
    with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
        data = f1.read() + f2.read()
    with open(output, 'wb') as f:
        f.write(data)
    print(f"合并完成: {len(data)} 字节")

def patch_uint64(data, offset, value):
    """在指定偏移处写入uint64"""
    struct.pack_into('<Q', data, offset, value)

def patch_uint32(data, offset, value):
    """在指定偏移处写入uint32"""
    struct.pack_into('<I', data, offset, value)

IDA脚本 - 导出重定位表

Python
import idaapi
import struct

def dump_rela_table(start_ea, count, output_path):
    """导出重定位表"""
    data = b''
    for i in range(count):
        ea = start_ea + i * 24  # sizeof(Elf64_Rela) = 24
        r_offset = idaapi.get_qword(ea)
        r_info = idaapi.get_qword(ea + 8)
        r_addend = idaapi.get_qword(ea + 16)
        data += struct.pack('<QQq', r_offset, r_info, r_addend)
    
    with open(output_path, 'wb') as f:
        f.write(data)
    print(f"导出了 {count} 个重定位条目")

# 使用示例
dump_rela_table(0x7CFF4E8, 2259, '/tmp/shell_rela.bin')

总结

这个"借尸还魂"的修复过程本质上是手动实现Android Linker的文件组装:

  1. 代码和数据移植:将解密的子so内容嵌入壳so
  2. 元数据修复:修正Program Header、Dynamic段、重定位表、符号表
  3. 初始化链修复:确保init函数正确执行
  4. 结构完整性:添加最小Section Header满足linker要求

核心难点在于理解ELF文件格式和动态链接机制,需要对以下内容非常熟悉:

  • ELF文件结构(Header、Segment、Section)
  • 动态链接过程(重定位类型、符号解析)
  • 地址计算(文件偏移 ↔ 虚拟地址 ↔ 物理地址)
 
 
 

从内存Dump子so数据的详细操作流程

一、确定Dump时机

1.1 理解加载流程

text
第一个INIT → 第二个INIT → 第三个INIT(关键)
                          解密RC4
                          load_segment(映射到内存)
                          prelink
                          relocate(重定位)← 在这之前dump
                          call_init

最佳Dump时机:第三个INIT函数执行relocate之前

  • 此时代码段和数据段已解密并映射到内存
  • 重定位表和符号表还是原始状态(未被修改)
  • Program Header等元数据已经构建完成

二、定位关键地址

2.1 在IDA中分析第三个INIT函数

C
// 找到第三个init函数(已去花指令的壳so)
__int64 third_init() {
    ...
    // 1. 解密代码段和数据段
    v23 = decrypt_rc4(v15, *(_DWORD *)(v4 + 116), v11, ...);
    
    // 2. load_segment操作
    load_segment_to_memory(...);
    
    // 3. prelink操作
    v33 = build_soinfo(...);  // 构建soinfo结构
    prelink(v6);              // 解析dynamic段
    
    // 4. relocate操作 ← 在这之前下断点
    v8 = relocate(v6);
    ...
}

2.2 确定soinfo结构体地址

C
// soinfo结构体包含了所有关键信息
struct soinfo {
    const char* name;              // +0x00
    void* base;                    // +0x08  base地址
    void* phdr;                    // +0x10  program header地址
    Elf64_Dyn* dynamic;            // +0x18  dynamic段地址
    
    // 从dynamic段解析出的信息
    const char* strtab;            // +0x30  字符串表
    Elf64_Sym* symtab;             // +0x40  符号表
    
    Elf64_Rela* plt_rela;          // +0x50  PLT重定位表
    size_t plt_rela_count;         // +0x58
    
    Elf64_Rela* rela;              // +0x60  重定位表
    size_t rela_count;             // +0x68
    
    void* init_array;              // +0xC0  初始化数组
    size_t init_array_count;       // +0xC8
    
    // ... 其他字段
};

三、方法1:使用IDA动态调试Dump

3.1 配置IDA调试环境

Bash
# 1. 手机端准备
adb root
adb shell setenforce 0
adb forward tcp:23946 tcp:23946

# 2. 启动android_server64
adb push android_server64 /data/local/tmp/
adb shell chmod 755 /data/local/tmp/android_server64
adb shell /data/local/tmp/android_server64 &

# 3. IDA中attach到目标进程
Debugger -> Attach to process -> 选择游戏进程

3.2 设置断点

C
// 在第三个init函数的relocate调用之前下断点
// 假设relocate函数地址是 base + 0x7D16A74

// IDA中:
1. 跳转到该地址:Press 'G',输入地址
2. 下断点:F2
3. 运行:F9

3.3 断点命中后提取地址信息

Python
# IDA Python脚本:提取soinfo信息

import idaapi
import idc

def read_ptr(ea):
    """读取64位指针"""
    return idaapi.get_qword(ea)

def read_dword(ea):
    """读取32位整数"""
    return idaapi.get_dword(ea)

def extract_soinfo(soinfo_addr):
    """从soinfo结构体提取所有关键信息"""
    info = {}
    
    # 基础信息
    info['base'] = read_ptr(soinfo_addr + 0x08)
    info['phdr'] = read_ptr(soinfo_addr + 0x10)
    info['dynamic'] = read_ptr(soinfo_addr + 0x18)
    
    # 字符串表和符号表
    info['strtab'] = read_ptr(soinfo_addr + 0x30)
    info['strtab_size'] = read_dword(soinfo_addr + 0x38)
    info['symtab'] = read_ptr(soinfo_addr + 0x40)
    
    # 重定位表
    info['rela'] = read_ptr(soinfo_addr + 0x60)
    info['rela_size'] = read_dword(soinfo_addr + 0x68)
    info['rela_count'] = info['rela_size'] // 24  # sizeof(Elf64_Rela)
    
    # PLT重定位表
    info['plt_rela'] = read_ptr(soinfo_addr + 0x50)
    info['plt_rela_size'] = read_dword(soinfo_addr + 0x58)
    
    # init_array
    info['init_array'] = read_ptr(soinfo_addr + 0xC0)
    info['init_array_count'] = read_dword(soinfo_addr + 0xC8)
    
    return info

# 使用示例(在断点处执行)
soinfo_addr = idc.get_reg_value("X6")  # 假设X6寄存器保存soinfo指针
info = extract_soinfo(soinfo_addr)

print("=" * 60)
print("子so信息:")
print(f"Base地址: 0x{info['base']:X}")
print(f"符号表: 0x{info['symtab']:X}")
print(f"字符串表: 0x{info['strtab']:X} (大小: {info['strtab_size']})")
print(f"重定位表: 0x{info['rela']:X} (数量: {info['rela_count']})")
print("=" * 60)

3.4 从program header提取段信息

Python
def extract_segments(phdr_addr, phdr_count):
    """从program header提取段信息"""
    segments = []
    
    for i in range(phdr_count):
        entry_addr = phdr_addr + i * 56  # sizeof(Elf64_Phdr) = 56
        
        p_type = read_dword(entry_addr + 0)
        p_flags = read_dword(entry_addr + 4)
        p_offset = read_ptr(entry_addr + 8)
        p_vaddr = read_ptr(entry_addr + 16)
        p_filesz = read_ptr(entry_addr + 32)
        p_memsz = read_ptr(entry_addr + 40)
        
        # 只提取PT_LOAD类型的段
        if p_type == 1:  # PT_LOAD
            segments.append({
                'index': i,
                'type': 'LOAD',
                'flags': p_flags,
                'vaddr': p_vaddr,
                'size': p_memsz,
                'is_code': (p_flags & 1) != 0,  # 可执行
                'is_writable': (p_flags & 2) != 0
            })
    
    return segments

# 使用
# 从soinfo获取phdr地址,从ELF头获取phdr数量
phdr_addr = info['phdr']
base_addr = info['base']

# 读取ELF头的e_phnum字段(偏移0x38)
phdr_count = idc.get_wide_word(base_addr + 0x38)

segments = extract_segments(phdr_addr, phdr_count)

for seg in segments:
    print(f"段{seg['index']}: 地址=0x{seg['vaddr']:X}, "
          f"大小={seg['size']}, "
          f"权限={'RWX' if seg['is_code'] else 'RW_'}")

3.5 Dump各个部分

Python
# IDA dump脚本
import idaapi

def dump_memory(start_ea, size, output_path):
    """从内存dump数据"""
    data = idaapi.dbg_read_memory(start_ea, size)
    if data:
        with open(output_path, 'wb') as f:
            f.write(data)
        print(f"✓ Dump完成: {output_path} ({size} 字节)")
        return True
    else:
        print(f"✗ Dump失败: 0x{start_ea:X}")
        return False

# 执行dump(在断点处运行)
soinfo_addr = idc.get_reg_value("X6")
info = extract_soinfo(soinfo_addr)
base = info['base']
segments = extract_segments(info['phdr'], phdr_count)

# 1. Dump代码段(第一个LOAD段)
code_seg = segments[0]
dump_memory(code_seg['vaddr'], code_seg['size'], 
            '/sdcard/dump_seg1')

# 2. Dump数据段(第二个LOAD段)
data_seg = segments[1]
dump_memory(data_seg['vaddr'], data_seg['size'], 
            '/sdcard/dump_seg2')

# 3. Dump符号表
# 计算符号表大小(到字符串表开始)
symtab_size = info['strtab'] - info['symtab']
dump_memory(info['symtab'], symtab_size, 
            '/sdcard/dump_sym')

# 4. Dump重定位表
dump_memory(info['rela'], info['rela_size'], 
            '/sdcard/dump_relo')

# 5. Dump字符串表
dump_memory(info['strtab'], info['strtab_size'], 
            '/sdcard/dump_strtab')

# 6. Dump program header
phdr_size = phdr_count * 56
dump_memory(info['phdr'], phdr_size, 
            '/sdcard/dump_phdr')

# 7. Dump dynamic段(从第一个phdr遍历找PT_DYNAMIC)
for i in range(phdr_count):
    entry_addr = info['phdr'] + i * 56
    if read_dword(entry_addr) == 2:  # PT_DYNAMIC
        dyn_vaddr = read_ptr(entry_addr + 16)
        dyn_size = read_ptr(entry_addr + 32)
        dump_memory(dyn_vaddr, dyn_size, '/sdcard/dump_dynamic')
        break

print("\n所有dump完成!使用以下命令拉取:")
print("adb pull /sdcard/dump_* .")

四、方法2:使用Frida Dump

4.1 编写Frida脚本

JavaScript
// dump_il2cpp.js

function dump_memory(addr, size, filename) {
    var base = ptr(addr);
    var data = base.readByteArray(size);
    var file = new File("/sdcard/" + filename, "wb");
    file.write(data);
    file.close();
    console.log("[+] Dumped: " + filename + " (0x" + size.toString(16) + " bytes)");
}

function read_u64(addr) {
    return ptr(addr).readU64();
}

function read_u32(addr) {
    return ptr(addr).readU32();
}

function extract_soinfo(soinfo_ptr) {
    console.log("[*] 解析soinfo @ " + soinfo_ptr);
    
    var info = {
        base: read_u64(soinfo_ptr.add(0x08)),
        phdr: read_u64(soinfo_ptr.add(0x10)),
        dynamic: read_u64(soinfo_ptr.add(0x18)),
        strtab: read_u64(soinfo_ptr.add(0x30)),
        strtab_size: read_u32(soinfo_ptr.add(0x38)),
        symtab: read_u64(soinfo_ptr.add(0x40)),
        rela: read_u64(soinfo_ptr.add(0x60)),
        rela_size: read_u32(soinfo_ptr.add(0x68)),
        plt_rela: read_u64(soinfo_ptr.add(0x50)),
        plt_rela_size: read_u32(soinfo_ptr.add(0x58)),
        init_array: read_u64(soinfo_ptr.add(0xC0)),
        init_array_count: read_u32(soinfo_ptr.add(0xC8))
    };
    
    return info;
}

function extract_segments(phdr_addr, count) {
    var segments = [];
    
    for (var i = 0; i < count; i++) {
        var entry = ptr(phdr_addr).add(i * 56);
        var p_type = entry.readU32();
        
        if (p_type == 1) {  // PT_LOAD
            segments.push({
                index: i,
                flags: entry.add(4).readU32(),
                vaddr: entry.add(16).readU64(),
                size: entry.add(40).readU64()
            });
        }
    }
    
    return segments;
}

function main() {
    // 等待so加载
    var libil2cpp = Process.findModuleByName("libil2cpp.so");
    if (!libil2cpp) {
        console.log("[-] libil2cpp.so 未加载");
        return;
    }
    
    console.log("[+] libil2cpp base: " + libil2cpp.base);
    
    // Hook relocate函数
    var relocate_addr = libil2cpp.base.add(0x7D16A74); // 偏移需要根据实际调整
    
    Interceptor.attach(relocate_addr, {
        onEnter: function(args) {
            console.log("\n[*] relocate被调用!");
            
            // args[0] 是soinfo指针
            var soinfo_ptr = args[0];
            var info = extract_soinfo(soinfo_ptr);
            
            console.log("Base: 0x" + info.base.toString(16));
            console.log("符号表: 0x" + info.symtab.toString(16));
            console.log("重定位表: 0x" + info.rela.toString(16) + 
                       " (大小: " + info.rela_size + ")");
            
            // 读取phdr数量
            var phdr_count = ptr(info.base).add(0x38).readU16();
            console.log("Program headers: " + phdr_count);
            
            var segments = extract_segments(info.phdr, phdr_count);
            console.log("Loadable段数量: " + segments.length);
            
            // Dump所有内容
            console.log("\n[*] 开始dump...");
            
            // 1. Dump代码段
            if (segments.length > 0) {
                dump_memory(segments[0].vaddr, segments[0].size, "dump_seg1");
            }
            
            // 2. Dump数据段
            if (segments.length > 1) {
                dump_memory(segments[1].vaddr, segments[1].size, "dump_seg2");
            }
            
            // 3. Dump符号表
            var symtab_size = info.strtab - info.symtab;
            dump_memory(info.symtab, symtab_size, "dump_sym");
            
            // 4. Dump重定位表
            dump_memory(info.rela, info.rela_size, "dump_relo");
            
            // 5. Dump字符串表
            dump_memory(info.strtab, info.strtab_size, "dump_strtab");
            
            // 6. Dump program header
            dump_memory(info.phdr, phdr_count * 56, "dump_phdr");
            
            // 7. Dump dynamic段
            // 遍历program header找PT_DYNAMIC
            for (var i = 0; i < phdr_count; i++) {
                var entry = ptr(info.phdr).add(i * 56);
                if (entry.readU32() == 2) {  // PT_DYNAMIC
                    var dyn_vaddr = entry.add(16).readU64();
                    var dyn_size = entry.add(32).readU64();
                    dump_memory(dyn_vaddr, dyn_size, "dump_dynamic");
                    break;
                }
            }
            
            console.log("\n[+] Dump完成!");
            console.log("[+] 拉取文件:adb pull /sdcard/dump_* .");
        }
    });
    
    console.log("[*] 等待relocate调用...");
}

setImmediate(main);

4.2 运行Frida脚本

Bash
# 1. 启动应用
adb shell am start -n com.example.game/.MainActivity

# 2. 运行Frida
frida -U -f com.example.game -l dump_il2cpp.js --no-pause

# 或者attach到已运行进程
frida -U com.example.game -l dump_il2cpp.js

# 3. 等待输出
# [*] relocate被调用!
# Base: 0x72376dc000
# [+] Dumped: dump_seg1 (0x448b520 bytes)
# [+] Dumped: dump_seg2 (0x1610ad8 bytes)
# ...

# 4. 拉取dump文件
adb pull /sdcard/dump_* .

五、方法3:使用GDB Dump

5.1 启动GDB调试

Bash
# 1. 手机端启动gdbserver
adb shell
su
gdbserver64 :1234 --attach $(pidof com.example.game)

# 2. PC端连接
adb forward tcp:1234 tcp:1234
gdb-multiarch

# 在gdb中:
(gdb) target remote :1234
(gdb) set sysroot /path/to/android/ndk/sysroot

5.2 设置断点

gdb
# 找到libil2cpp.so基址
(gdb) info sharedlibrary
# 找到类似 0x00007237600000 libil2cpp.so

# 计算relocate函数地址
(gdb) set $base = 0x7237600000
(gdb) b *($base + 0x7D16A74)

# 继续执行
(gdb) c

5.3 断点命中后提取信息

gdb
# 查看寄存器(soinfo通常在x0或x6)
(gdb) info registers

# 假设soinfo在x6
(gdb) set $soinfo = $x6

# 读取soinfo字段
(gdb) x/gx $soinfo+0x08    # base
(gdb) x/gx $soinfo+0x40    # symtab
(gdb) x/gx $soinfo+0x60    # rela
(gdb) x/wx $soinfo+0x68    # rela_size

# 保存地址
(gdb) set $base = *(long*)($soinfo+0x08)
(gdb) set $symtab = *(long*)($soinfo+0x40)
(gdb) set $strtab = *(long*)($soinfo+0x30)
(gdb) set $rela = *(long*)($soinfo+0x60)
(gdb) set $rela_size = *(int*)($soinfo+0x68)

5.4 Dump内存

gdb
# 从program header获取段信息
(gdb) set $phdr = *(long*)($soinfo+0x10)
(gdb) set $phdr_count = *(short*)($base+0x38)

# 查看第一个LOAD段(代码段)
(gdb) x/14gx $phdr           # 显示第一个program header
# 手动记录 p_vaddr 和 p_memsz

# Dump代码段
(gdb) dump memory dump_seg1 <p_vaddr> <p_vaddr+p_memsz>
# 例如:
(gdb) dump memory dump_seg1 0x72376dc000 0x7237b67520

# Dump数据段(第二个LOAD段)
(gdb) x/14gx $phdr+56        # 下一个program header
(gdb) dump memory dump_seg2 <p_vaddr> <p_vaddr+p_memsz>

# Dump符号表
(gdb) print $strtab - $symtab
(gdb) dump memory dump_sym $symtab $strtab

# Dump重定位表
(gdb) dump memory dump_relo $rela ($rela+$rela_size)

# Dump字符串表
(gdb) set $strtab_size = *(int*)($soinfo+0x38)
(gdb) dump memory dump_strtab $strtab ($strtab+$strtab_size)

六、验证Dump的数据

6.1 验证代码段

Bash
# 查看magic和机器类型
hexdump -C dump_seg1 | head -n 20

# 应该看到类似:
# 00000000  01 00 00 00 00 00 00 00  04 00 00 00 14 00 00 00
# 不应该全是加密数据或全是00

# 用objdump验证(虽然不是完整ELF)
file dump_seg1
# 应该输出:data

6.2 验证符号表

Python
# verify_symtab.py
import struct

def parse_elf64_sym(data, offset):
    """解析Elf64_Sym结构"""
    st_name, st_info, st_other, st_shndx, st_value, st_size = \
        struct.unpack_from('<IBBHQQ', data, offset)
    return {
        'st_name': st_name,
        'st_info': st_info,
        'st_value': st_value,
        'st_size': st_size
    }

# 读取符号表
with open('dump_sym', 'rb') as f:
    symtab_data = f.read()

# 读取字符串表
with open('dump_strtab', 'rb') as f:
    strtab_data = f.read()

# 解析几个符号
print("前10个符号:")
for i in range(10):
    sym = parse_elf64_sym(symtab_data, i * 24)
    if sym['st_name'] < len(strtab_data):
        # 提取符号名
        name_end = strtab_data.find(b'\x00', sym['st_name'])
        name = strtab_data[sym['st_name']:name_end].decode('utf-8', errors='ignore')
        print(f"[{i}] {name} @ 0x{sym['st_value']:X} (size={sym['st_size']})")
    else:
        print(f"[{i}] <invalid name offset>")

# 应该看到类似:
# [0]  @ 0x0 (size=0)
# [1] __cxa_finalize @ 0x0 (size=0)
# [2] il2cpp_init @ 0xB0D580 (size=...)

6.3 验证重定位表

Python
# verify_relo.py
import struct

def parse_elf64_rela(data, offset):
    """解析Elf64_Rela结构"""
    r_offset, r_info, r_addend = struct.unpack_from('<QQq', data, offset)
    r_type = r_info & 0xFFFFFFFF
    r_sym = r_info >> 32
    return {
        'r_offset': r_offset,
        'r_type': r_type,
        'r_sym': r_sym,
        'r_addend': r_addend
    }

with open('dump_relo', 'rb') as f:
    rela_data = f.read()

count = len(rela_data) // 24
print(f"重定位条目数量: {count}")

# 统计重定位类型
type_counts = {}
for i in range(min(count, 100)):  # 只检查前100个
    rela = parse_elf64_rela(rela_data, i * 24)
    rtype = rela['r_type']
    type_counts[rtype] = type_counts.get(rtype, 0) + 1
    
    if i < 5:
        print(f"[{i}] offset=0x{rela['r_offset']:X}, "
              f"type=0x{rtype:X}, sym={rela['r_sym']}, "
              f"addend=0x{rela['r_addend']:X}")

print("\n重定位类型分布:")
for rtype, count in sorted(type_counts.items()):
    type_name = {
        0x403: 'R_AARCH64_RELATIVE',
        0x402: 'R_AARCH64_GLOB_DAT',
        0x401: 'R_AARCH64_JUMP_SLOT',
        0x405: 'R_AARCH64_ABS64'
    }.get(rtype, f'UNKNOWN_{rtype:X}')
    print(f"  {type_name}: {count}")

# 应该看到合理的类型分布:
# R_AARCH64_RELATIVE: 大部分
# R_AARCH64_GLOB_DAT: 少量
# R_AARCH64_JUMP_SLOT: 少量

6.4 验证Program Header

Python
# verify_phdr.py
import struct

def parse_elf64_phdr(data, offset):
    """解析Elf64_Phdr结构"""
    p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align = \
        struct.unpack_from('<IIQQQQQQ', data, offset)
    return {
        'p_type': p_type,
        'p_flags': p_flags,
        'p_offset': p_offset,
        'p_vaddr': p_vaddr,
        'p_filesz': p_filesz,
        'p_memsz': p_memsz,
        'p_align': p_align
    }

with open('dump_phdr', 'rb') as f:
    phdr_data = f.read()

count = len(phdr_data) // 56
print(f"Program Header 数量: {count}\n")

type_names = {
    0: 'PT_NULL',
    1: 'PT_LOAD',
    2: 'PT_DYNAMIC',
    3: 'PT_INTERP',
    4: 'PT_NOTE',
    6: 'PT_PHDR',
    0x6474e550: 'PT_GNU_EH_FRAME',
    0x6474e551: 'PT_GNU_STACK',
    0x6474e552: 'PT_GNU_RELRO'
}

for i in range(count):
    phdr = parse_elf64_phdr(phdr_data, i * 56)
    ptype_name = type_names.get(phdr['p_type'], f"UNKNOWN_{phdr['p_type']:X}")
    
    flags = ""
    if phdr['p_flags'] & 4: flags += "R"
    if phdr['p_flags'] & 2: flags += "W"
    if phdr['p_flags'] & 1: flags += "X"
    
    print(f"[{i}] {ptype_name:20s} {flags:3s} "
          f"vaddr=0x{phdr['p_vaddr']:X} "
          f"filesz=0x{phdr['p_filesz']:X} "
          f"memsz=0x{phdr['p_memsz']:X}")

# 应该看到类似:
# [0] PT_LOAD      R X vaddr=0x0 filesz=0x... memsz=0x...
# [1] PT_LOAD      RW  vaddr=0x... filesz=0x... memsz=0x...
# [2] PT_DYNAMIC   RW  vaddr=0x... filesz=0x1A0 memsz=0x1A0

七、常见问题排查

问题1:Dump的数据全是0或垃圾数据

text
原因:
1. Dump时机太早(还未解密)
2. Dump时机太晚(已被清除)
3. 地址计算错误

解决:
1. 确认在relocate之前、prelink之后dump
2. 打印soinfo各字段确认地址
3. 用IDA的Hex View验证内存内容

问题2:找不到relocate函数

text
原因:函数偏移在不同版本的壳中可能不同

解决:
1. 在IDA中搜索特征码:
   - "relocate"字符串引用
   - 重定位类型的switch case (0x403, 0x402等)
   
2. 从第三个init函数向下跟踪
   - 找到build_soinfo
   - 找到prelink
   - 下一个调用就是relocate

问题3:Frida脚本attach失败

text
原因:游戏有反调试

解决:
1. 使用frida-server-with-gadget
2. 修改游戏的AndroidManifest.xml添加debuggable
3. 使用magisk模块隐藏frida

问题4:dump出的段大小不对

text
原因:p_memsz和p_filesz不同

解决:
dump时使用p_memsz(内存中的大小)
不要使用p_filesz(文件中的大小)

八、自动化脚本汇总

完整Frida自动化脚本

JavaScript
// auto_dump.js - 完整自动化dump脚本

Java.perform(function() {
    console.log("[*] 脚本已加载");
});

var dumped = false;  // 防止重复dump

function hexdump(addr, length) {
    try {
        var buf = ptr(addr).readByteArray(length);
        console.log(hexdump(buf, { offset: 0, length: length, header: true, ansi: true }));
    } catch(e) {
        console.log("hexdump失败: " + e);
    }
}

function wait_for_module(module_name, callback) {
    var module = Process.findModuleByName(module_name);
    if (module) {
        callback(module);
    } else {
        setTimeout(function() {
            wait_for_module(module_name, callback);
        }, 100);
    }
}

wait_for_module("libil2cpp.so", function(module) {
    console.log("[+] libil2cpp.so 已加载: " + module.base);
    
    // Hook relocate (需要根据实际情况调整偏移)
    var relocate_offset = 0x7D16A74;  // 根据IDA中的地址调整
    var relocate_addr = module.base.add(relocate_offset);
    
    console.log("[*] Hook relocate @ " + relocate_addr);
    
    Interceptor.attach(relocate_addr, {
        onEnter: function(args) {
            if (dumped) return;
            dumped = true;
            
            console.log("\n" + "=".repeat(60));
            console.log("[!] relocate 被调用!开始dump...");
            console.log("=".repeat(60));
            
            var soinfo = args[0];
            
            // 读取soinfo结构
            var base = soinfo.add(0x08).readPointer();
            var phdr = soinfo.add(0x10).readPointer();
            var symtab = soinfo.add(0x40).readPointer();
            var strtab = soinfo.add(0x30).readPointer();
            var strtab_size = soinfo.add(0x38).readU32();
            var rela = soinfo.add(0x60).readPointer();
            var rela_size = soinfo.add(0x68).readU32();
            
            console.log("[+] Base: " + base);
            console.log("[+] Program Headers: " + phdr);
            console.log("[+] 符号表: " + symtab);
            console.log("[+] 字符串表: " + strtab + " (" + strtab_size + " bytes)");
            console.log("[+] 重定位表: " + rela + " (" + rela_size + " bytes)");
            
            // 读取program header数量
            var phdr_count = base.add(0x38).readU16();
            console.log("[+] Program header 数量: " + phdr_count);
            
            // 解析segments
            var segments = [];
            for (var i = 0; i < phdr_count; i++) {
                var entry = phdr.add(i * 56);
                var p_type = entry.readU32();
                
                if (p_type == 1) {  // PT_LOAD
                    var seg = {
                        index: i,
                        vaddr: entry.add(16).readU64(),
                        memsz: entry.add(40).readU64()
                    };
                    segments.push(seg);
                    console.log("[+] 段" + i + ": " + ptr(seg.vaddr) + 
                               " (大小: 0x" + seg.memsz.toString(16) + ")");
                }
            }
            
            // 执行dump
            var output_dir = "/sdcard/il2cpp_dump/";
            
            try {
                // 创建目录(可能失败,忽略)
                var mkdir = new NativeFunction(
                    Module.findExportByName("libc.so", "mkdir"),
                    'int', ['pointer', 'int']
                );
                mkdir(Memory.allocUtf8String(output_dir), 0755);
            } catch(e) {}
            
            function save_file(filename, addr, size) {
                try {
                    var path = output_dir + filename;
                    var file = new File(path, "wb");
                    var data = ptr(addr).readByteArray(size);
                    file.write(data);
                    file.close();
                    console.log("  ✓ " + filename + " (0x" + size.toString(16) + " bytes)");
                    return true;
                } catch(e) {
                    console.log("  ✗ " + filename + " 失败: " + e);
                    return false;
                }
            }
            
            console.log("\n[*] 开始dump文件...");
            
            // Dump各个部分
            if (segments.length > 0) {
                save_file("dump_seg1", segments[0].vaddr, segments[0].memsz);
            }
            
            if (segments.length > 1) {
                save_file("dump_seg2", segments[1].vaddr, segments[1].memsz);
            }
            
            var symtab_size = strtab.sub(symtab).toInt32();
            save_file("dump_sym", symtab, symtab_size);
            
            save_file("dump_relo", rela, rela_size);
            save_file("dump_strtab", strtab, strtab_size);
            save_file("dump_phdr", phdr, phdr_count * 56);
            
            // Dump dynamic段
            for (var i = 0; i < phdr_count; i++) {
                var entry = phdr.add(i * 56);
                if (entry.readU32() == 2) {  // PT_DYNAMIC
                    var dyn_vaddr = entry.add(16).readU64();
                    var dyn_size = entry.add(32).readU64();
                    save_file("dump_dynamic", dyn_vaddr, dyn_size);
                    break;
                }
            }
            
            console.log("\n" + "=".repeat(60));
            console.log("[+] Dump完成!");
            console.log("[+] 拉取命令: adb pull " + output_dir + " .");
            console.log("=".repeat(60) + "\n");
        }
    });
});

使用方法

Bash
# 保存脚本为 auto_dump.js

# 方式1:spawn模式(应用启动时注入)
frida -U -f com.example.game -l auto_dump.js --no-pause

# 方式2:attach模式(附加到运行中的进程)
frida -U com.example.game -l auto_dump.js

# 等待输出完成后拉取文件
adb pull /sdcard/il2cpp_dump/ .

# 验证dump的文件
ls -lh il2cpp_dump/
hexdump -C il2cpp_dump/dump_seg1 | head -n 10

这样就完成了从内存dump子so所有必要数据的完整流程!

 
 
 
 
 
 
 
 
 
 
 
 

 花指令清除脚本深度解析

这是一个针对Unity IL2CPP加壳保护的自动化去混淆工具,用于清除ARM64二进制文件中的花指令。


📋 代码结构分析

1️⃣ 定义的指令模式

Python
# 模式1:16字节(4条ARM64指令)
mod1 = [0x86, 0x10, 0x40, 0xb9,  # LDR W6, [X4, #0x10]
        0xa6, 0x19, 0x00, 0x18,  # LDR W6, =0x????     (垃圾指令)
        0xff, 0x43, 0x00, 0xd1,  # SUB SP, SP, #0x10   (假栈帧)
        0xc0, 0x03, 0x5f, 0xd6]  # RET                 (假返回!)

# 模式2:24字节(6条ARM64指令)
mod2 = [0x86, 0x08, 0x40, 0xf9,  # LDR X6, [X4, #0x10]
        0xff, 0x43, 0x00, 0xd1,  # SUB SP, SP, #0x10
        0x00, 0x00, 0x00, 0x1b,  # MADD W0, W0, W0, W0 (垃圾运算)
        0xFF, 0x25, 0x00, 0x18,  # LDR W?, =0x????     (垃圾加载)
        0xff, 0x43, 0x00, 0xd1,  # SUB SP, SP, #0x10
        0xc0, 0x03, 0x5f, 0xd6]  # RET

# NOP指令(用于覆盖)
nop = [0x1f, 0x20, 0x03, 0xd5]   # NOP (实际是MOV X31, X31)

🎯 花指令的作用机制

🔴 问题:假返回指令(Opaque Predicate)

assembly
; 正常函数代码
0x1000:  STP  X29, X30, [SP, #-0x10]!
0x1004:  MOV  X29, SP
...
0x1020:  LDR  W6, [X4, #0x10]      ; ← 花指令开始
0x1024:  LDR  W6, =0x12345678      ; ← 永远不会执行的加载
0x1028:  SUB  SP, SP, #0x10        ; ← 假装是函数序言
0x102C:  RET                       ; ← 假返回!!!

; IDA在这里认为函数结束了 ❌

0x1030:  ADD  X0, X1, X2            ; ← 实际代码被隐藏
0x1034:  BL   real_function
0x1038:  LDP  X29, X30, [SP], #0x10
0x103C:  RET                        ; ← 真正的返回

🟢 修复后:

assembly
0x1020:  NOP                        ; ✅ 清除花指令
0x1024:  NOP
0x1028:  NOP
0x102C:  NOP
0x1030:  ADD  X0, X1, X2            ; ← IDA现在能识别到这里
0x1034:  BL   real_function
0x1038:  LDP  X29, X30, [SP], #0x10
0x103C:  RET

🛠️ 函数功能详解

match(data, index, mod, ignorerange)

Python
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

作用:模糊匹配指令模式

  • ignorerange=[4,5,6,7]:忽略立即数字段
    • 因为 LDR W6, =0x???? 中的 0x???? 每次混淆可能不同
    • 只匹配操作码,不匹配操作数

patch(data, index) 的奇怪行为

Python
def patch(data, index):
    start = index - 0x10  # ⚠️ 为什么是 -0x10?
    patch_word(data, start, nop)     # 清除 index-0x10
    patch_word(data, start + 4, nop)  # 清除 index-0x0C
    patch_word(data, start + 8, nop)  # 清除 index-0x08

推测:花指令前面还有额外的混淆代码!

可能的完整模式:

assembly
0x1010:  ??? (4字节)    ← 被patch函数清除
0x1014:  ??? (4字节)    ← 被patch函数清除
0x1018:  ??? (4字节)    ← 被patch函数清除
0x101C:  [花指令开始]   ← 被patch_mod1清除
0x1020:  LDR W6, [X4, #0x10]
0x1024:  LDR W6, =0x????
0x1028:  SUB SP, SP, #0x10
0x102C:  RET

总共清除:12 (patch) + 16 (patch_mod1) = 28字节


🔬 主循环逻辑

Python
for i in range(len(data)):
    # 匹配模式1
    if match(data, i, mod1, [4, 5, 6, 7]):  
        # 忽略字节4-7 (第2条指令的立即数)
        patch(data, i)        # 清除前导垃圾 (12字节)
        patch_mod1(data, i)   # 清除mod1本身 (16字节)
    
    # 匹配模式2
    if match(data, i, mod2, [12, 13, 14, 15]):
        # 忽略字节12-15 (第4条指令的立即数)
        patch(data, i)        # 清除前导垃圾 (12字节)
        patch_mod2(data, i)   # 清除mod2本身 (24字节)

🎭 花指令类型识别

混淆技术实现方式危害
假返回 插入 RET 指令 IDA认为函数提前结束
垃圾加载 LDR W6, =0x???? 污染寄存器,干扰数据流分析
假栈帧 SUB SP, SP, #0x10 伪装成函数序言,混淆CFG
无用运算 MADD W0, W0, W0, W0 增加指令数量,破坏模式识别

📊 修复效果对比

修复前:

text
Functions: 298
Exports:   0
Strings:   未提取
  • IDA被花指令欺骗,提前终止函数分析
  • 大量真实代码被标记为数据段

修复后:

text
Functions: 330 (+32)
Exports:   2030
Strings:   50513
  • 清除假返回后,IDA能继续反汇编
  • 识别出隐藏的函数和符号

🔥 实战案例

假设有如下代码:

C
// 原始C++代码
int decrypt_key(char* data, int len) {
    for(int i=0; i<len; i++) {
        data[i] ^= 0x42;
    }
    return len;
}

加壳后的汇编:

assembly
decrypt_key:
    STP X29, X30, [SP, #-0x10]!
    MOV X29, SP
    ; === 循环开始 ===
.loop:
    LDRB W3, [X0], #1
    EOR W3, W3, #0x42
    STRB W3, [X0, #-1]
    
    ; ⚠️ 花指令插入点
    LDR W6, [X4, #0x10]      ; 垃圾
    LDR W6, =0x87654321      ; 垃圾
    SUB SP, SP, #0x10        ; 假栈帧
    RET                      ; 假返回!!!
    
    ; === IDA认为函数在上面结束了 ===
    
    SUBS X1, X1, #1          ; 真实代码被隐藏
    B.NE .loop
    MOV X0, X1
    LDP X29, X30, [SP], #0x10
    RET

IDA反汇编结果:

text
decrypt_key:
    STP X29, X30, [SP, #-0x10]!
    ...
    RET              ; IDA认为这里结束
    
; 下面的代码被当作独立函数或数据
sub_????:
    SUBS X1, X1, #1  ; 被误认为是新函数
    ...

🛡️ 对抗检测的技巧

1. 动态立即数

Python
ignorerange=[4,5,6,7]  # 每次编译立即数不同

效果:

  • LDR W6, =0x12345678
  • LDR W6, =0xABCDEF00
  • LDR W6, =0x99999999

都能被匹配!

2. 只匹配关键操作码

Python
mod1[0:4]   # 只检查第1条指令
mod1[8:12]  # 只检查第3条指令(SUB SP)
mod1[12:16] # 只检查第4条指令(RET)

✅ 总结

功能实现
检测花指令 字节模式匹配 + 忽略可变字段
清除方法 用NOP指令覆盖(0x1F2003D5)
支持模式 2种(16字节和24字节)
额外清理 清除花指令前12字节的前导垃圾
效果 函数识别率提升 10.7% (298→330)

这段代码是逆向Unity IL2CPP游戏的关键前置步骤!

 
 
 
 不同版本的加固方法也不一样
czymf游戏
第一个init函数

image

 

 

image

 而案例中的

 

image

 我们分析后发现第一个是两个函数实现,而第二个是一个函数实现

 含有一種是

image

 

 第二個

image

 这个和第二个样例是一杨的

第三个不完全一样

image

 

 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 

posted on 2025-11-05 11:21  GKLBB  阅读(105)  评论(0)    收藏  举报