HTB - Impossible Password Write-Up
The Challenge
Tools used to solve this challenge
Technology | Description |
---|---|
VirtualBox (Ubuntu 19.10 (Eoan Ermine) Virtual Machine) | Virtualisation to provide a sufficiently secure environment for HTB Reverse Engineering challenges |
tmux | For partitioning terminal windows |
Ghidra | Disassembler |
strings command | Data gathering from binary file’s mapped to memory |
file command | Provides us with potentially useful information about the type of binary file. |
Challenge Message
Are you able to cheat me and get the flag?
Walk-Through
The first thing to do is download the zip archive for this challenge and verify the authenticity of the downloaded file by its SHA256 checksum.
The extracted file is a binary file. My first move is to run the binary file against the file command, to determine the kind of binary file we are dealing with. The binary file is an executable ELF file
Notice that the file has been stripped. This is most likely to make debugging it the binary more of a challenge.
strings shows us the standard header information and strings stored in the binary executable file. A few strings stand out here.
- SuperSeKretKey: This hints at a key that might be used when the program is run.
- %20s: This looks like some formatting based upon the % symbol and the same with [%s].
- Red Hat with GCC: an executable compiled from C code. Also the presence of __libc_start_main in the strings output indicates that a main() function is present in this C program.
There is also malloc for memory allocation and strcmp to compare strings which could be used for password verification, if for example this executable recieves user input.
Static Analysis
Now, we have some tangible information about the type of binary file we are working with and some useful strings that we can use a signposts, we can now dig deeper into the behaviour of our binary. For this, I will use Ghidra.
Since the binary file has been stripped, the main function was not interpreted directly by Ghidra, so working through the disassembly I deduced which function was main(). I then read through the disassembly for each function called in main() and renamed them based upon what I think they are doing and converted the local variables to characters for ease of readability. This is the result.
void main(void) {
int iVar1;
char *stage2_str;
byte local_48;
undefined local_47;
undefined local_46;
undefined local_45;
undefined local_44;
undefined local_43;
undefined local_42;
undefined local_41;
undefined local_40;
undefined local_3f;
undefined local_3e;
undefined local_3d;
undefined local_3c;
undefined local_3b;
undefined local_3a;
undefined local_39;
undefined local_38;
undefined local_37;
undefined local_36;
undefined local_35;
char user_input [20];
int local_14;
char *stage1_str;
stage1_str = "SuperSeKretKey";
local_48 = 'A';
local_47 = ']';
local_46 = 'K';
local_45 = 'r';
local_44 = '=';
local_43 = '9';
local_42 = 'k';
local_41 = '0';
local_40 = '=';
local_3f = '0';
local_3e = 'o';
local_3d = '0';
local_3c = ';';
local_3b = 'k';
local_3a = '1';
local_39 = '?';
local_38 = 'k';
local_37 = '8';
local_36 = '1';
local_35 = 't';
printf("* ");
__isoc99_scanf(&format,user_input);
printf("[%s]\n",user_input);
local_14 = strcmp(user_input,stage1_str);
if (local_14 != 0) {
exit(1);
}
printf("** ");
__isoc99_scanf(&format,user_input);
stage2_str = (char *)generate_second_key(0x14);
iVar1 = strcmp(user_input,stage2_str);
if (iVar1 == 0) {
print_decrypted_flag(&local_48);
}
return;
}
printf(), scanf() and strcmp() were all determined by Ghidra. generate_second_key()
and print_decrypted_flag
were renamed manually based upon what I thought these function are doing. generate_second_key()
is interesting. It uses
C’s rand() in-built function and conducts some kind of re-ordering or data. However, as you will see in the Dynamic Analysis
below, I bypassed this function to retrieve the HTB flag.
Dynamic Analysis
Above, we discovered that the binary file has been stripped. This means a flag was set during compilation that directs the compiler to discard debugging symbols. Therefore, we are required to manually determine the entrypoint to the program.
For Dynamic Analysis, I am using GDB.
We can do this with either the info file
or the info target
command.
So our entrypoint to the program is at the address 0x4006a0
in hex. In other words, the program data starts at this
location in memory. Our objective is to arrive at the main() function, which we need to find.
Setting a breakpoint at 0x4006a0
and typing and returning r
(short for run), the program is run up to this memory location. To display the binary diassembly in GDB for a stripped file, we can run the following command.
If you are working with a file that is not stripped, you can run disas main
for example where disas
is short for
disassemble.
Cross-referencing the dis-assembly in Ghidra with GDB, the main function must reside at the location 0x40085d
in memory.
Setting a breakpoint at main()
and continuing there with the command c
(short for continue), and displaying the
disassembly.
At the beginning of main()
, twenty characters are mapped contiguously onto the stack. These characters make up an
encrypted form of the HTB flag. print_decrypted_flag()
is responsible for the decryption of this flag, and returns
the key to stdout.
...
printf("* ");
__isoc99_scanf(&format,user_input);
printf("[%s]\n",user_input);
local_14 = strcmp(user_input,stage1_str);
if (local_14 != 0) {
exit(1);
}
printf("** ");
__isoc99_scanf(&format,user_input);
stage2_str = (char *)generate_second_key(0x14);
iVar1 = strcmp(user_input,stage2_str);
if (iVar1 == 0) {
print_decrypted_flag(&local_48);
}
return;
}
The Ghidra disassembly above showed us that there are two key stages in the program. If you were to run the program, you
would be initially presented with an asterix. The program then awaits input from the user. Granted your input matches stage1_str
, which turns out to be SuperSeKretKey, you succeed to the second stage.
void * generate_second_key(int param_1) {
int iVar1;
time_t tVar2;
void *pvVar3;
int local_c;
tVar2 = time((time_t *)0x0);
DAT_00601074 = DAT_00601074 + 1;
srand(DAT_00601074 + (int)tVar2 * param_1);
pvVar3 = malloc((long)(param_1 + 1));
if (pvVar3 != (void *)0x0) {
local_c = 0;
while (local_c < param_1) {
iVar1 = rand();
*(char *)((long)local_c + (long)pvVar3) = (char)(iVar1 % 0x5e) + '!';
local_c = local_c + 1;
}
*(undefined *)((long)pvVar3 + (long)param_1) = 0;
return pvVar3;
}
exit(1);
}
We can set a breakpoint at the instruction where the user’s input is tested against the second key generated by
generate_second_key()
. This way, the program will run us through the second stage of the program, and the value we
return does not matter as we will see.
At the second stage of the program, the user is presented with two asterix characters in stdout and awaits input from the user
, like in the first stage. This time, however, the key is randomly generated. In order to print the decrypted flag,
iVar1
must resolve to zero. iVar1
is the return value from strcmp
which is stored in the rax
register.
Therefore, we can trick the program into thinking that we provide it with the key that generate_second_key()
has
generated, by setting the rax
to zero before continuing the program.
We can use the layout regs
command to provide us with a view of the values in the processor’s general purpose registers.
Let’s set the rax
register to zero.
If we now continue the program, we are presented with the decrypted flag.