AI逆向视频签名算法X-Medusa全过程

admin 2026-06-09 04:42:44 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细记录了对某视频App29.3版本中native签名头X-Medusa的逆向分析全过程。作者通过动态trace与Python重建相结合的方法,成功还原了该签名的完整生成逻辑,包括VM解释器特性、数据包结构、reverse-xor变换以及基于SM3的密钥派生机制。最终验证Python重建结果与native运行完全一致,为移动端安全研究和算法分析提供了可复现的方法论。 综合评分: 90 文章分类: 逆向分析,移动安全,WEB安全,安全工具,红队


cover_image

AI逆向视频签名算法X-Medusa全过程

scxc scxc

看雪学苑

2026年6月8日 17:59 上海

在小说阅读器读本章

去阅读

本文仅记录一次针对移动端 native 签名逻辑的逆向分析过程,用于安全研究、算法学习和逆向工程方法论交流。文中涉及的脚本、地址和结论均来自本地样本与模拟环境验证,不讨论任何绕过风控、批量请求或业务滥用场景。

#

这次分析的目标是某视频 App 29.3 版本中的一个 native 签名头:X-Medusa

样本位于 Android native so:

libmetasec_ml.fully_deobf.so

外层调用函数是:

sub_D4F3C(url, headers_blob)

这个函数会一次性生成多个签名头:

X-Argus
X-Gorgon
X-Helios
X-Khronos
X-Ladon
X-Medusa

本文重点只讲 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。

最终验证结果:

[rebuild]src_a_match=True x_medusa_match=True

也就是说,Python 重建出的明文 src_a 和最终 X-Medusa 都与 native 同一次运行完全一致。

分析环境

本次使用的是一个基于 ExAndroidNativeEmu 的本地模拟环境。目录中已有调用示例:

dy_sign_send.py

它负责:

1. 初始化 Android native emu
2. 加载 libmetasec_ml.fully_deobf.so
3. 调 JNI_OnLoad
4. 调用 sub_D4F3C
5. 解析返回的签名 header

分析过程中还遇到一个环境问题:系统里存在不匹配的 Unicorn dylib,会影响 emu 运行。后面所有 native 对照命令都统一这样跑:

env -u DYLD_LIBRARY_PATH python3 ...

避免 Python 加载错误的 Unicorn 动态库。

第一步:先定位 X-Medusa 在 sub_D4F3C 中的位置

一开始没有直接钻 VM,而是先观察 sub_D4F3C 的输出 map/string 插入位置。

最终确认各 header 的插入点:

0xca684-> X-Gorgon
0xcaa5c-> X-Khronos
0xcb09c-> X-Argus
0xcb4dc-> X-Ladon
0xcba30-> X-Medusa
0xcbe64-> X-Helios
0x46a70-> X-Neptune

其中 X-Medusa 的关键路径是:

sub_D4F3C
-> native wrapper
-> VM entry lib+0x445b8
-> Medusa VM bytecode call lib+0xd8978
-> bytecode entry base+0x18bdd0
-> X-Medusa 插入点 lib+0xcba30

这里最重要的是确定 VM 执行边界:

VM interpreter entry: lib+0x445b8
Medusa bytecode call: lib+0xd8978
VM bytecode entry:    base+0x18bdd0

有了这个边界,后续 hook 只在 Medusa VM 活跃期间记录,避免被 JNI 初始化、其它 header 或环境探测逻辑干扰。

第二步:确认 VM 解释器形态

最开始静态看 0x445b8 这个 VM 入口,会发现它很像 MIPS 风格解释器:

32 个虚拟寄存器
load/store
branch
R/I/J 类似的指令形态

但不能直接把它当标准 MIPS。

我写了一个 Python VM 模型和 native 状态对比脚本,核心思路是:

1. 在 native VM 初始化后抓 VM state
2. 在 VM fetch/decode 点抓每一步 PC 和寄存器
3. Python 模型模拟一条
4. 和 native 下一步状态对比

VM fetch/decode 点:

lib+0x4466c

对比后得到一个重要结论:

这个 VM 是 MIPS-like,但不是标准 MIPS。
r0 也不是硬编码 zero。

也就是说,不能做这样的假设:

r0 永远等于 0

