The Vault
6 minutes to read
We are given a binary called 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
Decompilation
Let’s open it in Ghidra and decompile it. This is the entry
function:
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);
}
So, the “main” function is FUN_0010c450
(the function name is its address because the binary is stripped), which just calls another one:
undefined8 FUN_0010c450() {
FUN_0010c220();
return 0;
}
And this function (FUN_0010c220
) is the one we need to focus on:
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();
}
The weird name of the functions tell that the binary is compiled from C++.
Understanding the program
First of all, the function opens a file called 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);
}
If the file does not exists, then it fails and exits. We can run the binary to check it out:
$ ./vault
Could not find credentials
$ echo 'HTB{f4k3_fl4g_4_t35t1ng}' > flag.txt
$ ./vault
Incorrect Credentials - Anti Intruder Sequence Activated...
So, the program is checking if the content of flag.txt
is the expected one.
The content of the file is stored in local_218
. After that, the function uses a while
loop and at the end there is an if
statement comparing local_219
and 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;
}
It looks like local_234
holds the index of the flag characters, and the length of the expected flag is 0x19
(25), due to this if
statement:
if (local_234 < 0x19) {
local_241 = _ZNKSt9basic_iosIcSt11char_traitsIcEE4goodEv((long) local_218 + *(long *) (local_218[0] - 0x18));
}
These lines of code seem to be setting the tested character in local_219
and performing some action with the index (local_234
):
_ZNSi3getERc(local_218, &local_219);
bVar2 = (***(code ***) (&PTR_PTR_00117880) [(byte) (&DAT_0010e090) [(int) local_234]])();
The use of ***(code ***) ...
makes me think that the program is using a virtual method table (vtable), which is just a list of pointers to functions. So, there is a certain function for each index of the flag, which returns a certain character.
For instance, when local_234
is 0
, (byte) (&DAT_0010e090) [(int) local_234]
is 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
...
And then, the program calls (***(code ***) (&PTR_PTR_00117880) [0xe0])()
, where (&PTR_PTR_00117880) [0xe0])
is at address 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
...
So, we have 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
Then, PTR_FUN_001152a8
:
PTR_FUN_001152a8 XREF[1]: 00117780(*)
001152a8 60 d2 10 addr FUN_0010d260
00 00 00
00 00
And finally, FUN_0010d260
:
undefined8 FUN_0010d260() {
return 0x48;
}
As can be seen, the above function returns 0x48
, which corresponds with the ASCII value for H
in hexadecimal format (the first character of the flag: HTB{...}
).
Solution
We could use the above knowledge of vtables to find all functions in the correct order and reconstruct the flag. Instead, it is easier to use GDB and set a breakpoint at the if
statement that checks if the character is the same as the vtable function output.
Inspecting the disassembly code in Ghidra, we see that the if
statement is in address 0x10c3a1
:
0010c3a1 39 c8 CMP EAX,ECX
0010c3a3 0f 84 07 JZ LAB_0010c3b0
00 00 00
We must set the breakpoint at 0x555555554000 + 0xc3a1
(Ghidra puts an additional 0x10
at the beginning), the base address is due to the binary having PIE configuration:
$ 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 ?? ()
As can be seen, we can check the value of $rcx
(the expected one), set the value of $rax
to equal $rcx
and continue to the next character. We are able to automate this easily with pwntools
and Python.
Flag
Using the script, we can find the 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...
The full script can be found in here: solve.py
.