Android Native 层反调试技术完全指南

Android Native 层反调试技术完全指南

基于看雪论坛文章: https://bbs.kanxue.com/thread-285790.htm
适合 Android 逆向 Native 层初学者
从零基础到深度应用,由浅入深详细讲解


📚 目录


第一部分: 基础知识篇

1. Frida 基础

1.1 Frida 是什么?

Frida 是一个动态代码插桩工具,可以在程序运行时修改其行为。

特点:

  • 支持 JavaScript 编写脚本
  • 无需重新编译应用
  • 可以操作 Native 代码
  • 跨平台支持 (Android/iOS/Windows/Linux)

1.2 基本用法

// 1. 获取函数地址
let addr = Module.findExportByName("libc.so", "open");

// 2. 创建 NativeFunction 对象
let openFunc = new NativeFunction(addr, 'int', ['pointer', 'int']);
// 参数说明:
//   - 第一个参数: 返回值类型
//   - 第二个参数: [参数类型数组]

// 3. 调用函数
let path_ptr = Memory.allocUtf8String("/etc/hosts");
let result = openFunc(path_ptr, 0);
console.log("文件描述符:", result);

// 4. Hook 函数
Interceptor.attach(addr, {
    onEnter: function(args) {
        console.log("open 被调用, 路径:", args[0].readUtf8String());
    },
    onLeave: function(retval) {
        console.log("返回值:", retval);
    }
});

1.3 常用 API

API 用途 示例
Module.findExportByName() 查找导出函数 Module.findExportByName("libc.so", "open")
Module.findBaseAddress() 查找模块基址 Module.findBaseAddress("libtest.so")
Memory.alloc() 分配内存 Memory.alloc(1024)
Memory.copy() 复制内存 Memory.copy(dst, src, size)
Memory.protect() 修改内存权限 Memory.protect(addr, size, "rwx")
ptr() 创建指针 ptr(0x7000000)

2. Linux 进程内存基础

2.1 /proc/self/maps 文件

这是 Linux 系统中最重要的内存映射信息文件

查看方式:

# Android 设备
adb shell cat /proc/self/maps

# Linux 系统
cat /proc/self/maps

输出示例:

地址范围              权限  偏移     设备  inode  文件路径
7b8c000000-7b8c001000 r--p 00000000 fd:03 123   /system/lib64/libtest.so
7b8c001000-7b8c020000 r-xp 00001000 fd:03 123   /system/lib64/libtest.so
7b8c020000-7b8c021000 rw-p 00020000 fd:03 123   /system/lib64/libtest.so
7b8c021000-7b8c022000 rw-p 00000000 00:00 0     [anon:libc_malloc]

2.2 字段详解

地址范围: 7b8c000000-7b8c001000

  • 起始地址: 0x7b8c000000
  • 结束地址: 0x7b8c001000
  • 大小: 0x1000 (4KB)

权限标识:

标志 含义 说明
r read 可读
w write 可写
x execute 可执行
p private 私有映射 (Copy-on-Write)
s shared 共享映射

常见权限组合:

  • r--p: 只读数据段 (如 .rodata)
  • r-xp: 可执行代码段 (如 .text)
  • rw-p: 可读写数据段 (如 .data, .bss)

文件路径特殊值:

  • /path/to/file: 文件映射
  • [anon:xxx]: 匿名内存
  • [stack]: 线程栈
  • [heap]: 堆内存

2.3 为什么 maps 重要?

对于攻击者:

# 1. 定位目标 SO
cat /proc/12345/maps | grep libsecurity.so

# 2. 找到代码段 (r-xp)
7f8c001000-7f8c050000 r-xp  libsecurity.so

# 3. dump 内存
dd if=/proc/12345/mem bs=1 skip=$((0x7f8c001000)) count=$((0x4F000)) of=code.bin

# 4. IDA 分析
# 还原算法逻辑

对于防守方:

  • 隐藏真实代码位置
  • 显示假的内存映射
  • 防止内存 dump

3. ELF 文件格式基础

ELF (Executable and Linkable Format) 是 Linux/Android 的可执行文件标准格式

3.1 ELF 文件结构

┌─────────────────────┐  ← 文件起始
│   ELF Header        │  64 字节,包含文件基本信息
├─────────────────────┤
│  Program Headers    │  程序头表,描述如何加载到内存
├─────────────────────┤
│   .text 段          │  机器代码
│   .rodata 段        │  只读数据 (字符串常量等)
│   .data 段          │  已初始化的全局变量
│   .bss 段           │  未初始化的全局变量
│   .dynsym 段        │  动态符号表
│   .dynstr 段        │  动态字符串表
│   .dynamic 段       │  动态链接信息
│   ... 其他段 ...    │
├─────────────────────┤
│  Section Headers    │  节头表,描述各个段的详细信息
└─────────────────────┘  ← 文件结束

3.2 重要的段 (Section)

段名 用途 权限 链接时 运行时
.text 机器代码 r-x 必需 必需
.rodata 只读常量 r-- 必需 必需
.data 已初始化全局变量 rw- 必需 必需
.bss 未初始化全局变量 rw- 必需 必需
.dynsym 动态符号表 r-- 可选 必需
.dynstr 动态字符串表 r-- 可选 必需
.dynamic 动态链接信息 rw- 可选 必需
.symtab 完整符号表 r-- 必需 可选
.strtab 完整字符串表 r-- 必需 可选
.shstrtab 节名字符串表 r-- 必需 可选

注意:

  • .symtab.strtab 用于调试和链接,可以被 strip 掉
  • .dynsym.dynstr 用于运行时动态链接,不能删除

3.3 ELF Header 结构

C 语言定义 (64位):

typedef struct {
    unsigned char e_ident[16];  // 魔数: 0x7F 'E' 'L' 'F'
    uint16_t e_type;            // 文件类型: ET_EXEC(可执行) / ET_DYN(共享库)
    uint16_t e_machine;         // 架构: EM_X86_64 / EM_AARCH64
    uint32_t e_version;         // 版本
    uint64_t e_entry;           // 入口点地址
    uint64_t e_phoff;           // 程序头表偏移 ⭐ (offset 32)
    uint64_t e_shoff;           // 节头表偏移 ⭐ (offset 40)
    uint32_t e_flags;           // 处理器特定标志
    uint16_t e_ehsize;          // ELF header 大小
    uint16_t e_phentsize;       // 程序头表项大小
    uint16_t e_phnum;           // 程序头表项数 ⭐ (offset 56)
    uint16_t e_shentsize;       // 节头表项大小
    uint16_t e_shnum;           // 节头表项数 ⭐ (offset 60)
    uint16_t e_shstrndx;        // 节名字符串表索引 ⭐ (offset 62)
} Elf64_Ehdr;  // 总大小: 64 字节

代码中的读取:

// 打开文件
let open_addr = Module.findExportByName("libc.so", "open");
let openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);
let path_addr = Memory.allocUtf8String("/path/to/libtest.so");
let fd = openFunc(path_addr, 0);

// 读取 ELF Header (64字节)
let read_addr = Module.findExportByName("libc.so", "read");
let readFunc = new NativeFunction(read_addr, 'ssize_t', ['int', 'pointer', 'size_t']);

let ehdr = Memory.alloc(64);
readFunc(fd, ehdr, 64);

// 读取各个字段
let magic = ehdr.readByteArray(4);  // offset 0: 0x7F 'E' 'L' 'F'
console.log("Magic:", hexdump(magic));

let e_phoff = ehdr.add(32).readU64();  // offset 32: 程序头表偏移
console.log("程序头表偏移:", ptr(e_phoff));

let e_shoff = ehdr.add(40).readU64();  // offset 40: 节头表偏移
console.log("节头表偏移:", ptr(e_shoff));

let e_phnum = ehdr.add(56).readU16();  // offset 56: 程序头表项数
console.log("程序头数量:", e_phnum);

let e_shnum = ehdr.add(60).readU16();  // offset 60: 节头表项数
console.log("节头数量:", e_shnum);

let e_shstrndx = ehdr.add(62).readU16();  // offset 62: 节名字符串表索引
console.log("字符串表索引:", e_shstrndx);

// 关闭文件
let close_addr = Module.findExportByName("libc.so", "close");
let closeFunc = new NativeFunction(close_addr, 'int', ['int']);
closeFunc(fd);

第二部分: 中级技术篇

4. 程序头表 (Program Header Table)

4.1 作用

程序头表告诉操作系统如何加载程序到内存,主要用于运行时

4.2 Program Header 结构

C 语言定义 (64位):

typedef struct {
    uint32_t p_type;      // 段类型 (offset 0)
    uint32_t p_flags;     // 段标志 (offset 4)
    uint64_t p_offset;    // 文件偏移 (offset 8)
    uint64_t p_vaddr;     // 虚拟地址 (offset 16)
    uint64_t p_paddr;     // 物理地址 (offset 24)
    uint64_t p_filesz;    // 文件中的大小 (offset 32)
    uint64_t p_memsz;     // 内存中的大小 (offset 40)
    uint64_t p_align;     // 对齐 (offset 48)
} Elf64_Phdr;  // 总大小: 0x38 (56字节)

4.3 p_type 类型

名称 说明
0 PT_NULL 未使用
1 PT_LOAD 可加载段 ⭐
2 PT_DYNAMIC 动态链接信息
3 PT_INTERP 解释器路径
4 PT_NOTE 附加信息
6 PT_PHDR 程序头表自身

4.4 p_flags 标志

名称 说明
0x1 PF_X 可执行
0x2 PF_W 可写
0x4 PF_R 可读

组合示例:

  • 0x5 = PF_R | PF_X = 可读可执行 (代码段)
  • 0x6 = PF_R | PF_W = 可读可写 (数据段)
  • 0x4 = PF_R = 只读 (常量段)

4.5 代码示例 - 查找可执行段

function findSoExecSegmentFromFile(so_path) {
    // 获取系统调用
    let open_addr = Module.findExportByName("libc.so", "open");
    let openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);
    
    let read_addr = Module.findExportByName("libc.so", "read");
    let readFunc = new NativeFunction(read_addr, 'ssize_t', ['int', 'pointer', 'size_t']);
    
    let lseek_addr = Module.findExportByName("libc.so", "lseek");
    let lseekFunc = new NativeFunction(lseek_addr, 'int64', ['int', 'int64', 'int']);
    
    let close_addr = Module.findExportByName("libc.so", "close");
    let closeFunc = new NativeFunction(close_addr, 'int', ['int']);
    
    // 打开文件
    let path_addr = Memory.allocUtf8String(so_path);
    let fd = openFunc(path_addr, 0);
    if (fd === -1) {
        console.log("打开文件失败");
        return -1;
    }
    
    // 读取 ELF Header
    let ehdr = Memory.alloc(64);
    let ret = readFunc(fd, ehdr, 64);
    if (ret <= 0) {
        closeFunc(fd);
        return -1;
    }
    
    // 获取程序头表信息
    let e_phoff = ehdr.add(32).readU64();  // 程序头表偏移
    let e_phnum = ehdr.add(56).readU16();  // 程序头表项数
    console.log("程序头表偏移:", ptr(e_phoff), "数量:", e_phnum);
    
    // 定位到程序头表
    lseekFunc(fd, e_phoff, 0);  // SEEK_SET = 0
    
    // 分配 phdr 缓冲区
    let phdr = Memory.alloc(0x38);
    
    // 遍历程序头表
    for (let i = 0; i < e_phnum; i++) {
        // 读取一个程序头
        ret = readFunc(fd, phdr, 0x38);
        if (ret != 0x38) break;
        
        let p_type = phdr.add(0).readU32();      // offset 0
        let p_flags = phdr.add(4).readU32();     // offset 4
        let p_offset = phdr.add(8).readU64();    // offset 8
        let p_filesz = phdr.add(32).readU64();   // offset 32
        
        console.log(`程序头 ${i}: type=${p_type}, flags=0x${p_flags.toString(16)}, offset=${ptr(p_offset)}, size=${ptr(p_filesz)}`);
        
        // 查找 PT_LOAD (1) 且可执行 (PF_X = 1)
        if (p_type === 1 && (p_flags & 1)) {
            console.log("✓ 找到可执行段!");
            
            // 定位到可执行段
            lseekFunc(fd, p_offset, 0);
            
            // 读取可执行段数据
            let exec_data = Memory.alloc(p_filesz);
            readFunc(fd, exec_data, p_filesz);
            
            closeFunc(fd);
            
            return {
                start: exec_data,       // 数据缓冲区
                size: p_filesz,         // 大小
                p_offset: ptr(p_offset) // 文件偏移
            };
        }
    }
    
    closeFunc(fd);
    return -1;
}

5. 节头表 (Section Header Table)

5.1 作用

节头表描述文件的各个节 (Section),主要用于链接和调试

程序头 vs 节头:

  • 程序头: 运行时使用,描述如何加载
  • 节头: 链接时使用,描述文件结构

5.2 Section Header 结构

C 语言定义 (64位):

typedef struct {
    uint32_t sh_name;       // 节名在 .shstrtab 中的偏移 (offset 0)
    uint32_t sh_type;       // 节类型 (offset 4)
    uint64_t sh_flags;      // 节标志 (offset 8)
    uint64_t sh_addr;       // 运行时地址 (offset 16)
    uint64_t sh_offset;     // 文件偏移 ⭐ (offset 24)
    uint64_t sh_size;       // 节大小 ⭐ (offset 32)
    uint32_t sh_link;       // 链接到其他节 (offset 40)
    uint32_t sh_info;       // 附加信息 (offset 44)
    uint64_t sh_addralign;  // 对齐 (offset 48)
    uint64_t sh_entsize;    // 表项大小 (offset 56)
} Elf64_Shdr;  // 总大小: 0x40 (64字节)

5.3 sh_type 类型

名称 说明
0 SHT_NULL 未使用
1 SHT_PROGBITS 程序数据
2 SHT_SYMTAB 符号表
3 SHT_STRTAB 字符串表
6 SHT_DYNAMIC 动态链接信息 ⭐
11 SHT_DYNSYM 动态符号表 ⭐

5.4 代码示例 - 读取节表

function findSoSectionFromFile(so_path) {
    // ... 打开文件,读取 ELF Header ...
    
    // 读取节头表信息
    let e_shoff = ehdr.add(40).readU64();       // 节头表偏移
    let e_shnum = ehdr.add(60).readU16();       // 节头表项数
    let shstrtab_index = ehdr.add(62).readU16(); // 节名字符串表索引
    
    console.log("节头表偏移:", ptr(e_shoff));
    console.log("节头数量:", e_shnum);
    console.log("字符串表索引:", shstrtab_index);
    
    // 定位节头表
    lseekFunc(fd, e_shoff, 0);
    
    let shdr = Memory.alloc(0x40);
    
    // 先读取 .shstrtab (节名字符串表)
    let pread_addr = Module.findExportByName("libc.so", "pread");
    let preadFunc = new NativeFunction(pread_addr, 'int', ['int', 'pointer', 'int', 'int64']);
    
    // 读取 .shstrtab 的节头
    preadFunc(fd, shdr, 0x40, e_shoff + shstrtab_index * 0x40);
    
    let shstrtab_offset = shdr.add(24).readU64();  // offset 24
    let shstrtab_size = shdr.add(32).readU64();    // offset 32
    console.log("字符串表偏移:", ptr(shstrtab_offset), "大小:", shstrtab_size);
    
    // 读取 .shstrtab 内容
    let shstrtab = Memory.alloc(shstrtab_size);
    preadFunc(fd, shstrtab, shstrtab_size, shstrtab_offset);
    
    // 重新定位到节头表
    lseekFunc(fd, e_shoff, 0);
    
    // 遍历所有节
    let sections = {};
    for (let i = 0; i < e_shnum; i++) {
        readFunc(fd, shdr, 0x40);
        
        let sh_name_off = shdr.add(0).readU32();   // offset 0
        let sh_offset = shdr.add(24).readU64();    // offset 24
        let sh_size = shdr.add(32).readU64();      // offset 32
        
        // 获取节名
        let sh_name = shstrtab.add(sh_name_off).readCString();
        
        console.log(`[${sh_name}] offset=${ptr(sh_offset)}, size=${ptr(sh_size)}`);
        
        // 保存关键节信息
        if (sh_name === ".dynstr") {
            sections.dynstr = { offset: ptr(sh_offset), size: sh_size };
        } else if (sh_name === ".dynsym") {
            sections.dynsym = { offset: ptr(sh_offset), size: sh_size };
        } else if (sh_name === ".dynamic") {
            sections.dynamic = { offset: ptr(sh_offset), size: sh_size };
        }
    }
    
    closeFunc(fd);
    return sections;
}

6. 动态链接相关节

6.1 .dynsym - 动态符号表

存储运行时可见的符号 (导出的函数和变量)。

符号表结构:

typedef struct {
    uint32_t st_name;    // 符号名在 .dynstr 中的偏移
    uint8_t st_info;     // 符号类型和绑定信息
    uint8_t st_other;    // 保留
    uint16_t st_shndx;   // 所属节索引
    uint64_t st_value;   // 符号地址 (相对基址)
    uint64_t st_size;    // 符号大小
} Elf64_Sym;  // 大小: 24 字节

示例:

索引  符号名      地址      大小   类型
0     (null)      0x0       0      NOTYPE
1     open        0x1234    100    FUNC
2     close       0x1298    80     FUNC
3     global_var  0x5000    4      OBJECT

6.2 .dynstr - 动态字符串表

存储所有符号名的字符串,以 \0 分隔。

内存布局:

偏移    内容
0x00:   \0
0x01:   o p e n \0
0x06:   c l o s e \0
0x0C:   g l o b a l _ v a r \0

访问方式:

// 符号表条目的 st_name = 0x01
let symbol_name = dynstr.add(0x01).readCString();  // "open"

6.3 .dynamic - 动态链接信息

存储动态链接器需要的信息。

结构:

typedef struct {
    int64_t d_tag;    // 类型标签
    uint64_t d_val;   // 值或地址
} Elf64_Dyn;  // 大小: 16 字节

常见标签:

d_tag 名称 说明
1 DT_NEEDED 依赖的库
5 DT_STRTAB 字符串表地址
6 DT_SYMTAB 符号表地址
7 DT_RELA 重定位表地址
12 DT_INIT 初始化函数
13 DT_FINI 终止函数

示例:

d_tag         d_val
DT_NEEDED     0x10 (字符串偏移 → "libc.so")
DT_SYMTAB     0x1000 (符号表地址)
DT_STRTAB     0x2000 (字符串表地址)
DT_INIT       0x3000 (初始化函数地址)

第三部分: 高级技术篇

7. Android 动态链接器 (linker)

7.1 linker 的作用

Android 的 linker64 (64位) 或 linker (32位) 负责:

  1. 加载 SO 文件到内存
  2. 解析依赖关系 (DT_NEEDED)
  3. 符号解析 (dlsym)
  4. 重定位 (relocation)
  5. 维护已加载 SO 列表

7.2 soinfo 结构体

linker 使用 soinfo 结构体管理每个已加载的 SO。

简化版结构 (Android 10+):

struct soinfo {
    // 基本信息
    const char* realpath_;             // offset 0x00: SO 完整路径
    void* phdr;                        // offset 0x08: 程序头表
    void* base;                        // offset 0x10: 加载基址 ⭐
    size_t size;                       // offset 0x18: 内存大小
    
    // 动态链接信息
    ElfW(Dyn)* dynamic;               // offset 0x20: .dynamic段 ⭐
    soinfo* next;                      // offset 0x28: 链表下一个节点 ⭐
    uint32_t flags_;                   // offset 0x30: 标志
    
