KipodAfterFree CTF 2020 – Shadow stuck

This challenge was from KipodAfterFree CTF 2020 The challenge was interesting as it is an implementation of a Shadow stack to save a backup of saved RIP addresses and compare them before function returns, that is a good way to detect Buffer overflow exploitation attemps and block them.

The main vulnerability in the challenge was a Use-After-Free (UAF), that leads to write-what-where due to an insuficient check of the pointers in the linked list. I managed to solve the challenges in multiple ways.
void ss_init(void) {
  int flg;
  void *addr;
  uint *__g;
  
  addr = mmap((void *)0,12288,0,34,0,0);
  if (addr == (void *)0) {
    fwrite("Could not create shadowstack pages\n",1,35,stderr);
    _exit(1);
  }
  flg = mprotect((void *)((long)addr + 4096),4096,3);
  if (flg != 0) {
    __g = (uint *)__errno_location();
    fprintf(stderr,"Could not set RW protections on shadowstack page, errno: %d\n",(ulong)*__g);
    _exit(1);
  }
  printf("Shadow stack set up at %p\n",(long)addr + 4096);
  SHADOW_STACK = (long)addr + 4096;
  return;
}

This function performs the shadow stack initialization. First it creates an mmap memory page, and then mprotect’s it with Read-Write permissions. Finally saves in a BSS entry it’s address.
int main(void) {
  int c;
  int x;
  long local_res0;
  char buf [9];
  
  __cyg_profile_func_enter(main,local_res0);
  setvbuf(stdout,(char *)0,2,0);
  puts("Welcome to KEMS, the Kipod Employee Management System!\nWhat would you like to do today?");
  buf[0] = '\0';
  do {
    printf(
          "[A]dd a new a employee\n[F]ire an employee\n[C]hange employee name\n[R]ead employeename\n[Q]uit\n> "
          );
    c = getc(stdin);
    buf[0] = (char)c;
    do {
      x = getc(stdin);
    } while (x != 10);
    switch(buf[0]) {
    case 'A':
      kems_add();
      break;
    default:
      puts("\nInvalid action! Please try again.");
      break;
    case 'C':
      kems_change();
      break;
    case 'F':
      kems_fire();
      break;
    case 'Q':
      puts("Sure you want to leave? Here, have a BOF on me.");
      fgets(buf,64,stdin);
      remove_newlines((long)buf,64);
      __cyg_profile_func_exit(main,local_res0);
      return 0;
    case 'R':
      kems_read();
    }
  } while( true );
}

Apart from the typical menu we usually find on CTFs, the most interesting lines are the ones that call the shadow-stack-implementation functions __cyg_profile_func_enter(main,local_res0), which can remind us to the __chk_stack_fail of the canary implementation.
void __cyg_profile_func_enter(undefined8 addr,undefined8 value) {
  *SHADOW_STACK = value;
  SHADOW_STACK = SHADOW_STACK + 1;
  return;
}

The first function basically adds the saved RIP value to the shadow stack.
void __cyg_profile_func_exit(undefined8 addr,long value) {
  SHADOW_STACK = (long *)((long)SHADOW_STACK + -8);
  if (value != *SHADOW_STACK) {
    puts("Return address tampering detected, exiting.");
    _exit(1);
  }
  return;
}

Finally at the end of the function it checks whether the value coincides with the shadow stack one or it changed, if so, it will exit printing the message “Return address tampering detected, exiting.”.
Now, let’s analyze the available operations in the program.
undefined8 manager_add(char *buf) {
  void *ptr;
  size_t x;
  undefined8 n;
  long local_res0;
  void *p;
  
  __cyg_profile_func_enter(manager_add,local_res0);
  ptr = calloc(1,24);
  x = strlen(buf);
  if (((buf == (char *)0) || (15 < (uint)x)) || (ptr == (void *)0)) {
    n = 1;
  }
  else {
    memcpy(ptr,buf,x & 4294967295);
    if (g_list_root == (void *)0) {
      n = 0;
      g_list_root = ptr;
    }
    else {
      p = g_list_root;
      while (*(long *)((long)p + 16) != 0) {
        p = *(void **)((long)p + 16);
      }
      *(void **)((long)p + 16) = ptr;
      n = 0;
    }
  }
  __cyg_profile_func_exit(manager_add,local_res0);
  return n;
}


This function adds chunks with custom values up to 15 bytes length, something important about this function is that it allocates memory using calloc instead of malloc, which can make more difficult the exploitation as it zeroes-out the chunk content before returning it to the user, also when using calloc, tcache chunks are not returned.
Another important thing is that the chunks are identified using IDs among a linked-list, which uses chunk_ptr+0x10 to store the chunk to the next element of the list.
long manager_read(int id) {
  long local_res0;
  long x;
  long i;
  
  __cyg_profile_func_enter(manager_read,local_res0);
  if (g_list_root == 0) {
    x = 0;
  }
  else {
    i = 0;
    x = g_list_root;
    while (x != 0) {
      if (i == (long)id) goto LAB_00101ca8;
      x = *(long *)(x + 16);
      i = i + 1;
    }
    x = 0;
  }
LAB_00101ca8:
  __cyg_profile_func_exit(manager_read,local_res0);
  return x;
}

