HTB Don’t panic writeup - Pez1181/CTF GitHub Wiki
Category: Reversing
Difficulty: Easy
Platform: Linux (Rust binary)
Tools: Ghidra, Ghidra Bridge, Python
Protection Bypassed: Rust function pointer indirection
We’re given a 64-bit Rust binary called dontpanic
. When run, it prints:
🤖💬 < Have you got a message for me? > 🗨️ 🤖:
It then takes input, and — if incorrect — simply exits. No errors. No clues.
Let’s dive in and reverse it to find the expected input.
Check binary protections:
checksec --file=dontpanic
Output:
RELRO STACK CANARY NX PIE
Full RELRO No canary found NX enabled PIE enabled
Binary has no stack canary and PIE enabled, which just means addresses are randomized at runtime. But we’re not doing exploitation — we’re reverse engineering.
Open the binary in Ghidra. Look for the function:
src::check_flag::h<hash>
This is the heart of the validation logic.
Inside, you'll find:
- An array of 31 function pointers
- Each one is called on a single character from the user’s input
- If all match, it succeeds
Install Ghidra Bridge on your system:
pip install ghidra_bridge
In Ghidra:
- Go to Script Manager
- Run
ghidra_bridge_server.py
(install viaghidra_bridge.install_server
if needed)
In Python:
import ghidra_bridge
b = ghidra_bridge.GhidraBridge(namespace=globals())
Then run this script to extract the characters:
def getSymbol(name):
return next(getState().getCurrentProgram().getSymbolTable().getSymbols(name))
def getAddress(offset):
return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(offset)
listing = currentProgram.getListing()
start_addr = 0x0010912d # First LEA in check_flag
fn_body = currentProgram.getFunctionManager().getFunctionContaining(getAddress(start_addr)).getBody()
instructions = listing.getInstructions(fn_body, True)
print("Extracting...")
result = ['x'] * 35
state = {}
for instruction in instructions:
ins_str = str(instruction)
if "LEA" in ins_str:
parts = ins_str.split(",")
reg = parts[0].split()[1]
try:
addr = int(parts[1].split("[")[1].split("]")[0], 16)
state[reg] = addr
except:
pass
if "MOV qword ptr" in ins_str:
try:
offset_str = ins_str.split("RSP + ")[1].split("]")[0]
target = (int(offset_str, 16) - 16) // 8
reg = ins_str.split(",")[1].strip()
target_addr = state[reg] + 1
target_instr = str(getInstructionAt(getAddress(target_addr)))
char = chr(int(target_instr.split(",")[1], 16))
result[target] = char
print(f"Pos {target:02}: {char}")
except:
pass
flag = "".join(result).strip('x')
print(f"\n✅ Recovered Flag: {flag}")
HTB{d0nt_p4n1c_c4tch_the_3rror}
- Rust binaries can be intimidating due to aggressive inlining and function naming.
- This one used function pointer arrays with closures, which is very Rust-y.
- Ghidra Bridge lets us script Ghidra’s analysis engine from Python — super useful for these kinds of tasks.
- No dynamic analysis or exploitation needed — pure static reversing!
- Decompile with Ghidra
- Spot the closure array in
check_flag
- Use Ghidra Bridge to read the logic
- Rebuild the flag byte-by-byte
- Profit
##Mission complete. Rusty bridge crossed 🫡