The Vault
6 minutos de lectura
Se nos da un binario llamado vault
:
$ file vault
vault: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 4.4.0, stripped
Descompilación
Abrimos Ghidra para descompilarlo. Esta es la función entry
:
void processEntry entry(undefined8 param_1, undefined8 param_2) {
undefined auStack_8[8];
__libc_start_main(FUN_0010c450, param_2, &stack0x00000008, FUN_0010d460, FUN_0010d4d0, param_1, auStack_8);
do {
// WARNING: Do nothing block with infinite loop
} while (true);
}
Entonces, la función “main” es FUN_0010c450
(el nombre de la función es su dirección porque el binario está despojado de sus símbolos), que solo llama a otra:
undefined8 FUN_0010c450() {
FUN_0010c220();
return 0;
}
Y esta función (FUN_0010c220
) es en la que nos tenemos que centrar:
void FUN_0010c220() {
bool bVar1;
byte bVar2;
long in_FS_OFFSET;
byte local_241;
uint local_234;
char local_219;
long local_218[65];
long local_10;
local_10 = *(long *) (in_FS_OFFSET + 0x28);
_ZNSt14basic_ifstreamIcSt11char_traitsIcEEC1EPKcSt13_Ios_Openmode(local_218, "flag.txt", 8);
// try { // try from 0010c25e to 0010c400 has its CatchHandler @ 0010c2a5
bVar2 = _ZNSt14basic_ifstreamIcSt11char_traitsIcEE7is_openEv(local_218);
if ((bVar2 & 1) == 0) {
_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(&_ZSt4cout, "Could not find credentials\n");
// WARNING: Subroutine does not return
exit(-1);
}
bVar1 = true;
local_234 = 0;
while (true) {
local_241 = 0;
if (local_234 < 0x19) {
local_241 = _ZNKSt9basic_iosIcSt11char_traitsIcEE4goodEv((long) local_218 + *(long *) (local_218[0] - 0x18));
}
if ((local_241 & 1) == 0) break;
_ZNSi3getERc(local_218, &local_219);
bVar2 = (***(code ***) (&PTR_PTR_00117880) [(byte) (&DAT_0010e090) [(int) local_234]])();
if ((int) local_219 != (uint) bVar2) {
bVar1 = false;
}
local_234 = local_234 + 1;
}
if (bVar1) {
_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(&_ZSt4cout, "Credentials Accepted! Vault Unlocking...\n");
} else {
_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(&_ZSt4cout, "Incorrect Credentials - Anti Intruder Sequence Activated...\n");
}
_ZNSt14basic_ifstreamIcSt11char_traitsIcEED1Ev(local_218);
if (*(long *) (in_FS_OFFSET + 0x28) == local_10) {
return;
}
// WARNING: Subroutine does not return
__stack_chk_fail();
}
El extraño nombre de las funciones nos dice que el binario está compilado en C++.
Entendiendo el programa
En primer lugar, la función abre un archivo llamado flag.txt
:
_ZNSt14basic_ifstreamIcSt11char_traitsIcEEC1EPKcSt13_Ios_Openmode(local_218, "flag.txt", 8);
// try { // try from 0010c25e to 0010c400 has its CatchHandler @ 0010c2a5
bVar2 = _ZNSt14basic_ifstreamIcSt11char_traitsIcEE7is_openEv(local_218);
if ((bVar2 & 1) == 0) {
_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(&_ZSt4cout, "Could not find credentials\n");
// WARNING: Subroutine does not return
exit(-1);
}
Si el archivo no existe, entonces falla y sale. Podemos ejecutar el binario para verlo:
$ ./vault
Could not find credentials
$ echo 'HTB{f4k3_fl4g_4_t35t1ng}' > flag.txt
$ ./vault
Incorrect Credentials - Anti Intruder Sequence Activated...
Entonces, el programa está verificando si el contenido de flag.txt
es el esperado.
El contenido del archivo se almacena en local_218
. Después de eso, la función usa un bucle while
y al final hay un bloque if
que compara local_219
con bVar2
:
while (true) {
local_241 = 0;
if (local_234 < 0x19) {
local_241 = _ZNKSt9basic_iosIcSt11char_traitsIcEE4goodEv((long) local_218 + *(long *) (local_218[0] - 0x18));
}
if ((local_241 & 1) == 0) break;
_ZNSi3getERc(local_218, &local_219);
bVar2 = (***(code ***) (&PTR_PTR_00117880) [(byte) (&DAT_0010e090) [(int) local_234]])();
if ((int) local_219 != (uint) bVar2) {
bVar1 = false;
}
local_234 = local_234 + 1;
}
Parece que local_234
contiene el índice de los caracteres de la flag, y la longitud de la flag esperada es 0x19
(25), debido a este bloque if
:
if (local_234 < 0x19) {
local_241 = _ZNKSt9basic_iosIcSt11char_traitsIcEE4goodEv((long) local_218 + *(long *) (local_218[0] - 0x18));
}
Estas líneas de código parecen estar configurando el carácter probado en local_219
y realizar alguna acción con el índice (local_234
):
_ZNSi3getERc(local_218, &local_219);
bVar2 = (***(code ***) (&PTR_PTR_00117880) [(byte) (&DAT_0010e090) [(int) local_234]])();
El uso de ***(code ***) ...
me hace pensar que el programa está utilizando una tabla de métodos virtuales (vtable), que es tan solo una lista de punteros a funciones. Entonces, hay una cierta función para cada índice de la flag, que devuelve un cierto carácter.
Por ejemplo, cuando local_234
es 0
, (byte) (&DAT_0010e090) [(int) local_234]
es 0xe0
:
DAT_0010e090 XREF[2]: FUN_0010c220:0010c35a(*),
FUN_0010c220:0010c361(*)
0010e090 e0 ?? E0h
0010e091 d1 ?? D1h
0010e092 bb ?? BBh
0010e093 27 ?? 27h '
0010e094 f6 ?? F6h
0010e095 72 ?? 72h r
0010e096 db ?? DBh
0010e097 a3 ?? A3h
0010e098 83 ?? 83h
0010e099 b9 ?? B9h
0010e09a 69 ?? 69h i
0010e09b 23 ?? 23h #
0010e09c db ?? DBh
0010e09d 63 ?? 63h c
0010e09e b9 ?? B9h
0010e09f 23 ?? 23h #
0010e0a0 05 ?? 05h
...
Y entonces, el programa llama a (***(code ***) (&PTR_PTR_00117880) [0xe0])()
, donde (&PTR_PTR_00117880) [0xe0])
está en la dirección 0x117880 + 0xe0 * 8 = 0x117f80
:
PTR_PTR_00117880 XREF[2]: FUN_0010c220:0010c367(*),
FUN_0010c220:0010c36e(*)
00117880 80 70 11 addr PTR_PTR_FUN_00117080 = 00113da8
00 00 00
00 00
00117888 88 70 11 addr PTR_PTR_FUN_00117088 = 00113dc0
00 00 00
00 00
00117890 90 70 11 addr PTR_PTR_FUN_00117090 = 00113dd8
00 00 00
00 00
...
00117f70 70 77 11 addr PTR_PTR_FUN_00117770 = 00115278
00 00 00
00 00
00117f78 78 77 11 addr PTR_PTR_FUN_00117778 = 00115290
00 00 00
00 00
00117f80 80 77 11 addr PTR_PTR_FUN_00117780 = 001152a8
00 00 00
00 00
00117f88 88 77 11 addr PTR_PTR_FUN_00117788 = 001152c0
00 00 00
00 00
...
Y así, tenemos PTR_PTR_FUN_00117780
:
PTR_PTR_FUN_00117780 XREF[1]: 00117f80(*)
00117780 a8 52 11 addr PTR_FUN_001152a8 = 0010d260
00 00 00
00 00
Luego, PTR_FUN_001152a8
:
PTR_FUN_001152a8 XREF[1]: 00117780(*)
001152a8 60 d2 10 addr FUN_0010d260
00 00 00
00 00
Y finalmente, FUN_0010d260
:
undefined8 FUN_0010d260() {
return 0x48;
}
Como se puede ver, la función anterior devuelve 0x48
, que corresponde con el valor ASCII para la letra H
en formato hexadecimal (el primer carácter de la flag: HTB{...}
).
Solución
Podríamos usar el conocimiento anterior de vtables para encontrar todas las funciones en el orden correcto y reconstruir la flag. En su lugar, es más fácil usar GDB y establecer un breakpoint en el bloque if
que verifica si el carácter es el mismo que la salida de la función de vtable.
Inspeccionando el desensamblado en Ghidra, vemos que el bloque if
está en la dirección 0x10c3a1
:
0010c3a1 39 c8 CMP EAX,ECX
0010c3a3 0f 84 07 JZ LAB_0010c3b0
00 00 00
Debemos poner el breakpoint en 0x555555554000 + 0xc3a1
(Ghidra añade un 0x10
al principio), la dirección base se debe a que el binario tiene configuración de PIE:
$ gdb -q vault
Reading symbols from vault...
(No debugging symbols found in vault)
gef➤ break *(0x555555554000 + 0xc3a1)
Breakpoint 1 at 0x5555555603a1
gef➤ run
Starting program: ./vault
Breakpoint 1, 0x00005555555603a1 in ?? ()
gef➤ x/i $rip
=> 0x5555555603a1: cmp eax,ecx
gef➤ p/c $rax
$1 = 0x48
gef➤ p/c $rcx
$2 = 0x48
gef➤ set $rax = $rcx
gef➤ continue
Continuing.
Breakpoint 1, 0x00005555555603a1 in ?? ()
Como se puede ver, podemos verificar el valor de $rcx
(el esperado), poner el registro $rax
con el valor de $rcx
y continuar al siguiente carácter. Podemos automatizar esto fácilmente con pwntools
y Python.
Flag
Usando el script, podemos encontrar la flag:
$ python3 solve.py
[+] Starting local process '/usr/bin/gdb': pid 9861
[+] Flag: HTB{vt4bl3s_4r3_c00l_huh}
[*] Stopped process '/usr/bin/gdb' (pid 9861)
$ echo 'HTB{vt4bl3s_4r3_c00l_huh}' > flag.txt
$ ./vault
Credentials Accepted! Vault Unlocking...
El script completo se puede encontrar aquí: solve.py
.