部分 R-type 指令的目标寄存器编码也和标准 MIPS 有差异。所以后续还原关键逻辑时,我没有完全依赖静态反汇编,而是优先使用动态 trace 的输入、输出和实际内存读写变化。

第三步:从最终 X-Medusa 反推 packet 结构

接下来先看最终 X-Medusa 是什么。

通过跟踪 base64 调用链:

0xdc608->0xee588->0x10dd84

确认 X-Medusa 是标准 base64,使用普通字母表:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

base64 解码后得到一个 raw packet。继续跟踪最终 packet 的 copy 序列,得到结构:

packet = fixed20 || var2 || 00 || 01 || body_first || body

fixed20     = c312f41237faabe817c5282916383c6297dd950a
var2        = 2 字节变量,后来确认等于 d71bc_seed32_le[:2]
00 01       = 固定 flags
body_first  = AES-like 输出的第 32 字节
body        = patch 后的 second_buffer

VM copy 点如下:

vm+0x191234: copy dst+0x00len=20
vm+0x191264: copy dst+0x14len=2
vm+0x1912a4: copy dst+0x16len=1
vm+0x1912dc: copy dst+0x17len=1
vm+0x191320: copy dst+0x18len=1
vm+0x191360: copy dst+0x19len=N

这个阶段先写出最外层 Python:

packet = fixed20 + var2 + b"\x00\x01" + bytes([body_first]) + body
X_Medusa = base64.b64encode(packet)

第四步:倒推 body 的来源

继续追 packet body,发现最终 body 来自一个中间 buffer,但不是直接复制出来的。

它的生成分成两步:

1. 先拼 second_buffer
2. 再 patch second_buffer 前 31 个 64-bit word 的稀疏 bit 位

second_buffer 布局:

second_buffer =
byte[0]      = 0xa6
byte[1:9]   = seed8
byte[9:...] = first_intermediate
tail= tail2

其中:

seed8 = uint32_le(rand@vm+0x18ec30) || 013a0b00
tail2 = uint32_le(rand@vm+0x18e408)[:2]

关键 VM 写点:

vm+0x18eda4  写 first byte
vm+0x18edcccopy8-byte seed
vm+0x18edf4copy first_intermediate
vm+0x18ee24copy tail2
vm+0x1910b831 次 sparse word patch

这一步对应的 Python lift:

defmedusa_second_buffer_layout(first_byte, seed8, first_intermediate, tail2):
returnbytearray(bytes([first_byte]) + seed8 + first_intermediate + tail2)

验证方式是同一次 native run 中抓取 copy 的 source/destination/len,然后和 Python 拼出来的 buffer 做 byte-for-byte 比较。

第五步:还原 reverse-xor

first_intermediate 继续往前追,来到 VM 片段:

vm+0x18eb4c..0x18eb98

静态看这里时很容易把 source 和 destination 看反。所以我对这个范围做了窄范围动态 trace,记录每一轮:

source pointer
destination pointer
source byte
keybyte
written byte
loop index

最终确认真实逻辑:

for (i = 0; i < n; i++)
&nbsp; &nbsp; dst[i] = src[n - 1 - i] ^ key4[i & 3];

也就是:

反向读取 source
正向写入 destination
使用&nbsp;4&nbsp;字节循环&nbsp;xorkey

key 的来源在前面:

vm+0x18eacc..0x18eb00

它调用一个短 VM helper,对 tail2 做 hash,取低 16 位组成 4 字节 key:

ret&nbsp;= block_189850_tail2_hash(tail2)
k&nbsp;= ret &&nbsp;0xffff
key4&nbsp;= bytes([k >>&nbsp;8, k &&nbsp;0xff, k >>&nbsp;8, k &&nbsp;0xff])

验证样例:

fe1e&nbsp;->&nbsp;ret=fff80f18&nbsp;->&nbsp;key4=0f180f18
a057&nbsp;->&nbsp;ret=fffaff0d&nbsp;->&nbsp;key4=ff0dff0d
c2c0&nbsp;->&nbsp;ret=fff9effb&nbsp;->&nbsp;key4=effbeffb

#

第六步:找到 reverse-xor 的上游 d71bc

reverse-xor 的输入不是原始明文,而是:

reverse_source&nbsp;=&nbsp;8&nbsp;zero bytes || d71bc_output

继续追上游,发现:

