首页
社区
课程
招聘
[原创]数字加固还原dexvmp
发表于: 1小时前 192

[原创]数字加固还原dexvmp

1小时前
192

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_STRTABDT_SYMTABDT_STRSZDT_SYMENT hash table

6. 按页写出映射,不可读页才填零并计数


本次执行linker_dump.js脚本的json数据,

zeroFilledBytes=0 说明预期映射全部可读;syntheticHeader=true说明 ELF 头是根据 private soinfo 恢复的

各字段来自下面这些运行时数据

使用SoFixer 修复和验证,关键结果如下

可见符号包括 interpreter_wrap_int64_tinterpreter_wrap_floatinterpreter_wrap_doubleJNI_OnLoadgetSoName2makekey

我们把修复好的so拖入ida验证一下,看看符号表是否完全显示出来

至此linker so修复完成

Dex 中已经看不到原方法体,凭什么知道本次被抽取的方法是 onCreate

答案不是MainActivity 通常都有 onCreate,而是从 Manifest 只能得到包名和启动 Activity 类名 com.flass.myapplication.MainActivity,它只能缩小观察范围,不能证明被抽取的是哪个方法。真正的方法名、签名和 native 入口来自壳运行时提交给 ART RegisterNatives 方法表。



完整的发现顺序如下

也就是说,RegisterNatives 负责发现候选方法,后面的 closuredispatchermethod record Dex 字符串负责证明这个候选确实是本文正在 trace 的被抽取方法。只看到一个名为 onCreate的注册项(数字加固默认抽取这个好像),还不能直接宣布找到了它的 CodeItem


壳不会保证业务 native 地址出现在静态导出表中,但无论它怎样隐藏导出符号,最终都要向 ART 提交类似下面的数组。每一项同时给出方法名、JNI 签名和运行时函数地址

壳不会保证业务 native 地址出现在静态导出表中,但无论它怎样隐藏导出符号,最终都要向 ART 提交类似下面的数组。每一项同时给出方法名、JNI 签名和运行时函数地址

下面就是使用 Frida 侦察脚本,它只 attach 标准的 JNIEnv->RegisterNatives来读取注册表,不 attachreplace 或主动调用表中的 fnPtr动态 trampoline。为了减少 Android 框架和第三方库日志,使用 Manifest 包名 com.flass.myapplication作为类名前缀过滤条件;不负责选择 MainActivityonCreate。循环仍会把该包下每个类、每个方法名、每种签名和对应 fnPtr全部打印出来

这里还要说明过滤范围:Java 类不一定都位于 APK applicationId 包名前缀下。如果目标 APK 的业务类分散在其他包,或者目标是清点全部被保护方法,应当扩大 packagePrefix、改成多个前缀,或临时取消此前缀过滤,再根据 jclassfnPtr 所在内存和后续 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.BundleL开始、包名用 / 分隔; 结束。(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=closuredispatcher-linker=0x53B24fn+0x80的指针读取发生在脚本中,日志保存了读取结果,但没有保存该地址处的 8-byte 原始 Hexdump,因此这里只引用实际输出的指针值

动态 fn是约 0x88字节的 trampoline

本次值为

因此fnPtrclosuredispatcher linker base 已经连在同一条运行时关系上

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

这里的校验顺序是:

1. RegisterNatives给出精确三元组和fn=0x6fbbe64000

2. fn+0x6C=0x6fbbe6406cdispatcher 0x53BA4时,寄存器 X24也等于该 closure,脚本才开启活跃 trace 区间。

3. 同一活跃区间中的 `X20` method recordX23DexFilerecord+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 初始化逻辑”

它依次给出注册三元组和 fnPtrtrampolinedispatcherclosuremethod recordCodeItem 以及从同一 Dex 上下文解析出的类名和方法名

需要注意不要直接 hook trampoline早期直接Interceptor.attach(fn)后进入:

trampoline 依赖调用点、寄存器、TLS、栈和一次性状态。最终脚本只读取其中的 closure/dispatcher,不 attach、不 replace、不主动调用 trampoline

AndroidPrybar trace 是一个我觉得非常好用的trace工具,这里也可以使用其他的trace工具,不使用unidbg的原因是补环境太麻烦且费时间

libtrace.soAndroidPrybar 的设备端执行引擎,封装了 Unicorn vc_make_handle/vc_hook_add/vc_reg_read等接口。用它把 dispatcher 放进可逐指令观察的环境,找出 0x53BA40x7CD4C0x61F0C0x55618等稳定位置

so 大量使用控制流由双入口表和间接跳转拼成,单看 F5 很难判断哪条路径属于 onCreateAndroidPrybar dispatcher 放进 Unicorn,逐条输出实际经过的 ARM64 指令与关键寄存器

创建 dispatcher wrapper

trace结果如下

741954 1955 行把 method record 就绪点、decoder 调用点和 decoder 入口连在同一条真实控制流上

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

注册VC_HOOK_CODEVC_HOOK_EXTERNAL_JUMP,输出每条指令和分支处的 X0/X1/X2/X8/X19..X24

本次记录6331ARM64 指令。开头

结尾

下面是 1951 1991 行中的原始 AndroidPrybar 执行 trace,中间省略的是同一文件中 0x7CD5C..0x7CDA0decoder 内部指令。

调用点与 decoder 入口

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

日志的时间顺序给出四个直接事实:0x61F08的实际目标是 0x7CD4Cdecoder 末尾把查表结果收窄到 W0,本次返回值为 0x13,下一条实际执行指令是 0x61F0C的比较节点。

由此确定:

0x53B24dispatcher 入口。

0x53BA4method record 稳定点。

0x7CD4C:虚拟 opcode decoder

0x61F0Chandler 分发表根。

0x55618dispatcher 正常 RET

linker是我的so名称,dispatcher 入口 linker+0x53B24

结合动态寄存器:

method record 稳定点 linker+0x53BA4,

AndroidPrybar trace 到这里时:


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

上传的附件:
收藏
免费 9
打赏
分享
最新回复 (2)
雪    币: 306
活跃值: (1332)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
真牛啊,研究了多久
1小时前
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
1
26分钟前
0
游客
登录 | 注册 方可回帖
返回