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.

· 5 min read

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:

  1. ghidraRun → File > New Project → Non-Shared Project.
  2. File > Import File and pick the binary. Accept the defaults.
  3. Double-click the binary and answer Yes to Analyze.
  4. 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 ghidraRun from a terminal to see the error.
  • ltrace shows nothing: the binary is static or written in Go, neither of which uses the dynamic libc. Switch to strace or Ghidra.
  • ilspycmd: command not found: add ~/.dotnet/tools to your PATH.
  • pycdc leaves functions half done: no decompiler handles every Python version well. Check the version with file on the .pyc and try another decompiler, or read the bytecode with pycdas.
  • 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.

Practise it on real machines

Hack The Box machines where this technique is used. Each ▶ jumps to the minute of the IppSec video where it shows up.

Reverse engineering 37 machines

See all 37 machines →

Spotted a mistake or something missing? Tell us