    // 符号表
    const char* strtab_;               // offset 0x38: 字符串表 ⭐
    ElfW(Sym)* symtab_;               // offset 0x40: 符号表 ⭐
    size_t nbucket_;                   // offset 0x48: hash bucket 数量
    size_t nchain_;                    // offset 0x50: hash chain 数量
    uint32_t* bucket_;                 // offset 0x58: hash bucket
    uint32_t* chain_;                  // offset 0x60: hash chain
    
    // ... 更多字段 ...
    
    // link_map (用于 gdb)
    link_map link_map_head;            // offset 0xD0: linker map ⭐
    
    // ... 更多字段 ...
};

关键字段偏移计算 (64位, Process.pointerSize = 8):

// 假设 soinfo 指针为 soinfo_ptr

// base (offset 0x10 = Process.pointerSize * 2)
let base = ptr(soinfo_ptr).add(Process.pointerSize * 2).readPointer();

// dynamic (offset 0x20 = Process.pointerSize * 4)
let dynamic = ptr(soinfo_ptr).add(Process.pointerSize * 4).readPointer();

// next (offset 0x28 = Process.pointerSize * 5)
let next = ptr(soinfo_ptr).add(Process.pointerSize * 5).readPointer();

// strtab_ (offset 0x38 = Process.pointerSize * 7)
let strtab = ptr(soinfo_ptr).add(Process.pointerSize * 7).readPointer();

// symtab_ (offset 0x40 = Process.pointerSize * 8)
let symtab = ptr(soinfo_ptr).add(Process.pointerSize * 8).readPointer();

// link_map_head (offset 0xD0 = Process.pointerSize * 26)
let link_map = ptr(soinfo_ptr).add(Process.pointerSize * 26).readPointer();

7.3 获取 soinfo 链表头

function getSolist() {
    // 查找 linker64 模块
    let module = Process.findModuleByName("linker64");
    if (!module) {
        module = Process.findModuleByName("linker");  // 32位
    }
    
    // 枚举符号
    let symbols = module.enumerateSymbols();
    
    // 查找 solist 全局变量
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];
        // solist 的 mangled name: __dl__ZL6solist
        if (symbol.name.indexOf("__dl__ZL6solist") !== -1) {
            console.log("找到 solist:", symbol.address);
            // solist 是指针变量,需要解引用
            return symbol.address.readPointer();
        }
    }
    
    console.log("未找到 solist");
    return null;
}

7.4 获取 SO 路径

function getRealpath(soinfo) {
    // 查找 get_realpath 函数
    let module = Process.findModuleByName("linker64") || Process.findModuleByName("linker");
    let symbols = module.enumerateSymbols();
    
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];
        if (symbol.name.indexOf("get_realpath") !== -1) {
            // 创建函数对象
            // const char* get_realpath(soinfo* si)
            let get_realpath = new NativeFunction(
                symbol.address,
                'pointer',  // 返回 const char*
                ['pointer'] // 参数 soinfo*
            );
            
            // 调用函数
            let path_ptr = get_realpath(ptr(soinfo));
            let path = ptr(path_ptr).readCString();
            
            return path;
        }
    }
    
    return null;
}

7.5 遍历 soinfo 链表

function listAllLoadedSOs() {
    let soinfo = getSolist();
    if (!soinfo) {
        console.log("无法获取 soinfo 链表");
        return;
    }
    
    let index = 0;
    console.log("=== 已加载的 SO 列表 ===");
    
    do {
        // 获取路径
        let path = getRealpath(soinfo);
        
        // 获取基址
        let base = ptr(soinfo).add(Process.pointerSize * 2).readPointer();
        
        // 获取大小
        let size = ptr(soinfo).add(Process.pointerSize * 3).readU64();
        
        console.log(`[${index}] ${path}`);
        console.log(`     基址: ${base}, 大小: ${ptr(size)}`);
        
        // 获取下一个节点
        soinfo = ptr(soinfo).add(Process.pointerSize * 5).readPointer();
        index++;
        
    } while (soinfo.toUInt32() !== 0);
    
    console.log(`总计: ${index} 个 SO`);
}

8. ### 8. Linux 内存管理系统调用

8.1 mmap - 内存映射

函数原型:

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:

  • addr: 期望的映射地址(通常传 NULL 让系统自动选择)
  • length: 映射区域大小
  • prot: 内存保护标志
  • flags: 映射类型标志
  • fd: 文件描述符(文件映射时使用)
  • offset: 文件偏移量

保护标志 (prot):

标志 说明
PROT_NONE 0 不可访问
PROT_READ 1 可读
PROT_WRITE 2 可写
PROT_EXEC 4 可执行

常用组合:

  • 7 = PROT_READ | PROT_WRITE | PROT_EXEC (可读可写可执行)
  • 5 = PROT_READ | PROT_EXEC (可读可执行)
  • 3 = PROT_READ | PROT_WRITE (可读可写)

映射类型标志 (flags):

标志 说明
MAP_SHARED 0x01 共享映射
MAP_PRIVATE 0x02 私有映射(COW)
MAP_FIXED 0x10 固定地址映射
MAP_ANONYMOUS 0x20 匿名映射(不关联文件)

8.2 Frida 中使用 mmap

示例1: 创建匿名内存

let mmap_addr = Module.findExportByName("libc.so", "mmap");
let mmapFunc = new NativeFunction(mmap_addr, 'pointer', 
    ['pointer', 'int', 'int', 'int', 'int', 'int']
);

// 创建 4KB 的匿名可读写内存
// mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
let new_mem = mmapFunc(
    ptr(0),           // addr = NULL (自动选择)
    0x1000,           // length = 4KB
    3,                // prot = PROT_READ | PROT_WRITE
    0x22,             // flags = MAP_PRIVATE(0x02) | MAP_ANONYMOUS(0x20)
    -1,               // fd = -1 (匿名映射)
    0                 // offset = 0
);

console.log("新分配的内存地址:", new_mem);

示例2: 映射文件到内存

let open_addr = Module.findExportByName("libc.so", "open");
let openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);

let close_addr = Module.findExportByName("libc.so", "close");
let closeFunc = new NativeFunction(close_addr, 'int', ['int']);

// 打开文件
let path = Memory.allocUtf8String("/data/local/tmp/test.bin");
let fd = openFunc(path, 0);  // O_RDONLY = 0

// 映射文件到内存
let file_mem = mmapFunc(
    ptr(0),           // addr = NULL
    0x10000,          // length = 64KB
    5,                // prot = PROT_READ | PROT_EXEC
    0x02,             // flags = MAP_PRIVATE
    fd,               // 文件描述符
    0                 // offset = 0
);

console.log("文件映射地址:", file_mem);

// 关闭文件
closeFunc(fd);

8.3 munmap - 释放内存映射

函数原型:

int munmap(void* addr, size_t length);

使用示例:

let munmap_addr = Module.findExportByName("libc.so", "munmap");
let munmapFunc = new NativeFunction(munmap_addr, 'int', ['pointer', 'int']);

// 释放之前分配的内存
let ret = munmapFunc(new_mem, 0x1000);
if (ret === 0) {
    console.log("内存释放成功");
} else {
    console.log("内存释放失败");
}

8.4 mremap - 重新映射内存

函数原型:

void* mremap(void* old_addr, size_t old_size, size_t new_size, int flags, void* new_addr);

参数说明:

  • old_addr: 原内存地址
  • old_size: 原大小
  • new_size: 新大小
  • flags: 操作标志
  • new_addr: 新地址(当 flags 包含 MREMAP_FIXED 时使用)

标志 (flags):

标志 说明
MREMAP_MAYMOVE 1 允许移动到新位置
MREMAP_FIXED 2 固定到指定地址

8.5 mremap 的强大用途

核心技巧: 将内存移动到指定地址

这是反调试技术的关键!通过 mremap 可以实现:

  1. 将匿名内存移动到 SO 的位置
  2. 将 SO 内存移动到匿名区域
let mremap_addr = Module.findExportByName("libc.so", "mremap");
let mremapFunc = new NativeFunction(mremap_addr, 'pointer', 
    ['pointer', 'int64', 'int64', 'int64', 'pointer']
);

// 场景: 将临时内存移动到目标SO的位置
// 步骤1: 创建临时匿名内存
let temp_mem = mmapFunc(ptr(0), 0x10000, 7, 0x22, -1, 0);
console.log("临时内存:", temp_mem);

// 步骤2: 复制SO内容到临时内存
let target_so_addr = Module.findBaseAddress("libtest.so");
Memory.copy(temp_mem, target_so_addr, 0x10000);

// 步骤3: 使用 mremap 将临时内存移动到 SO 地址
// mremap(temp_mem, 0x10000, 0x10000, MREMAP_MAYMOVE|MREMAP_FIXED, target_so_addr)
let new_addr = mremapFunc(
    temp_mem,                    // 临时内存地址
    0x10000,                     // 原大小
    0x10000,                     // 新大小(保持不变)
    3,                           // MREMAP_MAYMOVE(1) | MREMAP_FIXED(2)
    target_so_addr               // 目标地址
);

if (new_addr.equals(target_so_addr)) {
    console.log("✓ 成功将内存移动到 SO 地址!");
    console.log("现在 maps 中该地址显示为匿名内存");
}

效果:

  • 移动前: maps 中显示 7f8c000000-7f8c010000 r-xp ... /data/app/libtest.so
  • 移动后: maps 中显示 7f8c000000-7f8c010000 r-xp ... [anon:libc_malloc]

8.6 完整示例 - 隐藏 SO 名称

function hideSONameInMaps(so_name) {
    // 1. 获取系统调用
    let mmap_addr = Module.findExportByName("libc.so", "mmap");
    let mmapFunc = new NativeFunction(mmap_addr, 'pointer', 
        ['pointer', 'int', 'int', 'int', 'int', 'int']
    );
    
    let munmap_addr = Module.findExportByName("libc.so", "munmap");
    let munmapFunc = new NativeFunction(munmap_addr, 'int', ['pointer', 'int']);
    
    let mremap_addr = Module.findExportByName("libc.so", "mremap");
    let mremapFunc = new NativeFunction(mremap_addr, 'pointer', 
        ['pointer', 'int64', 'int64', 'int64', 'pointer']
    );
    
    // 2. 获取 SO 的内存范围
    let module = Process.findModuleByName(so_name);
    if (!module) {
        console.log("SO 未找到:", so_name);
        return;
    }
    
    let so_base = module.base;
    let so_size = module.size;
    console.log(`SO地址: ${so_base}, 大小: ${ptr(so_size)}`);
    
    // 3. 创建临时匿名内存
    let temp_mem = mmapFunc(ptr(0), so_size, 7, 0x22, -1, 0);
    console.log("临时内存地址:", temp_mem);
    
    // 4. 复制 SO 内容到临时内存
    Memory.copy(temp_mem, so_base, so_size);
    console.log("✓ SO 内容已复制");
    
    // 5. 使用 mremap 替换原 SO 内存
    let ret = mremapFunc(temp_mem, so_size, so_size, 3, so_base);
    if (ret.equals(so_base)) {
        console.log("✓ SO 已被匿名内存替换");
        console.log("现在 maps 中不再显示 SO 路径!");
    } else {
        console.log("✗ mremap 失败");
    }
    
    // 6. 释放原临时内存(已经被移动走了)
    munmapFunc(temp_mem, so_size);
}

// 使用示例
hideSONameInMaps("libtest.so");

8.7 重要概念 - COW (Copy-on-Write)

什么是 COW?

当使用 MAP_PRIVATE 映射文件时:

  1. 初始: 多个进程共享同一物理内存页
  2. 写入时: 系统自动创建副本,修改只影响当前进程

在反调试中的应用:

// 场景: 在 maps 中创建假的 SO 条目

// 1. 打开真实 SO 文件
let fd = openFunc(Memory.allocUtf8String("/path/to/real.so"), 0);

// 2. 使用 MAP_PRIVATE 创建映射
let fake_mem = mmapFunc(
    ptr(0),
    0x10000,
    5,              // PROT_READ | PROT_EXEC
    0x02,           // MAP_PRIVATE (关键!)
    fd,
    0
);

console.log("假的 SO 映射地址:", fake_mem);
// maps 中会显示: 7f8c000000-7f8c010000 r-xp ... /path/to/real.so

// 3. 修改内容(触发 COW)
Memory.protect(fake_mem, 0x1000, "rwx");
fake_mem.writeByteArray([0x00, 0x00, 0x00, 0x00]);

// 现在这块内存的内容与文件不同,但 maps 中仍显示文件路径

8.8 内存操作的安全性

注意事项:

  1. 内存对齐: mmap 的地址和大小通常需要页对齐(4KB)
  2. 权限检查: 修改内存前要检查权限
  3. 地址冲突: mremap 移动到已占用地址会失败
  4. SELinux: Android 上某些操作可能被 SELinux 阻止

页对齐示例:

// 获取页大小
let page_size = 4096;  // 通常是 4KB

// 对齐地址
function alignDown(addr, alignment) {
    return addr.and(ptr(alignment - 1).not());
}

function alignUp(addr, alignment) {
    return alignDown(addr.add(alignment - 1), alignment);
}

// 使用
let unaligned_addr = ptr(0x12345678);
let aligned_addr = alignDown(unaligned_addr, page_size);
console.log("原地址:", unaligned_addr);
console.log("对齐后:", aligned_addr);  // 0x12345000

9. 内存保护

9.1 Memory.protect() - Frida 方法

基本用法:

Memory.protect(address, size, protection);

参数:

  • address: 内存起始地址(需要页对齐)
  • size: 保护区域大小(需要页对齐)
  • protection: 保护字符串

保护字符串:

字符串 说明 对应 mprotect 值
"---" 无权限 PROT_NONE (0)
"r--" 只读 PROT_READ (1)
"rw-" 可读写 PROT_READ | PROT_WRITE (3)
"r-x" 可读可执行 PROT_READ | PROT_EXEC (5)
"rwx" 可读写执行 PROT_READ | PROT_WRITE | PROT_EXEC (7)

示例:

let addr = Module.findBaseAddress("libtest.so");

// 修改为可读写执行
Memory.protect(addr, 0x1000, "rwx");
console.log("权限已修改为 rwx");

// 写入数据
addr.writeByteArray([0x90, 0x90, 0x90, 0x90]);  // NOP 指令

// 恢复为只读可执行
Memory.protect(addr, 0x1000, "r-x");
console.log("权限已恢复为 r-x");

9.2 mprotect - 系统调用

函数原型:

int mprotect(void* addr, size_t len, int prot);

使用示例:

let mprotect_addr = Module.findExportByName("libc.so", "mprotect");
let mprotectFunc = new NativeFunction(mprotect_addr, 'int', 
    ['pointer', 'int', 'int']
);

let target = ptr(0x7b8c000000);

// 修改为可读写 (PROT_READ | PROT_WRITE = 3)
let ret = mprotectFunc(target, 0x1000, 3);
if (ret === 0) {
    console.log("✓ 权限修改成功");
} else {
    console.log("✗ 权限修改失败:", ret);
}

9.3 页对齐问题

为什么需要页对齐?

Linux 的内存保护是以页 (Page) 为单位的,通常页大小为 4KB (0x1000)。

错误示例:

let addr = ptr(0x7b8c001234);  // 未对齐的地址

// ✗ 这会失败!
Memory.protect(addr, 100, "rwx");
// Error: mprotect failed: Invalid argument

正确示例:

let page_size = 4096;

function pageAlign(addr) {
    // 向下对齐到页边界
    return addr.and(ptr(page_size - 1).not());
}

let addr = ptr(0x7b8c001234);
let aligned_addr = pageAlign(addr);

console.log("原地址:", addr);        // 0x7b8c001234
console.log("对齐后:", aligned_addr); // 0x7b8c001000

// ✓ 成功!
Memory.protect(aligned_addr, page_size, "rwx");

9.4 获取页大小

方法1: 使用 sysconf

let sysconf_addr = Module.findExportByName("libc.so", "sysconf");
let sysconfFunc = new NativeFunction(sysconf_addr, 'long', ['int']);

// _SC_PAGESIZE = 30
let page_size = sysconfFunc(30);
console.log("页大小:", page_size);  // 通常是 4096

方法2: 硬编码

// Android/Linux 几乎总是 4KB
const PAGE_SIZE = 4096;

9.5 完整的页对齐工具函数

const PAGE_SIZE = 4096;
const PAGE_MASK = ~(PAGE_SIZE - 1);

// 向下对齐
function pageAlignDown(addr) {
    if (typeof addr === 'number') {
        return addr & PAGE_MASK;
    }
    return ptr(addr).and(PAGE_MASK);
}

// 向上对齐
function pageAlignUp(addr) {
    if (typeof addr === 'number') {
        return (addr + PAGE_SIZE - 1) & PAGE_MASK;
    }
    return ptr(addr).add(PAGE_SIZE - 1).and(PAGE_MASK);
}

// 计算对齐后的大小
function getAlignedSize(addr, size) {
    let start = pageAlignDown(addr);
    let end = pageAlignUp(ptr(addr).add(size));
    return end.sub(start).toInt32();
}

// 使用示例
let addr = ptr(0x7b8c001234);
let size = 100;

let aligned_start = pageAlignDown(addr);
let aligned_size = getAlignedSize(addr, size);

console.log("原始: addr =", addr, "size =", size);
console.log("对齐: addr =", aligned_start, "size =", ptr(aligned_size));

Memory.protect(aligned_start, aligned_size, "rwx");

9.6 修改只读内存的完整流程

function writeToReadOnlyMemory(address, data) {
    try {
        // 1. 对齐地址
        let page_start = pageAlignDown(address);
        let page_size = getAlignedSize(address, data.length);
        
        console.log(`修改内存: ${address} (页: ${page_start}, 大小: ${ptr(page_size)})`);
        
        // 2. 修改为可写
        Memory.protect(page_start, page_size, "rwx");
        console.log("✓ 权限已修改为 rwx");
        
        // 3. 写入数据
        address.writeByteArray(data);
        console.log("✓ 数据已写入");
        
        // 4. 恢复权限
        Memory.protect(page_start, page_size, "r-x");
        console.log("✓ 权限已恢复为 r-x");
        
        return true;
    } catch (e) {
        console.log("✗ 写入失败:", e.message);
        return false;
    }
}

// 使用示例
let target = Module.findExportByName("libtest.so", "check_license");
let nop_code = [0x00, 0x00, 0x80, 0xD2, 0xC0, 0x03, 0x5F, 0xD6];  // ARM64: MOV X0, #0; RET

writeToReadOnlyMemory(target, nop_code);

9.7 SELinux 权限问题

在 Android 上,某些内存操作可能被 SELinux 阻止。

检查 SELinux 状态:

function checkSELinuxStatus() {
    let getenforce_addr = Module.findExportByName("libselinux.so", "security_getenforce");
    if (!getenforce_addr) {
        console.log("SELinux 函数未找到");
        return;
    }
    
    let getenforceFunc = new NativeFunction(getenforce_addr, 'int', []);
    let status = getenforceFunc();
    
    console.log("SELinux 状态:", status === 1 ? "Enforcing" : "Permissive/Disabled");
}

常见错误:

mprotect failed: Permission denied

解决方法:

  1. Root 设备并设置 SELinux 为 Permissive
  2. 使用更高级的技术绕过限制
  3. 使用 Memory.copy + mremap 代替直接修改

9.8 检测内存权限

