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位) 负责:
- 加载 SO 文件到内存
- 解析依赖关系 (DT_NEEDED)
- 符号解析 (dlsym)
- 重定位 (relocation)
- 维护已加载 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 可以实现:
- 将匿名内存移动到 SO 的位置
- 将 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 映射文件时:
- 初始: 多个进程共享同一物理内存页
- 写入时: 系统自动创建副本,修改只影响当前进程
在反调试中的应用:
// 场景: 在 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 内存操作的安全性
注意事项:
- 内存对齐: mmap 的地址和大小通常需要页对齐(4KB)
- 权限检查: 修改内存前要检查权限
- 地址冲突: mremap 移动到已占用地址会失败
- 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
解决方法:
- Root 设备并设置 SELinux 为 Permissive
- 使用更高级的技术绕过限制
- 使用
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]
核心思路:
- 创建匿名内存
- 复制 SO 内容到匿名内存
- 使用
mremap将匿名内存移动到 SO 原位置 - 释放临时内存
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): 固定到指定地址
效果:
- 内核将
temp_addr的页表项移动到startAddress temp_addr变成未映射状态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)
核心思路:
- 将可执行段内存改为匿名映射(真实代码)
- 在其他位置创建文件映射(假代码,全0)
- 攻击者 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 ⭐⭐
// ...
};
需要修改的字段:
base(offset 0x10): SO 基址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
解决方案: 只修改 base 和 link_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
// ...
};
需要修改的字段:
dynamic(offset 0x20):.dynamic段指针strtab_(offset 0x38):.dynstr段指针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,可以:
- 监控所有线程的创建
- 识别哪些线程来自目标 SO
- 阻止某些线程运行
- 替换线程函数
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 + 技术5
- 适合: 快速实施,最小副作用
- 防御: maps 隐藏 + 线程监控
-
标准保护: 技术1 + 技术2 + 技术5
- 适合: 大多数应用场景
- 防御: maps 隐藏 + 代码段隐藏 + 线程监控
-
强化保护: 全部五个技术
- 适合: 高安全要求
- 防御: 全方位保护
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 局限性分析
技术局限:
-
静态分析无法防御
- APK 解包后仍可分析 SO 文件
- 解决: 结合代码混淆、加固
-
内存扫描可能绕过
- 攻击者可扫描 ELF 头特征
- 解决: 加密内存、动态解密
-
性能开销
- Hook 和内存操作有性能损耗
- 解决: 优化代码、减少 hook 数量
-
兼容性问题
- 不同 Android 版本 soinfo 结构不同
- 解决: 版本检测、适配
-
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 信息读取
学习资源:
- 看雪论坛: https://bbs.kanxue.com
- 吾爱破解: https://www.52pojie.cn
- Android Security Awesome: https://github.com/ashishb/android-security-awesome
19.4 实战项目
项目1: SO 保护框架
目标: 开发通用的 SO 保护框架
功能:
- 自动检测 SO 加载
- 动态应用保护技术
- 性能监控
- 日志记录
项目2: 攻防演练平台
目标: 搭建攻防对抗环境
内容:
- 提供受保护的示例 APP
- 提供攻击工具集
- 评分系统
- 教学模式
项目3: 反 Frida 系统
目标: 检测并对抗 Frida 注入
技术:
- 进程名检测
- 端口扫描
- 内存特征检测
- 自动防御
20. 致谢与参考
20.1 致谢
本文档基于看雪论坛文章整理扩展,感谢原作者的技术分享。
特别感谢:
- 看雪论坛技术社区
- Frida 开发团队
- Android 安全研究社区
20.2 参考文献
-
原始文章
- 标题: Android Native层反调试技术
- 链接: https://bbs.kanxue.com/thread-285790.htm
-
技术文档
- ELF File Format Specification
- Android Bionic Linker Source Code
- Linux Programmer's Manual
-
相关论文
- "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
第五部分: 实战应用
├── 完整攻防流程
├── 进阶技巧
└── 调试方法
第六部分: 总结与扩展
├── 技术总结
├── 扩展阅读
└── 实战项目
核心要点:
- 理解原理比记住代码更重要
- 安全防护没有银弹,需要多层防御
- 攻防是持续演进的过程
- 合理使用技术,避免过度保护影响性能
持续学习建议:
- 关注 Android 安全动态
- 参与技术社区讨论
- 实践攻防演练
- 阅读源码深入理解
最后提醒:
技术本身是中性的,希望读者:
- 用于正当防护目的
- 保护自己的知识产权
- 提升安全防护能力
- 遵守法律法规
祝学习愉快! 🚀
EOF

浙公网安备 33010602011771号