Reverse Engineering CTF Challenges with Ghidra — HackerXone

Reverse Engineering CTF Challenges with Ghidra

At DEF CON CTF 2024, dozens of teams stalled on a 200-point reversing challenge because they treated the binary as a black box. The ones who solved it in under an hour opened Ghidra, found a single obfuscated comparison function, and patched their way to the flag in minutes. That gap — between staring at strings output and actually reading decompiled logic — is exactly what this walkthrough closes.

Loading the Binary and Finding Your Bearings

Start by grabbing basic intel before Ghidra opens. Run file and checksec on the target binary first — both take seconds and tell you the architecture, protections, and whether you need to worry about PIE or stack canaries.

$ file crackme_final
crackme_final: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped

$ checksec --file=crackme_final
[*] '/home/ctfuser/challenges/crackme_final'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

No PIE means the binary loads at a fixed base address — 0x400000. That matters because any address Ghidra shows you is the real address at runtime. No canary means stack-based overflow is possible if you need it later. Not stripped means function names survived — a gift in CTF work.

Now import into Ghidra. Create a new project, drag the binary in, accept the auto-analysis defaults, and let it run. Once analysis finishes, open the Symbol Tree on the left and look for main. Double-click it. The Decompiler window on the right is where the real work happens.

Reading the Decompiled Logic — A Real Example

Here is what Ghidra’s decompiler output looks like for a typical CTF password-check binary. The function names are auto-recovered because the binary wasn’t stripped.

undefined8 main(void)
{
  int iVar1;
  char local_48 [64];

  printf("Enter the password: ");
  fgets(local_48, 64, stdin);
  iVar1 = check_password(local_48);
  if (iVar1 == 0) {
    puts("Wrong password.");
  }
  else {
    puts("Correct! Flag: ");
    print_flag();
  }
  return 0;
}

The logic is plain: your input goes into local_48, gets passed to check_password(), and the return value decides your fate. Zero means wrong. Anything else prints the flag. Your next move is obvious — double-click check_password in the decompiler and read it.

undefined4 check_password(char *param_1)
{
  int iVar1;
  char local_28 [32];

  strcpy(local_28, "r3v3rs3_m3");
  local_28[10] = local_28[10] ^ 0x42;
  iVar1 = strcmp(param_1, local_28);
  return (undefined4)(iVar1 == 0);
}

Now you have something concrete. The binary builds a string "r3v3rs3_m3", then XORs the character at index 10 with 0x42. Index 10 is \0 — the null terminator — so XOR with 0x42 appends the byte 0x42 (ASCII B). The target string is "r3v3rs3_m3B" without the newline that fgets adds. Strip that newline and you have your password.

Verify it immediately:

$ printf 'r3v3rs3_m3B' | ./crackme_final
Enter the password: Correct! Flag:
CTF{gh1dra_d3c0mp1l3r_w1ns}

That XOR trick is everywhere in CTF reversing. Whenever you see a comparison against a dynamically-built buffer, trace every mutation step before the strcmp. Ghidra shows you each assignment inline — just read top to bottom.

Using Ghidra’s Search and Rename Features to Move Faster

Speed matters in timed CTFs. Two Ghidra features cut your analysis time in half.

Search for strings: Use Search → For Strings and filter by minimum length 4. In most CTF binaries you will see obvious candidates — partial flag formats, error messages, or encoded blobs. Right-click any string and choose Show References to jump directly to the function that uses it. This beats manually navigating the Symbol Tree every time.

Rename variables and functions: When you figure out what a variable holds, press L in the decompiler to rename it. Change local_48 to user_input and iVar1 to password_match. The decompiler updates everywhere that variable appears. On a binary with four or five interconnected functions, this discipline saves you from re-reading the same confusing output three times.

One more fast technique: if a function looks like it decrypts something, look for a loop with an XOR or ADD operation over a byte array. Right-click the array in the listing view and use Data → Create Array to view it as bytes. Then script the decryption in Python directly from what Ghidra shows you — key, length, operation — and run it locally without ever executing untrusted code.

What To Do Now

Pull a beginner reversing challenge from picoCTF — specifically the “vault-door” series — load it into Ghidra, and practice the full loop: read main, trace the comparison function, rename every mystery variable, and extract the flag without running the binary. Do it once and the muscle memory sticks. That is the skill that separates teams on the scoreboard.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *