3年前刚刚接触安卓逆向的时候就遇到了数字加固,当时的视频教程也偏少都是基于热修复和hook实现对目标的修改,就像是刚刚接触游戏的新手就遇到大boss的既视感,接下来的分析过程如果有问题还请大佬指正,第一次发帖不太熟悉和篇幅的原因完整版可以查看附件,请大家谅解
使用的工具有:frida,ida,mt管理器,Layout Inspect模块(脱dex),AndroidPrybar 工具,SoFixer
APK/壳 Dex
| 确认 StubApp、目标 Activity、加固资产(asstes目录下出现libjiagu.so)
v
libjiagu_a64.so
| 卡在私有 linker 完成装载、目标 JNI_OnLoad 尚未执行的时刻(这里有差异化,老版本会在so的外层套个魔改upx)
v
内存 linker.so + private soinfo
| 合成 ELF header,回填 private dynamic table,SoFixer 修复
v
linker_fixed.so / IDA
| RegisterNatives -> trampoline -> dispatcher
| method record -> CodeItem
| opcode decoder -> dispatch tree -> handlers -> operand accessors
v
AndroidPrybar ARM64 trace
| 确认真实执行顺序与稳定 hook RVA
v
Frida 原始执行 trace
| CodeItem header + VM PC + opcode + 全部 code units + DEX/JNI 符号
v
重建器
| 连续覆盖验证、CodeItem/DebugInfo 输出、SHA-256
v
可验证 Smali
样本的话是26年5月份加固的,因为这个demo结构比较简单,更容易对照


本次我们主要分析的so为libjiagu_a64.so,前面也是说过了,旧版本在linker的基础上加了魔改的upx需要进行一次dump的操作,但是这个样本没有upx但是我还是按照习惯dump了一遍
这里我讲的不是很好,详细的可以看看逆天而行大佬的这篇帖子[原创]某加固so层脱壳-Android安全-看雪安全社区|专业技术交流与安全研究论坛
打开ida进行分析,发现了如下代码
精准时机:
先进行第一遍的dump,脚本配置为
handleGlobalOffset=0x291B08 正是 ADRP X22,#0x291000 与 [X22,#0xB08] 的组合。
这里有个小插曲,为什么第一次handle为null呢?
这是因为hook 地址本身是 `LDR X0,[X22,#imm]`。Frida 进入 onEnter时,被 hook 指令尚未执行,所以不能直接读 X0。应在安装 hook 前解码原指令,回调中手工读取 X22+imm,从固定全局 libjiagu+0x291B08读取,以及使用 libjiagu+0x5240 私有加载函数的已缓存返回值。
如果找不到 7F 45 4C 46,执行:
1. 根据 phdr/phnum 解析 `Elf64_Phdr[]。
2. 计算所有 PT_LOAD 的 image span。
3. 合成 Elf64_Ehdr,架构 EM_AARCH64、类型 ET_DYN。
4. 把 private soinfo+0x100 保存的 Elf64_Dyn[] 覆盖回 PT_DYNAMIC。
5. 检查 DT_STRTAB、DT_SYMTAB、DT_STRSZ、DT_SYMENT 与 hash table。
6. 按页写出映射,不可读页才填零并计数
本次执行linker_dump.js脚本的json数据,
zeroFilledBytes=0 说明预期映射全部可读;syntheticHeader=true说明 ELF 头是根据 private soinfo 恢复的
各字段来自下面这些运行时数据

使用SoFixer 修复和验证,关键结果如下
可见符号包括 interpreter_wrap_int64_t、interpreter_wrap_float、interpreter_wrap_double、JNI_OnLoad、getSoName2、makekey
我们把修复好的so拖入ida验证一下,看看符号表是否完全显示出来

至此linker so修复完成
壳 Dex 中已经看不到原方法体,凭什么知道本次被抽取的方法是 onCreate?
答案不是MainActivity 通常都有 onCreate,而是从 Manifest 只能得到包名和启动 Activity 类名 com.flass.myapplication.MainActivity,它只能缩小观察范围,不能证明被抽取的是哪个方法。真正的方法名、签名和 native 入口来自壳运行时提交给 ART 的 RegisterNatives 方法表。

完整的发现顺序如下
也就是说,RegisterNatives 负责发现候选方法,后面的 closure、dispatcher、method record 和 Dex 字符串负责证明这个候选确实是本文正在 trace 的被抽取方法。只看到一个名为 onCreate的注册项(数字加固默认抽取这个好像),还不能直接宣布找到了它的 CodeItem
壳不会保证业务 native 地址出现在静态导出表中,但无论它怎样隐藏导出符号,最终都要向 ART 提交类似下面的数组。每一项同时给出方法名、JNI 签名和运行时函数地址
壳不会保证业务 native 地址出现在静态导出表中,但无论它怎样隐藏导出符号,最终都要向 ART 提交类似下面的数组。每一项同时给出方法名、JNI 签名和运行时函数地址
下面就是使用 Frida 侦察脚本,它只 attach 标准的 JNIEnv->RegisterNatives来读取注册表,不 attach、replace 或主动调用表中的 fnPtr动态 trampoline。为了减少 Android 框架和第三方库日志,使用 Manifest 包名 com.flass.myapplication作为类名前缀过滤条件;不负责选择 MainActivity或 onCreate。循环仍会把该包下每个类、每个方法名、每种签名和对应 fnPtr全部打印出来
这里还要说明过滤范围:Java 类不一定都位于 APK 的 applicationId 包名前缀下。如果目标 APK 的业务类分散在其他包,或者目标是清点全部被保护方法,应当扩大 packagePrefix、改成多个前缀,或临时取消此前缀过滤,再根据 jclass、fnPtr 所在内存和后续 dispatcher 关系筛选。本文的 com.flass.myapplication`只适用于当前样本,不能当成通用的全部业务类判断规则,逆向不同版本的加固样本需要注意
如果注册表里出现多个 onCreate,不能按名字选第一个,也不能用一个 fnPtr覆盖另一个。DEX 方法的基本身份是类描述符 + 方法名 + 完整签名,例如下面三项是三个不同的方法:
处理多项时按下面的规则执行