vm+0x1897d0..0x1897f0&nbsp; memset(dst,&nbsp;0x20, len)
vm+0x1897f4..0x18982cnative&nbsp;helper call
native&nbsp;helper &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;lib+0xd71bc

进入 lib+0xd71bc 时参数:

x0&nbsp;= dst
x1&nbsp;= src_a
x2&nbsp;= len(src_a)
x3&nbsp;= key32
x4&nbsp;=&nbsp;0x20

这里一开始也容易误判为某种标准加密算法,但动态 trace 后发现它不是 AES/SM4这种标准 block cipher,而是一个混淆过的字节状态机。

整体结构:

pass1:
&nbsp; 正向读 src
&nbsp; 反向写 dst
&nbsp; 每个字节混入 key[(i*4)%32] 和 key[((i*4)|1)%32]

pass2:
&nbsp; 使用 dst[i-1], dst[i-2], i 做前向反馈

tail:
&nbsp; dst[n-1] ^= dst[n-2]
&nbsp; dst[0] = (dst[0] ^ dst[1]) +&nbsp;sum(dst[1:])

把它 lift 成 Python:

block_d71bc_encode(src, key)

然后用 native dump 的 src_a/key/dst 验证:

block_d71bc_encode(src_a, key) ==&nbsp;native&nbsp;dst

#

第七步:还原 d71bc 的 key

d71bc 的 key32 不是固定表,而是 SM3 结果。

调用链:

vm+0x18b6c0->&nbsp;wrapper lib+0xdc6bc->&nbsp;native lib+0xd9bc0

对 b"abc" 做验证后确认 lib+0xd9bc0 是标准 SM3:

SM3("abc") =
66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0

key material 的构造:

sign_key_b64&nbsp;= rBrarpWnr5SlEUqzs6l92ABQqgo5MUxAUoyuyVJWwow=
sign_key&nbsp; &nbsp; &nbsp;= base64_decode(sign_key_b64)
seed32_le&nbsp; &nbsp; = uint32_le(rand@vm+0x18e408)

material68&nbsp;= sign_key || seed32_le || sign_key
key32&nbsp; &nbsp; &nbsp; = SM3(material68)

所以:

d71bc_key&nbsp;= SM3(sign_key || uint32_le(d71bc_rand) || sign_key)

这也解释了前面 tail2

tail2&nbsp;= uint32_le(d71bc_rand)[:2]

同一个 rand 同时参与:

1.&nbsp;d71bc key
2.&nbsp;reverse-xor key 派生
3.&nbsp;packet var2/tail2

#

第八步:确认三个 rand 的作用

只在 Medusa VM 活跃期间 trace rand wrapper,确认共有三次:

vm+0x18c788->&nbsp;top.f3
vm+0x18e408->&nbsp;d71bc seed / tail2 / reverse_key4
vm+0x18ec30->&nbsp;second_buffer seed8

分别对应:

top_rand:
&nbsp; top.f3 =&nbsp;zigzag32(top_rand)

d71bc_rand:
&nbsp; seed32_le =&nbsp;uint32_le(d71bc_rand)
&nbsp; tail2 = seed32_le[:2]
&nbsp; d71bc_key =&nbsp;SM3(sign_key || seed32_le || sign_key)

seed8_rand:
&nbsp; seed8 =&nbsp;uint32_le(seed8_rand) ||&nbsp;013a0b00

这里有一个坑:--lock-time 并不会固定这三个 rand。模拟器里的 rand hook 来自 Python random.randint(0, 0xffffffff),所以想复现同一次签名,必须捕获这三个 rand,或者额外固定随机源。

#

第九步:还原 src_a 明文 protobuf

d71bc 的输入 src_a 是一个 protobuf-like 明文消息。

入口参数里可以直接拿到:

lib+0xd71bc:
&nbsp; x1 = src_a
&nbsp; x2 =&nbsp;len(src_a)

解析后字段如下:

