Headache
9 minutos de lectura
Tenemos un binario llamado headache:
$ file headache
headache: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
Reconocimiento básico
Al ejecutarlo, escribe Initialising y tras unos segundos, nos pregunta por una clave:
$ ./headache
Initialising.....
Enter the key: asdf
Login Failed!
Vamos a depurarlo un poco con GDB. Una vez que nos pide la clave, podemos presionar ^C y poner un breakpoint después de la instrucción read:
$ gdb -q headache
Reading symbols from headache...
(No debugging symbols found in headache)
gef➤ run
Starting program: ./headache
Initialising.....
Enter the key: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ecafd2 in __GI___libc_read (fd=0x0, buf=0x55555555a7f0, nbytes=0x400) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
gef➤ backtrace
#0 0x00007ffff7ecafd2 in __GI___libc_read (fd=0x0, buf=0x55555555a7f0, nbytes=0x400) at ../sysdeps/unix/sysv/linux/read.c:26
#1 0x00007ffff7e4db9f in _IO_new_file_underflow (fp=0x7ffff7fa9980 <_IO_2_1_stdin_>) at libioP.h:948
#2 0x00007ffff7e4ef86 in __GI__IO_default_uflow (fp=0x7ffff7fa9980 <_IO_2_1_stdin_>) at libioP.h:948
#3 0x00007ffff7e4086c in __GI__IO_getline_info (fp=fp@entry=0x7ffff7fa9980 <_IO_2_1_stdin_>, buf=buf@entry=0x7fffffffe5a0 "", n=n@entry=0x1d, delim=delim@entry=0xa, extract_delim=extract_delim@entry=0x1, eof=eof@entry=0x0) at iogetline.c:60
#4 0x00007ffff7e4096c in __GI__IO_getline (fp=fp@entry=0x7ffff7fa9980 <_IO_2_1_stdin_>, buf=buf@entry=0x7fffffffe5a0 "", n=n@entry=0x1d, delim=delim@entry=0xa, extract_delim=extract_delim@entry=0x1) at iogetline.c:34
#5 0x00007ffff7e3f6ca in _IO_fgets (buf=0x7fffffffe5a0 "", n=0x1e, fp=0x7ffff7fa9980 <_IO_2_1_stdin_>) at iofgets.c:53
#6 0x0000555555555478 in ?? ()
#7 0x0000000000000000 in ?? ()
gef➤ break *0x0000555555555478
Breakpoint 1 at 0x555555555478
gef➤ x/100i 0x0000555555555478
0x555555555478: lea rax,[rbp-0x30]
0x55555555547c: lea rsi,[rip+0x1bc3] # 0x555555557046
0x555555555483: mov rdi,rax
0x555555555486: call 0x555555555130
0x55555555548b: movabs rax,0x3a0e551c0f31312a
0x555555555495: movabs rdx,0x55112d1c12460d02
0x55555555549f: mov QWORD PTR [rbp-0x50],rax
0x5555555554a3: mov QWORD PTR [rbp-0x48],rdx
0x5555555554a7: mov DWORD PTR [rbp-0x40],0x19145c51
0x5555555554ae: mov BYTE PTR [rbp-0x3c],0x0
0x5555555554b2: lea rax,[rbp-0x30]
0x5555555554b6: mov rdi,rax
0x5555555554b9: call 0x555555555060
0x5555555554be: cmp rax,0x14
0x5555555554c2: je 0x5555555554da
0x5555555554c4: lea rdi,[rip+0x1b7d] # 0x555555557048
0x5555555554cb: call 0x555555555050
0x5555555554d0: mov edi,0x1
0x5555555554d5: call 0x555555555150
0x5555555554da: mov DWORD PTR [rbp-0x8],0x0
0x5555555554e1: jmp 0x555555555526
0x5555555554e3: mov edx,DWORD PTR [rbp-0x8]
0x5555555554e6: lea rax,[rbp-0x50]
0x5555555554ea: mov esi,edx
0x5555555554ec: mov rdi,rax
0x5555555554ef: call 0x55555555535f
0x5555555554f4: mov eax,DWORD PTR [rbp-0x8]
0x5555555554f7: cdqe
0x5555555554f9: movzx edx,BYTE PTR [rbp+rax*1-0x50]
0x5555555554fe: mov eax,DWORD PTR [rbp-0x8]
0x555555555501: cdqe
0x555555555503: movzx eax,BYTE PTR [rbp+rax*1-0x30]
0x555555555508: cmp dl,al
0x55555555550a: je 0x555555555522
0x55555555550c: lea rdi,[rip+0x1b35] # 0x555555557048
0x555555555513: call 0x555555555050
0x555555555518: mov edi,0x1
0x55555555551d: call 0x555555555150
0x555555555522: add DWORD PTR [rbp-0x8],0x1
0x555555555526: cmp DWORD PTR [rbp-0x8],0x13
0x55555555552a: jle 0x5555555554e3
0x55555555552c: lea rdi,[rip+0x1b23] # 0x555555557056
0x555555555533: call 0x555555555050
0x555555555538: mov edi,0x0
0x55555555553d: call 0x555555555150
0x555555555542: push rbp
0x555555555543: mov rbp,rsp
0x555555555546: push rbx
0x555555555547: sub rsp,0x28
0x55555555554b: mov QWORD PTR [rbp-0x28],rdi
0x55555555554f: mov DWORD PTR [rbp-0x14],0x0
0x555555555556: jmp 0x555555555580
0x555555555558: mov eax,DWORD PTR [rbp-0x14]
0x55555555555b: movsxd rdx,eax
0x55555555555e: mov rax,QWORD PTR [rbp-0x28]
0x555555555562: add rax,rdx
0x555555555565: movzx ecx,BYTE PTR [rax]
0x555555555568: mov eax,DWORD PTR [rbp-0x14]
0x55555555556b: movsxd rdx,eax
0x55555555556e: mov rax,QWORD PTR [rbp-0x28]
0x555555555572: add rax,rdx
0x555555555575: xor ecx,0x61
0x555555555578: mov edx,ecx
0x55555555557a: mov BYTE PTR [rax],dl
0x55555555557c: add DWORD PTR [rbp-0x14],0x1
0x555555555580: mov eax,DWORD PTR [rbp-0x14]
0x555555555583: movsxd rbx,eax
0x555555555586: mov rax,QWORD PTR [rbp-0x28]
0x55555555558a: mov rdi,rax
0x55555555558d: call 0x555555555060
0x555555555592: cmp rbx,rax
0x555555555595: jb 0x555555555558
0x555555555597: nop
0x555555555598: add rsp,0x28
0x55555555559c: pop rbx
0x55555555559d: pop rbp
0x55555555559e: ret
0x55555555559f: push rbp
0x5555555555a0: mov rbp,rsp
0x5555555555a3: sub rsp,0x50
0x5555555555a7: mov QWORD PTR [rbp-0x38],rdi
0x5555555555ab: mov QWORD PTR [rbp-0x40],rsi
0x5555555555af: mov QWORD PTR [rbp-0x48],rdx
0x5555555555b3: mov rax,QWORD PTR [rbp-0x40]
0x5555555555b7: add rax,0x2
0x5555555555bb: movabs rdx,0xaaaaaaaaaaaaaaab
0x5555555555c5: mul rdx
0x5555555555c8: mov rax,rdx
0x5555555555cb: shr rax,1
0x5555555555ce: lea rdx,[rax*4+0x0]
0x5555555555d6: mov rax,QWORD PTR [rbp-0x48]
0x5555555555da: mov QWORD PTR [rax],rdx
0x5555555555dd: mov rax,QWORD PTR [rbp-0x48]
0x5555555555e1: mov rax,QWORD PTR [rax]
0x5555555555e4: mov rdi,rax
0x5555555555e7: call 0x5555555550c0
0x5555555555ec: mov QWORD PTR [rbp-0x18],rax
0x5555555555f0: cmp QWORD PTR [rbp-0x18],0x0
0x5555555555f5: jne 0x555555555601
0x5555555555f7: mov eax,0x0
Arriba vemos un poco del código ensamblador. Vamos a continuar:
gef➤ continue
Continuing.
ABCD
Breakpoint 1, 0x0000555555555478 in ?? ()
Ahora podemos usar ni para ir por las instrucciones paso por paso. En algún punto, llegaremos a esta instrucción:
gef➤ x/i $rip
=> 0x5555555554be: cmp rax,0x14
gef➤ p/x $rax
$1 = 0x4
gef➤ x/s $rdx
0x7fffffffe5a0: "ABCD"
Con esta instrucción se intuye que la longitud de la flag tiene que ser 0x14 (20 caracteres). Entonces, vamos a poner un breakpoint aquí y a ejecutar el programa otra vez:
gef➤ break *0x5555555554be
Breakpoint 2 at 0x5555555554be
gef➤ run
Starting program: ./headache
Initialising.....
Enter the key: AAAAAAAAAAAAAAAAAAAA
Breakpoint 1, 0x0000555555555478 in ?? ()
Continuamos el primer breakpoint:
gef➤ continue
Continuing.
Breakpoint 2, 0x00005555555554be in ?? ()
Y llegamos a la comprobación de la longitud:
gef➤ x/i $rip
=> 0x5555555554be: cmp rax,0x14
gef➤ p/x $rax
$2 = 0x14
gef➤ x/s $rdx
0x7fffffffe5a0: 'A' <repeats 20 times>
Efectivamente, todo va bien de momemto. Luego, el programa entra en un bucle donde se llama a algunas funciones. En otro punto, podemos encontrar una instrucción cmp dl, al, donde $rax sontiene un carácter de nuestra clave (A, 0x41) y $rdx contiene el carácter esperado (H, 0x48):
gef➤ x/i $rip
=> 0x555555555508: cmp dl,al
gef➤ p/x $rax
$3 = 0x41
gef➤ p/x $rdx
$4 = 0x48
Automatización
Entonces, podemos poner un breakpoint y modificar $rax para que coincida con $rdx:
gef➤ break *0x555555555508
Breakpoint 3 at 0x555555555508
gef➤ set $rax = $rdx
gef➤ continue
Continuing.
Breakpoint 3, 0x0000555555555508 in ?? ()
Luego vemos que el siguiente carácter esperado en $rdx es (T, 0x54):
gef➤ p/x $rax
$5 = 0x41
gef➤ p/x $rdx
$6 = 0x54
gef➤ set $rax = $rdx
gef➤ continue
Continuing.
Breakpoint 3, 0x0000555555555508 in ?? ()
Y el siguiente: (B, 0x42):
gef➤ p/x $rax
$7 = 0x41
gef➤ p/x $rdx
$8 = 0x42
Entonces, podemos asumir que será la flag. Podemos hacerlo de forma manual, pero será mucho mejor automatizar el proceso con un script de Python y pwntools:
$ python3 solve.py
[+] Starting local process '/usr/bin/gdb': pid 1677093
[+] Flag: HTB{w0w_th4ts_c000l}
[*] Stopped process '/usr/bin/gdb' (pid 1677093)
Y vemos una flag. Pero no es correcta. De hecho, hay otra flag falsa en el propio binario:
$ strings headache | grep HTB
HTB{not_so_easy_lol}
Mientras se analizaban los pasos anteriores, se vio que no solo $rdx tiene caracteres de una flag, sino que también $rsi. Pero la flag sigue siendo incorrecta. No obstante, el registro $rcx también guarda un solo byte. Si lo cogemos, vemos una clave:
$ python3 solve.py
[+] Starting local process '/usr/bin/gdb': pid 1709433
[+] Flag: bestkeyeverforrealxd
[*] Stopped process '/usr/bin/gdb' (pid 1709433)
Pero es inútil. Parece que esto va a ser un dolor de cabeza… Podemos notar que la clave no es correcta fuera de GDB:
$ ./headache
Initialising.....
Enter the key: HTB{w0w_th4ts_c000l}
Login Failed!
$ gdb -q headache
Reading symbols from headache...
(No debugging symbols found in headache)
gef➤ run
Starting program: ./headache
Initialising.....
Enter the key: HTB{w0w_th4ts_c000l}
Login success!
[Inferior 1 (process 1688844) exited normally]
Bypass de métodos anti-depuración
Por tanto, tienen que existir métodos anti-depuración. Vamos a tratar de saltárnoslos. Para esto, tenemos que analizar el código ensamblador del principio:
$ gdb -q headache
Reading symbols from headache...
(No debugging symbols found in headache)
gef➤ start
Stopped due to shared library event (no libraries added or removed)
[*] PIC binary detected, retrieving text base address
[+] Breaking at entry-point: 0x555555555190
gef➤ x/70i $rip
=> 0x555555555190: xor ebp,ebp
0x555555555192: mov r9,rdx
0x555555555195: pop rsi
0x555555555196: mov rdx,rsp
0x555555555199: and rsp,0xfffffffffffffff0
0x55555555519d: push rax
0x55555555519e: push rsp
0x55555555519f: lea r8,[rip+0xdea] # 0x555555555f90
0x5555555551a6: lea rcx,[rip+0xd83] # 0x555555555f30
0x5555555551ad: lea rdi,[rip+0xdfb] # 0x555555555faf
0x5555555551b4: call QWORD PTR [rip+0x3e26] # 0x555555558fe0
0x5555555551ba: hlt
0x5555555551bb: nop DWORD PTR [rax+rax*1+0x0]
0x5555555551c0: lea rdi,[rip+0x3f81] # 0x555555559148
0x5555555551c7: lea rax,[rip+0x3f7a] # 0x555555559148
0x5555555551ce: cmp rax,rdi
0x5555555551d1: je 0x5555555551e8
0x5555555551d3: mov rax,QWORD PTR [rip+0x3dfe] # 0x555555558fd8
0x5555555551da: test rax,rax
0x5555555551dd: je 0x5555555551e8
0x5555555551df: jmp rax
0x5555555551e1: nop DWORD PTR [rax+0x0]
0x5555555551e8: ret
0x5555555551e9: nop DWORD PTR [rax+0x0]
0x5555555551f0: lea rdi,[rip+0x3f51] # 0x555555559148
0x5555555551f7: lea rsi,[rip+0x3f4a] # 0x555555559148
0x5555555551fe: sub rsi,rdi
0x555555555201: sar rsi,0x3
0x555555555205: mov rax,rsi
0x555555555208: shr rax,0x3f
0x55555555520c: add rsi,rax
0x55555555520f: sar rsi,1
0x555555555212: je 0x555555555228
0x555555555214: mov rax,QWORD PTR [rip+0x3dd5] # 0x555555558ff0
0x55555555521b: test rax,rax
0x55555555521e: je 0x555555555228
0x555555555220: jmp rax
0x555555555222: nop WORD PTR [rax+rax*1+0x0]
0x555555555228: ret
0x555555555229: nop DWORD PTR [rax+0x0]
0x555555555230: cmp BYTE PTR [rip+0x3f31],0x0 # 0x555555559168
0x555555555237: jne 0x555555555268
0x555555555239: push rbp
0x55555555523a: cmp QWORD PTR [rip+0x3db6],0x0 # 0x555555558ff8
0x555555555242: mov rbp,rsp
0x555555555245: je 0x555555555253
0x555555555247: mov rdi,QWORD PTR [rip+0x3e7a] # 0x5555555590c8
0x55555555524e: call 0x555555555180
0x555555555253: call 0x5555555551c0
0x555555555258: mov BYTE PTR [rip+0x3f09],0x1 # 0x555555559168
0x55555555525f: pop rbp
0x555555555260: ret
0x555555555261: nop DWORD PTR [rax+0x0]
0x555555555268: ret
0x555555555269: nop DWORD PTR [rax+0x0]
0x555555555270: jmp 0x5555555551f0
0x555555555275: push rbp
0x555555555276: mov rbp,rsp
0x555555555279: sub rsp,0x40
0x55555555527d: mov rax,0x1
0x555555555284: mov rdi,0x1
0x55555555528b: mov rdx,0x64
0x555555555292: xor rax,rdx
0x555555555295: xor rdi,rdi
0x555555555298: xor rdx,rdx
0x55555555529b: syscall
0x55555555529d: mov DWORD PTR [rbp-0x4],eax
0x5555555552a0: movabs rax,0x68686a4d6c5a575a
0x5555555552aa: movabs rdx,0x784d575a79636a4e
0x5555555552b4: mov QWORD PTR [rbp-0x40],rax
Ahí hay una syscall sospechosa. Si la analizamos, vemos que $rdx tiene el valor 0x64 y $rax tiene 0x1, y luego pasará por xor rax, rdx, por lo que $rax = 0x64 ^ 0x1 = 0x65. Si echamos un vistazo a una tabla de syscall en sistemas de 64 bits, tenemos un sys_ptrace. Esta syscall analiza el proceso y es capaz de identificar si el proceso está siendo depurado. Por tanto, para saltarnos esto, podemos poner un breakpoint en la dirección de la syscall (0x55555555529b) y saltarla con jump 0x55555555529d, de manera que pasamos a la siguiente instrucción.
También se puede aplicar un parche al binario para quitar la instrucción syscall (transformar 0f05 en 9090, instrucciones nop). Pero no sirve para nada, el programa se comportará igual que si estuviéramos en GDB:
$ xxd -p headache | tr -d \\n | sed s/0f05/9090/g | xxd -r -p > headache_patched
$ chmod +x headache_patched
$ ./headache_patched
Initialising.....
Enter the key: HTB{w0w_th4ts_c000l}
Login success!
Por tanto, podemos pensar que sys_ptrace está ahí para construir la flag. Tenemos que hacer pensar al binario que está fuera de un depurador. Esto se puede hacer en GDB con catch syscall ptrace. Y una vez que se realiza la syscall, tenemos que poner $rax en 0 como si la syscall se hubiera completado correctamente.
En total, hay cuatro llamadas a sys_ptrace. Después, podemos continuar con la ejecución del programa. En algunos puntos, veremos más flags falsas:
gef➤ x/s $rdx
0x7fffffffe530: "HTB{th4t_w4s_h4rd}"
Y finalmente, llegaremos a otra instrucción cmp edx, eax en un bucle.
Flag
Esta vez, la flag sí es la buena. Entonces, podemos actualizar el script de Python añadiendo el bypass de sys_ptrace y obtener la flag:
$ python3 solve.py
[+] Starting local process '/usr/bin/gdb': pid 67724
[+] Flag: HTB{l4yl3_w4s_h3r3!}
[*] Stopped process '/usr/bin/gdb' (pid 67724)
$ ./headache
Initialising.....
Enter the key: HTB{l4yl3_w4s_h3r3!}
Login success!
El script completo puede encontrarse aquí: solve.py.