Reversing for CTF and HTB: from strings and ltrace to Ghidra and dnSpy
Step-by-step reverse engineering for pentesting: identify the binary, strings, ltrace, Ghidra, gdb, and decompile .NET, Java and Python to pull out passwords and keys.
On Hack The Box you do not need to be an assembly expert to make use of reversing. Most of the time the goal is modest: an executable you find on the box holds a password, an encryption key, or calls a program without an absolute path. Getting it out takes minutes if you start with the simple tools and only open Ghidra when you have to.
This guide is that order: cheapest first, most expensive last.
Requirements
On Kali or Parrot:
sudo apt install file binutils ltrace strace gdb ghidra jadx
pipx install pyinstxtractor-ng
- Ghidra needs Java 21: if it will not start,
sudo apt install openjdk-21-jdk. - For .NET from Linux:
dotnet tool install -g ilspycmd(needs the .NET SDK). On Windows, dnSpy or the ILSpy GUI. - The Ghidra tool page has the shortcuts.
- To practise: Irked, Blocky and Support, all three easy.
Warning
Do not run an unknown binary on your main machine. Run it on the target itself or in a throwaway VM.
Step 1: what are you looking at
file binary
binary: ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped
The output decides the path:
file says |
What it is | Tool |
|---|---|---|
ELF ... not stripped |
Linux binary with function names | strings, ltrace, Ghidra |
ELF ... stripped |
No names: more work in Ghidra | Ghidra, gdb |
PE32 ... Mono/.Net assembly |
.NET | ILSpy, dnSpy, ilspycmd |
Java archive data (JAR) |
Java | jadx-gui or JD-GUI |
python ... byte-compiled, or an ELF with PyInstaller in its strings |
Packaged Python | pyinstxtractor and a decompiler |
Go BuildID in the strings |
Go | Ghidra (slow and verbose) |
Step 2: strings
Before opening anything, read the text strings:
strings -n 6 binary | less
strings -n 6 -e l binary | less
The second line looks for UTF-16 strings, which is how .NET and many Windows programs store text. Look for passwords, paths, SQL queries, URLs and command names:
strings -n 6 binary | grep -iE "pass|key|secret|user|/bin/|/tmp/|http|select"
On Shoppy the master password for jaeger's password manager comes straight out of the binary's strings.
Step 3: ltrace and strace
Run the binary while watching which library functions it calls (ltrace) or which system calls it makes (strace):
ltrace ./binary
puts("This application is being devleo"...) = 69
...
setuid(0) = -1
system("/tmp/listusers"sh: 1: /tmp/listusers: not found
This is the SUID viewuser binary on Irked: it runs /tmp/listusers, which does not exist and which you can create. Put /bin/bash in it, make it executable (chmod +x /tmp/listusers) and run viewuser to get a root shell.
With a crackme that asks for a password, ltrace often shows the comparison outright:
strcmp("what-you-typed", "the-real-password") = -1
Step 4: Ghidra
When the above is not enough, open the binary in Ghidra:
ghidraRun→ File > New Project → Non-Shared Project.- File > Import File and pick the binary. Accept the defaults.
- Double-click the binary and answer Yes to Analyze.
- In the Symbol Tree, expand Functions and open
main. The decompiled C code is on the right.
The shortcuts you will use most:
| Key | Action |
|---|---|
L |
Rename a variable or function |
Ctrl+L |
Retype a variable (for example to char *) |
G |
Go to an address |
| Window > Defined Strings | Every string; one click takes you to whoever uses it |
| Right-click > References > Show References to | Who calls a function or uses a piece of data |
If the binary is stripped and you cannot see main, find the entry function: the first argument to __libc_start_main is main. Rename variables as you figure out what they do: after ten minutes the code reads itself.
Tip
If you see an odd string walked by a loop with a ^ (XOR), it is home-made "encryption". Copy the bytes and the key and redo the operation in Python: bytes(a ^ b for a, b in zip(data, key * 100)).
Step 5: gdb to see values at runtime
Sometimes it is quicker to stop the program and look at memory than to understand the algorithm. For a string comparison:
gdb -q ./binary
(gdb) break strcmp
(gdb) run
(gdb) x/s $rdi
(gdb) x/s $rsi
0x7fffffffe3a0: "what-you-typed"
0x555555556010: "the-real-password"
On x86-64 Linux the first two arguments go in rdi and rsi. If the binary calls fork(), add set follow-fork-mode child before run. To exploit memory bugs with gdb, move on to buffer overflow and ret2libc.
Step 6: .NET, Java and Python decompile almost completely
These languages keep so much information that you get the source code back nearly as written.
.NET (very common on Windows boxes):
ilspycmd -p -o out/ UserInfo.exe
grep -rn "password\|key" out/
On Support, UserInfo.exe stores the LDAP password "encrypted" with an XOR, and the key is right there in the code. On Cascade, CascAudit.exe and its DLL contain the AES key that decrypts ArkSvc's password stored in a SQLite database. On Nest you simply recompile the program and let it decrypt the password for you.
Java (.jar, .war, .apk):
jadx -d out/ BlogCore.jar
grep -rn "password" out/
On Blocky the BlogCore.jar Minecraft plugin carries the database password, which is also the notch user's. On Crafty the same pattern repeats with a plugin on Windows.
Python packaged with PyInstaller:
pyinstxtractor-ng treport
It leaves a treport_extracted/ folder with .pyc files. Decompile the main one with pycdc or, for Python 3.8 and older, uncompyle6. That is the case on Unicode; on Wall a loose .pyc turns up directly.
Common mistakes
- Ghidra will not start or closes: wrong Java version. Install JDK 21 and launch
ghidraRunfrom a terminal to see the error. ltraceshows nothing: the binary is static or written in Go, neither of which uses the dynamic libc. Switch tostraceor Ghidra.ilspycmd: command not found: add~/.dotnet/toolsto yourPATH.pycdcleaves functions half done: no decompiler handles every Python version well. Check the version withfileon the.pycand try another decompiler, or read the bytecode withpycdas.- Go binaries are unreadable in Ghidra: look for the
main.*functions in the Symbol Tree and ignore the rest, which is the Go runtime.
What next
When the binary has a memory bug instead of a secret, move on to buffer overflow and ret2libc on Linux. And if what you pull out is a hash rather than a password, see hashcat and John.