top.f1 &nbsp; bytes &nbsp; f7e85ffad7d7dc3bd62ac87057cf6118
top.f2 &nbsp; varint &nbsp;6
top.f3 &nbsp; varint &nbsp;zigzag32(rand@0x18c788)
top.f4 &nbsp;&nbsp;string"3019"
top.f6 &nbsp;&nbsp;string"1611921764"
top.f7 &nbsp;&nbsp;string"29.3.0"
top.f8 &nbsp;&nbsp;string"v04.05.05-ml-android"
top.f9 &nbsp; varint &nbsp;0x80a0a00
top.f10 &nbsp;bytes &nbsp;&nbsp;8&nbsp;zero bytes
top.f12 &nbsp;varint &nbsp;0xd3e825e8
top.f13 &nbsp;bytes &nbsp; ea24463898fd615efc1982685c362167a1a349ba
top.f14 &nbsp;bytes &nbsp; SM3(URL query)[0:6]
top.f15 &nbsp;message small tuple
top.f20 &nbsp;string"none"
top.f21 &nbsp;varint &nbsp;738
top.f23 &nbsp;message device/time&nbsp;env
top.f24 &nbsp;string&nbsp; JSON env

native 辅助函数也能印证这一点:

0x10d580..0x10d718 &nbsp;varint / zigzag encoder
0x10c1ec..0x10c32c &nbsp;protobuf field writer

所以这一段不应该按加密算法理解,而应该按 protobuf builder 还原。

对应 Python 中实现了:

proto_varint()
proto_key()
proto_field_varint()
proto_field_bytes()
proto_field_fixed32()
medusa_src_a_rebuild()
medusa_src_a_from_runtime_values()

验证方式:

native&nbsp;src_a == Python rebuilt src_a

#

第十步:top.f14 是 URL query 的 SM3 前 6 字节

对 SM3 helper 的 IO 做 trace 后确认:

top.f14&nbsp;= SM3(URL query bytes)[0:6]

注意这里是 query,不包含 path,也不包含问号前面的部分。

样例:

SM3(query)=
6cc3b8b4c20643691762996898e9a999546377e6713affda2d66b09c19020aca

top.f14&nbsp;=
6cc3b8b4c206

#

第十一步:f23 里的 pid 和时间

src_a.f23 是一段嵌套环境 message,其中几个字段是动态值。

最终确认:

f23.f7&nbsp; &nbsp; &nbsp; &nbsp;= zigzag32(getpid())
f23.12.f28&nbsp; = zigzag64(X-Khronos_seconds *&nbsp;1000)
f23.12.f40&nbsp; = zigzag64(current_epoch_milliseconds)

对应 native 证据:

lib+0xdc8e4 &nbsp;调 getpid
lib+0x101c64 gettimeofday -> sec *&nbsp;1000&nbsp;+ usec /&nbsp;1000
lib+0x101c9c gettimeofday_ms /&nbsp;1000

这里要区分:

f23.12.f28 跟 X-Khronos 秒相关
f23.12.f40 跟当前 epoch ms 相关

即使锁定 URL 里的 ts 或 emu 的 --lock-timef40 仍可能变化。要复现同一次签名,就必须使用 native run 里抓到的当前毫秒值。

第十二步:f24 是 UUID、MD5、CRC32 组合

src_a.f24 是 JSON 字符串,形态如下:

{"cmr":16777216,"cmr2":16777216,"un_h":0,"vpn":0,"kd":694367,"fkd":...,"pd":...,"dyn":"","do":0,"tk":true}

先从最终 JSON 里的 fkd/pd 回溯 source string:

vm+0x18db5c &nbsp;CRC32(fkd_source)
vm+0x18dd2c &nbsp;CRC32(pd_source)

确认:

pd&nbsp; = signed_crc32(uuid_source)
fkd&nbsp;= unsigned_crc32(md5(uuid_source ||&nbsp;"694367").hexdigest())

继续追 uuid_source,发现它不是直接把 /dev/urandom 的 16 字节格式化成 UUID,而是:

/dev/urandom first16
->&nbsp;作为 xorshift128+ seed
->&nbsp;PRNG 调用两次
->&nbsp;raw16 =&nbsp;le64(out0) ||&nbsp;le64(out1)
->&nbsp;按 native nibble 顺序填 UUID 模板

PRNG 逻辑:

x = s0
y = s1
x ^= (x << 23) & 0xffffffffffffffff
new_s1 = x ^ y ^ (y >> 5) ^ (x >> 18)
out = (new_s1 + y) & 0xffffffffffffffff
new_s0 = y

UUID 模板:

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

填充规则:

每个 byte 先用低 nibble,再用高 nibble
x -> alphabet[nibble]
y -> alphabet[(nibble & 3) | 8]
固定的版本位&nbsp;4&nbsp;不消耗随机 nibble