function getMemoryProtection(address) {
    let fopen_addr = Module.findExportByName("libc.so", "fopen");
    let fopenFunc = new NativeFunction(fopen_addr, "pointer", ["pointer", "pointer"]);
    
    let fgets_addr = Module.findExportByName("libc.so", "fgets");
    let fgetsFunc = new NativeFunction(fgets_addr, "pointer", ["pointer", "int", "pointer"]);
    
    let fclose_addr = Module.findExportByName("libc.so", "fclose");
    let fcloseFunc = new NativeFunction(fclose_addr, "int", ["pointer"]);
    
    // 对齐地址到页边界
    let page_addr = pageAlignDown(address);
    let target_addr_str = page_addr.toString(16);
    
    // 读取 /proc/self/maps
    let file = fopenFunc(
        Memory.allocUtf8String("/proc/self/maps"), 
        Memory.allocUtf8String("r")
    );
    
    let line = Memory.alloc(1024);
    while (ptr(fgetsFunc(line, 1024, file)).toInt32() !== 0) {
        let line_str = line.readCString();
        
        // 检查是否包含目标地址
        if (line_str.indexOf(target_addr_str) === 0) {
            // 解析权限
            let match = line_str.match(/^[0-9a-f]+-[0-9a-f]+\s+([rwxp-]+)/);
            if (match) {
                let perms = match[1];
                fcloseFunc(file);
                return {
                    address: page_addr,
                    permissions: perms,
                    readable: perms[0] === 'r',
                    writable: perms[1] === 'w',
                    executable: perms[2] === 'x',
                    private: perms[3] === 'p'
                };
            }
        }
    }
    
    fcloseFunc(file);
    return null;
}

// 使用示例
let addr = Module.findBaseAddress("libtest.so");
let prot = getMemoryProtection(addr);

console.log("地址:", prot.address);
console.log("权限:", prot.permissions);
console.log("可读:", prot.readable);
console.log("可写:", prot.writable);
console.log("可执行:", prot.executable);

9.9 批量修改内存权限

function protectMemoryRange(start_addr, end_addr, protection) {
    let current = pageAlignDown(start_addr);
    let end = pageAlignUp(end_addr);
    
    console.log(`修改内存范围: ${current} - ${end}`);
    
    while (current.compare(end) < 0) {
        try {
            Memory.protect(current, PAGE_SIZE, protection);
            console.log(`✓ ${current}`);
        } catch (e) {
            console.log(`✗ ${current}: ${e.message}`);
        }
        current = current.add(PAGE_SIZE);
    }
}

// 使用示例
let module = Process.findModuleByName("libtest.so");
protectMemoryRange(module.base, module.base.add(module.size), "rwx");

9.10 内存保护的常见陷阱

陷阱1: 跨页写入

// ✗ 错误: 数据跨越了页边界
let addr = ptr(0x7b8c001ff0);  // 距离页边界只有 16 字节
let data = new Array(32).fill(0x90);  // 32 字节数据

// 需要保护两个页
let page1 = pageAlignDown(addr);
let page2 = page1.add(PAGE_SIZE);

Memory.protect(page1, PAGE_SIZE, "rwx");
Memory.protect(page2, PAGE_SIZE, "rwx");

addr.writeByteArray(data);

Memory.protect(page1, PAGE_SIZE, "r-x");
Memory.protect(page2, PAGE_SIZE, "r-x");

陷阱2: 递归保护

// ✗ 错误: Memory.protect 内部可能也需要写内存
Interceptor.attach(Module.findExportByName("libc.so", "mprotect"), {
    onEnter: function(args) {
        // 如果在这里调用 Memory.protect 可能导致死循环
        // Memory.protect(args[0], args[1].toInt32(), "rwx");
    }
});

陷阱3: 权限不匹配

// ✗ 错误: 代码段不应该有写权限
let code_addr = Module.findExportByName("libtest.so", "func");

Memory.protect(code_addr, 0x1000, "rw-");  // 移除了执行权限!
// 现在调用这个函数会崩溃: Segmentation fault (SIGSEGV)

第四部分: 核心隐藏技术详解

10. 技术1: 隐藏 SO 在 maps 中的名称

10.1 技术原理

目标: 将 /proc/self/maps 中的 SO 路径替换为匿名内存标识

变化效果:

修改前:
7b8c000000-7b8c010000 r-xp 00000000 fd:03 123 /data/app/com.example/lib/arm64/libsecret.so

修改后:
7b8c000000-7b8c010000 r-xp 00000000 00:00 0   [anon:libc_malloc]

核心思路:

  1. 创建匿名内存
  2. 复制 SO 内容到匿名内存
  3. 使用 mremap 将匿名内存移动到 SO 原位置
  4. 释放临时内存

10.2 关键系统调用

mremap 的魔法:

void* mremap(void* old_address, size_t old_size, size_t new_size, 
             int flags, void* new_address);

当使用 MREMAP_MAYMOVE | MREMAP_FIXED 标志时:

  • old_address 的内存移动new_address
  • old_address 变成未映射状态
  • new_address 继承 old_address 的匿名属性

10.3 完整实现代码

function hiddenSoInMaps(so_name) {
    console.log("=== 开始隐藏 SO:", so_name, "===");
    
    // 1. 获取系统调用
    let mmap_addr = Module.findExportByName("libc.so", "mmap");
    let mmapFunc = new NativeFunction(mmap_addr, 'pointer', 
        ['pointer', 'int', 'int', 'int', 'int', 'int']
    );
    
    let munmap_addr = Module.findExportByName("libc.so", "munmap");
    let munmapFunc = new NativeFunction(munmap_addr, 'int', ['pointer', 'int']);
    
    let mremap_addr = Module.findExportByName("libc.so", "mremap");
    let mremapFunc = new NativeFunction(mremap_addr, 'pointer', 
        ['pointer', 'int64', 'int64', 'int64', 'pointer']
    );
    
    // 2. 从 maps 中获取 SO 的地址范围
    let soRange = findSoRangeFromMaps(so_name);
    let startAddress = soRange.base;
    let size = soRange.size;
    
    console.log("SO 基址:", startAddress);
    console.log("SO 大小:", ptr(size));
    
    if (startAddress.toInt32() === 0 || size === 0) {
        console.log("✗ SO 未找到或大小为 0");
        return;
    }
    
    // 3. 创建匿名内存(临时存储)
    // mmap(NULL, size, PROT_READ|PROT_WRITE|PROT_EXEC, 
    //      MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
    let temp_addr = mmapFunc(
        ptr(0),              // addr = NULL (自动分配)
        size,                // length
        7,                   // prot = rwx
        0x22,                // flags = MAP_PRIVATE(0x02) | MAP_ANONYMOUS(0x20)
        -1,                  // fd = -1
        0                    // offset = 0
    );
    
    console.log("临时内存地址:", temp_addr);
    
    // 4. 复制 SO 内容到临时内存
    Memory.copy(temp_addr, startAddress, size);
    console.log("✓ SO 内容已复制到临时内存");
    
    // 5. 使用 mremap 将临时内存移动到 SO 原地址
    // mremap(temp_addr, size, size, MREMAP_MAYMOVE|MREMAP_FIXED, startAddress)
    let ret = mremapFunc(
        temp_addr,           // old_address
        size,                // old_size
        size,                // new_size (保持不变)
        3,                   // flags = MREMAP_MAYMOVE(1) | MREMAP_FIXED(2)
        startAddress         // new_address (目标地址)
    );
    
    if (ret.equals(startAddress)) {
        console.log("✓ mremap 成功! 返回地址:", ret);
        console.log("✓ SO 已被匿名内存替换");
    } else {
        console.log("✗ mremap 失败! 返回:", ret);
        munmapFunc(temp_addr, size);
        return;
    }
    
    // 6. 释放原临时内存(已经被移动走了)
    munmapFunc(temp_addr, size);
    console.log("✓ 临时内存已释放");
    
    console.log("=== 完成! 现在 maps 中不显示 SO 路径 ===");
}

// 辅助函数: 从 maps 中查找 SO 的地址范围
function findSoRangeFromMaps(so_name) {
    let fopen_addr = Module.findExportByName("libc.so", "fopen");
    let fopenFunc = new NativeFunction(fopen_addr, "pointer", ["pointer", "pointer"]);
    
    let fgets_addr = Module.findExportByName("libc.so", "fgets");
    let fgetsFunc = new NativeFunction(fgets_addr, "pointer", ["pointer", "int", "pointer"]);
    
    let fclose_addr = Module.findExportByName("libc.so", "fclose");
    let fcloseFunc = new NativeFunction(fclose_addr, "int", ["pointer"]);
    
    let startAddress = ptr(0);
    let endAddress = ptr(0);
    let firstLine = true;
    
    // 打开 /proc/self/maps
    let file = fopenFunc(
        Memory.allocUtf8String("/proc/self/maps"), 
        Memory.allocUtf8String("r")
    );
    
    let line = Memory.alloc(1024);
    
    // 逐行读取
    while (ptr(fgetsFunc(line, 1024, file)).toInt32() !== 0) {
        let line_str = line.readCString();
        
        // 查找包含 so_name 的行
        if (line_str.indexOf(so_name) !== -1) {
            // 解析地址范围: "7b8c000000-7b8c010000 ..."
            const match = line_str.match(/^([0-9a-f]+)-([0-9a-f]+)/);
            if (match) {
                let start = ptr(parseInt(match[1], 16));
                let end = ptr(parseInt(match[2], 16));
                
                // 第一次遇到时记录起始地址
                if (firstLine) {
                    startAddress = start;
                    firstLine = false;
                }
                
                // 持续更新结束地址(处理多段映射)
                endAddress = end;
            }
        }
    }
    
    fcloseFunc(file);
    
    let size = endAddress.sub(startAddress).toInt32();
    return {
        base: startAddress,
        size: size
    };
}

10.4 代码详解

步骤1: 获取 SO 的完整内存范围

为什么要遍历所有匹配的行?

示例 maps 输出:
7b8c000000-7b8c001000 r--p 00000000 fd:03 123 /data/app/libtest.so  ← 第一段
7b8c001000-7b8c020000 r-xp 00001000 fd:03 123 /data/app/libtest.so  ← 第二段
7b8c020000-7b8c021000 rw-p 00020000 fd:03 123 /data/app/libtest.so  ← 第三段

一个 SO 通常有多个段(只读、可执行、可读写),我们需要:

  • 起始地址: 第一段的开始 (0x7b8c000000)
  • 结束地址: 最后一段的结束 (0x7b8c021000)
  • 总大小: 0x21000

步骤2: mmap 创建匿名内存

let temp_addr = mmapFunc(
    ptr(0),    // NULL → 让内核选择地址
    size,      // 大小
    7,         // PROT_READ | PROT_WRITE | PROT_EXEC
    0x22,      // MAP_PRIVATE | MAP_ANONYMOUS
    -1,        // 匿名映射必须是 -1
    0          // offset (匿名映射时忽略)
);

标志解析:

  • 0x02 = MAP_PRIVATE: 私有映射,修改不影响其他进程
  • 0x20 = MAP_ANONYMOUS: 不关联文件,纯内存分配

步骤3: Memory.copy 复制内容

Memory.copy(temp_addr, startAddress, size);

这会复制:

  • 代码段 (.text)
  • 数据段 (.data)
  • 只读数据 (.rodata)
  • BSS 段 (.bss)
  • 所有其他段

步骤4: mremap 原子替换

let ret = mremapFunc(
    temp_addr,           // 源: 匿名内存
    size,                // 源大小
    size,                // 目标大小
    3,                   // MREMAP_MAYMOVE | MREMAP_FIXED
    startAddress         // 目标: SO 原地址
);

关键标志:

  • MREMAP_MAYMOVE (1): 允许移动内存
  • MREMAP_FIXED (2): 固定到指定地址

效果:

  1. 内核将 temp_addr 的页表项移动到 startAddress
  2. temp_addr 变成未映射状态
  3. startAddress 现在是匿名内存,但内容与原 SO 相同

10.5 验证效果

方法1: 查看 maps

function printMaps(keyword) {
    let fopen_addr = Module.findExportByName("libc.so", "fopen");
    let fopenFunc = new NativeFunction(fopen_addr, "pointer", ["pointer", "pointer"]);
    
    let fgets_addr = Module.findExportByName("libc.so", "fgets");
    let fgetsFunc = new NativeFunction(fgets_addr, "pointer", ["pointer", "int", "pointer"]);
    
    let fclose_addr = Module.findExportByName("libc.so", "fclose");
    let fcloseFunc = new NativeFunction(fclose_addr, "int", ["pointer"]);
    
    let file = fopenFunc(
        Memory.allocUtf8String("/proc/self/maps"), 
        Memory.allocUtf8String("r")
    );
    
    let line = Memory.alloc(1024);
    console.log("=== /proc/self/maps 包含", keyword, "的行 ===");
    
    while (ptr(fgetsFunc(line, 1024, file)).toInt32() !== 0) {
        let line_str = line.readCString();
        if (line_str.indexOf(keyword) !== -1) {
            console.log(line_str.trim());
        }
    }
    
    fcloseFunc(file);
}

// 使用
console.log("修改前:");
printMaps("libtest.so");

hiddenSoInMaps("libtest.so");

console.log("\n修改后:");
printMaps("libtest.so");  // 应该没有输出
printMaps("anon");        // 应该看到新的匿名内存

方法2: 测试功能是否正常

// 隐藏前
let func = Module.findExportByName("libtest.so", "test_function");
console.log("隐藏前调用:", func);
let ret1 = new NativeFunction(func, 'int', [])();
console.log("返回值:", ret1);

// 隐藏
hiddenSoInMaps("libtest.so");

// 隐藏后
console.log("隐藏后调用:", func);  // 地址不变
let ret2 = new NativeFunction(func, 'int', [])();
console.log("返回值:", ret2);  // 功能正常

console.log("功能验证:", ret1 === ret2 ? "✓ 通过" : "✗ 失败");

10.6 注意事项和限制

1. 权限问题

某些情况下 mremap 可能失败:

if (ret.toInt32() === -1) {
    console.log("✗ mremap 失败,可能原因:");
    console.log("  - SELinux 阻止");
    console.log("  - 地址范围冲突");
    console.log("  - 权限不足");
}

2. 时机选择

最佳调用时机:

  • JNI_OnLoad
  • .init_array 构造函数中
  • ✓ 目标函数首次调用前
  • ✗ 不要在 SO 初始化过程中(可能导致崩溃)

3. 动态链接影响

隐藏后会影响:

  • dlopen 不再能找到该 SO
  • dlsym 不再能解析符号
  • ✓ 已经获取的函数指针仍然有效
  • ✓ 代码执行完全正常

4. 调试影响

修改后:
- IDA/Ghidra 无法通过 SO 名称定位
- gdb 的 info sharedlibrary 不显示
- /proc/pid/maps 中看不到 SO 路径
- Frida 的 Process.enumerateModules() 可能找不到

10.7 绕过该技术的方法(攻击者视角)

方法1: Hook mmap/mremap

Interceptor.attach(Module.findExportByName("libc.so", "mremap"), {
    onEnter: function(args) {
        console.log("mremap 调用:");
        console.log("  old_addr:", args[0]);
        console.log("  new_addr:", args[4]);
    }
});

方法2: 在加载时 dump SO

// 在 dlopen 时立即 dump
Interceptor.attach(Module.findExportByName("linker64", "dlopen"), {
    onLeave: function(retval) {
        // dump 刚加载的 SO
    }
});

方法3: 内存扫描特征码

// 扫描内存寻找 ELF 头
Memory.scan(ptr(0x7000000000), 0x1000000000, "7f 45 4c 46", {
    onMatch: function(address, size) {
        console.log("发现 ELF 文件头:", address);
    }
});

10.8 增强版实现

function hiddenSoInMapsEnhanced(so_name) {
    try {
        // 1. 检查 SO 是否存在
        let module = Process.findModuleByName(so_name);
        if (!module) {
            console.log("✗ SO 未加载:", so_name);
            return false;
        }
        
        // 2. 保存关键信息
        let exports_backup = {};
        module.enumerateExports().forEach(exp => {
            exports_backup[exp.name] = exp.address;
        });
        
        console.log("✓ 已备份", Object.keys(exports_backup).length, "个导出函数");
        
        // 3. 执行隐藏
        hiddenSoInMaps(so_name);
        
        // 4. 验证功能
        let all_ok = true;
        for (let name in exports_backup) {
            let old_addr = exports_backup[name];
            try {
                // 尝试读取函数开头
                old_addr.readU32();
            } catch (e) {
                console.log("✗ 函数", name, "不可访问");
                all_ok = false;
            }
        }
        
        if (all_ok) {
            console.log("✓ 所有函数验证通过");
            return true;
        } else {
            console.log("✗ 部分函数验证失败");
            return false;
        }
        
    } catch (e) {
        console.log("✗ 隐藏失败:", e.message);
        console.log(e.stack);
        return false;
    }
}

11. 技术2: 隐藏可执行段在 maps 中

11.1 技术原理

目标: 将真实代码段隐藏,在 maps 中展示假的代码段

变化效果:

修改前:
7b8c000000-7b8c001000 r--p 00000000 fd:03 123 /data/app/libtest.so  (只读段)
7b8c001000-7b8c020000 r-xp 00001000 fd:03 123 /data/app/libtest.so  ← 真实代码
7b8c020000-7b8c021000 rw-p 00020000 fd:03 123 /data/app/libtest.so  (数据段)

修改后:
7b8c000000-7b8c001000 r--p 00000000 fd:03 123 /data/app/libtest.so  (只读段)
7b8c001000-7b8c020000 r-xp 00000000 00:00 0   [anon:libc_malloc]     ← 真实代码(匿名)
7b8c020000-7b8c021000 rw-p 00020000 fd:03 123 /data/app/libtest.so  (数据段)
7f9d000000-7f9d01f000 r-xp 00001000 fd:03 123 /data/app/libtest.so  ← 假代码(全0)

核心思路:

  1. 将可执行段内存改为匿名映射(真实代码)
  2. 在其他位置创建文件映射(假代码,全0)
  3. 攻击者 dump 到的是假代码

11.2 为什么只隐藏可执行段?

原因分析:

内存段 是否关键 是否隐藏 原因
.text (r-xp) ✓ 是 ✓ 是 包含所有算法逻辑
.rodata (r--p) 一般 ✗ 否 只读数据,不含逻辑
.data (rw-p) ✗ 否 运行时数据,不重要
.bss (rw-p) ✗ 否 未初始化数据

代码段才是最有价值的:

.text 段包含:
- 加密算法实现
- 完整性校验逻辑
- License 验证代码
- 关键业务逻辑

11.3 完整实现代码

