声明:
libDexHelper.so 是一个典型加固壳早期 native 组件。它不是普通 JNI 业务库,而是承担初始化门卫、隐藏代码装载、ELF 重定位、JNI_OnLoad 调度和析构生命周期管理等职责。
这篇文档按“先看图,再逐函数”的顺序组织:
建议阅读路线:
第 1 章只保留两张入口图:一张静态整体调用链,一张 init_proc 总流程。其余子流程图放回对应函数章节,避免开头连续堆图。
先只看 5 行主链路:
图上真正需要优先记住的是 7 个逻辑函数:
28 个 0x2a30..0x2bf0 短函数先按 导入跳板层 整体理解,完整地址表见第 11 章附录。

读图结论:

读图结论:
主流程可以压缩为:
第 1 章解决“函数在图中的位置”,本章解决“按什么顺序读函数”。后面的第 3-8 章会按这些分组展开,避免把 28 个短导入跳板和 7 个真实逻辑函数混在同一层级阅读。
当前 IDA 函数边界共 35 个函数:
按职责可分为 6 组:

读图结论:
这里的 start 是 IDA 自动命名,不是 ELF entry,也不是程序启动入口。动态段中的 .fini_array[0] 指向 0x2c00:
sub_2AA0 是 __cxa_finalize 导入跳板,因此这个函数的实际职责是 fini bridge: 把当前 DSO handle 传给析构收口函数。
sub_4F84 是析构注册桥:
它把 loc_4F6C、调用者传入对象、unk_16000 组织成 __cxa_atexit(func, arg, dso_handle) 形式。由此可见:

读图结论:
建议先读 4.1 和 4.2 建立正常入口与检测态,再读 4.3 失败分支,最后用 4.4 回到主 loader 入口。
sub_4A78 是 .init_array[0] 的实际入口承载函数。.init_array 重定位指向 sub_4A78 内部的 0x4d1c,而不是函数起始地址 0x4a78。
它包含两个状态:
初始化态的关键步骤:
关键反汇编:
检测态的核心逻辑:
对应导入跳板:
这段检测并不是按进程名判断,也不是单纯搜索某个字符串;它关注的是 libc.so/libdl.so/linker 相关映射中是否存在小型 rwx 区域,大小阈值为 0x4000。
检测返回值经以下指令进入分支:
失败分支:
语义:
检测结果为 0 时,函数继续复制 dword_16010..16028 一组状态,并在 0x4ee8 调用 sub_36E0:
因此 sub_4A78 的静态定位是:

读图结论:
先按阶段 A-G 读标题,确认 loader 的大方向;再进入每个阶段的代码范围、输入输出和下游函数。dynamic section 和 JNI_OnLoad 的细节图放在本章对应小节里。
sub_36E0 是 libDexHelper.so 的主体函数,大小 0x1398。它的职责不是普通 JNI 调用,而是从当前 so 的隐藏材料中恢复后续 ELF/payload,并执行装载器工作。
主线速读:
本节后面的 A-G 阶段都按同一格式阅读: 先看代码范围,再看行为,最后看关联导入。这样可以把 sub_36E0 从一个大函数拆成 7 个连续装载动作。
代码范围:
行为:
目的:
代码范围:
行为:
关联导入:
代码范围:
行为:
sub_2C0C 的核心语义:
代码范围:
行为:
典型解码调用形态:
相关导入:

读图结论:
代码范围:
本阶段的作用是把动态段中的 tag 翻译成 loader 后续可直接使用的状态字段。读这一段时不要把所有 tag 当成同等重要;先抓四类输出即可:
静态特征:
随后 sub_2B10 -> dlopen 加载 DT_NEEDED 依赖,并保存 handle 数组供 relocation 使用。
sub_36E0 调用 sub_2DC0 两次:
两轮 relocation 对应两个 relocation 表或两类 relocation 记录。sub_2DC0 负责遍历条目、解析符号、写回修正地址。

读图结论:
relocation 完成后,loader 继续:

读图结论:
阅读重点是符号解析链路: 先尝试 dlsym,失败后看 dlerror 文本,再在特定错误条件下进入 ELF hash fallback。
sub_2DC0 大小 0x4b4,是手动 loader 完成符号修复的关键函数。
一句话结论: sub_2DC0 不是单纯写地址,它同时承担 找符号 和 按 relocation type 写回 的职责。前者决定 symbol value,后者决定目标位置最终写入什么。
按 sub_36E0 的调用约定,参数可解释为:
可以按三层理解:
关联函数:
已识别写回规则:

