hardcode
Using Ghidra to uncover hardcoded values.
Last updated
Using Ghidra to uncover hardcoded values.
Last updated
Unlike previous binaries, we won't do any disassembly using gdb
. Rather, we will use this challenge to demonstrate Ghidra instead. This does not replace using gdb
for dynamic analysis but is a good tool for static analysis.
We first use checksec
to see what security features are enabled:
This binary is compiled with all protections enabled. We can't stack smash, we can't overwrite the GOT table, and we can't use absolute addressing. What can we do? Not much.
The most common solution to dealing with fully-protected binaries is using the binary as expected and crafting input that reaches an unexpected state. Ghidra helps us understand the control flow of the program.
We can use Ghidra to disassemble the binary. If we open the binary in Ghidra, we see the following:
When looking at Ghidra, we notice a few things:
Ghidra resolves all the function arguments for us.
It chooses the names of variables based on their location on the stack. This is helpful, but we can rename them to make them more meaningful.
Ghidra always declares variables at the top of the function and then uses them later.
Ghidra shows the main()
function at first. The Functions folder on the left side lets us access the rest of the function. We can also double-click a function to jump to it.
As we click on lines of code, Ghidra highlights the corresponding assembly code. This helps us understand what the decompilation is doing.
Sometimes, Ghidra doesn't understand what data type a variable is. It will name it undefined
plus the number of bytes it thinks the type is.
Ghidra will not disassemble library functions by default because we did not provide the library file.
Below is the default disassembly of main()
provided by Ghidra:
We can clean this function by renaming variables, defining undefined types, and removing unnecessary variables. We can also remove the canary check at the end and simply track that the binary has a canary. Since we see that fgets()
is a secure call (0x28 == 40
), we know there is no chance this function overflows the buffer. Here is the cleaned-up version of main()
:
This function is easy to dissect. We are offered 40
bytes of input. This input is converted to an integer and compared with 0x12345
. If our input matches, it calls win()
. Let's check out win()
:
This does exactly what we'd expect it to.
Unlike the binex section, our goal isn't to exploit the binary. We aim to craft the right input to pass all the checks and reach the win()
function. In this case, we know we need to pass 0x12345
.
If we try to pass 0x12345
as input, we get the following:
Why doesn't this work? atoi()
converts from a string to an integer. This function does not understand hex. Instead, we can pass the integer equivalent of 0x12345
:
This gives us the flag! we can also write this script using pwntools:
If we print our payload (i.e., print(str(key).encode())
), we'd see our payload is b'74565'
.