function hiddenSoExecSegmentInMaps(so_path) {
    console.log("=== 开始隐藏可执行段:", so_path, "===");
    
    // 1. 获取系统调用
    let mmap_addr = Module.findExportByName("libc.so", "mmap");
    let mmapFunc = new NativeFunction(mmap_addr, 'pointer', 
        ['pointer', 'int', 'int', 'int', 'int', 'int']
    );
    
    let munmap_addr = Module.findExportByName("libc.so", "munmap");
    let munmapFunc = new NativeFunction(munmap_addr, 'int', ['pointer', 'int']);
    
    let mremap_addr = Module.findExportByName("libc.so", "mremap");
    let mremapFunc = new NativeFunction(mremap_addr, 'pointer', 
        ['pointer', 'int64', 'int64', 'int64', 'pointer']
    );
    
    let open_addr = Module.findExportByName("libc.so", "open");
    let openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);
    
    let memset_addr = Module.findExportByName("libc.so", "memset");
    let memsetFunc = new NativeFunction(memset_addr, 'pointer', ['pointer', 'int', 'int']);
    
    let close_addr = Module.findExportByName("libc.so", "close");
    let closeFunc = new NativeFunction(close_addr, 'int', ['int']);
    
    // 2. 从文件路径提取 SO 名称
    const parts = so_path.split('/');
    const so_name = parts.pop();
    
    // 3. 从 maps 中查找可执行段的地址范围
    let execRange = findSoExecSegmentRangeFromMaps(so_name);
    let exec_base = execRange.base;
    let exec_size = execRange.size;
    
    console.log("可执行段基址:", exec_base);
    console.log("可执行段大小:", ptr(exec_size));
    
    if (exec_base.toInt32() === 0 || exec_size === 0) {
        console.log("✗ 可执行段未找到");
        return;
    }
    
    // 4. 从文件中读取真实的可执行段数据
    let realExecData = findSoExecSegmentFromFile(so_path);
    console.log("文件中的可执行段偏移:", realExecData.p_offset);
    console.log("文件中的可执行段大小:", ptr(realExecData.size));
    
    // 5. 创建临时匿名内存
    let temp_mem = mmapFunc(ptr(0), exec_size, 7, 0x22, -1, 0);
    console.log("临时内存地址:", temp_mem);
    
    // 6. 复制当前可执行段内容到临时内存
    Memory.copy(temp_mem, exec_base, exec_size);
    console.log("✓ 可执行段已复制到临时内存");
    
    // 7. 使用 mremap 将可执行段变成匿名内存
    let ret = mremapFunc(temp_mem, exec_size, exec_size, 3, exec_base);
    if (!ret.equals(exec_base)) {
        console.log("✗ mremap 失败:", ret);
        munmapFunc(temp_mem, exec_size);
        return;
    }
    console.log("✓ 可执行段已变为匿名内存");
    
    // 8. 打开 SO 文件,准备创建假的映射
    let path_addr = Memory.allocUtf8String(so_path);
    let fd = openFunc(path_addr, 0);  // O_RDONLY
    if (fd === -1) {
        console.log("✗ 无法打开 SO 文件:", so_path);
        return;
    }
    console.log("✓ SO 文件已打开, fd:", fd);
    
    // 9. 创建假的可执行段映射(会在 maps 中显示文件路径)
    // 使用 MAP_PRIVATE 创建文件映射
    let fake_mem = mmapFunc(
        ptr(0),           // 让系统自动选择地址
        exec_size,        // 大小与真实段相同
        7,                // rwx(临时权限)
        0x02,             // MAP_PRIVATE(不是匿名!)
        fd,               // 文件描述符
        0                 // offset = 0(从文件开头映射)
    );
    
    console.log("✓ 假的可执行段地址:", fake_mem);
    
    // 10. 关闭文件
    closeFunc(fd);
    
    // 11. 将假的映射全部置0(清除内容)
    memsetFunc(fake_mem, 0, exec_size);
    console.log("✓ 假的映射已清零");
    
    // 12. 将文件中的真实代码复制到假的映射中(可选)
    // 这里复制一小部分保持文件头完整性
    Memory.copy(fake_mem, realExecData.start, Math.min(realExecData.size, 0x100));
    
    // 13. 设置假的映射为只读可执行
    Memory.protect(fake_mem, exec_size, "r-x");
    
    console.log("=== 完成! ===");
    console.log("真实代码在:", exec_base, "(匿名内存)");
    console.log("假代码在:", fake_mem, "(文件映射)");
}

// 辅助函数: 从 maps 中查找可执行段
function findSoExecSegmentRangeFromMaps(so_name) {
    let fopen_addr = Module.findExportByName("libc.so", "fopen");
    let fopenFunc = new NativeFunction(fopen_addr, "pointer", ["pointer", "pointer"]);
    
    let fgets_addr = Module.findExportByName("libc.so", "fgets");
    let fgetsFunc = new NativeFunction(fgets_addr, "pointer", ["pointer", "int", "pointer"]);
    
    let fclose_addr = Module.findExportByName("libc.so", "fclose");
    let fcloseFunc = new NativeFunction(fclose_addr, "int", ["pointer"]);
    
    let startAddress = ptr(0);
    let endAddress = ptr(0);
    
    let file = fopenFunc(
        Memory.allocUtf8String("/proc/self/maps"), 
        Memory.allocUtf8String("r")
    );
    
    let line = Memory.alloc(1024);
    
    while (ptr(fgetsFunc(line, 1024, file)).toInt32() !== 0) {
        let line_str = line.readCString();
        
        // 查找包含 so_name 且权限为 r-xp 的行
        if (line_str.indexOf(so_name) !== -1 && line_str.indexOf("r-xp") !== -1) {
            const match = line_str.match(/^([0-9a-f]+)-([0-9a-f]+)/);
            if (match) {
                let start = ptr(parseInt(match[1], 16));
                let end = ptr(parseInt(match[2], 16));
                
                if (startAddress.toInt32() === 0) {
                    startAddress = start;
                }
                endAddress = end;
            }
        }
    }
    
    fcloseFunc(file);
    
    let size = endAddress.sub(startAddress).toInt32();
    return {
        base: startAddress,
        size: size
    };
}

// 辅助函数: 从文件中读取可执行段
function findSoExecSegmentFromFile(so_path) {
    let open_addr = Module.findExportByName("libc.so", "open");
    let openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);
    
    let read_addr = Module.findExportByName("libc.so", "read");
    let readFunc = new NativeFunction(read_addr, 'ssize_t', ['int', 'pointer', 'size_t']);
    
    let lseek_addr = Module.findExportByName("libc.so", "lseek");
    let lseekFunc = new NativeFunction(lseek_addr, 'int64', ['int', 'int64', 'int']);
    
    let close_addr = Module.findExportByName("libc.so", "close");
    let closeFunc = new NativeFunction(close_addr, 'int', ['int']);
    
    // 打开文件
    let path_addr = Memory.allocUtf8String(so_path);
    let fd = openFunc(path_addr, 0);
    if (fd === -1) {
        console.log("✗ 无法打开文件:", so_path);
        return null;
    }
    
    // 读取 ELF Header (64字节)
    let ehdr = Memory.alloc(64);
    readFunc(fd, ehdr, 64);
    
    // 获取程序头表信息
    let e_phoff = ehdr.add(32).readU64();   // 程序头表偏移
    let e_phnum = ehdr.add(56).readU16();   // 程序头表项数
    
    // 定位到程序头表
    lseekFunc(fd, e_phoff, 0);  // SEEK_SET
    
    // 遍历程序头表,查找可执行段
    let phdr = Memory.alloc(0x38);  // 程序头大小: 56字节
    
    for (let i = 0; i < e_phnum; i++) {
        readFunc(fd, phdr, 0x38);
        
        let p_type = phdr.add(0).readU32();      // PT_LOAD = 1
        let p_flags = phdr.add(4).readU32();     // PF_X = 1
        let p_offset = phdr.add(8).readU64();    // 文件偏移
        let p_filesz = phdr.add(32).readU64();   // 文件大小
        
        // 查找 PT_LOAD 且可执行的段
        if (p_type === 1 && (p_flags & 1)) {
            console.log("✓ 找到可执行段: offset =", ptr(p_offset), "size =", ptr(p_filesz));
            
            // 定位到可执行段
            lseekFunc(fd, p_offset, 0);
            
            // 读取可执行段数据
            let data = Memory.alloc(p_filesz);
            readFunc(fd, data, p_filesz);
            
            closeFunc(fd);
            
            return {
                start: data,
                size: p_filesz,
                p_offset: ptr(p_offset)
            };
        }
    }
    
    closeFunc(fd);
    return null;
}

11.4 代码详解

核心步骤拆解:

步骤1: 识别可执行段

// maps 中查找 r-xp 权限的行
if (line_str.indexOf(so_name) !== -1 && line_str.indexOf("r-xp") !== -1) {
    // 这是可执行段
}

步骤2: 读取文件中的真实代码

为什么要从文件读取?

  • 内存中的代码可能已被修改(如之前的 hook)
  • 需要原始的干净代码

步骤3: mremap 变匿名

// 将临时内存移动到可执行段位置
mremapFunc(temp_mem, exec_size, exec_size, 3, exec_base);

// 结果: exec_base 现在是匿名内存,maps 中显示 [anon:xxx]

步骤4: 创建假的文件映射

// 关键: 使用文件描述符(不是 -1)
let fake_mem = mmapFunc(
    ptr(0),
    exec_size,
    7,
    0x02,      // MAP_PRIVATE (不是 MAP_ANONYMOUS!)
    fd,        // 文件描述符
    0
);

// 结果: maps 中会显示 SO 文件路径

步骤5: 清零假映射

memsetFunc(fake_mem, 0, exec_size);

// 攻击者 dump 到的是全0数据

11.5 效果对比

攻击者视角:

修改前:

# 1. 查看 maps
$ cat /proc/12345/maps | grep libtest.so
7b8c001000-7b8c020000 r-xp 00001000 fd:03 123 /data/app/libtest.so

# 2. dump 内存
$ dd if=/proc/12345/mem bs=1 skip=$((0x7b8c001000)) count=$((0x1F000)) of=code.bin

# 3. IDA 分析
# ✓ 可以看到完整的代码逻辑

修改后:

# 1. 查看 maps
$ cat /proc/12345/maps | grep libtest.so
7b8c001000-7b8c020000 r-xp 00000000 00:00 0 [anon:libc_malloc]  ← 真实代码
7f9d000000-7f9d01f000 r-xp 00001000 fd:03 123 /data/app/libtest.so  ← 假代码

# 2. dump 假的地址
$ dd if=/proc/12345/mem bs=1 skip=$((0x7f9d000000)) count=$((0x1F000)) of=fake.bin

# 3. IDA 分析
# ✗ 全是 0x00,无法分析

# 4. 即使找到真实地址
$ dd if=/proc/12345/mem bs=1 skip=$((0x7b8c001000)) count=$((0x1F000)) of=real.bin
# ✗ maps 中显示为匿名内存,难以定位

11.6 验证效果

function verifyHiddenExecSegment(so_name) {
    console.log("=== 验证可执行段隐藏效果 ===");
    
    let fopen_addr = Module.findExportByName("libc.so", "fopen");
    let fopenFunc = new NativeFunction(fopen_addr, "pointer", ["pointer", "pointer"]);
    
    let fgets_addr = Module.findExportByName("libc.so", "fgets");
    let fgetsFunc = new NativeFunction(fgets_addr, "pointer", ["pointer", "int", "pointer"]);
    
    let fclose_addr = Module.findExportByName("libc.so", "fclose");
    let fcloseFunc = new NativeFunction(fclose_addr, "int", ["pointer"]);
    
    let file = fopenFunc(
        Memory.allocUtf8String("/proc/self/maps"), 
        Memory.allocUtf8String("r")
    );
    
    let line = Memory.alloc(1024);
    let so_lines = [];
    let anon_exec_lines = [];
    
    while (ptr(fgetsFunc(line, 1024, file)).toInt32() !== 0) {
        let line_str = line.readCString();
        
        // 收集 SO 的可执行段
        if (line_str.indexOf(so_name) !== -1 && line_str.indexOf("r-xp") !== -1) {
            so_lines.push(line_str.trim());
        }
        
        // 收集匿名的可执行段
        if (line_str.indexOf("r-xp") !== -1 && line_str.indexOf("anon") !== -1) {
            anon_exec_lines.push(line_str.trim());
        }
    }
    
    fcloseFunc(file);
    
    console.log("SO 的可执行段 (应该在其他位置):");
    so_lines.forEach(line => console.log("  ", line));
    
    console.log("\n匿名可执行段 (真实代码在这里):");
    anon_exec_lines.forEach(line => console.log("  ", line));
    
    // 测试功能
    let module = Process.findModuleByName(so_name);
    if (module) {
        console.log("\n功能测试:");
        try {
            let exports = module.enumerateExports();
            console.log("✓ 仍能枚举", exports.length, "个导出函数");
            
            // 测试第一个导出函数
            if (exports.length > 0) {
                let test_func = exports[0];
                let data = test_func.address.readByteArray(16);
                console.log("✓ 函数", test_func.name, "仍可读取");
            }
        } catch (e) {
            console.log("✗ 功能异常:", e.message);
        }
    }
}

// 使用
hiddenSoExecSegmentInMaps("/data/app/com.example/lib/arm64/libtest.so");
verifyHiddenExecSegment("libtest.so");

11.7 与技术1的对比

特性 技术1: 隐藏 SO 名称 技术2: 隐藏可执行段
隐藏范围 整个 SO 仅可执行段
maps 变化 全部变匿名 仅 r-xp 段变匿名
假映射 有(全0数据)
实施难度 简单 中等
功能影响
检测难度

11.8 进阶技巧

技巧1: 假代码填充垃圾指令

// 不是全0,而是填充无意义的指令
// ARM64 NOP: 0xD503201F
let nop = 0xD503201F;
for (let i = 0; i < exec_size; i += 4) {
    fake_mem.add(i).writeU32(nop);
}

// 攻击者看到的是正常指令,但无实际逻辑

技巧2: 复制部分真实代码

// 复制函数开头,但修改关键跳转
Memory.copy(fake_mem, realExecData.start, 0x1000);

// 在关键位置插入死循环
fake_mem.add(0x500).writeByteArray([
    0x00, 0x00, 0x00, 0x14  // ARM64: B . (死循环)
]);

// 攻击者会卡在死循环,无法继续分析

技巧3: 动态切换

// 平时使用假代码(性能影响小)
// 关键时刻切换到真代码

let use_real_code = false;

function switchToRealCode() {
    Memory.protect(fake_mem, exec_size, "---");  // 禁用假代码
    use_real_code = true;
}

function switchToFakeCode() {
    Memory.protect(fake_mem, exec_size, "r-x");  // 启用假代码
    use_real_code = false;
}

11.9 注意事项

1. 文件映射的特性

// MAP_PRIVATE 的 COW 特性
// 修改假映射不会影响文件
fake_mem.writeByteArray([0x90, 0x90]);  // 只影响当前进程

// 但 maps 中仍显示文件路径

2. offset 参数

// mmap 的 offset 必须是页对齐的
let fake_mem = mmapFunc(
    ptr(0),
    exec_size,
    7,
    0x02,
    fd,
    0     // ✓ 正确: 0 是页对齐的
);

// ✗ 错误示例
let fake_mem = mmapFunc(
    ptr(0),
    exec_size,
    7,
    0x02,
    fd,
    0x100  // ✗ 错误: 0x100 不是页对齐的
);

3. 时序问题

// 必须在 SO 完全加载后执行
// 过早执行可能导致符号解析失败

setTimeout(function() {
    hiddenSoExecSegmentInMaps("/data/app/.../libtest.so");
}, 1000);  // 延迟1秒

12. 技术3: 修改 linker 中的 SO 基址

12.1 技术原理

目标: 修改 linker 内部 soinfo 结构体中的基址信息

为什么要修改 linker?

即使隐藏了 maps,攻击者仍可以通过 linker 找到 SO:

// 攻击者可以这样做:
let module = Process.findModuleByName("libtest.so");
console.log("找到 SO 基址:", module.base);

// 或者遍历 linker 的 soinfo 链表
// 从而绕过 maps 隐藏

解决方案: 修改 linker 中的 soinfo,让它指向假地址

12.2 soinfo 结构体回顾

struct soinfo {
    const char* realpath_;     // offset 0x00: SO 路径
    void* phdr;                // offset 0x08: 程序头表
    void* base;                // offset 0x10: 加载基址 ⭐⭐⭐
    size_t size;               // offset 0x18: 大小
    ElfW(Dyn)* dynamic;       // offset 0x20: .dynamic 段
    soinfo* next;              // offset 0x28: 下一个节点
    // ... 更多字段 ...
    link_map link_map_head;    // offset 0xD0: linker map ⭐⭐
    // ...
};

需要修改的字段:

  1. base (offset 0x10): SO 基址
  2. link_map_head.l_addr (offset 0xD0): 用于 gdb 调试

12.3 获取 soinfo 指针

function getSolist() {
    // 查找 linker64 或 linker
    let linker = Process.findModuleByName("linker64");
    if (!linker) {
        linker = Process.findModuleByName("linker");
    }
    
    if (!linker) {
        console.log("✗ 未找到 linker");
        return null;
    }
    
    // 枚举符号,查找 solist
    let symbols = linker.enumerateSymbols();
    
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];
        
        // solist 的 mangled name
        // Android 10+: __dl__ZL6solist
        // Android 7-9: __dl_g_solist
        if (symbol.name.indexOf("solist") !== -1) {
            console.log("✓ 找到 solist:", symbol.address);
            
            // solist 是指针,需要解引用
            let solist_ptr = symbol.address.readPointer();
            return solist_ptr;
        }
    }
    
    console.log("✗ 未找到 solist 符号");
    return null;
}

function getRealpath(soinfo_ptr) {
    // 查找 get_realpath 函数
    let linker = Process.findModuleByName("linker64") || Process.findModuleByName("linker");
    let symbols = linker.enumerateSymbols();
    
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];
        if (symbol.name.indexOf("get_realpath") !== -1) {
            // const char* get_realpath(soinfo* si)
            let get_realpath = new NativeFunction(
                symbol.address,
                'pointer',
                ['pointer']
            );
            
            let path_ptr = get_realpath(soinfo_ptr);
            return ptr(path_ptr).readCString();
        }
    }
    
    return null;
}

12.4 完整实现代码

function hiddenSobaseInMem(so_path) {
    console.log("=== 开始修改 linker 中的 SO 基址 ===");
    
    // 1. 获取系统调用
    let mmap_addr = Module.findExportByName("libc.so", "mmap");
    let mmapFunc = new NativeFunction(mmap_addr, 'pointer', 
        ['pointer', 'int', 'int', 'int', 'int', 'int']
    );
    
    // 2. 从路径提取 SO 名称
    const parts = so_path.split('/');
    const so_name = parts.pop();
    console.log("目标 SO:", so_name);
    
    // 3. 从文件中读取可执行段
    let soExecData = findSoExecSegmentFromFile(so_path);
    if (!soExecData) {
        console.log("✗ 无法读取 SO 文件");
        return;
    }
    
    // 4. 从 maps 获取 SO 的内存范围
    let soRange = findSoRangeFromMaps(so_name);
    let startAddress = soRange.base;
    let size = soRange.size;
    
    console.log("SO 基址:", startAddress);
    console.log("SO 大小:", ptr(size));
    
    // 5. 创建匿名内存(新的假基址)
    let new_addr = mmapFunc(ptr(0), size, 7, 0x22, -1, 0);
    console.log("新的假基址:", new_addr);
    
    // 6. 复制 SO 内容到新地址
    Memory.copy(new_addr, startAddress, size);
    console.log("✓ SO 内容已复制");
    
    // 7. 将文件中的真实可执行段复制到新地址
    Memory.copy(
        new_addr.add(soExecData.p_offset), 
        soExecData.start, 
        soExecData.size
    );
    console.log("✓ 真实代码已复制");
    
    // 8. 从 linker 获取 soinfo 链表
    let solist = getSolist();
    if (!solist) {
        console.log("✗ 无法获取 soinfo 链表");
        return;
    }
    
    // 9. 遍历 soinfo 链表,找到目标 SO
    let current = solist;
    let index = 0;
    
    console.log("开始遍历 soinfo 链表...");
    
    do {
        let path = getRealpath(current);
        console.log(`[${index}] ${path}`);
        
        // 找到目标 SO
        if (path && path.indexOf(so_name) !== -1) {
            console.log("✓ 找到目标 SO 的 soinfo:", current);
            
            // 修改 base (offset 0x10 = Process.pointerSize * 2)
            let base_offset = Process.pointerSize * 2;
            let base_ptr = current.add(base_offset);
            
            console.log("原 base:", base_ptr.readPointer());
            
            // 修改内存保护
            Memory.protect(pageAlignDown(base_ptr), 4096, "rw-");
            
            // 写入新的基址
            base_ptr.writePointer(new_addr);
            
            console.log("新 base:", base_ptr.readPointer());
            
            // 修改 link_map.l_addr (offset 0xD0 = Process.pointerSize * 26)
            let link_map_offset = Process.pointerSize * 26;
            let link_map_ptr = current.add(link_map_offset);
            
            console.log("原 link_map:", link_map_ptr.readPointer());
            
            Memory.protect(pageAlignDown(link_map_ptr), 4096, "rw-");
            link_map_ptr.writePointer(new_addr);
            
            console.log("新 link_map:", link_map_ptr.readPointer());
            
            console.log("✓ 修改完成!");
            break;
        }
        
        // 获取下一个节点 (offset 0x28 = Process.pointerSize * 5)
        let next_offset = Process.pointerSize * 5;
        current = current.add(next_offset).readPointer();
        index++;
        
    } while (current.toInt32() !== 0);
    
    console.log("遍历完成,共", index, "个 SO");
}

