HackTheBox: Impossible Password Challenge - Writeup
Published: 2021-05-26After a light entry to Reversing with the Baby Challenge, it’s time for something a bit harder.
Initial overview
As always, download the necessary files, import into Ghidra and let it analyze all. In this case, we again have an ELF file at our hands. When executing the file, it simply outputs ”* ”. Not much to go on, so let’s take a deep dive into it using Ghidra.
Poking around with Ghidra
Let’s start by looking around the symbol tree. There’s a lot of external functions, like strcmp
, rand
, etc, but not many internal functions that are properly named. However, one function in particular sticks out, entry()
:
As we can see, it calls a __libc_start_main
function in return, providing a third function as an argument. Looking through this third function, helpfully called FUN_0040085d
, we can actually see our entry point at the first printf("* ")
. Since Ghidra seems to generate a lot of variables in this function, I’ll provide a quick, cleaned overview instead:
int iVar1;
char *__s2;
char local_28 [20];
int local_14;
char *local_10 = "SuperSeKretKey";
undefined local_48 = 0x7431386b3f316b3b306f303d306b393d724b5d41;
printf("* ");
scanf("%s", local_28);
printf("[%s]\n", local_28);
local_14 = strcmp(local_28, local_10);
if (local_14 != 0) {
exit(1);
}
printf("** ");
scanf("%s", local_28);
__s2 = (char *)FUN_0040078d(0x14);
iVar1 = strcmp(local_28, __s2);
if (iVar1 == 0) {
FUN_00400978(&local_48);
}
return;
We can see that our first input should equal SuperSeKretKey
to get past the first strcmp
. For the second strcmp
, the number 14 is passed to a separate function and the return value used for comparison to our input. FUN_0040078d
looks like this:
I don’t think it’s feasible to analyze this by hand, since there’s a lot of random values, time etc involved. Instead, let’s take a step back and look through the Assembly code.
Assembly Instructions
Moving from the Decompile Window to the Listing, we can take a look at the Assembly Instructions that are being executed within main()
. Specifically, we are interested in what actually happens when the variable __s2
gets compared to our input and what the if
statement that follows looks like:
Here, we are starting from the __s2
function call and moving all the way down to FUN_00400978
, which is the function that will eventually output the flag. To make any sense of this requires some knowledge of x86 architecture and the registers involved, all of which I don’t have, so let’s go on a trip to Google together.
Architecture Basics
Reading up on X86 Architecture, we can see the identifiers for the General Purpose Registers. Note that we’re working on a 64-bit x86 architecture, which means that the naming convention of the identifiers is different than the 16-bit one shown initially.
In detail, there are 8 general-purpose registers, with the 64-bit naming convention shown below:
Register | Purpose |
---|---|
RAX | Accumulator - used in arithmetic ops |
RCX | Counter - Used in shift/rotate instructions and loops |
RDX | Data - Used in arithmetic and I/O ops |
RBX | Base - Used as a pointer to data |
RSP | Stack Pointer - Pointer to the top of the stack |
RBP | Stack Base Pointer - Pointer to the base of the stack |
RSI | Source Index - Used as a pointer to source in stream ops |
RDI | Used as a pointer to destination in stream ops |
For the purposes of this detour there’s no need to cover Segment Registers, as they don’t show up in the calls we need to work on.
Making sense of the instructions
Our first CALL
instruction calls the mysterious FUN_0040078d
. The return value of this function now lives inside the RAX
register, which at address 004000954
is then moved into our data register RDX
. Note that the right side of an instruction is usually the starting point. Given the following instruction MOV ax, bx
, we move the contents of register bx
into the register ax
.
The instructions from address 00400957
to 00400961
are all covering the call to strcmp
. Afterwards, there is a TEST
instruction. This instruction checks register EAX
(the 32-bit version of the RAX
register), which will contain the return value of the strcmp
call. Remember that if strcmp
returns 0, the strings are equal; otherwise, they are not. Depending on the test result, something called the Zero flag, or ZF for short, is set accordingly. If the ZF equals 0, then the strings are not equal and the program exits. On the other hand, if both strings are equal, the ZF will be set to 1 and our flag will be printed on screen, as the if condition is true.
Modifying Assembly
If we’re willing to jump through some hoops, we are able to modify the Assembly Instructions within Ghidra and save our changes back to the original program. I recommend taking a backup of the downloaded file before you do this.
As we saw in the last section, there is a TEST
and a JNZ
instruction, both covering the branching and standing in our way. Instead of analyzing FUN_0040078d
, we can simply remove the JNZ
instruction and eliminate the branching altogether.
To do this, we will first need to download a script called SavePatch. This is because Ghidra itself does not have a way to save changes back to the original file (no, Export File is not meant for that). Simply follow the instructions and enable the script in the Script Manager.
Ghidra allows us to Patch Instructions within the Listing, thereby modifying Assembly code. By right clicking on the JNZ
Instruction at address 00400968
and choosing Patch Instruction, we are able to modify the entire statement. So what do we do? We put a NOP
in there. This essentially tells the program that there is no operation at that point. It still sets the ZF after the TEST
instruction, but there is no conditional instruction to check the register. Instead, the function that outputs the flag is simply executed.
The modified instructions look like this, including Decompiler view:
Saving our changes
To save our changes back to the program, we need to make use of SavePatch. Select the NOP
instruction (so it’s green), go to the Script Manager, find SavePatch.py
and execute it. It will prompt you to choose a program to save to - you are free to save to a different one or overwrite the original impossible_password.bin
. Either way, once you have saved the program and executed it, you should be greeted with the following:
Conclusion
Finding this way took a lot of trial and error and banging my head against the wall with gdb. In the end, everything was actually doable right within Ghidra (thanks to the SavePatch script). I know that people managed to do it in other ways, but it was still pretty fun all things considered.