HackTheBox: Impossible Password Challenge - Writeup

Published: 2021-05-26

After 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():

Entry Function

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:

Compare Function

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:

Assembly

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:

Modified Assembly

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:

Result

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.

Reply by e-mail

Want to get started on a new venture?