vmpwn
基础知识
vmpwn常见设计
- 初始化分配模拟寄存器空间
- 初始化模拟栈空间
- 初始化分配模拟数据存储(buffer)空间 (data段)
- 初始化伪OPCODE空间 (text段)
常见流程
- 输入opcode
- 有一个分析器,循环分解我们输入的opcode来翻译出汇编指令,多为出入栈和调用寄存器
- 一般漏洞都在越界写和越界读之类的。
几个寄存器需要了解:
PC
程序计数器,存放的是一个内存地址,该地址中存放着下一条要执行的计算机指令;SP
指针寄存器,永远指向当前栈顶BP
基址寄存器,用于指向栈的某些地址,在调用函数的时候会用到AX
通用寄存器,用于存放一条指令执行后的结果
关于opcode
程序是怎么执行指令的?在编译的时候,编译器会将代码转化为汇编代码然后根据操作系统规定的规则进行机器码的一一对应置换,操作系统通过识别机器码去执行对应的操作。比如说随便取一个程序的一段汇编:
1 | .text:00000000000007DA mov edx, 64h ; 'd' ; nbytes |
在Hex View-1窗口中看到的视图是这样的
1 | 00000000000007D0 00 00 48 89 C7 E8 96 FE FF FF BA 64 00 00 00 48 ..H........d...H |
如果我们按照地址一一对应的话,就可以得到这样的对应关系:
1 | BA 64 00 00 00 mov edx, 64h |
这样的机制同样是使用与vmpwn的程序中的,左侧的二进制码就可以称为opcode
还有就是vm程序在运行过程中输出字符串的时候,我们在编程的时候会有写像这样的代码:
1 | printf("%d",buf); |
buf中存储的是字符串的地址,这样来输出字符串,但是我们不能写成下面这样
1 | printf("%d",'hello world!'); |
像这样的一个字符串按照编译的知识,它应该被存储在data段这样的数据存储区。
所以说我们在制作一个简单的VM,就需要具备一个程序应该有的一些结构和空间。比如:寄存器,栈,缓冲区域等。我们可以根据自己喜欢的方式来写属于自己的函数调用约定,写自己喜欢的存储方式。那么总结一下vm就是利用编写程序来实现模拟寄存器、stack、数据缓冲区来实现执行自己定义的虚拟指令(可能不太准确。
vmpwn大概就是利用程序规定的虚拟指令,来利用程序中的漏洞。
下面看几个名词解释:
虚拟机保护技术:所谓虚拟机保护技术,是指将代码翻译为机器和人都无法识别的一串伪代码字节流;在具体执行时再对这些伪代码进行一一翻译解释,逐步还原为原始代码并执行。这段用于翻译伪代码并负责具体执行的子程序就叫作虚拟机VM(好似一个抽象的CPU)。它以一个函数的形式存在,函数的参数就是字节码的内存地址。
VStartVM:虚拟机的入口函数,对虚拟机环境进行初始化。
VMDispather:解释opcode,并选择对应的Handler函数执行,当Handler执行完后会跳回这里,形成一个循环。
opcode:程序可执行代码转换成的操作码。
实践
[iscc2025擂台]vm_pwn
开逆:
main()
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
heap,sp_,bp,reg,execute 都是经过重命名后的(一些命名可能不太准确)
重点:这个vmpwn让输入的是字节流,如b'\x01'
,b'\x01\xf5\x01'
这个也就是opcode代码
1 | | op | src | dest | |
看一下bss段内存:
1 | .bss:0000000000004040 ; FILE *stderr |
这个heap模拟的是内存,应该是栈内存
fetch_opcode()
1 | __int64 __fastcall fetch_opcode(__int64 reg) |
这个函数的作用就是一次取一字节的opcode
fetch_next_qword()
1 | __int64 __fastcall sub_132C(__int64 a1) |
读取一个8字节数据,在程序中
push()
1 | unsigned __int64 __fastcall sub_1393(__int64 a1, __int64 a2) |
这个函数中首先栈顶指针sp -= 8
就说明了它是入栈操作,还将a2赋给了sp指向的地址
pop()
1 | __int64 __fastcall pop(__int64 a1) |
与上面push()相反就是出战喽
execute()
1 | v15 = __readfsqword(0x28u); |
因此推出指令集:
1 | ''' |
漏洞点与其他的vm题类似,这个数组是一个有符号的数组(通过下面movzx
看出),在\x01和\x02是有数组溢出的
1 | .text:00000000000017B1 def_152F: ; CODE XREF: execute+36↑j |
而且我们可以发现程序对数组的下标是没有任何检查的,因此我们可以通过负数的下标来读取got表内容,我们可以操作的内存有reg[0]~reg[3]
思路:
- 通过数组溢出读取got表的地址,因为got表不可写,所以我们通过got附近的地址-offest直接计算出malloc的got地址
- 用可操纵的内存计算libc_base并分别将
/bin/sh\x00
放入reg[0],system
放入reg[2] - 直接通过call调用即可getshell
exp:
1 | #!/usr/bin/env python3 |
[OGeek2019 Final]OVM
在buuctf中有该题目
逆向
main
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
fetch()
返回当前指令之后,跳转到下一条指令。
1 | __int64 fetch() |
作用:
取出当前PC(reg[15])指向的指令(memory[v0]),然后PC自增1。
返回该指令。
execute()
执行函数的逆向才是VMPWN的核心。
我们先了解几个宏函数:
1 |