大致是这个样子
Frida attach 到这个地址以后,只要进程调用 RegisterNatives,就能在函数入口读取这一次注册的四个参数。arm64 传递函数参数时,前几个参数优先放入寄存器;对这个函数来说,对应关系如下

Frida 回调中的 args[0]、args[1]、args[2]、args[3]正好对应这四个值。拿到 X2 和 W3后,脚本便能逐项读取方法表。arm64 下每项由三个 8-byte 指针组成,总长度是 0x18 bytes

类名由 X1中的 jclass 解析。脚本先用包名前缀 com.flass.myapplication 排除无关类;类名命中后,再无差别遍历该类 methods[] 中的全部项目,不按方法名和签名提前筛选。因此日志中同时出现下面四个字段时,才能确认本次样本实际注册的方法是 MainActivity.onCreate
其中 fn 是该次进程中的动态地址,会随 ASLR 和重新启动而变化;class/name/signature才是用来确定目标 Java 方法的完整描述。编译器也可能通过 wrapper 或拆分后的地址计算调用 RegisterNatives
执行 run7脚本进行第一次trace的日志如下,这条日志先于最终 hook 安装和 [METHOD-ENTER]出现。它不是从 CONFIG.methodName拼出的文本:脚本先逐项读取 JNINativeMethod[]并打印,再在打印之后才比较是否匹配最终目标

