本文仅记录一次针对移动端 native 签名逻辑的逆向分析过程,用于安全研究、算法学习和逆向工程方法论交流。文中涉及的脚本、地址和结论均来自本地样本与模拟环境验证,不讨论任何绕过风控、批量请求或业务滥用场景。
这次分析的目标是某视频 App 29.3 版本中的一个 native 签名头:X-Medusa。
样本位于 Android native so:
外层调用函数是:
这个函数会一次性生成多个签名头:
本文重点只讲 X-Medusa。
我最终将 X-Medusa 主路径还原成了纯 Python,可以在不启动 native VM 的情况下,只输入同一次运行的动态值,生成和 native 一致的 X-Medusa。
整个过程并不是一开始就直接进入算法还原。前半段我先用 Cursor 的 Opus 模型搭建和调通ExAndroidNativeEmu 调用环境,它帮我把 sub_D4F3C 的 native 签名调用跑起来,也就是能从本地 emu里拿到各个签名头。到这一步后,继续深入 X-Medusa 内部时,分析基本卡在 SM3 和周边混淆逻辑,无法继续稳定拆出后续 VM 路径。
后半段切到 Codex 后,分析方式变成了“动态 trace + 局部 Python lift + native 对照验证”。也就是本文后面记录的过程:不再只看静态伪代码,而是对每个 VM 片段抓输入、输出和内存副作用,再把能证明的局部逻辑写成 Python,最后组合成完整 pipeline。
最终验证结果:
也就是说,Python 重建出的明文 src_a 和最终 X-Medusa 都与 native 同一次运行完全一致。
本次使用的是一个基于 ExAndroidNativeEmu 的本地模拟环境。目录中已有调用示例:
它负责:
分析过程中还遇到一个环境问题:系统里存在不匹配的 Unicorn dylib,会影响 emu 运行。后面所有 native 对照命令都统一这样跑:
避免 Python 加载错误的 Unicorn 动态库。
一开始没有直接钻 VM,而是先观察 sub_D4F3C 的输出 map/string 插入位置。
最终确认各 header 的插入点:
其中 X-Medusa 的关键路径是:
这里最重要的是确定 VM 执行边界:
有了这个边界,后续 hook 只在 Medusa VM 活跃期间记录,避免被 JNI 初始化、其它 header 或环境探测逻辑干扰。
最开始静态看 0x445b8 这个 VM 入口,会发现它很像 MIPS 风格解释器:
但不能直接把它当标准 MIPS。
我写了一个 Python VM 模型和 native 状态对比脚本,核心思路是:
VM fetch/decode 点:
对比后得到一个重要结论:
也就是说,不能做这样的假设:
部分 R-type 指令的目标寄存器编码也和标准 MIPS 有差异。所以后续还原关键逻辑时,我没有完全依赖静态反汇编,而是优先使用动态 trace 的输入、输出和内存副作用。
接下来先看最终 X-Medusa 是什么。
通过跟踪 base64 调用链:
确认 X-Medusa 是标准 base64,使用普通字母表:
base64 解码后得到一个 raw packet。继续跟踪最终 packet 的 copy 序列,得到结构:
VM copy 点如下:
这个阶段先写出最外层 Python:
继续追 packet body,发现最终 body 来自一个中间 buffer,但不是直接复制出来的。
它的生成分成两步:
second_buffer 布局:
其中:
关键 VM 写点:
这一步对应的 Python lift:
验证方式是同一次 native run 中抓取 copy 的 source/destination/len,然后和 Python 拼出来的 buffer 做 byte-for-byte 比较。
first_intermediate 继续往前追,来到 VM 片段:
静态看这里时很容易把 source 和 destination 看反。所以我对这个范围做了窄范围动态 trace,记录每一轮:
最终确认真实逻辑:
也就是:
key 的来源在前面:
它调用一个短 VM helper,对 tail2 做 hash,取低 16 位组成 4 字节 key:
验证样例:
reverse-xor 的输入不是原始明文,而是:
继续追上游,发现:
进入 lib+0xd71bc 时参数:
这里一开始也容易误判为某种标准加密算法,但动态 trace 后发现它不是 AES/SM4 这种标准 block cipher,而是一个混淆过的字节状态机。
整体结构:
把它 lift 成 Python:
然后用 native dump 的 src_a/key/dst 验证:
d71bc 的 key32 不是固定表,而是 SM3 结果。
调用链:
对 b"abc" 做验证后确认 lib+0xd9bc0 是标准 SM3:
key material 的构造:
所以:
这也解释了前面 tail2:
同一个 rand 同时参与:
只在 Medusa VM 活跃期间 trace rand wrapper,确认共有三次:
分别对应:
这里有一个坑:--lock-time 并不会固定这三个 rand。模拟器里的 rand hook 来自 Python random.randint(0, 0xffffffff),所以想复现同一次签名,必须捕获这三个 rand,或者额外固定随机源。
d71bc 的输入 src_a 是一个 protobuf-like 明文消息。
入口参数里可以直接拿到:
解析后字段如下:
native 辅助函数也能印证这一点:
所以这一段不应该按加密算法理解,而应该按 protobuf builder 还原。
对应 Python 中实现了:
验证方式:
对 SM3 helper 的 IO 做 trace 后确认:
注意这里是 query,不包含 path,也不包含问号前面的部分。
样例:
src_a.f23 是一段嵌套环境 message,其中几个字段是动态值。
最终确认:
对应 native 证据:
这里要区分:
即使锁定 URL 里的 ts 或 emu 的 --lock-time,f40 仍可能变化。要复现同一次签名,就必须使用 native run 里抓到的当前毫秒值。
src_a.f24 是 JSON 字符串,形态如下:
先从最终 JSON 里的 fkd/pd 回溯 source string:
确认:
继续追 uuid_source,发现它不是直接把 /dev/urandom 的 16 字节格式化成 UUID,而是:
PRNG 逻辑:
UUID 模板:
填充规则:
用固定 /dev/urandom 输入 --urandom-int 1 验证:
最终 packet 的 body 还会经过一层 bit-slice 处理。
流程:
提取位置:
patch 位置:
稀疏 bit lanes:
byte bit permutation:
这一步看起来很绕,但动态验证很直接:
在 vm+0x193800 附近能看到明显的 AES GF(2^8) 乘法痕迹:
但它不是标准 AES。
继续 trace S-box、state permutation、key schedule 和 round-key 顺序后,最终确认它是一个自定义 AES-like 变换:
固定材料:
输入:
输出:
其中:
这一阶段逐轮验证了:
到这里,所有局部块都已经能和 native 对上。
最终 Python pipeline:
最终封装成:
以及纯 Python 命令行:
端到端验证脚本会在同一次 native run 中抓取:
然后 Python 用这些值重新生成:
验证结果:
这说明:
如果要让纯 Python 和某一次 native 运行输出完全一致,至少要提供:
其中最容易忽略的是:
这篇文章标题里有 “AI逆向”,但它不是指把 so 丢给 AI 然后自动出结果。
这次更接近一种分阶段的人机协作式逆向。
第一阶段用的是 Cursor 的 Opus 模型,重点解决工程入口问题:
这个阶段的价值很大,因为没有稳定 emu 调用,就谈不上后续动态验证。但它继续深入时基本只能推到 SM3 和一些外层 helper,面对 VM 内部的数据流、buffer 来源、bit-slice patch、AES-like transform 时,很难继续拆下去。
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。