VM逆向(上)

逆向虚拟机(VM)是一种通过解释操作码(opcode)来执行程序的技术,常用于保护程序代码的安全性。逆向虚拟机的核心包括虚拟机入口函数、调度器和操作码解释器。以下是逆向虚拟机的基本分析思路和实现细节。

一、虚拟机的基本结构

虚拟机通常由以下几个部分组成:

虚拟机入口函数(vm_start):初始化虚拟机并设置指令指针(如EIP)。

调度器(vm_dispatcher):根据操作码(即opcode)选择对应的处理函数(Handler)并执行。

操作码(vm_code):程序的核心逻辑以操作码形式存储,虚拟机通过解释这些操作码来执行程序。执行结束后就会将操作权返回给vm_dispatcher
image
其中,bitecode也可以叫做opcode

一些关键的结构体

“VMContext”结构体用于存储虚拟机执行环境中的各个虚拟寄存器状态。这个结构体包含了需要在虚拟机指令执行过程中保存和操作的所有重要寄存器。
struct VMContext{ DWORD v_eax; DWORD v_ebx; DWORD v_ecx; DWORD v_edx; //前四个都是通用寄存器,用来存储数据或者传参等作用 DWORD v_eip; //指向正在执行的opcode DWORD v_efl; // 符号寄存器(虚拟 EFLAGS) };

“VM_opcode”结构体用于实现解释器,有了解释器才可以选择合适的opcode
typedef struct{ unsigned char opcode; void (*handle)(void*); }vm_opcode;

在逆向题目中的一些常见形式

while(i<len(opcode)): if(opcode[i]==0x00): i+=1 elif(opcode[i]==0x01): i+=2 elif(opcode[i]==0x02): i+=3

二、逆向中的两个解法

手撕(使用z3)----直接逆向整个执行和加密流程,倒退回到flag

这个解法需要找到关键的操作数即opcode

frida 和gdb插桩爆破----最快也是最方便的方法,但是吃操作(本人不会)

三、给出一道题目,来实操一下。

本人不会frida和gdb,所以就直接使用ida静态分析来求解题目了。
首先在发现是一个rust编程,rustmain 是指针传给c的函数里面。
直接点进去看到主函数
image
直接分析这个sub_14000F9E0函数的内容

1.给出了一段src数值的数值

image
敏锐的人可以发现这32个数据几乎都是阶乘的结果
然后后续给出了一系列的函数调用,这些函数调用非常复杂,正常来说是无法一个一个去查看详细的调用过程的,所以可以丢给AI大概看一下,下面给出一些AI分析出来的东西,我会挑其中稍微有价值的东西
image
image
image
AI智力有限,仅供参考!!!
注意“D1no"这个是本题的flag名称(系列赛中d1no就等于flag,当设定就行了)
杂七杂八的东西很多,我们着重来看调用了“D1no”的东西
点进去这个函数发现意义不大,一个函数又调一个函数,很难辨明详细的作用。
后续的分析发现对逆向这个flag的信息有点少。
所以结合详细的这个vm逆向思路,我们来找它的opcode操作码。接下来就会详细的分析如何找到这个vm_run(vm_dispatcher)。

2.查找opcode操作码

来看这段代码
image
首先有一个对逐字符操作的环节
如果你输入的是小写字母,那么取小写字母ascii-96;如果是数字,那么取小写数字ascii-47。但是这个东西的意义不明(实际上可以根据猜测结合上述的src密文来解题),我们接下来继续看
来看到这个状态检测部分,如果v13不等于8,就说明状态异常,就会调用后续的函数。
说明v13关乎整个程序的结构,所以就可以来查看直接对v13进行操作的代码
image
追踪这个sub_14000BD80函数。
image
发现了一大串switch、case分支,说明找到了这个核心run,就是根据不同的opcode来选择Handler
同时根据汇编给出的分支图例
屏幕截图 2025-12-07 212948
已经很明显了,所以我们直接来看这个核心函数来看opcode了

opcode详细分析

可以从上面给出的图片发现,switch语句里面传的参数是v61,所以可以判断出代码中v61是指令操作码(Opcode),每个 case 对应一条 VM 指令,按功能分类如下:

指令码 功能分类 核心逻辑 加密 / 运算关联
1 算术 / 赋值指令 调用sub_14000B610→sub_140002CE0校验→sub_140006450赋值到 VM 内存 基础数值赋值,关联阶乘表运算
2-4 数据读取指令 sub_140001C30/1D60/1E90读取 VM 内存→sub_140002D50校验(返回 8 则合法) 读取加密表(Src)/ 中间结果
5 除法指令 双操作数读取→校验→v78 / v57除法运算→赋值到 VM 内存 阶乘数值的核心除法变换(解密关键)
11 字符串 / 资源操作 sub_14000F9A0→sub_140008220加载加密资源→sub_140019230解密 加载上一轮提到的阶乘加密表(Src)
12-14 寄存器操作 sub_14000B7E0读取寄存器→sub_140002DF0校验→修改 IP / 内存 虚拟寄存器间数据搬运
15 内存写入指令 sub_140006290读内存→sub_140008450封装数据→修改 IP 向加密表 (Src)写入变换后数值
16-18 比较指令 双操作数读取→等于 / 小于 / 大于比较→sub_140006450写入比较结果 校验阶乘数值是否符合加密规则
19 函数调用指令 sub_140013530解析函数指针→(*(...))()调用外部函数 调用加密 / 解密核心算法(如置换)
20-21 指令指针修改 读取寄存器→直接修改 IP (a1+136)
32 计数器更新 读取内存→更新a1+144计数器→重置 IP 加密循环计数(如 38 字节分组处理)
33-35 内存寻址 / 写入 偏移计算→边界校验→向 VM 内存写入数值 阶乘表(Src)的字节置换 / 替换
default 正常退出 返回 8(合法标志) VM 指令流解析完成

现在解法已经很明确了,要么patch所有的异常点,使得返回值为8,要么追踪那些校验函数,监控a1+136(IP)和a1+48(指令流),dump 出完整 VM 指令集,还原自定义加密算法的全部逻辑,分析加密过程写出解密脚本。

让 VM 直接返回合法标志(8),跳过所有加密指令解析

patch 位置 1:指令边界校验(v62 >= sub_140006190(a1 + 48))
找到cmp rax, [sub_140006190的返回值],直接 patch 为mov rax, 1; jmp 正常退出;
patch 位置 2:所有异常返回点(return sub_140002CA0(...))
将这些返回指令全部 patch 为mov al, 8; ret(强制返回合法标志);
patch 位置 3:default 分支(直接返回 8)
若不想逐指令 patch,可将 switch 的default分支改为jmp到所有指令码的入口,强制走正常退出。

还原出VM指令集

我们来重新梳理一遍

特征点 对应代码逻辑 VM 解释器特征匹配
循环读取指令指针 *(_QWORD *)(a1 + 136) 作为指令指针(IP),每次+1 IP 寄存器,逐行解析指令
指令码分发(switch) switch (v61) 匹配 1/2/3/5/11~35 等指令码 指令集分发核心
指令长度 / 边界校验 v62 >= sub_140006190(a1 + 48) 返回 8(正常退出) 指令流边界检查
寄存器 / 内存操作 a1 + 24/a1 + 88/a1 + 144 作为 VM 内存 / 寄存器 虚拟寄存器空间
异常退出 / 错误码 return sub_140002CA0(...) 返回非 8 值(异常) VM 执行异常处理

本题给了一个exe文件,还有一个vmdata文件,所以我们可以对第二个vmdata文件进行分析
我们将这个操作码dump下来,结合vmdata这几个东西来编写最后的解密脚本了

由于还原整个cpu运行逻辑的操作本人造诣不够,等我学习一下放在下篇来分析吧

posted @ 2025-12-07 23:40  Aques  阅读(23)  评论(0)    收藏  举报