首页
社区
课程
招聘
[原创]一种“非标准VM“的识别与分析
发表于: 19小时前 379

[原创]一种“非标准VM“的识别与分析

19小时前
379

提到 VM 保护,多数人的第一反应是 VMProtect/自研栈管理/通用虚拟指令集/虚拟上下文。但现实中还存在另一条技术路线——把一段专用的数据处理逻辑(解密、解压、解析)改写成"字节码 + 解释器"的形式,仅凭一个固定循环驱动。这类 VM 不做通用计算,控制流几乎是一条直线,语义全部藏在数据流里。

我们暂时管这种类型叫做非标准 VM。它与标准 VM 的差异如下:

本文的目标函数是 sub_522E8:一个 0x6D60 字节(约 28KB)的巨型函数,样本本身带花指令与 CFF 混淆(篇幅所限不展开去混淆过程)。于是分析结合到动态侧:用 unidbg 跑出全量指令级 trace,从数据中找结构。

sub_522E8的控制流图


对 trace 搜索 `0x523B0`与`0x5814c`(相关trace日志附件会提供)

x0指向模块自身的只读数据段(0x4027fbxx)——字节码常驻在 SO 里,解释器按 32 位字消费。循环尾部(0x5814C)做 `add x0, x8, #4` 后写回指针。仅凭循环还不能断定是 VM,它也可能只是普通数据循环。继续看解码部分。

这 17 条移位是无条件、恒定移位量的——不对数据做任何判断,只是机械拆包。这正是"指令解码器"的形态:与数据无关、每次都执行、输出喂给后续逻辑。

证据三:两级分发结构

0x4005245C: "and w15, w8, #0x3f"  ; w15 ← 低 6 位

低 6 位 `w15 = word & 0x3f` 是**第一级操作码**。对 12 个样本共 92513 次取指统计 w15 分布(共出现 29 种取值,下表为前 15 种,占 97.6%)

分布高度集中:0x35 一个操作码占 42.85%。如果这是普通数据处理的 switch,分支频率不会如此悬殊;这正是"指令序列"的特征——程序里总有几个高频指令。

第二级操作码藏在 w15=0x35 内部,解码公式为:

w10 = ror32((word & 0xfff) - 0x35, 6)

trace切片代码(trace009.txt 1760306行):

逐级比较,最终落入 0x21 的 handler 0x40057D7C 对应的伪c如下:

对 12 样本全量统计,w10 共出现24 种取值

至此"两级分发"成立:w15 是 29 个大类,w15=0x35 内部再按 w10 分出 24 种编码。这是与"单级跳转表"式 VM 的显著差异。

证据四:跨 handler 共享的状态载体

所有 handler 都通过栈上的两块共享区域通信

跨 handler 交互实例一:寄存器文件(trace009.txt)

0x35,0x21写table[6],0x2f写table[7],0x35,0x13同时读table[6]与table[7],3个handler中只有一张共享的table表进行交互。

跨 handler 交互实例二:数据内存

0x30(写数据内存)把寄存器文件里的 64 位状态暂存到数据内存槽位,随后的 0x2f(取指针)把它读回——两个 handler 通过数据内存交换数据。

判定结论与"回绕"机制

四条证据合起来:固定取指循环→ 恒定解码前导→ 两级分发→ 共享寄存器文件。这是一个 VM,且控制流近似直线——但并非完全没有循环。

三种状态在主循环入口的差异

状态0:0x58168 → 0x581F0 魔数检查 → 0x52394,走完整入口,先查第一边界 `cmp x0,[sp+0x20]`(相等则从 0x57F38 退出),再查第二边界 `cmp x0,[sp+0x28]`(相等则从 0x5902C 退出),还有 `cbz x0` 空指针检查,出现次数最多,应该是普通顺序执行。

状态2:0x5816C → 0x523A0,跳过第一边界检查([sp+0x20] 终点判断),只查第二边界与空指针,回绕前过渡迭代的快速路径。

状态3:0x58178 回绕路径 → 0x581F0 魔数检查 → 0x52394,指针已被 0x581EC 改写为回绕目标,从目标地址重新取指,循环回绕。

字节码里没有任意跳转指令。它只有一种受控的循环原语——字节码先把"回绕目标"写进 [x27-0x18]、把"循环状态"写进 [x27-0x20],解释器尾部按状态决定继续还是回绕,就是说:数据流携带条件性,控制流不携带语义。打个比方:标准程序像有岔道的铁路,火车走哪条道岔就是程序的意思;这个 VM 像一条环形传送带,轨道永远是那一圈,工人(handler)往工件(表里的值)上贴标签,传送带尽头的固定闸门(循环尾部)看一眼标签,决定"再转一圈"还是"放出去"。轨道从不变,货物决定一切。这与标准 VM"用跳转指令实现程序逻辑"的思路截然不同,也就是为什么静态分析无法识别这个VM的原因。


语义还原不需要一条条硬啃。我的方法是用unidbg协助倒推先找到所有 handler 中唯一对外产生输出的那条指令,然后沿数据流反推它的输入来自哪些 handler

`strb`(单字节写)只出现在一个地方:

0x40053420: "strb w10, [x8, x9]"   全 VM 唯一的字节输出点

我们先找0x78(unidbg反base64后第一个加密输出字节)的trace切片进行分析(trace009.txt)

从两个handler可以确认两条指令的语义:

w15=0x35,w10=0x20(异或):table[c] = table[a] ^ table[b]

w15=0x33(输出数据):*(uint8_t*)(table[d] + sext(off)) = (uint8_t)table[c]

w15=0x2f——数据内存取指针

语义:table[w] = *(uint64_t*)(data_area + bitfield_offset),这是字节码从数据内存取指针的指令

w15=0x30——写数据内存

语义:data[bitfield_offset] = table[a],与0x2f成对可以构成数据内存读写通道


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

收藏
免费 46
打赏
分享
最新回复 (13)
雪    币: 361
活跃值: (7994)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
大佬牛,我感觉libmetasec_ml 的vmp 像是编译器优化的结果,对比libEncryptor.so 的区别像分发handler 多了个加密寻址
19小时前
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
这种vm其实在脚本语言里很常见?
18小时前
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
感谢分享
11小时前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
666
10小时前
0
雪    币: 191
活跃值: (1110)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
之前分析这块的时候半天没看明白,看了这个文章终于明白了,感谢分享。
9小时前
0
雪    币: 411
活跃值: (806)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
66
8小时前
0
雪    币: 1560
活跃值: (5768)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
8
1111
8小时前
0
雪    币: 3618
活跃值: (7365)
能力值: ( LV11,RANK:185 )
在线值:
发帖
回帖
粉丝
9
1
4小时前
0
雪    币: 268
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
123123
3小时前
0
雪    币: 4182
活跃值: (4034)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
11
看看
2小时前
0
雪    币: 112
活跃值: (9345)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
tql
1小时前
0
雪    币: 35
活跃值: (177)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
1
1小时前
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
学习
1小时前
0
游客
登录 | 注册 方可回帖
返回