// 页对齐函数
function pageAlignDown(addr) {
    const PAGE_SIZE = 4096;
    return ptr(addr).and(~(PAGE_SIZE - 1));
}

12.5 代码详解

关键点1: soinfo 字段偏移计算

// 64位系统 (Process.pointerSize = 8)
let base_offset = Process.pointerSize * 2;        // 0x10
let next_offset = Process.pointerSize * 5;        // 0x28
let link_map_offset = Process.pointerSize * 26;   // 0xD0

// 32位系统 (Process.pointerSize = 4)
// 偏移会不同,需要调整

关键点2: 内存保护

// soinfo 结构体所在的内存可能是只读的
Memory.protect(pageAlignDown(base_ptr), 4096, "rw-");

// 修改后可以恢复为只读(可选)
// Memory.protect(pageAlignDown(base_ptr), 4096, "r--");

关键点3: link_map 的作用

struct link_map {
    ElfW(Addr) l_addr;     // SO 基址
    char* l_name;          // SO 名称
    ElfW(Dyn)* l_ld;      // .dynamic 段
    link_map* l_next;      // 下一个
    link_map* l_prev;      // 上一个
};

gdb 使用 link_map 来列出加载的 SO:

(gdb) info sharedlibrary
# 如果不修改 link_map,gdb 会显示真实地址

12.6 效果验证

function verifyLinkerModification(so_name) {
    console.log("=== 验证 linker 修改效果 ===");
    
    // 1. 通过 Frida API 查找
    let module = Process.findModuleByName(so_name);
    if (module) {
        console.log("Frida 找到的基址:", module.base);
    } else {
        console.log("✓ Frida 无法通过名称找到 SO");
    }
    
    // 2. 遍历 soinfo 链表
    let solist = getSolist();
    let current = solist;
    
    console.log("\nsoinfo 链表中的基址:");
    do {
        let path = getRealpath(current);
        if (path && path.indexOf(so_name) !== -1) {
            let base = current.add(Process.pointerSize * 2).readPointer();
            console.log("  soinfo.base:", base);
            
            let link_map = current.add(Process.pointerSize * 26).readPointer();
            console.log("  link_map.l_addr:", link_map);
        }
        
        current = current.add(Process.pointerSize * 5).readPointer();
    } while (current.toInt32() !== 0);
    
    // 3. 功能测试
    console.log("\n功能测试:");
    try {
        // 尝试调用 SO 中的函数(通过原地址)
        let real_base = findSoRangeFromMaps(so_name).base;
        console.log("真实基址:", real_base);
        
        // 读取 ELF 头
        let magic = real_base.readByteArray(4);
        console.log("ELF 魔数:", hexdump(magic, { length: 4 }));
        
        console.log("✓ SO 功能正常");
    } catch (e) {
        console.log("✗ SO 功能异常:", e.message);
    }
}

// 使用
hiddenSobaseInMem("/data/app/com.example/lib/arm64/libtest.so");
verifyLinkerModification("libtest.so");

12.7 与技术1、技术2的组合

最佳实践: 三管齐下

function hideSOCompletely(so_path) {
    const parts = so_path.split('/');
    const so_name = parts.pop();
    
    console.log("=== 完全隐藏 SO:", so_name, "===");
    
    // 技术1: 隐藏 maps 中的名称
    console.log("\n[1/3] 隐藏 maps 名称...");
    hiddenSoInMaps(so_name);
    
    // 技术2: 隐藏可执行段
    console.log("\n[2/3] 隐藏可执行段...");
    hiddenSoExecSegmentInMaps(so_path);
    
    // 技术3: 修改 linker 基址
    console.log("\n[3/3] 修改 linker 基址...");
    hiddenSobaseInMem(so_path);
    
    console.log("\n=== 完成! ===");
    console.log("现在 SO 已经:");
    console.log("✓ maps 中不显示路径");
    console.log("✓ 可执行段是匿名内存");
    console.log("✓ linker 中的基址被修改");
}

// 使用
hideSOCompletely("/data/app/com.example/lib/arm64/libsecret.so");

效果叠加:

攻击手段 技术1 技术2 技术3 全部
查看 maps
dump 代码段
Frida 枚举模块
遍历 soinfo
gdb 调试

12.8 注意事项

1. load_bias 问题

struct soinfo {
    // ...
    ElfW(Addr) load_bias;  // 加载偏移
    // ...
};

load_bias 用于符号解析,如果修改会导致 dlsym 失败:

// ✗ 不要修改 load_bias
// 否则会崩溃: Symbol not found

解决方案: 只修改 baselink_map,不修改 load_bias

2. 指针有效性

// soinfo 结构体可能被重新分配
// 不要长期持有 soinfo 指针

// ✓ 正确: 每次都重新获取
function modifyBase() {
    let solist = getSolist();
    // ... 修改 ...
}

// ✗ 错误: 缓存 soinfo 指针
let cached_soinfo = getSolist();
setTimeout(function() {
    // 1秒后这个指针可能无效
    cached_soinfo.add(0x10).writePointer(new_addr);
}, 1000);

3. 多线程安全

// linker 在加载 SO 时会修改 soinfo 链表
// 可能导致遍历过程中链表结构变化

// 建议在合适的时机执行:
// - 应用初始化完成后
// - 没有其他 SO 正在加载时

12.9 调试技巧

打印 soinfo 结构体:

function dumpSoinfo(soinfo_ptr) {
    console.log("=== soinfo 结构体 ===");
    console.log("地址:", soinfo_ptr);
    
    let realpath = getRealpath(soinfo_ptr);
    console.log("realpath:", realpath);
    
    let phdr = soinfo_ptr.add(Process.pointerSize * 1).readPointer();
    console.log("phdr:", phdr);
    
    let base = soinfo_ptr.add(Process.pointerSize * 2).readPointer();
    console.log("base:", base);
    
    let size = soinfo_ptr.add(Process.pointerSize * 3).readU64();
    console.log("size:", ptr(size));
    
    let dynamic = soinfo_ptr.add(Process.pointerSize * 4).readPointer();
    console.log("dynamic:", dynamic);
    
    let next = soinfo_ptr.add(Process.pointerSize * 5).readPointer();
    console.log("next:", next);
    
    let link_map = soinfo_ptr.add(Process.pointerSize * 26).readPointer();
    console.log("link_map:", link_map);
}

// 使用
let solist = getSolist();
dumpSoinfo(solist);

对比修改前后:

function compareBeforeAfter(so_path) {
    const so_name = so_path.split('/').pop();
    
    // 修改前
    console.log("=== 修改前 ===");
    let solist = getSolist();
    let current = solist;
    
    while (current.toInt32() !== 0) {
        let path = getRealpath(current);
        if (path && path.indexOf(so_name) !== -1) {
            let base_before = current.add(Process.pointerSize * 2).readPointer();
            console.log("base:", base_before);
            
            // 修改
            hiddenSobaseInMem(so_path);
            
            // 修改后
            console.log("\n=== 修改后 ===");
            let base_after = current.add(Process.pointerSize * 2).readPointer();
            console.log("base:", base_after);
            
            console.log("\n变化:", base_before, "→", base_after);
            break;
        }
        current = current.add(Process.pointerSize * 5).readPointer();
    }
}

12.10 绕过该技术(攻击者视角)

方法1: Hook linker 函数

// 在 SO 加载时记录真实地址
Interceptor.attach(Module.findExportByName("linker64", "dlopen"), {
    onLeave: function(retval) {
        // retval 是 soinfo 指针
        let base = retval.add(Process.pointerSize * 2).readPointer();
        console.log("真实 base:", base);
        // 保存到全局变量
    }
});

方法2: 分析内存布局

// 通过 ELF 头特征扫描内存
Memory.scan(ptr(0x7000000000), 0x1000000000, "7f 45 4c 46", {
    onMatch: function(address, size) {
        // 验证是否是目标 SO
        let e_ident = address.readByteArray(16);
        // ...
    }
});

方法3: 监控内存分配

// 监控 mmap,记录所有大块内存分配
Interceptor.attach(Module.findExportByName("libc.so", "mmap"), {
    onLeave: function(retval) {
        if (retval.toInt32() !== -1) {
            // 检查是否是 SO 内存
        }
    }
});

13. 技术4: 修改 linker 中的节表指针

13.1 技术原理

目标: 修改 soinfo 中的符号表和字符串表指针

为什么要修改节表指针?

即使隐藏了基址,攻击者仍可以通过符号表定位关键函数:

// 攻击者可以这样做:
let dlsym_addr = Module.findExportByName("libc.so", "dlsym");
let dlsymFunc = new NativeFunction(dlsym_addr, 'pointer', ['pointer', 'pointer']);

// 通过 dlsym 查找函数
let handle = Module.findBaseAddress("libtest.so");
let func = dlsymFunc(handle, Memory.allocUtf8String("secret_function"));

console.log("找到函数:", func);  // 仍然能找到!

解决方案: 将 soinfo 中的符号表指针指向假的符号表

13.2 soinfo 中的关键字段

struct soinfo {
    // ... 省略前面的字段 ...
    
    // 动态链接信息
    ElfW(Dyn)* dynamic;        // offset 0x20 ⭐
    
    // 符号表
    const char* strtab_;       // offset 0x38 ⭐ 字符串表
    ElfW(Sym)* symtab_;       // offset 0x40 ⭐ 符号表
    
    size_t nbucket_;           // offset 0x48: hash bucket 数量
    size_t nchain_;            // offset 0x50: hash chain 数量
    uint32_t* bucket_;         // offset 0x58: hash bucket
    uint32_t* chain_;          // offset 0x60: hash chain
    
    // ...
};

需要修改的字段:

  1. dynamic (offset 0x20): .dynamic 段指针
  2. strtab_ (offset 0x38): .dynstr 段指针
  3. symtab_ (offset 0x40): .dynsym 段指针

13.3 从文件中读取节表

function findSoSectionFromFile(so_path) {
    console.log("=== 从文件读取节表信息 ===");
    
    // 获取系统调用
    let open_addr = Module.findExportByName("libc.so", "open");
    let openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);
    
    let read_addr = Module.findExportByName("libc.so", "read");
    let readFunc = new NativeFunction(read_addr, 'ssize_t', ['int', 'pointer', 'size_t']);
    
    let lseek_addr = Module.findExportByName("libc.so", "lseek");
    let lseekFunc = new NativeFunction(lseek_addr, 'int64', ['int', 'int64', 'int']);
    
    let pread_addr = Module.findExportByName("libc.so", "pread");
    let preadFunc = new NativeFunction(pread_addr, 'int', ['int', 'pointer', 'int', 'int64']);
    
    let close_addr = Module.findExportByName("libc.so", "close");
    let closeFunc = new NativeFunction(close_addr, 'int', ['int']);
    
    // 打开文件
    let path_addr = Memory.allocUtf8String(so_path);
    let fd = openFunc(path_addr, 0);
    if (fd === -1) {
        console.log("✗ 无法打开文件");
        return null;
    }
    
    // 读取 ELF Header (64字节)
    let ehdr = Memory.alloc(64);
    readFunc(fd, ehdr, 64);
    
    // 获取节头表信息
    let e_shoff = ehdr.add(40).readU64();       // 节头表偏移
    let e_shnum = ehdr.add(60).readU16();       // 节头表项数
    let e_shstrndx = ehdr.add(62).readU16();    // 节名字符串表索引
    
    console.log("节头表偏移:", ptr(e_shoff));
    console.log("节头数量:", e_shnum);
    console.log("字符串表索引:", e_shstrndx);
    
    // 读取节名字符串表的节头
    let shdr = Memory.alloc(0x40);  // 节头大小: 64字节
    preadFunc(fd, shdr, 0x40, e_shoff + e_shstrndx * 0x40);
    
    let shstrtab_offset = shdr.add(24).readU64();   // sh_offset
    let shstrtab_size = shdr.add(32).readU64();     // sh_size
    
    console.log("字符串表偏移:", ptr(shstrtab_offset));
    console.log("字符串表大小:", ptr(shstrtab_size));
    
    // 读取节名字符串表
    let shstrtab = Memory.alloc(shstrtab_size);
    preadFunc(fd, shstrtab, shstrtab_size, shstrtab_offset);
    
    // 定位到节头表
    lseekFunc(fd, e_shoff, 0);
    
    // 遍历所有节,查找关键节
    let sections = {};
    
    for (let i = 0; i < e_shnum; i++) {
        readFunc(fd, shdr, 0x40);
        
        let sh_name_off = shdr.add(0).readU32();    // 节名偏移
        let sh_offset = shdr.add(24).readU64();     // 文件偏移
        let sh_size = shdr.add(32).readU64();       // 大小
        
        // 获取节名
        let sh_name = shstrtab.add(sh_name_off).readCString();
        
        console.log(`[${sh_name}] offset=${ptr(sh_offset)} size=${ptr(sh_size)}`);
        
        // 保存关键节
        if (sh_name === ".dynstr") {
            sections.dynstr = {
                offset: ptr(sh_offset),
                size: sh_size
            };
        } else if (sh_name === ".dynsym") {
            sections.dynsym = {
                offset: ptr(sh_offset),
                size: sh_size
            };
        } else if (sh_name === ".dynamic") {
            sections.dynamic = {
                offset: ptr(sh_offset),
                size: sh_size
            };
        }
    }
    
    closeFunc(fd);
    
    console.log("✓ 找到节表:", JSON.stringify(sections));
    return sections;
}

13.4 完整实现代码

function hiddenSectionInMem(so_path) {
    console.log("=== 开始修改 linker 中的节表指针 ===");
    
    // 1. 获取系统调用
    let mmap_addr = Module.findExportByName("libc.so", "mmap");
    let mmapFunc = new NativeFunction(mmap_addr, 'pointer', 
        ['pointer', 'int', 'int', 'int', 'int', 'int']
    );
    
    // 2. 提取 SO 名称
    const parts = so_path.split('/');
    const so_name = parts.pop();
    console.log("目标 SO:", so_name);
    
    // 3. 从文件读取节表信息
    let sections = findSoSectionFromFile(so_path);
    if (!sections || !sections.dynstr || !sections.dynsym || !sections.dynamic) {
        console.log("✗ 无法读取节表信息");
        return;
    }
    
    // 4. 从文件读取可执行段
    let execData = findSoExecSegmentFromFile(so_path);
    if (!execData) {
        console.log("✗ 无法读取可执行段");
        return;
    }
    
    // 5. 从 maps 获取 SO 的内存范围
    let soRange = findSoRangeFromMaps(so_name);
    let startAddress = soRange.base;
    let size = soRange.size;
    
    console.log("SO 基址:", startAddress);
    console.log("SO 大小:", ptr(size));
    
    // 6. 创建新的匿名内存
    let new_addr = mmapFunc(ptr(0), size, 7, 0x22, -1, 0);
    console.log("新内存地址:", new_addr);
    
    // 7. 复制 SO 内容到新内存
    Memory.copy(new_addr, startAddress, size);
    console.log("✓ SO 内容已复制");
    
    // 8. 将文件中的真实可执行段复制到新内存
    Memory.copy(
        new_addr.add(execData.p_offset),
        execData.start,
        execData.size
    );
    console.log("✓ 真实代码已复制");
    
    // 9. 从 linker 获取 soinfo 链表
    let solist = getSolist();
    if (!solist) {
        console.log("✗ 无法获取 soinfo 链表");
        return;
    }
    
    // 10. 遍历 soinfo 链表
    let current = solist;
    let index = 0;
    
    console.log("开始遍历 soinfo 链表...");
    
    do {
        let path = getRealpath(current);
        console.log(`[${index}] ${path}`);
        
        // 找到目标 SO
        if (path && path.indexOf(so_name) !== -1) {
            console.log("✓ 找到目标 SO 的 soinfo:", current);
            
            // 记录原始指针
            let dynamic_offset = Process.pointerSize * 4;
            let strtab_offset = Process.pointerSize * 7;
            let symtab_offset = Process.pointerSize * 8;
            
            let old_dynamic = current.add(dynamic_offset).readPointer();
            let old_strtab = current.add(strtab_offset).readPointer();
            let old_symtab = current.add(symtab_offset).readPointer();
            
            console.log("原 dynamic:", old_dynamic);
            console.log("原 strtab:", old_strtab);
            console.log("原 symtab:", old_symtab);
            
            // 计算新的指针(基于新内存地址)
            let new_dynamic = new_addr.add(sections.dynamic.offset);
            let new_strtab = new_addr.add(sections.dynstr.offset);
            let new_symtab = new_addr.add(sections.dynsym.offset);
            
            console.log("新 dynamic:", new_dynamic);
            console.log("新 strtab:", new_strtab);
            console.log("新 symtab:", new_symtab);
            
            // 修改内存保护并写入新指针
            
            // 修改 dynamic
            Memory.protect(pageAlignDown(current.add(dynamic_offset)), 4096, "rw-");
            current.add(dynamic_offset).writePointer(new_dynamic);
            console.log("✓ dynamic 已修改");
            
            // 修改 strtab
            Memory.protect(pageAlignDown(current.add(strtab_offset)), 4096, "rw-");
            current.add(strtab_offset).writePointer(new_strtab);
            console.log("✓ strtab 已修改");
            
            // 修改 symtab
            Memory.protect(pageAlignDown(current.add(symtab_offset)), 4096, "rw-");
            current.add(symtab_offset).writePointer(new_symtab);
            console.log("✓ symtab 已修改");
            
            console.log("=== 修改完成! ===");
            break;
        }
        
        // 获取下一个节点
        current = current.add(Process.pointerSize * 5).readPointer();
        index++;
        
    } while (current.toInt32() !== 0);
}

function pageAlignDown(addr) {
    const PAGE_SIZE = 4096;
    return ptr(addr).and(~(PAGE_SIZE - 1));
}

13.5 代码详解

关键概念1: 节表在内存中的位置

文件布局:
0x0000: ELF Header
0x0040: Program Headers
0x1000: .text (可执行段)
0x2000: .rodata
0x3000: .data
0x4000: .dynamic      ← offset 0x4000
0x4100: .dynstr       ← offset 0x4100
0x4500: .dynsym       ← offset 0x4500
...

