Category | Difficulty | Solves | Author |
---|---|---|---|
reverse |
medium |
3 | sudoBash418 |
Description
We thought we found a flag… but it’s hidden inside this executable!
Can you figure out its secret?
Players are given two files to download: flag-checker
and flag-checker-arm64
.
There are also three hints available:
- Static analysis is a good place to start. Ghidra should do the trick.
- Dynamic analysis can get you the flag too; try
gdb
. - The
flag-checker-arm64
file is for ARM64 computers, such as newer Apple devices. Most players will want theflag-checker
file, for x86-64 computers.
Note: I will use the x86-64 flag-checker
binary in this writeup, but my analysis and solution are valid for both binaries.
Analysis
We can run file flag-checker
to verify that the file is a normal dynamically-linked 64-bit executable:
flag-checker: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6bda6797ece59afe52606bbd39f8d88668abf443, for GNU/Linux 4.4.0, not stripped
The not stripped
part tells us the symbol table (containing function names) has not been removed.
This can be very helpful for debugging and reverse engineering.
Running the executable gives a short error message: usage: ./flag-checker FLAG
.
If you pass a random variable as the FLAG
, you’ll get a "Flag is incorrect"
message instead.
Next, we can open the binary in Ghidra (a free decompiler) for static analysis.
The decompiled main
function (with renamed + retyped variables) should look like this:
|
|
We can see that our input (argv[1]
) is passed to the verify_flag
function on line 7.
The return value determines which message is displayed to the user.
The verify_flag
function is more complicated:
|
|
There are two important takeaways from this function:
- On line 21, the length of our input must equal
0x28
(40) bytes, or the function will returnfalse
. - Our
input
is not referenced again until thebcmp
call, where it directly affects the function’s return value.
At this point, players can either keep using pure static analysis, or switch to dynamic analysis.
Solution 1 - Dynamic Analysis
From the verify_flag
function, we can deduce that the flag is first “decrypted” into memory, and then compared against our input using bcmp
.
This means that we can run the program under gdb
, set a breakpoint at the bcmp
call, and read the value of the local_60
variable to get the flag.
$ gdb --args ../out/flag-checker 0123456789012345678901234567890123456789
(gdb) break *verify_flag+170 # set breakpoint at bcmp call
(gdb) run
(gdb) x/s $rsi # display second argument to bcmp (as string)
0x7fffffffdda8: "clubeh{b1n4ry_r3v3r51ng_15_fun_f5dd17b2}"
A similar solution is possible using uftrace (ltrace doesn’t work because the (x86-64) binary lacks a PLT section).
$ uftrace -P bcmp --argument=bcmp@arg1/s,arg2/s ./flag-checker 0123456789012345678901234567890123456789
Flag is incorrect
# DURATION TID FUNCTION
0.190 us [2950126] | strlen();
42.080 us [2950126] | bcmp("0123456789012345678901234567890123456789", "clubeh{b1n4ry_r3v3r51ng_15_fun_f5dd17b2}");
5.500 us [2950126] | puts();
Another route is to use function hooking via LD_PRELOAD
.
We can intercept the bcmp
call by compiling a shared library with a matching bcmp
function and overriding the libc symbol with our own.
$ gcc -Wall -fPIC -shared -o libreplace.so libreplace.c
$ LD_PRELOAD=./libreplace.so ./flag-checker 0123456789012345678901234567890123456789
-- hooked bcmp('0123456789012345678901234567890123456789', 'clubeh{b1n4ry_r3v3r51ng_15_fun_f5dd17b2}', 40)
Flag is correct
libreplace.c
:
|
|
Solution 2 - Static Analysis
As a lazy CTF player, I like to avoid doing a bunch of static analysis by hand, so I wouldn’t normally solve a challenge this way.
With that said, let’s look at how we can use static analysis to solve this challenge.
The local variables are first loaded with static data, located in the .rodata
section.
Specifically, the embedded data is stored from .rodata+0x32
to .rodata+0x59
.
This data is then XORed byte-by-byte against 0x59
to reconstruct the bytes of the flag.
Extracted .rodata
bytes (can be found in Ghidra’s Bytes window):
3a 35 2c 3b 3c 31 22 3b 68 37 6d 2b 20 06 2b 6a 2f 6a 2b 6c 68 37 3e 06 68 6c 06 3f 2c 37 06 3f 6c 3d 3d 68 6e 3b 6b 24
Using CyberChef we can reconstruct the original flag using the XOR function:
Author’s Notes
This challenge was meant to be a beginner-friendly introduction to binary reversing.
Given its low solve count, I think I could’ve made it easier to solve using purely static analysis.
For example, I could’ve split the plaintext flag into 3 or 4 parts and reconstructed them without further obfuscation, making it easier to reconstruct by hand.
This challenge was primarily written in Rust, which came with some interesting challenges for me as an author.
I plan on making a separate writeup about my experience writing these challenges.