昨天,我们通过编写VM字节码程序了解了它的“取码、译码、执行”规则。今天,我们继续对那个程序进行一次深入的逆向分析,再次加深理解和感悟。
程序下载链接(历史文章):
smokestack.exe (对应《“逆向VM字节码程序”的学习(三)》)
SimpleVM.exe (对应本文分析的示例程序)
链接与提取码详见原文,此处不再赘述。
一、 程序概览
通过IDA Pro MCP分析,该程序是一个基于虚拟机(VM)的应用程序,使用自定义字节码来执行逻辑。
二、 VM架构分析
1、 VM解释器函数
- 地址:
0x41EBA8 (sub_41EBA8)
- 功能: 核心VM解释器,循环读取并执行字节码指令
2、 VM状态结构
VM状态存储在一个数据结构中,关键字段包括:
offset +5120 (0x1400): 程序计数器(PC)
offset +5124 (0x1404): 未知寄存器(-1初始值)
offset +5128 (0x1408): 停止标志(halt flag)
offset +0-4095: 字节码数组(1024个DWORD,即4KB字节码空间)
3、 VM指令集
通过反编译解释器函数,识别出以下13条指令:
指令 0: HALT
- 操作: 停止VM执行
- PC变化: PC++
- 功能: 程序正常退出
指令 1: PUSH (立即数)
- 操作: 将下一个DWORD作为立即数压入栈
- PC变化: PC += 2
- 功能: 压栈操作数
- 格式:
[1] [immediate_value]
指令 2: POP
- 操作: 从栈中弹出一个值
- PC变化: PC++
- 功能: 出栈
指令 3: ADD
- 操作: 弹出两个值,相加后压栈
- PC变化: PC++
- 功能: 加法运算 (v2 = pop(); v1 = pop(); push(v1 + v2))
- 错误检查: 检查溢出
指令 4: SUB
- 操作: 弹出两个值,相减后压栈
- PC变化: PC++
- 功能: 减法运算 (v2 = pop(); v1 = pop(); push(v1 - v2))
- 错误检查: 检查溢出
指令 5: MUL
- 操作: 弹出两个值,相乘后压栈
- PC变化: PC++
- 功能: 乘法运算 (v2 = pop(); v1 = pop(); push(v1 * v2))
- 错误检查: 检查乘法溢出
指令 6: DIV
- 操作: 弹出两个值,相除后压栈
- PC变化: PC++
- 功能: 除法运算 (v2 = pop(); v1 = pop(); push(v1 / v2))
- 错误检查: 除数为0时报错 "Division by zero"
指令 7: JMP (无条件跳转)
- 操作: 直接跳转到指定地址
- PC变化: PC = [PC+1]
- 功能: 无条件跳转
- 格式:
[7] [target_address]
指令 8: JNZ (条件跳转)
- 操作: 如果栈顶非零则跳转
- PC变化:
- 如果pop() != 0: PC += 2
- 否则: PC = [PC+1]
- 功能: 条件跳转
- 格式:
[8] [target_address]
- 操作: 读取用户输入并压栈
- PC变化: PC++
- 功能: 输入操作
指令 10: SWAP
- 操作: 交换栈顶两个元素
- PC变化: PC++
- 功能: v2 = pop(); v1 = pop(); push(v2); push(v1)
指令 11: OUTPUT
- 操作: 输出栈顶值
- PC变化: PC++
- 功能: 输出操作
指令 12: EXIT
- 操作: 设置停止标志
- PC变化: 不变
- 功能: 强制退出VM
三、 字节码数据位置
1、 字节码段1
- 地址:
0x423524
- 大小: 至少11个DWORD
- 内容:
01 00 00 00 ; PUSH
05 00 00 00 ; [value: 5]
01 00 00 00 ; PUSH
03 00 00 00 ; [value: 3]
01 00 00 00 ; PUSH
02 00 00 00 ; [value: 2]
05 00 00 00 ; MUL
03 00 00 00 ; ADD
0b 00 00 00 ; OUTPUT
0c 00 00 00 ; EXIT
00 00 00 00 ; HALT
分析: 这段代码计算 5 + (3 * 2) = 11 并输出
2、 字节码段2
后续还有函数指针表数据。
四、 程序执行流程
1 初始化流程
(1)start (0x421984) - 程序入口
(2)sub_41F0C4 / sub_41F104 - 加载字节码到VM
(3)sub_41F060 - 拷贝字节码数组
(4)sub_41E9B8 - 初始化VM状态
(5)sub_41EBA8 - 执行字节码
2 VM执行过程
(1)检查halt标志
(2)检查PC是否越界 (PC < 0x400)
(3)读取当前指令: opcode = vm[PC]
(4)根据opcode执行相应操作
(5)更新PC
(6)循环执行直到halt
五、辅助函数
sub_41EA08: 栈PUSH操作
sub_41EA9C: 栈POP操作
sub_41EB30: INPUT操作
sub_405A70, sub_405D70, sub_4044E4: OUTPUT相关
sub_405F50: PC越界错误处理
sub_405F58: 溢出错误处理
sub_41B698: 错误信息显示
sub_407684: 错误退出
六、安全特性
程序包含多项安全检查:
- PC边界检查: 防止代码执行越界
- 算术溢出检查: ADD/SUB/MUL操作检查溢出
- 除零检查: DIV操作检查除数
- 栈边界检查: 防止栈溢出/下溢
七、逆向分析建议
1 提取完整字节码
可以通过以下方式提取完整字节码:
- 从
0x423524 和 0x423554 读取完整数据
- 分析调用
sub_41F060 时传递的长度参数
2 编写反汇编器
基于识别的指令集,可以编写Python脚本将字节码反汇编为可读格式。
3 编写VM模拟器
可以用Python重新实现VM解释器,用于动态分析和调试。
八、 示例分析
字节码段1执行过程:
PUSH 5 ; stack = [5]
PUSH 3 ; stack = [5, 3]
PUSH 2 ; stack = [5, 3, 2]
MUL ; stack = [5, 6] (3*2)
ADD ; stack = [11] (5+6)
OUTPUT ; 输出: 11
EXIT ; 退出
HALT
字节码段2执行过程:
PUSH 5 ; stack = [5]
PUSH 5 ; stack = [5, 5]
SUB ; stack = [0] (5-5)
JNZ 11 ; 0==0, 不跳转
PUSH 888 ; stack = [888]
OUTPUT ; 输出: 888
EXIT ; 退出
九、 总结
这是一个设计良好的基于栈的虚拟机实现,具有:
- 完整的算术运算指令集
- 控制流指令(JMP/JNZ)
- I/O操作(INPUT/OUTPUT)
- 完善的错误检查机制
- 清晰的架构设计
该VM可用于:
十、 进一步研究方向
- 提取并分析所有字节码段
- 分析INPUT函数的具体实现
- 查找可能的flag检查逻辑
- 编写完整的VM模拟器用于动态分析
- 寻找可能的VM漏洞或绕过机制
十一、感悟
通过这个系列的学习,我们完成了从逆向分析到理解VM工作原理的闭环。字节码技术作为编程语言与硬件之间的桥梁,其核心价值在于跨平台性、安全性和可优化性。掌握这类逆向工程技能,不仅能深入理解软件保护机制,也为解决复杂的安全挑战(如CTF中的VM逆向题)打下坚实基础。
如果你对逆向工程、虚拟机技术或CTF挑战有更多兴趣,欢迎到 云栈社区 与更多技术爱好者交流探讨。
|