内存布局(加载后):
base + 0x0000: ELF Header
base + 0x1000: .text
base + 0x2000: .rodata
base + 0x3000: .data
base + 0x4000: .dynamic      ← 指向这里
base + 0x4100: .dynstr       ← 指向这里
base + 0x4500: .dynsym       ← 指向这里

关键概念2: 为什么要修改这些指针?

// dlsym 查找符号的过程:
// 1. 从 soinfo.symtab_ 读取符号表
// 2. 遍历符号,比较名称(从 soinfo.strtab_ 读取)
// 3. 返回符号地址

// 修改后:
// soinfo.symtab_ 指向假的符号表(空的或错误的)
// 攻击者无法通过 dlsym 找到函数

关键概念3: dynamic 段的作用

// .dynamic 段包含动态链接信息
typedef struct {
    int64_t d_tag;      // 类型
    uint64_t d_val;     // 值
} Elf64_Dyn;

// 常见类型:
// DT_SYMTAB (6): 符号表地址
// DT_STRTAB (5): 字符串表地址
// DT_HASH (4): hash 表地址

修改 dynamic 指针会影响:

  • 符号解析
  • 重定位
  • 依赖库加载

13.6 效果验证

function verifySymbolTableModification(so_name) {
    console.log("=== 验证符号表修改效果 ===");
    
    // 1. 尝试通过 Module.findExportByName 查找
    console.log("\n测试1: Module.findExportByName");
    try {
        let func = Module.findExportByName(so_name, "test_function");
        if (func) {
            console.log("✗ 仍能找到函数:", func);
        } else {
            console.log("✓ 无法找到函数");
        }
    } catch (e) {
        console.log("✓ 查找失败:", e.message);
    }
    
    // 2. 尝试通过 dlsym 查找
    console.log("\n测试2: dlsym");
    try {
        let dlsym_addr = Module.findExportByName("libc.so", "dlsym");
        let dlsymFunc = new NativeFunction(dlsym_addr, 'pointer', ['pointer', 'pointer']);
        
        let handle = Module.findBaseAddress(so_name);
        if (handle) {
            let func_name = Memory.allocUtf8String("test_function");
            let func = dlsymFunc(handle, func_name);
            
            if (func.toInt32() === 0) {
                console.log("✓ dlsym 无法找到函数");
            } else {
                console.log("✗ dlsym 仍能找到:", func);
            }
        }
    } catch (e) {
        console.log("✓ dlsym 失败:", e.message);
    }
    
    // 3. 枚举符号
    console.log("\n测试3: 枚举符号");
    try {
        let module = Process.findModuleByName(so_name);
        if (module) {
            let exports = module.enumerateExports();
            console.log("导出函数数量:", exports.length);
            
            if (exports.length === 0) {
                console.log("✓ 无法枚举导出函数");
            } else {
                console.log("✗ 仍能枚举函数:");
                exports.slice(0, 5).forEach(exp => {
                    console.log("  -", exp.name);
                });
            }
        }
    } catch (e) {
        console.log("✓ 枚举失败:", e.message);
    }
    
    // 4. 检查 soinfo 中的指针
    console.log("\n测试4: 检查 soinfo");
    let solist = getSolist();
    let current = solist;
    
    while (current.toInt32() !== 0) {
        let path = getRealpath(current);
        if (path && path.indexOf(so_name) !== -1) {
            let dynamic = current.add(Process.pointerSize * 4).readPointer();
            let strtab = current.add(Process.pointerSize * 7).readPointer();
            let symtab = current.add(Process.pointerSize * 8).readPointer();
            
            console.log("soinfo.dynamic:", dynamic);
            console.log("soinfo.strtab:", strtab);
            console.log("soinfo.symtab:", symtab);
            
            // 验证是否指向新地址
            let module_base = Module.findBaseAddress(so_name);
            if (symtab.compare(module_base) > 0 && 
                symtab.compare(module_base.add(0x100000)) < 0) {
                console.log("✗ 指针仍在原 SO 范围内");
            } else {
                console.log("✓ 指针已指向新位置");
            }
            break;
        }
        current = current.add(Process.pointerSize * 5).readPointer();
    }
}

// 使用
hiddenSectionInMem("/data/app/com.example/lib/arm64/libtest.so");
verifySymbolTableModification("libtest.so");

13.7 创建假的符号表

更高级的技巧: 不仅修改指针,还创建假的符号表来欺骗攻击者

function createFakeSymbolTable() {
    console.log("=== 创建假的符号表 ===");
    
    // 1. 创建假的字符串表
    let fake_strtab_size = 0x100;
    let fake_strtab = Memory.alloc(fake_strtab_size);
    
    // 填充假的符号名
    let offset = 1;  // 字符串表第一个字节通常是 \0
    fake_strtab.writeU8(0);
    
    let fake_names = [
        "fake_function_1",
        "fake_function_2",
        "fake_data",
        "decoy_entry"
    ];
    
    let name_offsets = [];
    fake_names.forEach(name => {
        name_offsets.push(offset);
        fake_strtab.add(offset).writeUtf8String(name);
        offset += name.length + 1;
    });
    
    console.log("✓ 假字符串表已创建");
    
    // 2. 创建假的符号表
    // 每个符号: 24 字节 (Elf64_Sym)
    let fake_symtab_count = fake_names.length + 1;  // +1 for NULL entry
    let fake_symtab_size = fake_symtab_count * 24;
    let fake_symtab = Memory.alloc(fake_symtab_size);
    
    // NULL 符号(第一个符号总是 NULL)
    fake_symtab.writeByteArray(new Array(24).fill(0));
    
    // 填充假符号
    for (let i = 0; i < fake_names.length; i++) {
        let sym_ptr = fake_symtab.add((i + 1) * 24);
        
        // st_name (offset 0): 符号名偏移
        sym_ptr.add(0).writeU32(name_offsets[i]);
        
        // st_info (offset 4): 类型和绑定
        // STB_GLOBAL (1) << 4 | STT_FUNC (2) = 0x12
        sym_ptr.add(4).writeU8(0x12);
        
        // st_other (offset 5): 保留
        sym_ptr.add(5).writeU8(0);
        
        // st_shndx (offset 6): 节索引
        sym_ptr.add(6).writeU16(1);  // .text section
        
        // st_value (offset 8): 符号地址(假地址)
        sym_ptr.add(8).writeU64(0xdeadbeef);
        
        // st_size (offset 16): 符号大小
        sym_ptr.add(16).writeU64(0x100);
    }
    
    console.log("✓ 假符号表已创建");
    
    return {
        strtab: fake_strtab,
        strtab_size: fake_strtab_size,
        symtab: fake_symtab,
        symtab_size: fake_symtab_size
    };
}

// 使用假符号表
function hiddenSectionWithFakeTable(so_path) {
    const so_name = so_path.split('/').pop();
    
    // 创建假表
    let fakes = createFakeSymbolTable();
    
    // 修改 soinfo 指针
    let solist = getSolist();
    let current = solist;
    
    while (current.toInt32() !== 0) {
        let path = getRealpath(current);
        if (path && path.indexOf(so_name) !== -1) {
            // 指向假表
            Memory.protect(pageAlignDown(current.add(Process.pointerSize * 7)), 4096, "rw-");
            current.add(Process.pointerSize * 7).writePointer(fakes.strtab);
            
            Memory.protect(pageAlignDown(current.add(Process.pointerSize * 8)), 4096, "rw-");
            current.add(Process.pointerSize * 8).writePointer(fakes.symtab);
            
            console.log("✓ 已指向假符号表");
            break;
        }
        current = current.add(Process.pointerSize * 5).readPointer();
    }
}

13.8 注意事项

1. 符号解析的影响

修改符号表后会影响:

  • dlsym 无法工作
  • dlopen 加载依赖库可能失败
  • ✗ 运行时符号解析失败

解决方案: 在所有符号解析完成后再修改

// 在 JNI_OnLoad 返回后执行
Java.perform(function() {
    // 等待初始化完成
    setTimeout(function() {
        hiddenSectionInMem("/data/app/.../libtest.so");
    }, 2000);
});

2. 已获取的函数指针不受影响

// 在修改前获取函数指针
let func_addr = Module.findExportByName("libtest.so", "encrypt");

// 修改符号表
hiddenSectionInMem("/data/app/.../libtest.so");

// 函数指针仍然有效!
let encryptFunc = new NativeFunction(func_addr, 'int', ['pointer', 'int']);
encryptFunc(data, len);  // ✓ 正常工作

3. 内存泄漏

// 创建的新内存不会被自动释放
// 应用退出时才会释放

// 如果需要手动清理:
let munmap_addr = Module.findExportByName("libc.so", "munmap");
let munmapFunc = new NativeFunction(munmap_addr, 'int', ['pointer', 'int']);
munmapFunc(new_addr, size);

13.9 组合所有技术

function ultimateHideSO(so_path) {
    const so_name = so_path.split('/').pop();
    
    console.log("╔════════════════════════════════════╗");
    console.log("║  终极 SO 隐藏 - 组合所有技术  ║");
    console.log("╚════════════════════════════════════╝");
    
    // 技术1: 隐藏 maps 名称
    console.log("\n[1/4] 隐藏 maps 中的 SO 名称...");
    hiddenSoInMaps(so_name);
    console.log("✓ 完成");
    
    // 技术2: 隐藏可执行段
    console.log("\n[2/4] 隐藏可执行段...");
    hiddenSoExecSegmentInMaps(so_path);
    console.log("✓ 完成");
    
    // 技术3: 修改 linker 基址
    console.log("\n[3/4] 修改 linker 中的 SO 基址...");
    hiddenSobaseInMem(so_path);
    console.log("✓ 完成");
    
    // 技术4: 修改节表指针
    console.log("\n[4/4] 修改 linker 中的节表指针...");
    hiddenSectionInMem(so_path);
    console.log("✓ 完成");
    
    console.log("\n╔════════════════════════════════════╗");
    console.log("║       所有隐藏技术已应用!        ║");
    console.log("╚════════════════════════════════════╝");
    
    // 验证
    console.log("\n验证结果:");
    console.log("- maps 中无 SO 路径: ✓");
    console.log("- 可执行段已匿名: ✓");
    console.log("- linker 基址已修改: ✓");
    console.log("- 符号表已隐藏: ✓");
}

// 使用
ultimateHideSO("/data/app/com.example/lib/arm64/libsecret.so");

14. 技术5: Hook pthread_create

14.1 技术原理

目标: 监控和控制 SO 创建的所有线程

为什么需要这个技术?

很多 SO 会在子线程中执行关键操作:

  • 完整性校验
  • 反调试检测
  • 网络通信
  • 加密解密

通过 hook pthread_create,可以:

  1. 监控所有线程的创建
  2. 识别哪些线程来自目标 SO
  3. 阻止某些线程运行
  4. 替换线程函数

14.2 pthread_create 函数

函数原型:

int pthread_create(pthread_t* thread,
                   const pthread_attr_t* attr,
                   void* (*start_routine)(void*),
                   void* arg);

参数说明:

  • thread: 线程 ID 输出参数
  • attr: 线程属性(NULL 表示默认)
  • start_routine: 线程函数入口 ⭐⭐⭐
  • arg: 传递给线程函数的参数

返回值: 0 表示成功,非 0 表示失败

14.3 基础 Hook 实现

function hook_pthread_create() {
    console.log("=== Hook pthread_create ===");
    
    // 获取 pthread_create 地址
    let pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    
    if (!pthread_create_addr) {
        console.log("✗ 未找到 pthread_create");
        return;
    }
    
    console.log("✓ pthread_create 地址:", pthread_create_addr);
    
    // Hook pthread_create
    Interceptor.attach(pthread_create_addr, {
        onEnter: function(args) {
            // args[0]: pthread_t* thread
            // args[1]: const pthread_attr_t* attr
            // args[2]: void* (*start_routine)(void*)  ← 线程函数
            // args[3]: void* arg
            
            let start_routine = args[2];
            
            // 查找线程函数属于哪个模块
            let module = Process.findModuleByAddress(start_routine);
            
            if (module) {
                let offset = start_routine.sub(module.base);
                console.log(`[pthread_create] ${module.name}!${offset}`);
                console.log("  线程函数:", start_routine);
                console.log("  参数:", args[3]);
            } else {
                console.log("[pthread_create] 未知模块:", start_routine);
            }
        },
        onLeave: function(retval) {
            // retval: 返回值 (0 = 成功)
            if (retval.toInt32() !== 0) {
                console.log("  ✗ 线程创建失败:", retval);
            }
        }
    });
    
    console.log("✓ pthread_create 已被 hook");
}

// 使用
hook_pthread_create();

14.4 过滤特定 SO 的线程

function hook_pthread_create_filtered(target_so_name) {
    console.log("=== Hook pthread_create (过滤模式) ===");
    console.log("目标 SO:", target_so_name);
    
    let pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    
    Interceptor.attach(pthread_create_addr, {
        onEnter: function(args) {
            let start_routine = args[2];
            let module = Process.findModuleByAddress(start_routine);
            
            // 只处理目标 SO 的线程
            if (module && module.name.indexOf(target_so_name) !== -1) {
                let offset = start_routine.sub(module.base);
                
                console.log("╔════════════════════════════════════╗");
                console.log("║  检测到目标 SO 创建线程!        ║");
                console.log("╚════════════════════════════════════╝");
                console.log("SO:", module.name);
                console.log("基址:", module.base);
                console.log("线程函数偏移:", offset);
                console.log("线程函数地址:", start_routine);
                console.log("参数:", args[3]);
                
                // 打印调用栈
                console.log("\n调用栈:");
                console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
                    .map(DebugSymbol.fromAddress).join('\n'));
                
                // 保存上下文,用于 onLeave
                this.is_target = true;
                this.start_routine = start_routine;
            }
        },
        onLeave: function(retval) {
            if (this.is_target) {
                if (retval.toInt32() === 0) {
                    console.log("✓ 目标线程创建成功");
                } else {
                    console.log("✗ 目标线程创建失败:", retval);
                }
            }
        }
    });
    
    console.log("✓ Hook 完成");
}

// 使用
hook_pthread_create_filtered("libtest.so");

14.5 阻止线程创建

function hook_pthread_create_block(target_so_name) {
    console.log("=== Hook pthread_create (阻止模式) ===");
    
    let pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    
    Interceptor.attach(pthread_create_addr, {
        onEnter: function(args) {
            let start_routine = args[2];
            let module = Process.findModuleByAddress(start_routine);
            
            if (module && module.name.indexOf(target_so_name) !== -1) {
                let offset = start_routine.sub(module.base);
                
                console.log("✗ 阻止线程创建:", module.name, "偏移:", offset);
                
                // 方法1: 直接返回错误
                // 修改返回值为 EAGAIN (11)
                this.block = true;
            }
        },
        onLeave: function(retval) {
            if (this.block) {
                // 返回错误码
                retval.replace(11);  // EAGAIN
                console.log("✓ 已阻止线程创建");
            }
        }
    });
    
    console.log("✓ Hook 完成 (阻止模式)");
}

// 使用
hook_pthread_create_block("libtest.so");

14.6 替换线程函数

function hook_pthread_create_replace(target_so_name) {
    console.log("=== Hook pthread_create (替换模式) ===");
    
    let pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    
    // 创建空的线程函数
    let dummy_thread_func = new NativeCallback(function(arg) {
        console.log("[替换线程] 执行中...");
        console.log("原始参数:", arg);
        
        // 不执行任何操作,直接返回
        console.log("[替换线程] 退出");
        return ptr(0);
    }, 'pointer', ['pointer']);
    
    console.log("替换函数地址:", dummy_thread_func);
    
    Interceptor.attach(pthread_create_addr, {
        onEnter: function(args) {
            let start_routine = args[2];
            let module = Process.findModuleByAddress(start_routine);
            
            if (module && module.name.indexOf(target_so_name) !== -1) {
                let offset = start_routine.sub(module.base);
                
                console.log("✓ 替换线程函数:");
                console.log("  原函数:", start_routine, `(${module.name}!${offset})`);
                console.log("  新函数:", dummy_thread_func);
                
                // 替换线程函数
                args[2] = dummy_thread_func;
                
                // 可选: 也替换参数
                // args[3] = ptr(0);
            }
        }
    });
    
    console.log("✓ Hook 完成 (替换模式)");
}

// 使用
hook_pthread_create_replace("libtest.so");

14.7 高级应用: 线程函数分析

自动分析线程函数代码:

function analyzeThreadFunction(start_routine, module) {
    console.log("\n=== 分析线程函数 ===");
    console.log("地址:", start_routine);
    console.log("模块:", module.name);
    
    try {
        // 读取函数开头的字节码
        let code = start_routine.readByteArray(64);
        console.log("函数开头代码:");
        console.log(hexdump(code, { length: 64, ansi: true }));
        
        // ARM64 指令分析
        if (Process.arch === 'arm64') {
            // 检查是否有可疑指令
            let instr1 = start_routine.readU32();
            let instr2 = start_routine.add(4).readU32();
            
            console.log("\n指令分析:");
            console.log("  [+0] 0x" + instr1.toString(16).padStart(8, '0'));
            console.log("  [+4] 0x" + instr2.toString(16).padStart(8, '0'));
            
            // 检查是否调用了反调试函数
            // 例如: ptrace, open("/proc/self/status"), etc.
        }
        
        // 使用 Frida 的 Instruction 类解析
        let offset = ptr(0);
        for (let i = 0; i < 10; i++) {
            try {
                let instr = Instruction.parse(start_routine.add(offset));
                console.log(`  ${start_routine.add(offset)}: ${instr}`);
                offset = offset.add(instr.size);
            } catch (e) {
                break;
            }
        }
        
    } catch (e) {
        console.log("✗ 分析失败:", e.message);
    }
}

function hook_pthread_create_analyze(target_so_name) {
    let pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    
    Interceptor.attach(pthread_create_addr, {
        onEnter: function(args) {
            let start_routine = args[2];
            let module = Process.findModuleByAddress(start_routine);
            
            if (module && module.name.indexOf(target_so_name) !== -1) {
                analyzeThreadFunction(start_routine, module);
            }
        }
    });
}

// 使用
hook_pthread_create_analyze("libtest.so");

14.8 Hook 线程函数内部

不仅 hook pthread_create,还 hook 线程函数本身:

function hook_thread_function(target_so_name) {
    console.log("=== Hook 线程函数内部 ===");
    
    let pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    let hooked_functions = [];
    
    Interceptor.attach(pthread_create_addr, {
        onEnter: function(args) {
            let start_routine = args[2];
            let module = Process.findModuleByAddress(start_routine);
            
            if (module && module.name.indexOf(target_so_name) !== -1) {
                // 检查是否已经 hook 过
                if (hooked_functions.indexOf(start_routine.toString()) === -1) {
                    hooked_functions.push(start_routine.toString());
                    
                    console.log("✓ Hook 线程函数:", start_routine);
                    
                    // Hook 线程函数本身
                    Interceptor.attach(start_routine, {
                        onEnter: function(args) {
                            console.log("\n╔════════════════════════════════════╗");
                            console.log("║    线程函数被调用!              ║");
                            console.log("╚════════════════════════════════════╝");
                            console.log("函数:", start_routine);
                            console.log("参数:", args[0]);
                            console.log("线程 ID:", Process.getCurrentThreadId());
                            
                            // 打印调用栈
                            console.log("\n调用栈:");
                            console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
                                .map(DebugSymbol.fromAddress).join('\n'));
                        },
                        onLeave: function(retval) {
                            console.log("\n[线程函数] 返回值:", retval);
                            console.log("════════════════════════════════════\n");
                        }
                    });
                }
            }
        }
    });
}

