首页
社区
课程
招聘
[原创]APP加固还原dexvmp
发表于: 2026-8-5 03:29 46506

[原创]APP加固还原dexvmp

2026-8-5 03:29
46506

3年前刚刚接触安卓逆向的时候就遇到了APP加固,当时的视频教程也偏少都是基于热修复和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的注册项(APP加固默认抽取这个好像),还不能直接宣布找到了它的 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 到这里时:

IDA 中的静态代码:


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 2026-8-12 11:24 被kanxue编辑 ,原因:
上传的附件:
收藏
点赞 ・227
打赏
分享
最新回复 (138)
雪    币: 306
活跃值: (1659)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2 楼
真牛啊,研究了多久
2026-8-5 03:42
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3 楼
1
2026-8-5 04:51
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4 楼
太强了,向大佬学习
2026-8-5 06:32
0
雪    币: 487
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5 楼
666
2026-8-5 07:27
0
雪    币: 5365
活跃值: (3455)
能力值: (RANK:180 )
在线值:
发帖
回帖
粉丝
6 楼
很强,很不错的学习帖子。感谢分享
2026-8-5 07:53
0
雪    币: 2247
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7 楼
牛逼啊
2026-8-5 08:48
0
雪    币: 573
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8 楼
牛逼克拉斯
2026-8-5 08:49
0
雪    币: 2769
活跃值: (3986)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9 楼
666
2026-8-5 09:19
0
雪    币: 221
活跃值: (1478)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10 楼
666
2026-8-5 09:26
0
雪    币: 277
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11 楼
666
2026-8-5 09:37
0
雪    币: 325
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12 楼
666
2026-8-5 09:41
0
雪    币: 831
活跃值: (9556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13 楼
谢谢分享,直接把文件交给ai学习
2026-8-5 09:49
0
雪    币: 78
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14 楼
6666
2026-8-5 09:54
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15 楼
666
2026-8-5 09:58
0
雪    币: 1901
活跃值: (5923)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
16 楼
123
2026-8-5 10:06
0
雪    币: 85
活跃值: (1785)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17 楼
mark
2026-8-5 10:06
0
雪    币: 267
活跃值: (1473)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18 楼
666
2026-8-5 10:15
0
雪    币: 739
活跃值: (2175)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
19 楼
66666666666
2026-8-5 10:18
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20 楼
666
2026-8-5 10:24
0
雪    币: 3907
活跃值: (7431)
能力值: ( LV12,RANK:200 )
在线值:
发帖
回帖
粉丝
21 楼
999
2026-8-5 10:51
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22 楼
666
2026-8-5 10:56
0
雪    币: 24
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
23 楼
665
2026-8-5 11:02
0
雪    币: 110
活跃值: (9571)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24 楼
666
2026-8-5 11:13
1
雪    币: 1247
活跃值: (958)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25 楼
6969
2026-8-5 11:14
0
游客
登录 | 注册 方可回帖
返回