用固定 /dev/urandom 输入 --urandom-int 1 验证:

seed16 = 01000000000000000000000000000000
out0 &nbsp; = 0x0000000000800021
out1 &nbsp; = 0x0000000001040041
uuid &nbsp; = 12000800-0000-4000-8140-040100000000
md5(uuid ||&nbsp;"694367").hexdigest()
&nbsp; &nbsp; &nbsp; &nbsp;= 4a5edaf53834ae3aef2e1fcc298394c4
fkd &nbsp; &nbsp;= 922802938
pd &nbsp; &nbsp; = 508833512

#

第十三步:bit-slice 提取和 patch

最终 packet 的 body 还会经过一层 bit-slice 处理。

流程:

1. 从 second_buffer 前&nbsp;31&nbsp;个&nbsp;64-bit word 中提取&nbsp;31&nbsp;字节
2. 这&nbsp;31&nbsp;字节拼一个&nbsp;32-byte block,最后补&nbsp;00
3. 送入 AES-like&nbsp;transform
4.&nbsp;transform&nbsp;输出 aes32
5. aes32[:31]&nbsp;patch 回 second_buffer 的前&nbsp;31&nbsp;个&nbsp;64-bit word
6. aes32[31]&nbsp;放到 packet 的 body_first

提取位置:

vm+0x18ee54..0x18f024

patch 位置:

vm+0x190f4c..0x1910b8

稀疏 bit lanes:

bit positions:5,14,18,28,35,41,48,63

byte bit permutation:

in&nbsp;bit&nbsp;0->&nbsp;out bit&nbsp;2
in&nbsp;bit&nbsp;1->&nbsp;out bit&nbsp;0
in&nbsp;bit&nbsp;2->&nbsp;out bit&nbsp;4
in&nbsp;bit&nbsp;3->&nbsp;out bit&nbsp;7
in&nbsp;bit&nbsp;4->&nbsp;out bit&nbsp;5
in&nbsp;bit&nbsp;5->&nbsp;out bit&nbsp;6
in&nbsp;bit&nbsp;6->&nbsp;out bit&nbsp;1
in&nbsp;bit&nbsp;7->&nbsp;out bit&nbsp;3

这一步看起来很绕,但动态验证很直接:

记录每个 old_word、input_byte、new_word
Python 按相同 bit permutation 和 sparse&nbsp;mask&nbsp;计算
逐项对比 new_word

#

第十四步:AES-like transform

在 vm+0x193800 附近能看到明显的 AES GF(2^8) 乘法痕迹:

andi ..., 0x80
xori ..., 0x1b

但它不是标准 AES。

继续 trace S-box、state permutation、key schedule 和 round-key 顺序后,最终确认它是一个自定义 AES-like 变换:

1.&nbsp;使用 AES MixColumns arithmetic
2.&nbsp;使用自定义 substitution table
3.&nbsp;使用自定义 state permutation
4.&nbsp;使用自定义 round-key byte order
5.&nbsp;对两个 16-byte block 做 CBC-like chaining

固定材料:

material16&nbsp;= d31e3718288a1027baab59f146a09a9c
iv16&nbsp; &nbsp; &nbsp; &nbsp;= ea180a0336ed352fcd24e4d50018ae54

输入:

aes_input&nbsp;= extract31 ||&nbsp;00

输出:

aes32&nbsp;= medusa_aes_like_transform32(aes_input, material16)

其中:

aes32[:31]&nbsp;-> patch second_buffer
aes32[31]&nbsp; -> packet body_first

这一阶段逐轮验证了:

sub&nbsp;bytes
shift rows
mix columns
round&nbsp;keyxor
key&nbsp;schedule
完整 transform32

#

第十五步:组合成完整 pipeline

到这里,所有局部块都已经能和 native 对上。

最终 Python pipeline:

d71bc_seed32_le&nbsp;= uint32_le(d71bc_rand)
tail2&nbsp;= d71bc_seed32_le[:2]
d71bc_key&nbsp;= SM3(sign_key || d71bc_seed32_le || sign_key)
reverse_key4&nbsp;= hash_tail2_to_key4(tail2)
seed8&nbsp;= uint32_le(seed8_rand) + bytes.fromhex("013a0b00")