// 使用
hook_thread_function("libtest.so");

14.9 监控线程中的关键函数调用

function monitor_thread_apis(target_so_name) {
    console.log("=== 监控线程中的 API 调用 ===");
    
    let pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    let target_thread_ids = [];
    
    // 1. Hook pthread_create 记录线程 ID
    Interceptor.attach(pthread_create_addr, {
        onEnter: function(args) {
            let start_routine = args[2];
            let module = Process.findModuleByAddress(start_routine);
            
            if (module && module.name.indexOf(target_so_name) !== -1) {
                this.is_target = true;
                this.thread_ptr = args[0];
            }
        },
        onLeave: function(retval) {
            if (this.is_target && retval.toInt32() === 0) {
                // 读取创建的线程 ID
                let thread_id = this.thread_ptr.readPointer();
                target_thread_ids.push(thread_id.toString());
                console.log("✓ 记录目标线程 ID:", thread_id);
            }
        }
    });
    
    // 2. Hook 关键 API,只记录目标线程的调用
    let apis = [
        "open",
        "read",
        "write",
        "fopen",
        "fread",
        "ptrace",
        "access"
    ];
    
    apis.forEach(api_name => {
        let api_addr = Module.findExportByName("libc.so", api_name);
        if (api_addr) {
            Interceptor.attach(api_addr, {
                onEnter: function(args) {
                    let current_tid = Process.getCurrentThreadId();
                    
                    // 只记录目标线程的调用
                    if (target_thread_ids.indexOf(current_tid.toString()) !== -1) {
                        console.log(`\n[线程 ${current_tid}] ${api_name} 被调用`);
                        
                        // 根据不同 API 打印参数
                        if (api_name === "open" || api_name === "fopen") {
                            console.log("  路径:", args[0].readCString());
                        } else if (api_name === "ptrace") {
                            console.log("  请求:", args[0]);
                        }
                    }
                }
            });
        }
    });
    
    console.log("✓ 已 hook", apis.length, "个 API");
}

// 使用
monitor_thread_apis("libtest.so");

14.10 实战案例: 对抗反调试线程

场景: SO 创建一个线程不断检测调试器

function defeatAntiDebugThread(target_so_name) {
    console.log("=== 对抗反调试线程 ===");
    
    let pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    
    // 常见反调试 API
    let anti_debug_apis = [
        "ptrace",
        "kill",
        "exit",
        "_exit",
        "abort"
    ];
    
    // 记录可疑线程
    let suspicious_threads = [];
    
    // Hook pthread_create
    Interceptor.attach(pthread_create_addr, {
        onEnter: function(args) {
            let start_routine = args[2];
            let module = Process.findModuleByAddress(start_routine);
            
            if (module && module.name.indexOf(target_so_name) !== -1) {
                this.is_target = true;
                this.start_routine = start_routine;
                this.thread_ptr = args[0];
                
                // Hook 线程函数
                Interceptor.attach(start_routine, {
                    onEnter: function(args) {
                        let tid = Process.getCurrentThreadId();
                        console.log("[线程", tid, "] 开始执行");
                        
                        // 标记为可疑
                        suspicious_threads.push(tid);
                    }
                });
            }
        }
    });
    
    // Hook 反调试 API
    anti_debug_apis.forEach(api => {
        let addr = Module.findExportByName("libc.so", api);
        if (addr) {
            Interceptor.attach(addr, {
                onEnter: function(args) {
                    let tid = Process.getCurrentThreadId();
                    
                    if (suspicious_threads.indexOf(tid) !== -1) {
                        console.log("\n⚠️  检测到反调试行为!");
                        console.log("线程:", tid);
                        console.log("API:", api);
                        
                        // 阻止执行
                        if (api === "exit" || api === "_exit" || api === "abort") {
                            console.log("✓ 阻止进程退出");
                            // 替换为无害操作
                            this.prevent = true;
                        } else if (api === "ptrace") {
                            console.log("✓ 阻止 ptrace");
                            this.prevent = true;
                        }
                    }
                },
                onLeave: function(retval) {
                    if (this.prevent) {
                        // 返回成功
                        retval.replace(0);
                    }
                }
            });
        }
    });
    
    console.log("✓ 反调试对抗已启用");
}

// 使用
defeatAntiDebugThread("libsecurity.so");

14.11 完整示例

// linkerdemo.js 中的实现
function hook_pthread_create() {
    var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
    
    Interceptor.attach(pthread_create_addr, {
        onEnter: function (args) {
            var args2 = args[2];  // 线程函数地址
            var module = Process.findModuleByAddress(args2);
            
            if (module) {
                var offset = args2.sub(module.base);
                console.log(module.name + "!pthread_create!" + offset + " is called");
                
                // 可以根据需要阻止或替换线程
                // 例如:
                // if (module.name.indexOf("libtest.so") !== -1) {
                //     // 替换线程函数
                //     Interceptor.replace(args2, new NativeCallback(function () {
                //         console.log(module.name + "!pthread_create!" + offset + " is replaced");
                //     }, 'void', []))
                // }
            }
        }, 
        onLeave: function (retval) {
            // 处理返回值
        }
    });
}

// 调用
hook_pthread_create();

14.12 注意事项

1. 性能影响

// ✗ 不好: 每次都查找模块
Interceptor.attach(pthread_create_addr, {
    onEnter: function(args) {
        let module = Process.findModuleByAddress(args[2]);  // 慢
    }
});

// ✓ 更好: 缓存模块列表
let module_cache = {};
Interceptor.attach(pthread_create_addr, {
    onEnter: function(args) {
        let addr_str = args[2].toString();
        if (!module_cache[addr_str]) {
            module_cache[addr_str] = Process.findModuleByAddress(args[2]);
        }
        let module = module_cache[addr_str];
    }
});

2. 线程安全

// 多个线程可能同时创建线程
// 需要注意数据竞争

// 使用 Frida 的原子操作或锁机制(如果需要)

3. 时机选择

// 尽早 hook,以捕获所有线程创建
// 最好在 SO 加载时就 hook

// ✓ 好的时机
setTimeout(function() {
    hook_pthread_create();
}, 0);

// ✗ 可能错过早期线程
setTimeout(function() {
    hook_pthread_create();
}, 5000);

第五部分: 实战应用

15. 完整攻防流程

15.1 防御方实现流程

完整的 SO 保护脚本:

// ========================================
// SO 完全隐藏脚本
// ========================================

const TARGET_SO_PATH = "/data/app/com.example/lib/arm64/libsecret.so";
const TARGET_SO_NAME = "libsecret.so";

console.log("╔════════════════════════════════════╗");
console.log("║   Android Native 层隐藏保护      ║");
console.log("╚════════════════════════════════════╝");

// ----------------------------------------
// 工具函数
// ----------------------------------------

function pageAlignDown(addr) {
    const PAGE_SIZE = 4096;
    return ptr(addr).and(~(PAGE_SIZE - 1));
}

function getSolist() {
    let linker = Process.findModuleByName("linker64") || Process.findModuleByName("linker");
    if (!linker) return null;
    
    let symbols = linker.enumerateSymbols();
    for (let i = 0; i < symbols.length; i++) {
        if (symbols[i].name.indexOf("solist") !== -1) {
            return symbols[i].address.readPointer();
        }
    }
    return null;
}

function getRealpath(soinfo) {
    let linker = Process.findModuleByName("linker64") || Process.findModuleByName("linker");
    let symbols = linker.enumerateSymbols();
    
    for (let i = 0; i < symbols.length; i++) {
        if (symbols[i].name.indexOf("get_realpath") !== -1) {
            let func = new NativeFunction(symbols[i].address, 'pointer', ['pointer']);
            return ptr(func(soinfo)).readCString();
        }
    }
    return null;
}

// ----------------------------------------
// 主要隐藏函数
// ----------------------------------------

function applyAllProtections() {
    console.log("\n[步骤 1/5] 等待 SO 加载...");
    
    // 等待 SO 加载完成
    let wait_interval = setInterval(function() {
        let module = Process.findModuleByName(TARGET_SO_NAME);
        if (module) {
            clearInterval(wait_interval);
            console.log("✓ SO 已加载:", module.base);
            
            // 延迟执行,确保初始化完成
            setTimeout(function() {
                executeProtections();
            }, 1000);
        }
    }, 100);
}

function executeProtections() {
    console.log("\n[步骤 2/5] 备份关键信息...");
    
    // 备份导出函数
    let module = Process.findModuleByName(TARGET_SO_NAME);
    let exports_backup = {};
    
    module.enumerateExports().forEach(exp => {
        exports_backup[exp.name] = exp.address;
    });
    
    console.log("✓ 已备份", Object.keys(exports_backup).length, "个导出函数");
    
    // 应用隐藏技术
    console.log("\n[步骤 3/5] 应用隐藏技术...");
    
    try {
        // 技术1: 隐藏 maps 名称
        console.log("\n  [3.1] 隐藏 maps 名称...");
        hiddenSoInMaps(TARGET_SO_NAME);
        console.log("  ✓ 完成");
        
        // 技术2: 隐藏可执行段
        console.log("\n  [3.2] 隐藏可执行段...");
        hiddenSoExecSegmentInMaps(TARGET_SO_PATH);
        console.log("  ✓ 完成");
        
        // 技术3: 修改 linker 基址
        console.log("\n  [3.3] 修改 linker 基址...");
        hiddenSobaseInMem(TARGET_SO_PATH);
        console.log("  ✓ 完成");
        
        // 技术4: 修改节表指针
        console.log("\n  [3.4] 修改节表指针...");
        hiddenSectionInMem(TARGET_SO_PATH);
        console.log("  ✓ 完成");
        
    } catch (e) {
        console.log("✗ 隐藏失败:", e.message);
        console.log(e.stack);
        return;
    }
    
    // 验证功能
    console.log("\n[步骤 4/5] 验证功能完整性...");
    
    let all_ok = true;
    for (let name in exports_backup) {
        try {
            let addr = exports_backup[name];
            addr.readU32();  // 尝试读取
        } catch (e) {
            console.log("✗ 函数", name, "不可访问");
            all_ok = false;
        }
    }
    
    if (all_ok) {
        console.log("✓ 所有函数验证通过");
    } else {
        console.log("⚠️  部分函数验证失败");
    }
    
    // Hook 线程创建
    console.log("\n[步骤 5/5] 启用线程监控...");
    hook_pthread_create();
    console.log("✓ 线程监控已启用");
    
    // 完成
    console.log("\n╔════════════════════════════════════╗");
    console.log("║      所有保护已成功应用!        ║");
    console.log("╚════════════════════════════════════╝");
    
    printProtectionStatus();
}

function printProtectionStatus() {
    console.log("\n保护状态:");
    console.log("  [✓] maps 中的 SO 名称已隐藏");
    console.log("  [✓] 可执行段已变为匿名内存");
    console.log("  [✓] linker 中的基址已修改");
    console.log("  [✓] 符号表指针已修改");
    console.log("  [✓] 线程创建已被监控");
}

// ----------------------------------------
// 启动保护
// ----------------------------------------

applyAllProtections();

15.2 攻击方分析流程

攻击者的常见步骤:

// ========================================
// 攻击者分析脚本(演示用)
// ========================================

console.log("╔════════════════════════════════════╗");
console.log("║      攻击者视角: SO 分析        ║");
console.log("╚════════════════════════════════════╝");

// 步骤1: 枚举所有模块
console.log("\n[步骤 1] 枚举加载的模块...");
let modules = Process.enumerateModules();
console.log("找到", modules.length, "个模块");

let target_modules = [];
modules.forEach(mod => {
    if (mod.name.indexOf("lib") === 0 && mod.name.indexOf(".so") !== -1) {
        // 排除系统库
        if (mod.path.indexOf("/system/") === -1 && 
            mod.path.indexOf("/apex/") === -1) {
            target_modules.push(mod);
        }
    }
});

console.log("可疑模块:", target_modules.length, "个");
target_modules.forEach(mod => {
    console.log("  -", mod.name, "@", mod.base);
});

// 步骤2: 查看 maps
console.log("\n[步骤 2] 分析内存映射...");

function readMaps() {
    let fopen_addr = Module.findExportByName("libc.so", "fopen");
    let fopenFunc = new NativeFunction(fopen_addr, "pointer", ["pointer", "pointer"]);
    
    let fgets_addr = Module.findExportByName("libc.so", "fgets");
    let fgetsFunc = new NativeFunction(fgets_addr, "pointer", ["pointer", "int", "pointer"]);
    
    let fclose_addr = Module.findExportByName("libc.so", "fclose");
    let fcloseFunc = new NativeFunction(fclose_addr, "int", ["pointer"]);
    
    let file = fopenFunc(Memory.allocUtf8String("/proc/self/maps"), Memory.allocUtf8String("r"));
    let line = Memory.alloc(1024);
    let maps_data = [];
    
    while (ptr(fgetsFunc(line, 1024, file)).toInt32() !== 0) {
        let line_str = line.readCString();
        // 只看非系统库
        if (line_str.indexOf("/data/") !== -1) {
            maps_data.push(line_str.trim());
        }
    }
    
    fcloseFunc(file);
    return maps_data;
}

let maps = readMaps();
console.log("应用相关的内存映射:");
maps.forEach(line => console.log("  ", line));

// 步骤3: 尝试查找导出函数
console.log("\n[步骤 3] 查找导出函数...");

target_modules.forEach(mod => {
    console.log("\n模块:", mod.name);
    try {
        let exports = mod.enumerateExports();
        console.log("  导出函数数量:", exports.length);
        
        if (exports.length > 0) {
            console.log("  前5个函数:");
            exports.slice(0, 5).forEach(exp => {
                console.log("    -", exp.name, "@", exp.address);
            });
        } else {
            console.log("  ⚠️  无导出函数(可能被隐藏)");
        }
    } catch (e) {
        console.log("  ✗ 枚举失败:", e.message);
    }
});

// 步骤4: 扫描 ELF 头
console.log("\n[步骤 4] 扫描内存寻找 ELF 文件...");

function scanForELF() {
    console.log("开始扫描...");
    let found = [];
    
    // 扫描常见地址范围
    Memory.scan(ptr(0x7000000000), 0x100000000, "7f 45 4c 46", {
        onMatch: function(address, size) {
            console.log("  找到 ELF 头:", address);
            found.push(address);
            
            // 读取更多信息
            try {
                let e_type = address.add(16).readU16();
                let e_machine = address.add(18).readU16();
                console.log("    类型:", e_type === 3 ? "共享库" : "未知");
                console.log("    架构:", e_machine === 0xb7 ? "ARM64" : "未知");
            } catch (e) {
                // 忽略读取错误
            }
        },
        onComplete: function() {
            console.log("扫描完成,共找到", found.length, "个 ELF 头");
        }
    });
}

// scanForELF();  // 耗时较长,按需启用

console.log("\n╔════════════════════════════════════╗");
console.log("║          分析完成!              ║");
console.log("╚════════════════════════════════════╝");

15.3 对抗策略对比

攻击手段 防御技术 效果
Process.enumerateModules() 技术1: 隐藏 maps 名称 ✓ 无法通过名称找到
读取 /proc/self/maps 技术1: 隐藏 maps 名称 ✓ 不显示 SO 路径
dump 内存代码段 技术2: 隐藏可执行段 ✓ dump 到假代码
Module.findExportByName() 技术4: 修改符号表 ✓ 无法找到导出函数
dlsym 查找符号 技术4: 修改符号表 ✓ 返回 NULL
遍历 soinfo 链表 技术3: 修改基址 ✓ 指向假地址
gdb 调试 技术3: 修改 link_map ✓ 显示假地址
扫描 ELF 头 组合技术 △ 可能找到
静态分析 APK ✗ 无法防御

16. 进阶技巧和注意事项

16.1 性能优化

问题: 多次调用系统调用影响性能

解决方案: 缓存系统调用函数

// ✗ 不好: 每次都查找
function badExample() {
    let open_addr = Module.findExportByName("libc.so", "open");
    let openFunc = new NativeFunction(open_addr, 'int', ['pointer', 'int']);
    // ...
}

// ✓ 好: 全局缓存
const SysCallCache = {
    mmap: null,
    munmap: null,
    mremap: null,
    open: null,
    close: null,
    read: null,
    
    init: function() {
        this.mmap = new NativeFunction(
            Module.findExportByName("libc.so", "mmap"),
            'pointer', ['pointer', 'int', 'int', 'int', 'int', 'int']
        );
        
        this.munmap = new NativeFunction(
            Module.findExportByName("libc.so", "munmap"),
            'int', ['pointer', 'int']
        );
        
        // ... 其他函数
        
        console.log("✓ 系统调用缓存已初始化");
    }
};

// 使用
SysCallCache.init();
let new_mem = SysCallCache.mmap(ptr(0), 0x1000, 7, 0x22, -1, 0);

16.2 错误处理

完善的错误处理:

function safeHideSO(so_path) {
    let success = {
        maps_hidden: false,
        exec_hidden: false,
        base_modified: false,
        section_modified: false
    };
    
    try {
        console.log("[1/4] 隐藏 maps 名称...");
        hiddenSoInMaps(so_path.split('/').pop());
        success.maps_hidden = true;
        console.log("✓ 完成");
    } catch (e) {
        console.log("✗ 失败:", e.message);
    }
    
    try {
        console.log("[2/4] 隐藏可执行段...");
        hiddenSoExecSegmentInMaps(so_path);
        success.exec_hidden = true;
        console.log("✓ 完成");
    } catch (e) {
        console.log("✗ 失败:", e.message);
    }
    
    try {
        console.log("[3/4] 修改基址...");
        hiddenSobaseInMem(so_path);
        success.base_modified = true;
        console.log("✓ 完成");
    } catch (e) {
        console.log("✗ 失败:", e.message);
    }
    
    try {
        console.log("[4/4] 修改节表...");
        hiddenSectionInMem(so_path);
        success.section_modified = true;
        console.log("✓ 完成");
    } catch (e) {
        console.log("✗ 失败:", e.message);
    }
    
    // 总结
    console.log("\n总结:");
    console.log("  maps 隐藏:", success.maps_hidden ? "✓" : "✗");
    console.log("  可执行段隐藏:", success.exec_hidden ? "✓" : "✗");
    console.log("  基址修改:", success.base_modified ? "✓" : "✗");
    console.log("  节表修改:", success.section_modified ? "✓" : "✗");
    
    let count = Object.values(success).filter(v => v).length;
    console.log(`\n成功率: ${count}/4 (${count * 25}%)`);
    
    return success;
}

16.3 兼容性处理

不同 Android 版本的差异:

function getAndroidVersion() {
    let version = Java.androidVersion;
    console.log("Android 版本:", version);
    return parseInt(version);
}

function adaptiveHideSO(so_path) {
    let version = getAndroidVersion();
    
    if (version >= 10) {
        // Android 10+ : soinfo 结构可能不同
        console.log("使用 Android 10+ 适配方案");
        // ...
    } else if (version >= 7) {
        // Android 7-9
        console.log("使用 Android 7-9 适配方案");
        // ...
    } else {
        console.log("⚠️  不支持 Android", version);
        return;
    }
}

16.4 调试模式

添加详细日志:

const DEBUG_MODE = true;

function debugLog(message) {
    if (DEBUG_MODE) {
        console.log("[DEBUG]", message);
    }
}

