“逆向VM字节码程序”的学习

admin 2026-01-31 23:40:02 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文基于Flare-OnCTF题目解析逆向VM字节码保护的技术难点。内容涵盖VM架构优势、指令集定义及C++反汇编器的编写实现。文章演示了将二进制字节码转为可读汇编的过程,并总结出针对VM保护程序的关键策略:必须先理解解释器逻辑,再构建专属反汇编工具以还原核心算法。 综合评分: 90 文章分类: 逆向分析,二进制安全,CTF,安全工具


cover_image

“逆向VM字节码程序”的学习

原创

MicroPest MicroPest

MicroPest

2026年1月29日 19:47 安徽

这两天,碰到并学习了一个2016年Flare-On挑战赛3决赛中的一个CTF逆向题:一个包含了虚拟机字节码的smokestack.exe,真是开了眼,长了见识。

完全不同x86系列的代码,它是VM 字节码 = “虚拟机字节码”(Virtual Machine Bytecode),不是 x86/ARM 这类硬件指令,而是某个软件自己定义的一套“中间指令集”。由源码 → 编译成字节码 → 由一段“解释器/虚拟机”循环取 Opcode → 查表 → 执行对应 C/C++ 函数(或 JIT 成机器码)。通俗比喻:就像 Java 的  .class 、Python 的  .pyc ,只不过这里是作者自己写的“迷你 CPU”。

1、VM 字节码的用处

(1).体积/跨平台:只要带 2-3 KB 解释器,就能跑同一份字节码。

(2).抗逆向:硬件指令变成“自定义 Opcode”,IDA 默认认不出;得先写反汇编器(就是你贴的那段 C++)才能看逻辑。

(3).CTF/病毒常用:把关键校验(flag 检查、许可证)藏进字节码,分析者必须先逆解释器,再逆字节码,两步缺一不可。

2、指令格式

(1).每条指令固定 2 byte

+——–+———————–+

| opcode | 可选立即数 / 寄存器编号

+——–+————————+

(2). 指令集(14 条,RISC 风格)

0  push imm16        ; 把 16 位常数压栈

1  pop               ; 弹栈

2  add               ; 栈顶两数相加

3  sub               ; 相减

4  trm1              ; 作者自定义运算

5  trm2              ; 同上

6  xor               ; 异或

7  not               ; 按位取反

8  eq                ; 栈顶两数相等?置 0/1

9  sel               ; 三目选择

10 jmp imm16         ; 无条件跳

11 push reg16        ; 把 ax/bp/sp/ip 压栈

12 mov reg16, ST(0)  ; 栈顶值写到寄存器

13 nop

(3). 执行模型

三个寄存器:ax, bp, sp, ip(ip 指字节码偏移)。

一条小栈:push/pop 操作都在这条栈上完成。

解释器就是一个大  while(1) { fetch(); switch(opcode) { … } }  循环——对应你逆向时看到的  sub_401610  /  sub_402EE0  之类函数。

3、编写字节码反汇编器程序

#include<iostream>
#include<fstream>
#include<vector>
#include<stdexcept>
#include<string>
#include<iomanip>

conststd::vector<std::string>mnemonics=
{
"push","pop","add","sub",
"trm1","trm2","xor","not",
"eq","sel","jmp","push",
"mov","nop"
};

//{这个数组定义了14个VM操作码对应的助记符:

  • 索引0 = “push”
  • 索引2 = “add”
  • 索引10 = “jmp”
  • 等等…}
int&nbsp;main(intargc,char*argv[]);
constchar*GetRegisterName(std::uint16_tregister_id);

