Golfer - Part 1
8 minutos de lectura
Se nos proporciona un binario de 32 bits llamado golfer:
$ file golfer
golfer: ELF 32-bit
Ingeniería inversa
Es bastante extraño porque file no muestra información relevante. De hecho, no lo podemos analizar con Ghidra o objdump. Por tanto, vamos a mirar a su volcado hexadecimal con xxd:
$ xxd golfer
00000000: 7f45 4c46 0161 3466 5455 487d 7952 7b6c .ELF.a4fTUH}yR{l
00000010: 0200 0300 0100 0000 4c00 0008 2c00 0000 ........L...,...
00000020: 675f 3330 4272 efbe 3400 2000 0100 0000 g_30Br..4. .....
00000030: 0000 0000 0000 0008 0000 0008 3801 0000 ............8...
00000040: 3801 0000 0500 0000 0010 0000 e9d6 0000 8...............
00000050: 00fe c3fe c2b9 0a00 0008 e8d0 0000 00b9 ................
00000060: 0800 0008 e8c6 0000 00b9 2400 0008 e8bc ..........$.....
00000070: 0000 00b9 0e00 0008 e8b2 0000 00b9 0c00 ................
00000080: 0008 e8a8 0000 00b9 2300 0008 e89e 0000 ........#.......
00000090: 00b9 0900 0008 e894 0000 00b9 2100 0008 ............!...
000000a0: e88a 0000 00b9 0600 0008 e880 0000 00b9 ................
000000b0: 0d00 0008 e876 0000 00b9 2200 0008 e86c .....v...."....l
000000c0: 0000 00b9 2100 0008 e862 0000 00b9 0500 ....!....b......
000000d0: 0008 e858 0000 00b9 2100 0008 e84e 0000 ...X....!....N..
000000e0: 00b9 2000 0008 e844 0000 00b9 2300 0008 .. ....D....#...
000000f0: e83a 0000 00b9 0f00 0008 e830 0000 00b9 .:.........0....
00000100: 0700 0008 e826 0000 00b9 2200 0008 e81c .....&....".....
00000110: 0000 00b9 2500 0008 e812 0000 00b9 0b00 ....%...........
00000120: 0008 e808 0000 0030 c0fe c0b3 2acd 8055 .......0....*..U
00000130: 89e5 b004 cd80 c9c3 ........
Se trata de un binario muy pequeño. Si lo ejecutamos, no ocurre nada aparentemente. Si miramos con detenimiento a la salida de xxd, veremos algunos caracteres imprimibles en la parte superior. Podemos verlos con strings:
$ strings golfer
a4fTUH}yR{l
g_30Br
Hay una H, una T, una B y también { y }. Por tanto, podemos pensar que se trata de la flag, pero desordenada.
Con pwntools, podemos desemsamblar las intrucciones a bajo nivel y ver un patrón curioso:
$ xxd -p golfer | tr -d \\n
7f454c46016134665455487d79527b6c02000300010000004c0000082c000000675f33304272efbe340020000100000000000000000000080000000838010000380100000500000000100000e9d6000000fec3fec2b90a000008e8d0000000b908000008e8c6000000b924000008e8bc000000b90e000008e8b2000000b90c000008e8a8000000b923000008e89e000000b909000008e894000000b921000008e88a000000b906000008e880000000b90d000008e876000000b922000008e86c000000b921000008e862000000b905000008e858000000b921000008e84e000000b920000008e844000000b923000008e83a000000b90f000008e830000000b907000008e826000000b922000008e81c000000b925000008e812000000b90b000008e80800000030c0fec0b32acd805589e5b004cd80c9c3
$ pwn disasm 38010000380100000500000000100000e9d6000000fec3fec2b90a000008e8d0000000b908000008e8c6000000b924000008e8bc000000b90e000008e8b2000000b90c000008e8a8000000b923000008e89e000000b909000008e894000000b921000008e88a000000b906000008e880000000b90d000008e876000000b922000008e86c000000b921000008e862000000b905000008e858000000b921000008e84e000000b920000008e844000000b923000008e83a000000b90f000008e830000000b907000008e826000000b922000008e81c000000b925000008e812000000b90b000008e80800000030c0fec0b32acd805589e5b004cd80c9c3
0: 38 01 cmp BYTE PTR [ecx], al
2: 00 00 add BYTE PTR [eax], al
4: 38 01 cmp BYTE PTR [ecx], al
6: 00 00 add BYTE PTR [eax], al
8: 05 00 00 00 00 add eax, 0x0
d: 10 00 adc BYTE PTR [eax], al
f: 00 e9 add cl, ch
11: d6 (bad)
12: 00 00 add BYTE PTR [eax], al
14: 00 fe add dh, bh
16: c3 ret
17: fe c2 inc dl
19: b9 0a 00 00 08 mov ecx, 0x800000a
1e: e8 d0 00 00 00 call 0xf3
23: b9 08 00 00 08 mov ecx, 0x8000008
28: e8 c6 00 00 00 call 0xf3
2d: b9 24 00 00 08 mov ecx, 0x8000024
32: e8 bc 00 00 00 call 0xf3
37: b9 0e 00 00 08 mov ecx, 0x800000e
3c: e8 b2 00 00 00 call 0xf3
41: b9 0c 00 00 08 mov ecx, 0x800000c
46: e8 a8 00 00 00 call 0xf3
4b: b9 23 00 00 08 mov ecx, 0x8000023
50: e8 9e 00 00 00 call 0xf3
55: b9 09 00 00 08 mov ecx, 0x8000009
5a: e8 94 00 00 00 call 0xf3
5f: b9 21 00 00 08 mov ecx, 0x8000021
64: e8 8a 00 00 00 call 0xf3
69: b9 06 00 00 08 mov ecx, 0x8000006
6e: e8 80 00 00 00 call 0xf3
73: b9 0d 00 00 08 mov ecx, 0x800000d
78: e8 76 00 00 00 call 0xf3
7d: b9 22 00 00 08 mov ecx, 0x8000022
82: e8 6c 00 00 00 call 0xf3
87: b9 21 00 00 08 mov ecx, 0x8000021
8c: e8 62 00 00 00 call 0xf3
91: b9 05 00 00 08 mov ecx, 0x8000005
96: e8 58 00 00 00 call 0xf3
9b: b9 21 00 00 08 mov ecx, 0x8000021
a0: e8 4e 00 00 00 call 0xf3
a5: b9 20 00 00 08 mov ecx, 0x8000020
aa: e8 44 00 00 00 call 0xf3
af: b9 23 00 00 08 mov ecx, 0x8000023
b4: e8 3a 00 00 00 call 0xf3
b9: b9 0f 00 00 08 mov ecx, 0x800000f
be: e8 30 00 00 00 call 0xf3
c3: b9 07 00 00 08 mov ecx, 0x8000007
c8: e8 26 00 00 00 call 0xf3
cd: b9 22 00 00 08 mov ecx, 0x8000022
d2: e8 1c 00 00 00 call 0xf3
d7: b9 25 00 00 08 mov ecx, 0x8000025
dc: e8 12 00 00 00 call 0xf3
e1: b9 0b 00 00 08 mov ecx, 0x800000b
e6: e8 08 00 00 00 call 0xf3
eb: 30 c0 xor al, al
ed: fe c0 inc al
ef: b3 2a mov bl, 0x2a
f1: cd 80 int 0x80
f3: 55 push ebp
f4: 89 e5 mov ebp, esp
f6: b0 04 mov al, 0x4
f8: cd 80 int 0x80
fa: c9 leave
fb: c3 ret
Hay un montón de instrucciones mov ecx, <addr>; call 0xf3. Nótese que el binario se cargará en la dirección 0x8000000, por lo que todas las direcciones <addr> son relativas a esta dirección base. Por ejemplo, 0x800000a se refiere al décimo byte del volcado (H), luego 0x8000008 es el octavo byte (T), y 0x8000024 es para el byte en la posición 36 (B).
Solución
Podemos obtener estos caracteres usando head (pero los índices empiezan en 1, no en 0, por lo que tenemos que aumentar los offsets en 1):
$ head -c 11 golfer | tail -c 1
H
$ head -c 9 golfer | tail -c 1
T
$ head -c 37 golfer | tail -c 1
B
Genial, ahora solo falta automatizar la extracción de la flag usando un script en Python como este:
#!/usr/bin/env python3
with open('golfer', 'rb') as f:
binary = f.read()
flag = ''
index = binary.index(b'\xfe\xc2') + 3
while binary[index - 1] == 0xb9:
addr = int.from_bytes(binary[index:index + 4], 'little') - 0x8000000
flag += chr(binary[addr])
index += 10
print(flag)
Flag
Si ejecutamos el script, obtendremos la flag:
$ python3 solve.py
HTB{y0U_4R3_a_g0lf3r}
Otro enfoque
Según readelf, el punto de entrada del programa está en la dirección 0x800004c:
$ readelf -a golfer
ELF Header:
Magic: 7f 45 4c 46 01 61 34 66 54 55 48 7d 79 52 7b 6c
Class: ELF32
Data: <unknown: 61>
Version: 52 <unknown>
OS/ABI: <unknown: 66>
ABI Version: 84
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x800004c
Start of program headers: 44 (bytes into file)
Start of section headers: 808673127 (bytes into file)
Flags: 0xbeef7242
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 0 (bytes)
Number of section headers: 0
Section header string table index: 0
readelf: Warning: possibly corrupt ELF file header - it has a non-zero section header offset, but no section headers
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08000000 0x08000000 0x00138 0x00138 R E 0x1000
There is no dynamic section in this file.
Esto significa que encontraremos el código máquina en el offset 0x4c del binario (todo el archivo se mapea a la dirección virtual 0x8000000 en programas de 32 bytes):
$ xxd -p -s 0x4c golfer
e9d6000000fec3fec2b90a000008e8d0000000b908000008e8c6000000b9
24000008e8bc000000b90e000008e8b2000000b90c000008e8a8000000b9
23000008e89e000000b909000008e894000000b921000008e88a000000b9
06000008e880000000b90d000008e876000000b922000008e86c000000b9
21000008e862000000b905000008e858000000b921000008e84e000000b9
20000008e844000000b923000008e83a000000b90f000008e830000000b9
07000008e826000000b922000008e81c000000b925000008e812000000b9
0b000008e80800000030c0fec0b32acd805589e5b004cd80c9c3
$ pwn disasm $(xxd -p -s 0x4c golfer | tr -d \\n)
0: e9 d6 00 00 00 jmp 0xdb
5: fe c3 inc bl
7: fe c2 inc dl
9: b9 0a 00 00 08 mov ecx, 0x800000a
e: e8 d0 00 00 00 call 0xe3
13: b9 08 00 00 08 mov ecx, 0x8000008
18: e8 c6 00 00 00 call 0xe3
...
d1: b9 0b 00 00 08 mov ecx, 0x800000b
d6: e8 08 00 00 00 call 0xe3
db: 30 c0 xor al, al
dd: fe c0 inc al
df: b3 2a mov bl, 0x2a
e1: cd 80 int 0x80
e3: 55 push ebp
e4: 89 e5 mov ebp, esp
e6: b0 04 mov al, 0x4
e8: cd 80 int 0x80
ea: c9 leave
eb: c3 ret
Ahora el código tiene más sentido. Vemos que comienza con una instrucción de salto a 0xdb, que simplemente ejecuta un sys_exit, porque $eax está a 1, entonces el programa simplemente termina. El código de error es 42 (0x2a), establecido en $ebx:
$ ./golfer; echo $?
42
Sin embargo, si parcheamos la instrucción jmp 0xdb con instrucciones nop (0x90 en hexadecimal), el programa simplemente imprimirá la flag carácter a carácter, porque establece $ebx = 1 y $edx = 1, y posteriormente pone una dirección de carácter en $ecx y llama a 0xe3. Aquí, el programa ejecutará un sys_write porque $eax se pone a 4. El descriptor de archivo es $ebx = 1 (stdout), y la longitud para imprimir también es $edx = 1.
Entonces, hagamos esto:
$ xxd -p golfer | tr -d \\n | sed s/e9d6000000/9090909090/g | xxd -r -p > golfer_patched
$ chmod +x golfer_patched
$ ./golfer_patched
HTB{y0U_4R3_a_g0lf3r}