This is just a way to read the chunk content given the ID, so the chunk to read is identified in the linked-list by the position, which is the ID.
undefined8 manager_rename(int id,char *buf)

{
  size_t x;
  undefined8 n;
  long local_res0;
  void *e;
  long i;
  
  __cyg_profile_func_enter(manager_rename,local_res0);
  if (buf == (char *)0) {
    n = 1;
  }
  else {
    x = strlen(buf);
    if ((g_list_root == (void *)0) || (15 < (uint)x)) {
      n = 1;
    }
    else {
      i = 0;
      e = g_list_root;
      while (e != (void *)0) {
        if (i == (long)id) {
          memcpy(e,buf,x & 4294967295);
          n = 0;
          goto LAB_00101d7f;
        }
        e = *(void **)((long)e + 16);
        i = i + 1;
      }
      n = 1;
    }
  }
LAB_00101d7f:
  __cyg_profile_func_exit(manager_rename,local_res0);
  return n;
}

This function, like the manager_read, identifies the chunk to use by ID, but instead of reading it, we can edit it’s content with a value up to 15-bytes length.
void kems_fire(void)

{
  int x;
  char *ptr;
  long local_res0;
  char buf [16];
  
  __cyg_profile_func_enter(kems_fire,local_res0);
  printf("Which employee would you like to fire?\n> ");
  fgets(buf,16,stdin);
  remove_newlines((long)buf,16);
  _x = manager_remove(buf);
  if ((int)_x == 0) {
    printf("Employee %s removed. Please enter a short note as to why they were fired.\n> ",buf);
    ptr = (char *)malloc(24);
    fgets(ptr,24,stdin);
    remove_newlines((long)ptr,24);
    manager_log(0,(long)buf,(long)ptr);
    free(ptr);
  }
  else {
    printf("Could not fire employee with name %s. Maybe no such employee exists?\n",buf);
  }
  __cyg_profile_func_exit(kems_fire,local_res0);
  return;
}

kems_fire is a wrapper to call manager_remove, the important fact to analyze here is that it asks you a reason after deleting a manager entry, it allocates a 24-sized chunk, same as calloc so we can reutilize the calloc chunk using malloc, and now it allows us to edit up to 24 bytes, which includes the position of the linked list pointer.
undefined8 manager_remove(char *buf)

{
  int n;
  undefined8 f;
  long local_res0;
  char *i;
  char *w;
  
  __cyg_profile_func_enter(manager_remove,local_res0);
  if (g_list_root == (char *)0) {
    f = 1;
  }
  else {
    i = g_list_root;
    while (i != (char *)0) {
      n = strcmp(buf,i);
      w = g_list_root;
      if (n == 0) goto LAB_00101af3;
      i = *(char **)(i + 16);
    }
    f = 1;
  }
LAB_00101b2b:
  __cyg_profile_func_exit(manager_remove,local_res0);
  return f;
LAB_00101af3:
  while (*(long *)(w + 16) != 0) {
    if (i == *(char **)(w + 16)) {
      *(undefined8 *)(w + 16) = *(undefined8 *)(i + 16);
    }
  }
  free(i);
  f = 0;
  goto LAB_00101b2b;
}

Finally as we can see the chunks to free are very limited.
The main vulnerability here is, that the first chunk is not unlinked from the linked list, so it can be deleted and then read or edited, so we can trigger a Use-After-Free vulnerability. As the chunk is not unlinked, the pointer to next chunk is not restored, thus allowing us to point next value in the linked list to anywhere as in the malloc(24) we are able to modify it.
Now we figure out that the edit and read functions use ID to identify chunks, which is the position of the chunk in the linked list, this means we can edit the modified next chunk, which leads to write-what-where.
As we can read a chunk content, we can also trigger arbitrary read, which can help us to leak addresses. The easier address to leak is located in the shadow stack, and it is a known address, the first value in the shadow stack is the saved RIP of main, which is a pointer to the ret instruction of __libc_start_main, so now we have a libc leak.
There is a buffer overflow vulnerability in the program when calling Quit (located in main()). It reads from stdin 64 bytes into a buffer of just 9 bytes.
I could get shell locally using two main methods. In the first one, as we have libc leak, we can overwrite __free_hook to the address of system, then store “/bin/sh\x00” string in a chunk and then delete it. The result will be system("/bin/sh\x00") which will give us a shell.
The other method I used is triggering the Buffer overflow after overwriting the first value in the shadow stack by the first gadget in our ROP, this will be enough to bypass the shadow stack protection giving us the possibility to ROP.
I ROPed with system("/bin/sh\x00") and execve("/bin/sh\x00", NULL, NULL).
The full exploits and challenge binary can be found here.

Leave a Reply