src_a&nbsp;= build_src_a(...)
d71bc_output&nbsp;= block_d71bc_encode(src_a, d71bc_key)
reverse_source&nbsp;= b"\x00"&nbsp;*&nbsp;8&nbsp;+ d71bc_output
first_intermediate&nbsp;= reverse_xor(reverse_source, reverse_key4)
second_buffer&nbsp;= layout(0xa6, seed8, first_intermediate, tail2)
aes32&nbsp;= custom_aes_like(extract31(second_buffer), material16)
packet&nbsp;= assemble_packet(tail2, aes32, second_buffer)
X_Medusa&nbsp;= base64(packet)

最终封装成:

medusa_x_medusa_from_full_runtime_values(...)

以及纯 Python 命令行:

python3 medusa_pure_x_medusa.py&nbsp;\
--top-rand0xef26a2e7 \
--d71bc-rand0x2d39c8b6 \
--seed8-rand0x93bbc24b \
--pid92685&nbsp;\
--khronos-sec1777603264&nbsp;\
--current-epoch-ms1777965666036&nbsp;\
--f24-seed160x1

#

最终验证

端到端验证脚本会在同一次 native run 中抓取:

src_a
三个 rand
pid
current_epoch_ms
f24&nbsp;source
native X-Medusa

然后 Python 用这些值重新生成:

src_a
packet
X-Medusa

验证结果:

[rebuild]src_a_match=True&nbsp;x_medusa_match=True

这说明:

1.&nbsp;src_a 明文结构已还原
2. d71bc helper 已还原
3. reverse-xor 已还原
4. second_buffer layout 已还原
5.&nbsp;bit-slice extract/patch 已还原
6.&nbsp;AES-like transform 已还原
7.&nbsp;packet assembly 和 base64 已还原

#

复现同一次签名需要哪些输入

如果要让纯 Python 和某一次 native 运行输出完全一致,至少要提供:

URL/query
rand@vm+0x18c788
rand@vm+0x18e408
rand@vm+0x18ec30
pid
X-Khronos seconds
current epoch milliseconds
f24 UUID seed16,或直接给 device_uuid/device_hash_hex/f24_fkd/f24_pd

其中最容易忽略的是:

1.&nbsp;三个 rand 不会被 --lock-time 固定
2.&nbsp;f23.12.f40 是当前 epoch ms
3.&nbsp;f24 UUID 不是直接 urandom 格式化,而是 xorshift128+ 后再按 nibble 填模板

AI 在这次逆向中的作用

这篇文章标题里有 “AI逆向”,但它不是指把 so 丢给 AI 然后自动出结果。

这次更接近一种分阶段的人机协作式逆向。

第一阶段用的是 Cursor 的 Opus 模型,重点解决工程入口问题:

1.&nbsp;搭建 ExAndroidNativeEmu 调用环境
2.&nbsp;加载 so、跑 JNI_OnLoad
3. 调通 sub_D4F3C(url, headers_blob)
4. 拿到 X-Argus / X-Gorgon / X-Helios / X-Khronos / X-Ladon / X-Medusa

这个阶段的价值很大,因为没有稳定 emu 调用,就谈不上后续动态验证。但它继续深入时基本只能推到 SM3 和一些外层 helper,面对 VM 内部的数据流、buffer 来源、bit-slice patch、AES-like transform 时,很难继续拆下去。

第二阶段切到 Codex,重点从“能调用”转向“能解释和复现”:

1.&nbsp;围绕 Medusa VM 调用边界写 targeted trace
2.&nbsp;每次只回答一个小问题,例如某个 buffer 从哪来、某个字段怎么生成
3.&nbsp;根据同一次 native run 的 IO 写 Python lift
4.&nbsp;让 Python lift 和 native buffer 做 byte-for-byte 对比
5.&nbsp;最后把所有局部 lift 合成完整 X-Medusa pipeline

这也是后半段能继续推进的关键:不是让模型直接“猜算法”,而是让它不断写脚本、跑验证、缩小未知范围。

整体分工更像这样:

人负责:
&nbsp; -&nbsp;判断分析方向
&nbsp; -&nbsp;选择 hook 点
&nbsp; -&nbsp;判断哪些 trace 有价值
&nbsp; -&nbsp;识别哪些结论可能是误判

