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
.