HTB Don’t panic writeup - Pez1181/CTF GitHub Wiki

🧠 [Reversing] Don't Panic — HTB Business CTF 2024

Category: Reversing
Difficulty: Easy
Platform: Linux (Rust binary)
Tools: Ghidra, Ghidra Bridge, Python
Protection Bypassed: Rust function pointer indirection


📦 Challenge Overview

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.


🕵️ Step-by-Step Solution

1. 🧪 Initial Recon

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.


2. 🧠 Decompile with Ghidra

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

3. ⚙️ Automate with Ghidra Bridge (Python)

Install Ghidra Bridge on your system:

pip install ghidra_bridge

In Ghidra:

  • Go to Script Manager
  • Run ghidra_bridge_server.py (install via ghidra_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}")

🏁 Final Flag

HTB{d0nt_p4n1c_c4tch_the_3rror}

✍️ Notes for Beginners

  • 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!

☕ TL;DR

  • 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 🫡

⚠️ **GitHub.com Fallback** ⚠️