vm
原理
程序运行时通过解释操作码,选择对应的函数(handle)执行
本质上是作者自己实现了一套指令集
比如x86汇编中的0xFF是jmp,而0xE0, 0x48 mov
在虚拟机中既可以自定义指令对应的操作码
还可以自定义数组实现cpu的功能

pc指针是程序计数器,指向下一条指令
原始代码->前端编译器->虚拟指令(vmcode)->虚拟cpu解释执行->计算结果
0x01从零到一实现虚拟机
先从正向了解虚拟机
1.常量定义
1 |
|
2.指令集定定义(Opcode枚举)
最基础的操作码,ida常见于汇编
将在后面给出每个指令的具体实现
1 | typedef enum { |
flag是标志寄存器,用于存储CMP的比较结果
1 | // OP_CMP: r[dst] 与 r[src1] 比较 |
pc是程序计数器:指向下一条指令的地址
| 正常执行 | pc += INSN_SIZE(即 +4,取下一条指令) |
|---|---|
| 跳转 | pc = target(强制改道) |
| CALL | 先压栈返回地址,再 pc = target |
| RET | 从栈弹出返回地址,pc = 返回地址 |
| HALT | 停止,pc 不再前进 |
3.状态结构体
1 | typedef struct{ |
4.核心的执行函数:vm_step()
先取指:从内存中读取数据(类似code段,code段(代码段)就是存放程序指令的地方)
再执行:用switch+case匹配数据与对应字节码的操作,执行后移动指针向后推进一条指令(类似cs(代码段寄存器)、IP(指令指针寄存器
)的执行方式)
1 | void vm_step(VM *vm, const uint8_t *bytecode, size_t code_len) { |
int fprintf(FILE *stream, const char *format, …);(格式字符串输出)
5.主循环vm_run()
不断调用vm_step(),直到halted为真
1 | void vm_run(VM *vm, const uint8_t *bytecode, size_t code_len) { |
0x02一个虚拟机程序(flag验证)
1 |
|
注意的是输入v\nm会直接读个\n进去导致验证失败
0x03程序ida逆向
vm的核心主循环是vm_run,其中当不停时执行核心执行函数vm_step()
进去直接看graphic_view

一看就是switch-case用太多了
vm_step函数最开始检测程序是否结束
之后加载操作码和操作数
之后就是指令识别
0x04
开始逆向
解的时候关键看两个点,一个是vm_step()主要执行函数,另一个就是程序的二进制代码
先dump操作码
1 | 0x2,0x0,0x0,0x1F,0x14,0x1,0x0,0x0,0x7,0x1,0x1,0x0,0x2,0x2,0x0,0x67,0xC,0x1,0x2,0x0,0xF,0x0,0x0,0x48,0x14,0x1,0x0,0x0,0x7,0x1,0x1,0x0,0x2,0x2,0x0,0x72,0xC,0x1,0x2,0x0,0xF,0x0,0x0,0x48,0x2,0x0,0x0,0x4F,0x15,0x0,0x0,0x0,0x2,0x0,0x0,0x4B,0x15,0x0,0x0,0x0,0x2,0x0,0x0,0xA,0x15,0x0,0x0,0x0,0xD,0x0,0x0,0x60,0x2,0x0,0x0,0x4E,0x15,0x0,0x0,0x0,0x2,0x0,0x0,0x4F,0x15,0x0,0x0,0x0,0x2,0x0,0x0,0xA,0x15,0x0,0x0,0x0,0xFF,0x0,0x0,0x0 |
原有的格式是[opcode][dest][res1][resc2]
graphic view叠一起怎么办,选择layout还原布局
1 | bytecode = bytes([0x2,0x0,0x0,0x1F,0x14,0x1,0x0,0x0,0x7,0x1,0x1,0x0,0x2,0x2,0x0,0x67,0xC,0x1,0x2,0x0,0xF,0x0,0x0,0x48,0x14,0x1,0x0,0x0,0x7,0x1,0x1,0x0,0x2,0x2,0x0,0x72,0xC,0x1,0x2,0x0,0xF,0x0,0x0,0x48,0x2,0x0,0x0,0x4F,0x15,0x0,0x0,0x0,0x2,0x0,0x0,0x4B,0x15,0x0,0x0,0x0,0x2,0x0,0x0,0xA,0x15,0x0,0x0,0x0,0xD,0x0,0x0,0x60,0x2,0x0,0x0,0x4E,0x15,0x0,0x0,0x0,0x2,0x0,0x0,0x4F,0x15,0x0,0x0,0x0,0x2,0x0,0x0,0xA,0x15,0x0,0x0,0x0,0xFF,0x0,0x0,0x0]) |
解密的到汇编
0x05ida断点动态trace
首先去找switch-case函数去vm_step()函数中找到每个case函数的处理函数的地址
在每个case函数的入口点设置条件断点
在ida设置
location:填写函数地址
Condition:写 False (让断点不暂停执行)
edit scription编写脚本
第三步:断点脚本内容
1 | # 每次执行到 LOAD_CONST 这个 case 时自动运行 |
每一个case都有类似的脚本,再对应case位置设置脚本
第四步:输入真实的flag输入,让程序跑完
ida的output窗口会自动输出trace
1 | 0 LOAD_CONST sp=0 1 0 0 475378216224 |
如何从trace分析flag
重点关注
1 | 1.COMPARE->rcx,rbx就是两侧的操作数(比较了什么) |
例如
1 | 行8: COMPARE sp=3 136 [val_A] [val_B] |
06Funpyvm
python3.13pyc反编译网站
https://pychaos.io/
这道题一个难点就是怎么将高版本的pyc文件反编译为python文件
这是polarisctf2026-FunPyVM是个vm
opcode.bin正是操作码,四字节为一组,opcode+dst+rsc1+rsc2
主要是找vm_step
1 | import dis, marshal |
读取汇编
1 | LOAD_CONST 0 (0) |
可以看见压栈导入kernelvm可以去找这个文件
在main.exe_extracted\PYZ.pyz_extracted找到kennerlvm.pyc
1 | import sys |
这就是vm的核心
sys是什么,sys是个内置的模板,提供了与python解释器与内部环境的交互器,sys.argv是个列表,用于存储命令行参数,第一个元素是脚本名,其余元素是参数
f self.R0 in self.heap and opnum < len(self.heap[self.R0]):判断出来R0是堆指针
heap是堆,pc是程序计数器
opnum = bytecode[self.PC+1] if instruction_length == 2 else 0这是三元表达式
0x10:allocate immdite模拟动态分配
0x17明显是从内存中load数据
0x18是store内存中
STORE_ATTR 是**对象.heapnum = 值**这么个操作,出栈存在heapnum上
特此声明该反编译代码不全
1 | import dis, marshal |
还是得看汇编
1 | 78 L18: COPY 1 |
这就是 COMPARE_OP 弹出栈顶两个数,比较是否相等1,将True与False压栈
POP_TOP丢弃栈顶的值
1 |
|
1 | 78 L18: COPY 1 |
简单列一下32对应的操作为与255位与,取第四位存入r1寄存器中
1 |
|
得到汇编
1 | python agent.py opcode.bin |
主要逻辑是两轮
第一轮
1 | if input[i]!= 0: |
第二轮:
1 | if input[i]!=0: |
最后与
1 | [41, 71, 57, 26, 63, 80, 57, 38, 64, 95, 97, 99, 105, 56, 82, 113, 115, 96, 71, 124, 105, 80, 106, 115, 111, 130, 191] |
逆向就是((result-7-3*i)&0xFF)^55
1 |
|
得到why_you_think_this_is_true?,很明显是faker flag
一般来说pyd是动态链接文件类似于dll,也是PE文件,开头的魔法数字为MZ,而ntbase.pyd开头
10 64 10 32 20 00 51 12 63 显然是vm流
大概就是先不为0时加10,之后前缀异或最后为技术加0x31,为偶数家伙是那个0x10
1 | target = [ |
得出flag为F0n_And_3asyViMGa1v1eF9rY@u
python的set()集合
创建集合的两种方法,其支持集合的多种操作
1 | set1 = {1, 2, 3} |
添加元素
1 | set1.add(4) |
添加多个元素
1 | set1.update([5,6],{7,8}) |
删除元素的方法
remove(x)删除指定元素,不存在则报错
discard(x)移除指定元素,不存在时不报错
pop()随即清理一个元素
clear()清除随机一个元素
集合运算
交集a&b或者a.intersection(b)
并集a | b或者a.union(b)
差集 a-b或a.difference(b)
对称差集a^b,a.symmertic_difference(b) ——–>{1,2,3}^{3,4,5}—>{1,2,4,5}
判断关系
issubset()是否为子集
issuperset():判断是否为超集
isdisjoint():判断两个集合是否无交集
注意
- Set 中的元素必须是不可变类型,如数字、字符串、元组。
- Set 本身是可变的,因此不能作为字典的键或其他集合的元素。