读图结论:
sub_2CE8 是标准 ELF SysV hash 算法实现:
它的阅读价值不在算法本身,而在调用位置。本文中它有两个用途:

读图结论:
0x2a30..0x2bf0 是导入跳板区。典型短跳板形态:
sub_2A30 是 PLT0 resolver 入口:
这一节只需要建立一个判断: 0x2a50..0x2bf0 这些短函数大多不是业务逻辑,而是到 libc/libdl 的导入跳板。逐函数分析时应优先关注谁调用了它、传了什么参数,而不是在跳板内部寻找复杂逻辑。
样本 .dynsym 名称被抹空,但文件内保留了导入名字符串池,结合每个跳板的调用点和参数形态可以确定其语义。
libDexHelper.so 的静态结构可以归纳为:
按函数覆盖范围看,本文已覆盖 libDexHelper.so 当前 IDA 函数边界内的全部 35 个函数。
本节只汇总检测面,不重复 loader、relocation 和导入跳板细节。先给出完整链路:
libDexHelper.so 内真正构成检测逻辑的是 sub_4A78。它有两个入口状态:
检测对象是 /proc/self/maps。执行态通过 sub_2B40 -> fopen("/proc/self/maps", "r") 打开 maps,通过 sub_2A80 -> fgets(line, 0x400, fp) 逐行读取,再由 off_16040 -> sscanf 解析地址范围和权限字段。
这里的关键点是: 它不按进程名判断,也不依赖单一命名字符串;它关心的是内存映射形态。
检测匹配目标集中在三类路径: libc.so、libdl.so、linker。只有当前 maps 行路径匹配这些目标之一时,才继续解析权限字符。
权限格式来自字符串 %lx-%lx %c%c%c%*c %*s %*s %*d %*s,对应 start、end、r、w、x 等字段。
核心命中条件是小型 rwx 映射:
条件命中后 sub_4A78 返回 1;遍历结束仍未命中时关闭 maps 并返回 0。
检测结果在 0x4e08..0x4e0c 被消费:
其中 0x4e08 只做返回值规整,0x4e0c 才是是否进入失败链的条件跳转。返回值非零时进入 0x4f38;返回值为 0 时继续正常初始化,并在 0x4ee8 调用 sub_36E0。
失败链从 0x4f38 开始。sub_2A70 -> getpid 取得当前进程 pid,随后设置 x1 = 9、x8 = 0x81 并在 0x4f48 执行 svc #0。在 AArch64 Linux syscall 约定下,这就是 kill(getpid(), SIGKILL)。
如果 syscall 返回,后续还有一条后备退出链。sub_2A60 用于处理 errno 存储位置,随后 mov w0, #3; bl sub_2BB0 进入 _Exit(3) 风格退出。这个结构说明检测命中后的终止路径不是单点,而是 kill 加 _Exit 的双保险。
sub_2AD0 -> abort 是 relocation 处理器 sub_2DC0 的致命错误出口,不属于 maps 检测点。它在符号解析、relocation 类型或写回过程遇到不可恢复状态时终止流程。
sub_2AE0 -> __stack_chk_fail 是编译器栈保护失败出口,也不属于壳的主动 maps 检测点。它用于函数栈 canary 异常时的通用保护收口。
因此,本库可确认的主动检测面只有一条主线: sub_4A78 读取 /proc/self/maps,筛选 libc.so/libdl.so/linker,判断小型 rwx 映射,非零结果经 0x4e0c 转入 0x4f38 失败链。abort 和 __stack_chk_fail 是错误/保护出口,应在分析中单独归类。
本附录只用于查地址,不作为首次阅读入口。正文先读 7 个真实逻辑函数,28 个导入跳板按支撑层整体理解。
附件里有一个AI友好型的md文档,把所有图片都改成了mermaid,方便喂给AI
原先是想写过Frida的,但是思路和我上一篇文章差不多,就改让AI静动态全面分析了,还是可以学到不少东西的
该样本来自于 com.sfacg 5.2.74
本文仅用于学习交流
| 地址 |
IDA 名称 |
先读作用 |
0x4a78 |
sub_4A78 |
init 门卫、maps 检测、失败终止、进入 loader |
0x36e0 |
sub_36E0 |
手动 loader 主体 |
0x2dc0 |
sub_2DC0 |
relocation 处理器 |
0x2c0c |
sub_2C0C |
XOR 解码 helper |
0x2ce8 |
sub_2CE8 |
ELF SysV hash helper |
0x2c00 |
IDA 命名 start |
实际是 fini bridge,不是 ELF entry |
0x4f84 |
sub_4F84 |
__cxa_atexit 注册桥 |
| 分组 |
函数 |
说明 |
| 生命周期 |
IDA 命名 start、sub_4F84、sub_2AA0、sub_2B20 |
fini 触发、析构注册、DSO handle 管理 |
| 门卫链 |
sub_4A78、sub_2B10、sub_2B30、sub_2B40、sub_2A80、sub_2B60、sub_2A70、sub_2A60、sub_2BB0 |
自定位、maps 扫描、失败终止 |
| 主 loader |
sub_36E0、sub_2BD0、sub_2B00、sub_2AB0、sub_2AC0、sub_2B50、sub_2BA0、sub_2BE0、sub_2A90、sub_2BF0 |
读取、映射、解码、设权限、回写、关闭资源 |
| 解码 |
sub_2C0C |
单字节 XOR 解码 |
| relocation |
sub_2DC0、sub_2B30、sub_2A50、sub_2AF0、sub_2CE8、sub_2B80、sub_2AD0 |
符号解析、hash fallback、重定位写回、异常终止 |
| 辅助 |
sub_2A30、sub_2B70、sub_2B90、sub_2BC0、sub_2AE0 |
PLT0、dump 辅助、字符串长度、栈保护失败 |
| 函数 |
导入/API |
用途 |
| sub_2B40 |
fopen |
打开 /proc/self/maps |
| sub_2A80 |
fgets |
逐行读取 maps |
off_16040 |
sscanf |
解析映射起止地址和权限字符 |
| sub_2B60 |
fclose |
关闭 maps 文件句柄 |
| 函数 |
API |
用途 |
| sub_2BD0 |
pread |
早期大块读取 |
| sub_2B00 |
pread |
后续片段读取 |
| sub_2AB0 |
lseek |
调整 fd 偏移 |
| sub_2B50 |
memset |
清理工作缓冲 |
| 调用位置 |
参数形态 |
说明 |
0x3a14 |
offset=0, size=0x1000 |
解码头部窗口 |
0x4550 |
offset=0x40, size=0xf6cac |
解码大块段内容 |
0x4350 |
offset=0, size=0x9160 |
解码表区或映射片段 |
| 函数 |
API |
用途 |
| sub_2AC0 |
mmap |
创建装载镜像 |
| sub_2BA0 |
mprotect |
调整内存权限 |
| sub_2BE0 |
mprotect |
按 segment flags 二次设权 |
| sub_2A90 |
memcpy |
回写或复制元数据 |
| 函数 |
API/语义 |
用途 |
| sub_2B30 |
dlsym |
从依赖库 handle 查符号 |
| sub_2A50 |
dlerror |
读取符号解析错误 |
| sub_2AF0 |
strstr |
判断错误是否为非全局符号 |
| sub_2CE8 |
ELF SysV hash |
hash fallback |
| sub_2B80 |
strcmp |
确认候选符号名 |
| sub_2AD0 |
abort |
致命错误出口 |
| 地址 |
IDA 名称 |
大小 |
归属图层 |
作用 |
0x2c00 |
IDA 命名 start |
0x0c |
fini 生命周期 |
实际是 fini bridge,调用 __cxa_finalize(dso_handle) |
0x2c0c |
sub_2C0C |
0xdc |
解码图 |
单字节 XOR 解码 helper |
0x2ce8 |
sub_2CE8 |
0xd8 |
relocation/JNI 图 |
ELF SysV hash helper |
0x2dc0 |
sub_2DC0 |
0x4b4 |
relocation 图 |
relocation 处理器 |
0x36e0 |
sub_36E0 |
0x1398 |
主 loader 图 |
手动 loader 主体 |
0x4a78 |
sub_4A78 |
0x50c |
init 门卫图 |
自定位、maps 检测、失败终止和进入 loader |
0x4f84 |
sub_4F84 |
0x18 |
fini 生命周期 |
__cxa_atexit 注册桥 |
| 地址 |
IDA 名称 |
大小 |
归属图层 |
作用 |
0x2a30 |
sub_2A30 |
0x14 |
导入跳板区 |
PLT0 resolver 入口,所有导入跳板最终经它或相邻 GOT 槽跳转 |
0x2a50 |
sub_2A50 |
0x10 |
relocation 图 |
dlerror,在 dlsym 后读取错误信息 |
0x2a60 |
sub_2A60 |
0x10 |
sub_4A78 失败链 |
errno 存储位置,处理失败 syscall 返回 |
0x2a70 |
sub_2A70 |
0x10 |
sub_4A78 失败链 |
getpid,给 kill syscall 提供 pid |
0x2a80 |
sub_2A80 |
0x10 |
maps 读取链 |
fgets,逐行读取 /proc/self/maps |
0x2a90 |
sub_2A90 |
0x10 |
sub_36E0 收尾 |
memcpy,复制/回写运行时元数据 |
0x2aa0 |
sub_2AA0 |
0x10 |
fini 生命周期 |
__cxa_finalize,析构收口 |
0x2ab0 |
sub_2AB0 |
0x10 |
sub_36E0 loader |
lseek,调整 fd 偏移 |
0x2ac0 |
sub_2AC0 |
0x10 |
sub_36E0 loader |
mmap,映射 header window 和完整装载镜像 |
0x2ad0 |
sub_2AD0 |
0x10 |
relocation 图 |
abort,relocation 致命错误出口 |
0x2ae0 |
sub_2AE0 |
0x10 |
辅助/保护 |
__stack_chk_fail,栈保护失败出口 |
0x2af0 |
sub_2AF0 |
0x10 |
relocation 图 |
strstr,判断 dlerror 是否含非全局符号提示 |
0x2b00 |
sub_2B00 |
0x10 |
sub_36E0 loader |
pread,读取隐藏材料或 segment 数据 |
0x2b10 |
sub_2B10 |
0x10 |
init/loader 依赖链 |
dlopen,打开 libc 和 DT_NEEDED 依赖 |
0x2b20 |
sub_2B20 |
0x10 |
fini 生命周期 |
__cxa_atexit,注册析构回调 |
0x2b30 |
sub_2B30 |
0x10 |
init/relocation 图 |
dlsym,解析 sscanf 和依赖符号 |
0x2b40 |
sub_2B40 |
0x10 |
maps 读取链 |
fopen,打开 /proc/self/maps |
0x2b50 |
sub_2B50 |
0x10 |
sub_36E0 loader |
memset,清理工作缓冲 |
0x2b60 |
sub_2B60 |
0x10 |
maps 读取链 |
fclose,关闭 maps 文件句柄 |
0x2b70 |
sub_2B70 |
0x10 |
dump 辅助 |
isprint,判断可打印字符 |
0x2b80 |
sub_2B80 |
0x10 |
relocation/JNI 图 |
strcmp,确认符号名 |
0x2b90 |
sub_2B90 |
0x10 |
dump 辅助 |
sprintf,格式化十六进制 dump 行 |
0x2ba0 |
sub_2BA0 |
0x10 |
sub_36E0 loader |
mprotect,调整装载镜像权限 |
0x2bb0 |
sub_2BB0 |
0x10 |
sub_4A78 失败链 |
_Exit,kill 后备退出 |
0x2bc0 |
sub_2BC0 |
0x10 |
dump 辅助 |
strlen,取格式化字符串长度 |
0x2bd0 |
sub_2BD0 |
0x10 |
sub_36E0 loader |
pread,早期读取 0x80000 级隐藏材料 |
0x2be0 |
sub_2BE0 |
0x10 |
sub_36E0 loader |
mprotect,按 program header flags 设置页面保护 |
0x2bf0 |
sub_2BF0 |
0x10 |
sub_36E0 收尾 |
close,关闭 fd/资源 |
.init_array[0] -> sub_4A78@0x4d1c
-> maps 检测
-> sub_36E0 主 loader
-> sub_2DC0 relocation
-> JNI_OnLoad / fini bridge
Android linker
-> .init_array[0]
-> sub_4A78@0x4d1c
-> 找自身 ELF base
-> dlopen("libc.so")
-> dlsym("sscanf")
-> 递归进入 sub_4A78@0x4a78
-> fopen("/proc/self/maps")
-> fgets maps 行
-> 匹配 libc.so / libdl.so / linker
-> sscanf 解析权限
-> 命中小型 rwx 映射: kill/_Exit 分支
-> 未命中: sub_36E0 主 loader
-> 读取隐藏材料
-> mmap 新镜像
-> sub_2C0C XOR 解码
-> 解析 ELF header / program header / dynamic section
-> dlopen DT_NEEDED
-> sub_2DC0 relocation
-> sub_2CE8 + strcmp 查 JNI_OnLoad
-> init/JNI_OnLoad/析构注册/资源关闭
0x2a30..0x2bf0 28 个导入跳板/PLT 封装
0x2c00 IDA 命名 start,实际是 fini bridge
0x2c0c XOR 解码 helper
0x2ce8 ELF SysV hash helper
0x2dc0 relocation 处理器
0x36e0 主 loader
0x4a78 init 门卫 + maps 检测 + kill 分支
0x4f84 atexit 注册桥
0x2c00 adrl x0, unk_16000
0x2c08 b sub_2AA0
0x4f84 mov x1, x0
0x4f88 adrp x2, unk_16000
0x4f8c adrp x0, loc_4F6C
0x4f98 b sub_2B20
0x4dd8 adrp x3, n70
0x4ddc adrp x0, "libc.so"
0x4de0 mov w1,
0x4de8 str x2, [x3,
0x4dec bl sub_2B10 ; dlopen
0x4df0 adrl x1, "sscanf"
0x4df8 bl sub_2B30 ; dlsym
0x4e00 str x0, [off_16040]
0x4e04 bl sub_4A78 ; enter detector state
fp = fopen("/proc/self/maps", "r");
while (fgets(line, 1024, fp)) {
if (line_path_contains("libc.so") ||
line_path_contains("libdl.so") ||
line_path_contains("linker")) {
sscanf(line, "%lx-%lx %c%c%c...", &start, &end, &r, &w, &x);
if (r == 'r' && w == 'w' && x == 'x' && end - start <= 0x4000) {
return 1;
}
}
}
fclose(fp);
return 0;
0x4e04 bl sub_4A78
0x4e08 uxtb w0, w0
0x4e0c cbnz w0, loc_4F38
0x4f38 bl sub_2A70
0x4f3c sxtw x0, w0
0x4f40 mov x1,
0x4f44 mov x8,
0x4f48 svc
0x4f4c cmn x0,
0x4f50 mov x19, x0
0x4f54 b.ls loc_4F64
0x4f58 bl sub_2A60
0x4f5c neg w19, w19
0x4f60 str w19, [x0]
0x4f64 mov w0,
0x4f68 bl sub_2BB0
0x4f74 cbz x0, loc_4F7C
0x4f78 blr x0
0x4f7c ret
0x4ee8 bl sub_36E0
init array 入口
-> 自定位
-> 初始化 sscanf
-> maps 小型 rwx 检测
-> 失败: kill/_Exit
-> 通过: sub_36E0 主 loader
定位自身映射
-> 读取隐藏材料和 XOR key
-> mmap 头部窗口并解码
-> 遍历 program header,建立完整装载镜像
-> 解析 dynamic section,收集表地址和依赖项
-> 两轮 relocation
-> 查找并调用 JNI_OnLoad,注册析构并收尾
0x3844..0x3910
0x3914..0x39a8
0x39a8..0x3b8c
char *sub_2C0C(base, offset, size) {
p = base + offset;
for (i = 0; i < size; i++) {
p[i] ^= dword_16088;
}
return p;
}
0x3c20..0x43c8
0x4458..0x480c
0x44b0..0x4a1c
0x4838 -> sub_2DC0
0x4858 -> sub_2DC0
a1: dynsym/symbol table 基址或条目基址
a2: 字符串表/相关基址
a3: relocation 表
a4: relocation 数量
a5: dlopen handle 数组
a6: handle 数量
a7: 新映射基址/load bias
for each relocation:
1. 解析 relocation offset/info/addend
2. 从 info 拆出 symbol index 和 relocation type
3. 若 relocation 需要符号:
name = strtab + sym.st_name
for handle in dependency_handles:
addr = dlsym(handle, name)
if addr found:
use addr
else:
err = dlerror()
if strstr(err, "symbol found but not global:"):
走 ELF hash bucket/chain fallback
4. 按 relocation type 写目标地址
257 / 258 / 259: 目标位置 += addend + symbol_value
261 / 262: 目标位置 += addend - target_offset + symbol_value
1024 / 1025: 目标位置 = addend + symbol_value
1026: 目标位置 = addend + symbol_value
1027: 目标位置 = addend + load_bias
h = 0;
while (*s) {
h = *s++ + 16 * h;
g = h & 0xf0000000;
h = (g ^ h) ^ (g >> 24);
}
return h;
ADRP X16,
LDR X17, [X16,
ADD X16, X16,
BR X17
0x2a30 stp x16, x30, [sp,
0x2a34 adrp x16, qword_15F10
0x2a38 ldr x17, [x16,
0x2a3c add x16, x16,
0x2a40 br x17