AI 负责:
&nbsp; -&nbsp;快速写 trace 脚本
&nbsp; -&nbsp;根据动态 IO 归纳局部算法
&nbsp; -&nbsp;把局部算法整理成 Python lift
&nbsp; -&nbsp;反复跑 native 对照和 self-test
&nbsp; -&nbsp;维护 notes、脚本、验证命令

这类 VM 保护样本如果只靠静态反编译,很容易被以下问题拖住:

1.&nbsp;VM 指令格式和标准 MIPS 不完全一致
2.&nbsp;静态伪代码容易看反 src/dst
3.&nbsp;VM 栈 slot 在嵌套 helper 中会复用
4.&nbsp;native helper、对象字符串和 VM 寄存器混在一起
5.&nbsp;同一个字段可能来自 rand/time/pid/urandom 多个动态源

AI 的优势在于可以很快写出大量“小而准”的 trace:

trace packet
trace memcpy
trace heap writes
trace helper IO
trace rand
trace protobuf&nbsp;source
trace AES-like round
trace f24&nbsp;source&nbsp;generation
trace full rebuild

每个 trace 都只回答一个问题。只要问题拆得足够小,AI 生成代码和整理结论的效率很高。

方法论总结

这次逆向最重要的不是某一个算法,而是验证方式:

1.&nbsp;先确定最终输出位置
2.&nbsp;从最终 packet 往前倒推
3.&nbsp;每一段只关心输入、输出和实际内存读写变化
4.&nbsp;写最小 Python lift
5.&nbsp;和同一次 native run 做 byte-for-byte 对比
6.&nbsp;再把小块组合成完整 pipeline

在 VM 保护场景里,不要迷信静态反编译。

更稳的方式是:

用静态分析找大致范围
用动态 trace 确认真实数据流
用 Python lift 固化结论
用&nbsp;native&nbsp;对照防止自嗨

最终产物不是一份“看起来像”的伪代码,而是可以跑出同样结果的实现。

当前边界

这次完整还原的是 X-Medusa 主路径,不等于整个 so 或所有 header 都已经完全还原。

当前边界:

1.&nbsp;X-Medusa 主路径可以纯 Python 复现
2.&nbsp;X-Argus / X-Ladon / X-Gorgon 等其它头不在本文范围内
3.&nbsp;VM 解释器本身没有完整转成 C
4.&nbsp;三个 rand、pid、时间、f24 seed 仍属于运行时输入
5.&nbsp;要跨运行输出完全一致,必须固定或捕获这些动态值

如果后续继续做,可以有两个方向:

1.&nbsp;把 VM interpreter 的所有 handler 系统性转成 C/Python
2.&nbsp;继续还原其它 header 的完整算法链

对 X-Medusa 来说,目前更直接有效的方式是:不追求完整解释器,而是 lift 实际执行路径和关键 native helper。

结语

这次 X-Medusa 的逆向过程,从最初只知道 VM 入口 0x445b8,到最终得到纯 Python 复现,大致经历了:

定位 header 插入点
确定 VM 执行边界
分析 packet 外层结构
倒推 second_buffer
还原 reverse-xor
还原 d71bc&nbsp;byte-state-machine
确认 SM3&nbsp;key&nbsp;material
解析 src_a protobuf
还原 f23/f24 动态字段
还原 bit-slice patch
还原 AES-like&nbsp;transform
组合端到端 pipeline
native 对照验证

最终结果:

src_a_match=True
x_medusa_match=True

这也是本文最核心的判断标准:不是“猜到了算法像什么”,而是“同一次 native 输入下,Python 输出完全一致”。

#

看雪ID:scxc

https://bbs.kanxue.com/user-home-638330.htm

*本文为看雪论坛精华文章,由 scxc 原创,转载请注明来自看雪社区

第十届安全开发者峰会【议题征集】-欢迎投稿

往期推荐

我们绕过了 GarudaDefender 整套 Frida 检测,但这已经不是重点了

一次 Flutter App 实战:还原 encData 参数解密流程

单机DMA劫持HyperV!调试+取证两种思路解决2026腾讯游戏安全技术竞赛决赛

Android风险环境检测——签名校验

和爱豆更近一步——爱豆聊天App反调试绕过

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 scxc scxc《AI逆向视频签名算法X-Medusa全过程》

评论:0   参与:  0