function verboseHideSO(so_path) {
    debugLog("开始隐藏 SO: " + so_path);
    
    // 步骤1
    debugLog("读取 SO 文件...");
    let fd = openFunc(Memory.allocUtf8String(so_path), 0);
    debugLog("文件描述符: " + fd);
    
    // ...更多详细日志
}

16.5 内存泄漏预防

正确管理内存:

let allocated_memory = [];

function trackAllocation(addr, size, description) {
    allocated_memory.push({
        address: addr,
        size: size,
        description: description
    });
    console.log("✓ 分配内存:", addr, "大小:", ptr(size), "用途:", description);
}

function cleanupAll() {
    console.log("清理", allocated_memory.length, "块内存...");
    
    let munmap_addr = Module.findExportByName("libc.so", "munmap");
    let munmapFunc = new NativeFunction(munmap_addr, 'int', ['pointer', 'int']);
    
    allocated_memory.forEach(mem => {
        try {
            munmapFunc(mem.address, mem.size);
            console.log("✓ 释放:", mem.description);
        } catch (e) {
            console.log("✗ 释放失败:", mem.description, e.message);
        }
    });
    
    allocated_memory = [];
}

// 使用
let temp_mem = mmapFunc(ptr(0), 0x10000, 7, 0x22, -1, 0);
trackAllocation(temp_mem, 0x10000, "临时 SO 副本");

// 程序退出前
// cleanupAll();

17. 调试技巧

17.1 Frida 脚本调试

添加断点:

function debugHook(target_func_name) {
    let addr = Module.findExportByName(null, target_func_name);
    
    Interceptor.attach(addr, {
        onEnter: function(args) {
            console.log("\n=== 断点:", target_func_name, "===");
            console.log("参数:");
            for (let i = 0; i < 4; i++) {
                console.log(`  args[${i}]:`, args[i]);
            }
            
            // 打印调用栈
            console.log("\n调用栈:");
            console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
                .map(DebugSymbol.fromAddress).join('\n'));
            
            // 暂停(手动继续)
            console.log("\n按 Enter 继续...");
            // 在实际场景中,可以通过 RPC 或其他方式控制
        }
    });
}

// 使用
debugHook("mmap");
debugHook("mremap");

17.2 内存快照

保存内存状态:

function snapshotMemory(address, size, filename) {
    try {
        let data = address.readByteArray(size);
        let file = new File(filename, "wb");
        file.write(data);
        file.close();
        console.log("✓ 内存快照已保存:", filename);
    } catch (e) {
        console.log("✗ 保存失败:", e.message);
    }
}

// 使用
let so_base = Module.findBaseAddress("libtest.so");
snapshotMemory(so_base, 0x10000, "/data/local/tmp/so_snapshot.bin");

17.3 对比工具

对比修改前后:

function compareMemory(addr1, addr2, size) {
    console.log("对比内存:", addr1, "vs", addr2);
    
    let diff_count = 0;
    let sample_diffs = [];
    
    for (let i = 0; i < size; i++) {
        let byte1 = addr1.add(i).readU8();
        let byte2 = addr2.add(i).readU8();
        
        if (byte1 !== byte2) {
            diff_count++;
            
            if (sample_diffs.length < 10) {
                sample_diffs.push({
                    offset: i,
                    value1: byte1,
                    value2: byte2
                });
            }
        }
    }
    
    console.log("差异字节数:", diff_count, "/", size);
    console.log("差异示例:");
    sample_diffs.forEach(diff => {
        console.log(`  [+0x${diff.offset.toString(16)}] 0x${diff.value1.toString(16)} → 0x${diff.value2.toString(16)}`);
    });
}

17.4 实时监控

监控关键数据结构:

function monitorSoinfo(so_name, interval_ms) {
    console.log("开始监控 soinfo:", so_name);
    
    let monitor_interval = setInterval(function() {
        let solist = getSolist();
        let current = solist;
        
        while (current.toInt32() !== 0) {
            let path = getRealpath(current);
            
            if (path && path.indexOf(so_name) !== -1) {
                let base = current.add(Process.pointerSize * 2).readPointer();
                let dynamic = current.add(Process.pointerSize * 4).readPointer();
                let strtab = current.add(Process.pointerSize * 7).readPointer();
                let symtab = current.add(Process.pointerSize * 8).readPointer();
                
                console.log("\n[" + new Date().toISOString() + "]");
                console.log("  base:", base);
                console.log("  dynamic:", dynamic);
                console.log("  strtab:", strtab);
                console.log("  symtab:", symtab);
                
                break;
            }
            
            current = current.add(Process.pointerSize * 5).readPointer();
        }
    }, interval_ms);
    
    // 返回停止函数
    return function() {
        clearInterval(monitor_interval);
        console.log("监控已停止");
    };
}

// 使用
let stopMonitor = monitorSoinfo("libtest.so", 5000);

// 停止监控
// stopMonitor();

17.5 崩溃分析

捕获异常:

Process.setExceptionHandler(function(details) {
    console.log("\n╔════════════════════════════════════╗");
    console.log("║          程序崩溃!              ║");
    console.log("╚════════════════════════════════════╝");
    
    console.log("\n类型:", details.type);
    console.log("地址:", details.address);
    console.log("内存:", JSON.stringify(details.memory, null, 2));
    console.log("上下文:", JSON.stringify(details.context, null, 2));
    
    console.log("\n调用栈:");
    console.log(Thread.backtrace(details.context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress).join('\n'));
    
    // 尝试恢复(仅限某些情况)
    // return true; // 继续执行
    
    return false; // 让程序崩溃(默认行为)
});

第六部分: 总结与扩展

18. 技术总结

18.1 五大核心技术回顾

技术对比表:

技术 难度 效果 副作用 推荐度
技术1: 隐藏 SO 名称 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
技术2: 隐藏可执行段 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
技术3: 修改 linker 基址 ⭐⭐⭐ ⭐⭐⭐⭐ 可能影响符号解析 ⭐⭐⭐⭐
技术4: 修改符号表指针 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 影响 dlsym ⭐⭐⭐⭐
技术5: Hook pthread_create ⭐⭐ ⭐⭐⭐ 性能影响 ⭐⭐⭐⭐

推荐组合方案:

  1. 基础保护: 技术1 + 技术5

    • 适合: 快速实施,最小副作用
    • 防御: maps 隐藏 + 线程监控
  2. 标准保护: 技术1 + 技术2 + 技术5

    • 适合: 大多数应用场景
    • 防御: maps 隐藏 + 代码段隐藏 + 线程监控
  3. 强化保护: 全部五个技术

    • 适合: 高安全要求
    • 防御: 全方位保护

18.2 实施建议

最佳实践:

// ========================================
// 推荐的实施顺序
// ========================================

function implementProtection(so_path, level) {
    const so_name = so_path.split('/').pop();
    
    console.log("保护等级:", level);
    
    if (level === "basic") {
        // 基础保护
        console.log("\n[基础保护模式]");
        hiddenSoInMaps(so_name);
        hook_pthread_create();
        
    } else if (level === "standard") {
        // 标准保护
        console.log("\n[标准保护模式]");
        hiddenSoInMaps(so_name);
        hiddenSoExecSegmentInMaps(so_path);
        hook_pthread_create();
        
    } else if (level === "advanced") {
        // 强化保护
        console.log("\n[强化保护模式]");
        hiddenSoInMaps(so_name);
        hiddenSoExecSegmentInMaps(so_path);
        hiddenSobaseInMem(so_path);
        hiddenSectionInMem(so_path);
        hook_pthread_create();
    }
    
    console.log("\n✓ 保护已启用");
}

// 使用
implementProtection("/data/app/.../libsecret.so", "standard");

时机选择:

// 方法1: JNI_OnLoad 中
// 优点: 早期执行
// 缺点: 可能影响初始化

// 方法2: 延迟执行
Java.perform(function() {
    setTimeout(function() {
        implementProtection(so_path, "standard");
    }, 2000);  // 延迟2秒
});

// 方法3: 监听特定事件
// 等待关键函数被调用后再执行
Interceptor.attach(Module.findExportByName("libtest.so", "init"), {
    onLeave: function(retval) {
        console.log("初始化完成,启用保护...");
        implementProtection(so_path, "standard");
    }
});

18.3 局限性分析

技术局限:

  1. 静态分析无法防御

    • APK 解包后仍可分析 SO 文件
    • 解决: 结合代码混淆、加固
  2. 内存扫描可能绕过

    • 攻击者可扫描 ELF 头特征
    • 解决: 加密内存、动态解密
  3. 性能开销

    • Hook 和内存操作有性能损耗
    • 解决: 优化代码、减少 hook 数量
  4. 兼容性问题

    • 不同 Android 版本 soinfo 结构不同
    • 解决: 版本检测、适配
  5. Root 检测

    • Frida 需要 Root 或注入
    • 在生产环境中需要加固技术配合

无法防御的场景:

✗ 静态分析 APK 中的 SO 文件
✗ 使用专门的反 Frida 技术
✗ 内核级别的内存访问
✗ 硬件断点调试
✗ 某些模拟器环境

18.4 性能影响

性能测试示例:

function benchmarkProtection() {
    console.log("=== 性能测试 ===");
    
    // 测试1: 隐藏 maps 名称
    let start1 = Date.now();
    hiddenSoInMaps("libtest.so");
    let time1 = Date.now() - start1;
    console.log("技术1 耗时:", time1, "ms");
    
    // 测试2: 隐藏可执行段
    let start2 = Date.now();
    hiddenSoExecSegmentInMaps("/data/app/.../libtest.so");
    let time2 = Date.now() - start2;
    console.log("技术2 耗时:", time2, "ms");
    
    // ... 其他测试
    
    console.log("\n总耗时:", (time1 + time2), "ms");
}

优化建议:

// 1. 缓存系统调用
const syscalls = {
    mmap: new NativeFunction(Module.findExportByName("libc.so", "mmap"), ...),
    // ...
};

// 2. 减少内存复制
// 使用 Memory.dup() 代替 Memory.copy() 当可能时

// 3. 异步执行
setTimeout(function() {
    implementProtection(so_path, "standard");
}, 0);  // 不阻塞主线程

// 4. 按需加载
// 只在检测到攻击时才启用保护

19. 扩展阅读

19.1 相关技术

1. 代码加固

  • VMP (虚拟机保护)
  • 代码混淆
  • 控制流平坦化
  • 字符串加密

2. 反调试技术

  • ptrace 自检
  • TracerPid 检测
  • 时间差检测
  • 完整性校验

3. 反 Hook 技术

  • Inline Hook 检测
  • GOT/PLT 表检测
  • 函数开头校验
  • 内存权限检测

4. 运行时保护

  • DEX 加固
  • SO 加固
  • 资源加密
  • 动态加载

19.2 进阶方向

方向1: 自动化工具开发

# 自动化 SO 保护工具
# protect_so.py

import frida
import sys

def main(package_name, so_name):
    device = frida.get_usb_device()
    session = device.attach(package_name)
    
    script = session.create_script("""
        // 自动注入保护代码
        implementProtection("%s", "standard");
    """ % so_name)
    
    script.load()
    print("[+] 保护已注入")
    sys.stdin.read()

if __name__ == "__main__":
    main("com.example.app", "libsecret.so")

方向2: 动态配置

// 支持远程配置保护策略
function loadRemoteConfig() {
    // 从服务器获取配置
    let config = {
        "libsecret.so": {
            "level": "advanced",
            "techniques": [1, 2, 3, 4, 5],
            "delay": 2000
        }
    };
    
    // 应用配置
    for (let so in config) {
        let cfg = config[so];
        setTimeout(function() {
            implementProtection(so, cfg.level);
        }, cfg.delay);
    }
}

方向3: 检测系统

// 实时检测攻击行为
function setupDetection() {
    // 检测 Frida 特征
    let frida_check = setInterval(function() {
        let modules = Process.enumerateModules();
        modules.forEach(mod => {
            if (mod.name.indexOf("frida") !== -1) {
                console.log("⚠️  检测到 Frida!");
                // 采取对策
            }
        });
    }, 5000);
    
    // 检测内存修改
    // 检测 Hook
    // ...
}

19.3 相关资源

官方文档:

工具推荐:

  • IDA Pro: 逆向分析
  • Ghidra: 开源逆向工具
  • Radare2: 命令行逆向框架
  • objdump: ELF 文件查看工具
  • readelf: ELF 信息读取

学习资源:

19.4 实战项目

项目1: SO 保护框架

目标: 开发通用的 SO 保护框架
功能:
- 自动检测 SO 加载
- 动态应用保护技术
- 性能监控
- 日志记录

项目2: 攻防演练平台

目标: 搭建攻防对抗环境
内容:
- 提供受保护的示例 APP
- 提供攻击工具集
- 评分系统
- 教学模式

项目3: 反 Frida 系统

目标: 检测并对抗 Frida 注入
技术:
- 进程名检测
- 端口扫描
- 内存特征检测
- 自动防御

20. 致谢与参考

20.1 致谢

本文档基于看雪论坛文章整理扩展,感谢原作者的技术分享。

特别感谢:

  • 看雪论坛技术社区
  • Frida 开发团队
  • Android 安全研究社区

20.2 参考文献

  1. 原始文章

  2. 技术文档

    • ELF File Format Specification
    • Android Bionic Linker Source Code
    • Linux Programmer's Manual
  3. 相关论文

    • "Anti-debugging Techniques in Android Native Code"
    • "Dynamic Analysis of Android Native Libraries"

20.3 版本历史

v1.0 (2024-11) - 初始版本
- 完整的五大技术讲解
- 从基础到高级的完整路径
- 实战代码示例

v1.1 (规划中)
- 增加更多 Android 版本适配
- 性能优化方案
- 自动化工具

20.4 免责声明

本文档仅供学习和研究使用。

禁止将文档中的技术用于:
- 破解付费软件
- 窃取他人数据
- 非法入侵系统
- 其他违法行为

使用者需自行承担因使用本文档内容而产生的一切后果。
作者不对任何直接或间接损失承担责任。

20.5 许可协议

本文档采用 CC BY-NC-SA 4.0 协议

您可以:
✓ 分享 - 复制、发行本作品
✓ 修改 - 改编本作品

条件:
- 署名 - 必须给出适当的署名
- 非商业性使用 - 不得用于商业目的
- 相同方式共享 - 如修改本作品,必须采用相同协议

21. 附录

21.1 完整代码清单

主脚本 (main.js):

// ========================================
// Android Native 层 SO 完全隐藏脚本
// 版本: 1.0
// 日期: 2024-11
// ========================================

const CONFIG = {
    TARGET_SO_PATH: "/data/app/com.example/lib/arm64/libsecret.so",
    TARGET_SO_NAME: "libsecret.so",
    PROTECTION_LEVEL: "standard",  // basic | standard | advanced
    ENABLE_LOGGING: true,
    DELAY_MS: 2000
};

// ... [包含所有工具函数和技术实现]

// 主入口
function main() {
    console.log("╔════════════════════════════════════╗");
    console.log("║   Android Native 层保护系统      ║");
    console.log("╚════════════════════════════════════╝");
    
    // 等待 SO 加载
    waitForSOAndProtect();
}

function waitForSOAndProtect() {
    let check = setInterval(function() {
        let module = Process.findModuleByName(CONFIG.TARGET_SO_NAME);
        if (module) {
            clearInterval(check);
            console.log("✓ 目标 SO 已加载");
            
            setTimeout(function() {
                applyProtections();
            }, CONFIG.DELAY_MS);
        }
    }, 100);
}

function applyProtections() {
    if (CONFIG.PROTECTION_LEVEL === "basic") {
        hiddenSoInMaps(CONFIG.TARGET_SO_NAME);
        hook_pthread_create();
    } else if (CONFIG.PROTECTION_LEVEL === "standard") {
        hiddenSoInMaps(CONFIG.TARGET_SO_NAME);
        hiddenSoExecSegmentInMaps(CONFIG.TARGET_SO_PATH);
        hook_pthread_create();
    } else if (CONFIG.PROTECTION_LEVEL === "advanced") {
        hiddenSoInMaps(CONFIG.TARGET_SO_NAME);
        hiddenSoExecSegmentInMaps(CONFIG.TARGET_SO_PATH);
        hiddenSobaseInMem(CONFIG.TARGET_SO_PATH);
        hiddenSectionInMem(CONFIG.TARGET_SO_PATH);
        hook_pthread_create();
    }
    
    console.log("\n✓ 所有保护已应用!");
}

// 启动
main();

21.2 常见问题 FAQ

Q1: 为什么隐藏后程序崩溃?

A: 可能原因:

  • 在 SO 初始化过程中执行了隐藏
  • 修改了正在使用的内存
  • 符号解析尚未完成

解决: 增加延迟时间,确保 SO 完全初始化。

Q2: 如何验证隐藏是否成功?

A: 使用以下命令:

# 查看 maps
adb shell cat /proc/$(pidof com.example.app)/maps | grep libsecret

# 如果没有输出,说明隐藏成功

Q3: 性能影响有多大?

A:

  • 隐藏操作: 一次性耗时 100-500ms
  • 运行时开销: 基本无影响(除 pthread hook)
  • 内存开销: 增加约 1-2 倍 SO 大小的内存

Q4: 支持哪些 Android 版本?

A:

  • Android 7.0+ : 完全支持
  • Android 5.0-6.0 : 部分支持(需调整 soinfo 偏移)
  • Android 4.x : 不支持

Q5: 如何调试隐藏后的代码?

A:

  • 使用 Frida 的 Interceptor.attach hook 关键函数
  • 保存原始函数地址
  • 通过地址直接调用

21.3 术语表

术语 英文 解释
SO Shared Object Linux/Android 共享库文件
ELF Executable and Linkable Format 可执行和可链接格式
maps /proc/self/maps Linux 进程内存映射文件
linker Dynamic Linker 动态链接器
soinfo SO Info Structure SO 信息结构体
mmap Memory Map 内存映射系统调用
mremap Memory Remap 内存重映射系统调用
COW Copy-on-Write 写时复制
PLT Procedure Linkage Table 过程链接表
GOT Global Offset Table 全局偏移表

结语

本文档详细讲解了 Android Native 层的五大核心反调试技术,从基础概念到实战应用,形成完整的知识体系。

学习路径回顾:

第一部分: 基础知识篇
├── Frida 基础
├── Linux 进程内存
└── ELF 文件格式

第二部分: 中级技术篇
├── 程序头表
├── 节头表
└── 动态链接

第三部分: 高级技术篇
├── Android linker
├── 内存管理系统调用
└── 内存保护

第四部分: 核心技术详解
├── 技术1: 隐藏 SO 名称
├── 技术2: 隐藏可执行段
├── 技术3: 修改 linker 基址
├── 技术4: 修改节表指针
└── 技术5: Hook pthread_create

第五部分: 实战应用
├── 完整攻防流程
├── 进阶技巧
└── 调试方法

第六部分: 总结与扩展
├── 技术总结
├── 扩展阅读
└── 实战项目

核心要点:

  1. 理解原理比记住代码更重要
  2. 安全防护没有银弹,需要多层防御
  3. 攻防是持续演进的过程
  4. 合理使用技术,避免过度保护影响性能

持续学习建议:

  • 关注 Android 安全动态
  • 参与技术社区讨论
  • 实践攻防演练
  • 阅读源码深入理解

最后提醒:

技术本身是中性的,希望读者:

  • 用于正当防护目的
  • 保护自己的知识产权
  • 提升安全防护能力
  • 遵守法律法规

祝学习愉快! 🚀


EOF

posted @ 2025-11-25 16:58  公众号python学习开发  阅读(21)  评论(0)    收藏  举报