把注册三元组写成完整方法描述符,其中[REGISTERED-METHOD]同时给出 `class/name/signature/fn



DEX/JNI 描述符中,Landroid/os/Bundle;表示对象类型 android.os.Bundle:L开始、包名用 / 分隔; 结束。(Landroid/os/Bundle;)V因而表示“接收一个 Bundle,返回 void”。
用反编译工具的写法就是com.flass.myapplication.MainActivity->onCreate(Landroid/os/Bundle;)V
只是把 class + name + signature三个运行时字段合并成一行,便于和 Smali/DEX 工具的显示方式对应;`->` 不是日志中额外读出的字段。到这一步,onCreate由动态注册表证明的更加精确
如果同一业务包注册了多个方法,应全部保留,再根据研究目标分别建立映射,不能因为某个名字像入口就丢掉其他项。本文这次恢复选择该条记录,是因为它同时满足:注册类与 Manifest 的启动 Activity 一致,方法名和签名由 ART 注册表直接给出,随后还能与同一 fn 的 closure 和 method record 对上
将发现结果填入最终 trace 配置

RegisterNatives hook 仍会先打印每一项,然后才用精确三元组选择对应 fn

这个顺序很重要,打印行是原始注册表证据,比较语句只是使用证据选择目标
执行trace后从 fnPtr 取得 closure 和公共 dispatche

由日志可直接核算 fn+0x6C=closure和 dispatcher-linker=0x53B24。fn+0x80的指针读取发生在脚本中,日志保存了读取结果,但没有保存该地址处的 8-byte 原始 Hexdump,因此这里只引用实际输出的指针值
动态 fn是约 0x88字节的 trampoline

本次值为

因此fnPtr、closure、dispatcher 和 linker base 已经连在同一条运行时关系上
用 closure、method record 和 Dex 字符串闭合身份链:仅看到 [REGISTERED-METHOD]还只能证明 ART 把该 Java 方法绑定到了fnPtr。还要证明后续 trace 的 CodeItem 确实由这个 fnPtr对应的方法进入,而不是公共 dispatcher 中另一个方法的状态,连续输出中按身份链摘出下面这些原始行,这里只省略两行之间与身份判断无关的 opcode/JNI 日志

这里的校验顺序是:
1. RegisterNatives给出精确三元组和fn=0x6fbbe64000。
2. fn+0x6C=0x6fbbe6406c;dispatcher 到 0x53BA4时,寄存器 X24也等于该 closure,脚本才开启活跃 trace 区间。
3. 同一活跃区间中的 `X20` 是 method record,X23是 DexFile;record+8给出 `code_off=0xB94。
4. linker 紧接着从该 Dex 上下文解析出类描述符 `Lcom/flass/myapplication/MainActivity;和方法名 onCreate,与动态注册表再次吻合。
5. ins_size=2与实例方法的 this + Bundle`两个入参寄存器相容,但它只作结构交叉检查,不单独用于判断方法身份。

至此才能准确说:后文恢复的 49 个 code units 属于 com.flass.myapplication.MainActivity->onCreate(Landroid/os/Bundle;)V,而不是“因为它看起来像 Activity 初始化逻辑”
它依次给出注册三元组和 fnPtr、trampoline、dispatcher、closure、method record、CodeItem 以及从同一 Dex 上下文解析出的类名和方法名
需要注意不要直接 hook trampoline,早期直接Interceptor.attach(fn)后进入:
该 trampoline 依赖调用点、寄存器、TLS、栈和一次性状态。最终脚本只读取其中的 closure/dispatcher,不 attach、不 replace、不主动调用 trampoline。

AndroidPrybar trace 是一个我觉得非常好用的trace工具,这里也可以使用其他的trace工具,不使用unidbg的原因是补环境太麻烦且费时间
libtrace.so是 AndroidPrybar 的设备端执行引擎,封装了 Unicorn 和 vc_make_handle/vc_hook_add/vc_reg_read等接口。用它把 dispatcher 放进可逐指令观察的环境,找出 0x53BA4、0x7CD4C、0x61F0C和0x55618等稳定位置
so 大量使用控制流由双入口表和间接跳转拼成,单看 F5 很难判断哪条路径属于 onCreate。AndroidPrybar 把 dispatcher 放进 Unicorn,逐条输出实际经过的 ARM64 指令与关键寄存器
创建 dispatcher wrapper

trace结果如下
第 74、1954 至 1955 行把 method record 就绪点、decoder 调用点和 decoder 入口连在同一条真实控制流上

日志最后一行证明 wrapper 正常走到 dispatcher 返回点

注册VC_HOOK_CODE与 VC_HOOK_EXTERNAL_JUMP,输出每条指令和分支处的 X0/X1/X2/X8/X19..X24
本次记录6331条 ARM64 指令。开头

结尾

下面是 第 1951 至 1991 行中的原始 AndroidPrybar 执行 trace,中间省略的是同一文件中 0x7CD5C..0x7CDA0的 decoder 内部指令。
调用点与 decoder 入口

decoder 生成 0x13,返回后立即进入分发表

日志的时间顺序给出四个直接事实:0x61F08的实际目标是 0x7CD4C,decoder 末尾把查表结果收窄到 W0,本次返回值为 0x13,下一条实际执行指令是 0x61F0C的比较节点。
由此确定:
0x53B24:dispatcher 入口。
0x53BA4:method record 稳定点。
0x7CD4C:虚拟 opcode decoder。
0x61F0C:handler 分发表根。
0x55618:dispatcher 正常 RET。
linker是我的so名称,dispatcher 入口 linker+0x53B24
结合动态寄存器:

method record 稳定点 linker+0x53BA4,
AndroidPrybar trace 到这里时:
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。