逆向只是我的一个乐趣而已,风控才有意思欧,ios流量太少了来安卓圈子玩玩,写的不好多担当打开一看 插件好像秒不了,无语。在逆向重度混淆样本时,不要一来f5。按"由表及里、由元信息到代码"的层级逐级穿透:这个顺序非常重要。 30 秒的段表扫描能告诉你的信息量,远超在错误的函数上花 3 天做 F5。用 readelf -l 看 PT_LOAD 段:整理成表:几个立即能得出的结论:在 IDA 里看 .init_array:.init_array[0] = 0x55C1 基本只做 stack canary / 版本噪声检查;.init_array[1] = 0x7739 更有意思——它会做一批环境路径探测:这些是自动化测试工具(AutoJS、按键精灵、触动精灵)和 root 框架的特征目录。 .init_array 阶段就开始做环境检测,比 JNI_OnLoad 更早。这是本样本最有信息量的元信息之一。一个 643KB 的 SO 文件,导入表里只有 24 个符号。正常的 Android native 库(同等大小)通常有 100-300 个导入符号。24 个符号意味着:这个库极度自包含,几乎不依赖外部 libc 函数。 它很可能在内部实现了自己的内存管理、字符串处理、甚至系统调用封装。事实上,后续分析确认了这一点——target.so 使用 SVC #0 直接系统调用,绕过 libc wrapper,让 strace 级别的 hook 失效。更关键的是:没有 Java_* 导出符号。 这意味着 JNI native method 的注册不是通过标准的静态绑定(函数名匹配),而是通过 JNI_OnLoad 里的 RegisterNatives 动态注册。后续的 unidbg 实验证实了这一点。在 IDA 的 Functions 窗口里按大小排序,sub_275CC 以 1404 条 ARM 指令(约 5.6KB)排在前列。对于一个混淆库来说,单个函数 1404 条指令不算离谱,但它有两个异常特征:这个模式非常像解释器——同一个执行引擎,不同的字节码程序。打开 sub_275CC 的反汇编,主循环的核心 dispatch 序列如下:0x27638 的 LDR R5, [R0] 是 VM 的 fetch 指令——它从当前 VM PC(R0)处读取一个 32-bit bytecode word 到 R5。这个地址是后续所有动态 hook 的主锚点。0x276C4 的 ADD PC, LR, R1 是 dispatch 的核心——它是一个 computed goto,直接跳转到 dispatch_table[opcode] 对应的 handler。这里 LR 不是返回地址,而是被预设为 dispatch 表基址附近的地址。注意一个容易犯的错误: dispatch 表的起点是 0x276C8(ADD PC 的下一个 4 字节对齐地址),而不是 0x276C4。如果误把 0x276C4 当表起点,所有 handler 地址会整体偏 4 字节,后续还原全部会错。每个 handler 执行完毕后,会跳回 0x27638 重新 fetch 下一条指令,形成经典的 fetch-decode-execute 循环。ADD PC, LR, R1 的下一条指令地址是 0x276C8,这就是 dispatch 表的起点。表中每个条目是一个 4 字节的偏移量,加上 0x276C4(ADD PC 指令的地址)就得到对应 handler 的入口地址。注意一个容易犯的错误: dispatch 表的起点是 0x276C8 而不是 0x276C4。如果误把 0x276C4 当表起点,所有 handler 地址会整体偏 4 字节,后续还原全部会错。提取出的 64 条 dispatch 表:64 个 opcode 中有 29 个指向同一个 DEFAULT handler,这意味着实际有效的指令类型只有 13 种。29 个 DEFAULT opcode 的存在不是浪费——它们是 opcode 空间膨胀的结果,让逆向者无法简单地通过 opcode 出现频率来推断指令语义。target.so 里有三份几乎完全相同的解释器实现:用脚本做 byte-level 对比:差异全部集中在 BL(Branch with Link)指令的立即数偏移字段——因为 BL 使用 PC-relative 寻址,三份克隆在不同的 .text 地址上,所以同一个目标函数的偏移量自然不同。反汇编后,三份 BL 的目标地址完全相同。克隆 3 有 0 个交叉引用——它是死代码。 没有任何函数调用它、没有任何指针引用它。它存在的唯一目的是:让逆向者在找到它之后多花一周时间分析,最后发现它根本不被执行。 8KB × 2 份无用克隆 = 16KB 的 .text 空间,换来的是逆向者的时间成本。这也从侧面证实了"通用 IR 解释器"假说——只有解释器才值得被克隆 3 份。如果是普通的算法函数,克隆它没有意义。每条 VM 指令是一个 32-bit word。通过分析 sub_275CC 中各 handler 对指令 word 的位提取操作,还原出以下字段布局:对应的 native 汇编(在 OP11 handler 0x27F30 附近):Python 版的解码器:dst 字段的编码方式值得注意——它不是连续的 5 位,而是 bit[12:11] 拼上 bit[31]。这种非标准的字段拆分是 VMP 设计者有意为之,让自动化反汇编工具难以正确提取寄存器号。VM 有 32 个通用寄存器(R0-R31)和一个 64-bit 累加器(acc_lo + acc_hi),累加器主要用于 div/mod 运算的商和余数。frame 布局从 sub_275CC 的入口参数推导:这个公式是后续所有动态观测的基础。算错 frame 布局,所有寄存器值都会变成假数据。在 13 种 handler 中,OP11(primary = 11)是最复杂的一个。它不是一个单一操作,而是一个二级路由器——通过 ext 字段(word 的低 12 位)选择具体的 ALU 操作。OP11 handler 的入口 native 汇编(0x27F30):UDIVMOD handler 的关键实现(0x293E0 附近):这里 [R7, #-0x18] 和 [R7, #-0x14] 就是 acc_lo 和 acc_hi 在 frame 中的位置。sub_89AB0 是 compiler runtime 的 __aeabi_uidiv(unsigned integer division)。MLS 是 "Multiply and Subtract":R1 = R3 - R0 * R2,即余数。关键的 ext 值及其语义:反直觉点: 标准的 MAC/Hash 主循环通常不需要除法操作。0x54B UDIVMOD 的存在说明这不是传统的密码学算法——它更像是一个自定义的 byte-index mixing 方案,用除法余数作为表索引。在分析过程中发现了一条极其特殊的指令——ext = 0xF8B。它的表面行为像是一个间接跳转(PC
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291336.htm
某反作弊 VMP Native 层深度逆向:从 643KB 混淆 SO 到 RC4-like Mixer 的完整穿透路径
449 浏览
13 回复
感谢分享
看看
学习一下
大佬能出一篇ollvm混淆的么? 或者有已经好的ndk,我自己在编译ollvm,用的时候会有错误,导致有一些东西用不了混淆,我的是windows,
感谢分享
大佬的思路清晰,分析手法纯熟,值得学习
看看
感谢分享
tql
感谢分享
学习
看看
看看