int&nbsp;main(intargc,char*argv[])
{
static_cast<void>(argc);
static_cast<void>(argv);
//参数检查
if(argc!=2)
{
std::cout<<"Usage:\n";
std::cout<<"smokestack_disasm dump"<<std::endl;
//需要一个参数:字节码文件路径
return1;
}
//读取字节码文件到内存

std::vector<std::uint8_t>buffer;

try
{
std::fstreaminput_file;
input_file.open(argv[1],std::ios_base::in|
std::ios_base::binary);

if(!input_file)
throwstd::runtime_error("Failed to open the input file");

input_file.seekg(0,std::ios_base::end);
if(!input_file)
throwstd::runtime_error("Seek failed");

std::streamsizeinput_file_size=input_file.tellg();
if(!input_file)
throwstd::runtime_error("Failed to get the file size");

input_file.seekg(0);
if(!input_file)
throwstd::runtime_error("Seek failed");

buffer.resize(input_file_size);
if(buffer.size()!=input_file_size)
throwstd::runtime_error("Memory allocation failed");

input_file.read(reinterpret_cast<char*>(buffer.data()),
input_file_size);

if(!input_file)
throwstd::runtime_error("Failed to read the file");

input_file.close();
}

catch(conststd::exception&exception)
{
std::cout<<exception.what()<<std::endl;
return1;
}
//逐条解析字节码

conststd::uint8_t*ptr=buffer.data();
//解析并输出每条指令

while(ptr<buffer.data()+buffer.size())
{
//计算当前指令地址(以16位字为单位)
std::uint32_tinstruction_pointer=(ptr-buffer.data())/2;
//输出格式:地址+操作码+助记符

std::cout<<std::hex<<std::setfill('0')<<std::setw(4)
<<instruction_pointer;//地址

std::cout<<"\t\t";

std::cout<<std::hex<<std::setfill('0')<<std::setw(2)<<
static_cast<int>(*ptr);//操作码

std::cout<<"\t"<<mnemonics[*ptr]<<" ";//助记符

// opcodes that require immediate parameters needs to
// increment the instruction pointer twice
switch(*ptr)
{
// push <immediate>
case0:
{
ptr+=2;//跳过操作码,指向参数
//读取16位立即数
std::uint16_tvalue=
*reinterpret_cast<conststd::uint16_t*>(ptr);

std::cout<<"0x"<<std::hex<<std::setfill('0')<<
std::setw(4)<<value;

// 如果是可打印ASCII字符,显示字符形式
if(value>=0x20&&value<=0x7D)
{
std::cout<<" ; '"<<static_cast<char>(value)
<<"'";
}

break;
}

// push <register_id>
case11:
{
ptr+=2;

std::uint16_tvalue=
*reinterpret_cast<conststd::uint16_t*>(ptr);

std::cout<<GetRegisterName(value);//转换寄存器ID为名称

break;
}

// mov <register_id>, stack[sp]
case12:
{
ptr+=2;

std::uint16_tvalue=
*reinterpret_cast<conststd::uint16_t*>(ptr);

std::cout<<GetRegisterName(value);
std::cout<<", ST(0)";

break;
}

default:
break;
}

std::cout<<std::endl;

//跳转指令后添加空行,增加可读性
if(*ptr==10)//操作码10 = jmp
{
std::cout<<std::hex<<std::setfill('0')<<std::setw(4)
<<instruction_pointer<<std::endl;
}

ptr+=2;
}

return0;
}

constchar*GetRegisterName(std::uint16_tregister_id)
{
switch(register_id)
{
case0:
return"ax";

case1:
return"bp";

case2:
return"sp";

case3:
return"ip";

default:
throwstd::runtime_error("Invalid register id");
}
}
将二进制的VM字节码文件转换成可读的汇编指令,方便逆向分析。

4、使用方法

(1). 提取字节码

从Smokestack.exe的虚拟地址0x0040A140处提取VM字节码数据段,保存为文件。

(2). 编译反汇编器

g++ smokestack_disasm.cpp -o smokestack_disasm

(3). 运行反汇编

./smokestack_disasm vm_bytecode.bin&nbsp;>&nbsp;disassembly.txt

5、反汇编器的输出

如:

0000 &nbsp; &nbsp;00 &nbsp;push 0x0021
0002 &nbsp; &nbsp;02 &nbsp;add &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; \ adds 0x21 to the last character in the
0003 &nbsp; &nbsp;00 &nbsp;push 0x0091 &nbsp; ; / program argument
0005 &nbsp; &nbsp;08 &nbsp;eq
0006 &nbsp; &nbsp;00 &nbsp;push 0x0016
0008 &nbsp; &nbsp;00 &nbsp;push 0x000c &nbsp; ; \ this is what we should take. last char
000a &nbsp; &nbsp;09 &nbsp;sel &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; / is: 0x91 - 0x21 = 'p'
000b &nbsp; &nbsp;0a &nbsp;jmp
000b

6、CTF程序:smokestack.exe

这个反汇编器是逆向VM保护程序的标准工具,它的价值在于:

  1. ✅ 降低分析难度 – 将二进制转为可读代码
  2. ✅ 理解程序逻辑 – 看清楚VM在做什么运算
  3. ✅ 手工求解 – 根据反汇编结果推导约束条件
  4. ✅ 验证猜测 – 确认理解的VM指令集是否正确

本质上,这是为Smokestack的自定义VM编写的”IDA Pro”

没有这个工具,逆向工程师只能看着一堆数字发呆;有了它,就能像阅读普通汇编代码一样分析VM逻辑。这就是为什么作者说”写一个反汇编器”是解决这道题的关键步骤!

7、逆向策略对比

|  目标   |          普通EXE         |     VM保护EXE   |

|——–|———————-|——————|

|**工具**| IDA Pro直接反编译 | 需要理解VM架构 |

|**难度**| 中等 | 高 |

|**方法**| 直接分析汇编 |1. 识别VM2. 提取字节码3. 写反汇编器4. 分析虚拟汇编 |

|**调试**| 直接下断点 | 需要在VM引擎下断点 |

8、总结

**最关键的区别**:

VM架构本质上是”程序中的程序” – 一个用机器码实现的软件CPU,运行自定义的指令集!

这就是为什么Smokestack这道题需要先理解VM架构,再编写反汇编器,最后才能分析真实逻辑的原因。


免责声明:

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

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

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

本文转载自:MicroPest MicroPest MicroPest《“逆向VM字节码程序”的学习》

评论:0   参与:  0