Crack the Vault: Reverse Engineer an ELF

Reverse engineer a stripped, anti-debug ELF binary to extract a hidden passphrase.

Introduction

30 Second Summary

Every app you use started as source code that got compiled into a binary your computer can run. When you don't have that source code, you need a different set of skills to understand what the program is actually doing.

In this project, you will reverse engineer a protected binary executable to extract a hidden passphrase. You'll combine static analysis, dynamic debugging, and scripting to crack through multiple layers of protection.

What You'll Build

A complete reverse engineering workflow against a hardened ELF binary with anti-debugging protections and an XOR cipher hiding the passphrase.

By the end of this project, you'll have:

  • A compiled, stripped, anti-debug-protected crackme binary to practice your methodology against.
  • A Ghidra project with annotated functions and renamed symbols from static analysis.
  • A Python solve script that reverses the XOR cipher to recover the passphrase.
  • Secret Mission: Permanently patch the binary in Ghidra to neutralize all protections.

Are there any prerequisites?

This project assumes familiarity with C, basic command-line Linux usage, and an understanding of how compiled programs work. You do not need prior reverse engineering experience.

Before We Start

Reverse engineering a binary means working backwards from machine code to understand what a program does. In this project, you will crack a protected binary by combining two approaches: static analysis (reading the disassembled code) and dynamic analysis (watching it execute live).

Before any of that can happen, you need the right tools in place. This step sets up Ghidra for static analysis and compiles the target binary you will spend the rest of the project cracking open.

In this step, get ready to:
  • Install Ghidra 12.1.2 and verify it launches.
  • Compile the target binary with hardening flags.
  • Confirm the binary is stripped, non-PIE, and runs as expected.
Download and install Ghidra 12.1.2

Ghidra is an open-source reverse engineering framework developed by the NSA. It provides a decompiler that translates machine code back into readable C-like pseudocode.

Check whether you already have Ghidra installed by looking for an existing ghidra_12.1.2_PUBLIC_20260605 directory on your system.

✔️ I already have Ghidra 12.1.2

You are ready to go. Skip ahead to the next substep where you compile the target binary.

ⓧ I have an older version of Ghidra

Ghidra versions live in their own directories, so you can install 12.1.2 alongside your existing version without conflict. Follow the same download and extract steps below.

  • Move to your Desktop by running this command:
  • Download the Ghidra 12.1.2 release zip by running:
  • Verify the download integrity by running:
cd ~/Desktop
wget https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.1.2_build/ghidra_12.1.2_PUBLIC_20260605.zip
  • Verify the SHA-256 hash matches the official value by running:
sha256sum ghidra_12.1.2_PUBLIC_20260605.zip

You should see b62e81a0390618466c019c60d8c2f796ced2509c4c1aea4a37644a77272cf99d in the output.

  • Extract the zip and move into the Ghidra directory by running:
unzip ghidra_12.1.2_PUBLIC_20260605.zip
cd ghidra_12.1.2_PUBLIC_20260605
  • Launch Ghidra to confirm it works by running:
./ghidraRun

Accept the license agreement when prompted, then close the Ghidra window.

ⓧ I don't have Ghidra installed

Ghidra requires JDK 21 or higher (64-bit). Your environment already has this, so you can proceed directly with the download.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Download the Ghidra 12.1.2 release zip by running:
wget https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.1.2_build/ghidra_12.1.2_PUBLIC_20260605.zip
  • Verify the SHA-256 hash matches the official value by running:
sha256sum ghidra_12.1.2_PUBLIC_20260605.zip

You should see b62e81a0390618466c019c60d8c2f796ced2509c4c1aea4a37644a77272cf99d in the output.

  • Extract the zip and move into the Ghidra directory by running:
unzip ghidra_12.1.2_PUBLIC_20260605.zip
cd ghidra_12.1.2_PUBLIC_20260605
  • Launch Ghidra to confirm it works by running:
./ghidraRun

Accept the license agreement when prompted, then close the Ghidra window.

Ghidra window is blank or renders incorrectly?

If you use a non-reparenting window manager (XMonad, Sway, dwm, or similar tiling WMs common on Artix), Ghidra's Java Swing windows may render blank.

  • Open support/launch.properties inside your Ghidra directory.
  • Find the line #ENVVARS_LINUX=_JAVA_AWT_WM_NONREPARENTING=1 and remove the leading # to uncomment it.
  • Save the file and relaunch ./ghidraRun.

help me fix Ghidra rendering on my tiling window manager

Create a working directory and compile the target binary

The target binary is a "crackme" challenge. You will compile it from provided source code, then immediately treat it as a black box for the rest of the project.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project directory and move into it by running these commands:
mkdir vault-crackme
cd vault-crackme
  • Confirm the directory exists by running:
ls ~/Desktop | grep vault-crackme

You should see vault-crackme in the output.

  • Create the source file by running:
touch vault.c
  • Open vault.c in your preferred text editor and paste the following C source code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>

__attribute__((constructor))
void anti_debug(void) {
    if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) {
        printf("Access Denied.\n");
        exit(1);
    }
}

static const unsigned char key[] = {
    0x5a, 0x3f, 0x1e, 0x72, 0x4b, 0x6d, 0x11,
    0x28, 0x55, 0x3c, 0x7a, 0x09, 0x44, 0x67
};

static const unsigned char encrypted[] = {
    0x28, 0x0c, 0x68, 0x17, 0x39, 0x1e, 0x22,
    0x77, 0x38, 0x08, 0x09, 0x7d, 0x21, 0x15
};

#define PASSLEN 14

int check_pass(const char *input) {
    if (strlen(input) != PASSLEN) {
        return 0;
    }
    for (int i = 0; i < PASSLEN; i++) {
        if ((input[i] ^ key[i]) != encrypted[i]) {
            return 0;
        }
    }
    return 1;
}

int main(void) {
    char buf[64];
    printf("=== The Vault ===\n");
    printf("Enter passphrase: ");
    if (fgets(buf, sizeof(buf), stdin) == NULL) {
        return 1;
    }
    buf[strcspn(buf, "\n")] = '\0';

    if (check_pass(buf)) {
        printf("Access Granted. Welcome back.\n");
    } else {
        printf("Access Denied.\n");
    }
    return 0;
}

What does this code do?

You will reverse engineer this program without the source, but here is a quick overview of what it contains so you understand the challenge ahead.

  • The anti_debug function uses a ptrace call to detect if a debugger is attached. If it detects one, the program exits immediately.
  • Two byte arrays (key and encrypted) hold the data needed to validate the passphrase.
  • The check_pass function XORs user input against the key and compares the result byte-by-byte to the encrypted array.
  • Save vault.c.
  • Compile the binary with hardening flags by running:
gcc -O2 -s -no-pie -fno-stack-protector -o vault vault.c

What do these compiler flags do?

  • -O2 enables optimization, making the decompiled output more realistic (and harder to read).
  • -s strips the symbol table, removing function and variable names from the binary.
  • -no-pie produces a non-position-independent executable with fixed addresses. This means Ghidra's addresses will match GDB's runtime addresses exactly.
  • -fno-stack-protector removes stack canary checks, keeping the decompiled output cleaner so you can focus on the XOR logic.
  • Verify the binary runs by executing it:
./vault

You should see the banner === The Vault === followed by Enter passphrase: waiting for your input.

  • Type any random text and press Enter. You should see Access Denied. printed to the terminal.

Seeing 'sys/ptrace.h: No such file or directory'?

This header is part of the Linux kernel headers package. On Artix/Arch-based systems, it should already be present with a standard GCC installation.

  • Confirm kernel headers are installed by running pacman -Qs linux-headers. If nothing shows, install them with sudo pacman -S linux-headers.

help me fix the ptrace.h compilation error

Close the source and go dark

From this point forward, you will treat vault as a completely opaque black box. The goal is to extract the passphrase using only reverse engineering tools.

  • Close vault.c in your text editor. Do not refer back to it.
  • Confirm the binary type by running:
file vault

You should see ELF 64-bit LSB executable in the output (not "shared object", which would indicate PIE).

  • Confirm the binary is stripped by running:
readelf --syms vault

You should see no symbol table entries. This confirms the -s flag removed all function and variable names.

Why does stripping matter?

In a normal (unstripped) binary, you would see every function name like check_pass and anti_debug listed in the symbol table. Stripping removes these names, forcing you to identify functions by their behavior instead of their labels.

This is how real-world malware and proprietary binaries look. Learning to navigate without symbols is a core RE skill.

'readelf --syms vault' still shows symbols?

If you see entries in the output, the -s flag was not applied during compilation. Recompile with the exact command above and make sure -s is present.

Note that readelf --dyn-syms vault will still show dynamic symbols (like ptrace) because those are needed for dynamic linking. That is expected.

help me understand why my binary still shows symbols after using the -s flag

Your toolkit is ready and the target binary is locked down. Next up, you will perform reconnaissance on this binary using only command-line tools to gather clues before opening any heavyweight analysis frameworks.

Reconnaissance

Before cracking open a heavy disassembler, experienced reverse engineers always start with lightweight recon. A few standard Linux utilities can reveal architecture, protections, and behavioral hints in seconds.

In this step, you will profile the vault binary using file, readelf, and strings. Each finding narrows down your strategy for the static and dynamic analysis steps that follow.

In this step, get ready to:
  • Confirm architecture, ELF type, and section layout.
  • Extract readable strings that hint at the program's control flow.
  • Determine binary protections and dynamic dependencies.
Examine the ELF header and sections

The ELF header contains metadata about how the binary was compiled. Fields like the entry point address, ELF type, and machine architecture tell you exactly what you are working with before you read a single instruction.

  • Confirm the binary's architecture and linkage by running:
file vault

You should see output confirming ELF 64-bit LSB executable, x86-64, and dynamically linked.

  • Inspect the full ELF header to note the entry point address and type by running:
readelf --file-header vault

What should I see?

Look for two key fields in the output:

  • Type: should show EXEC (Executable file). This confirms non-PIE. A PIE binary would show DYN instead.
  • Entry point address: a fixed hex address (e.g. 0x401000 range). Write this down. You will see it again in Ghidra.
  • Examine which sections exist in the binary by running:
readelf --sections vault

Scroll through the section list. You should see .dynsym (dynamic symbols) present, but no .symtab and no .debug_* sections.

Why is .symtab missing?

The -s flag you passed to GCC during compilation stripped the .symtab section, which normally contains function and variable names. This is a common anti-analysis technique that forces reverse engineers to rely on decompiler heuristics instead of labeled functions.

The .dynsym section survives stripping because it is needed at runtime for dynamic linking. This means imported library functions like ptrace are still visible there.

Extract readable strings

The strings utility scans a binary for sequences of printable characters. In a stripped binary with no debug info, strings are often your best initial clue about what the program does.

  • Dump all printable strings from the binary by running:
strings vault

You should see a handful of recognizable strings in the output.

What should I look for?

Look for these key strings that reveal the program's control flow:

  • Enter passphrase: confirms the binary prompts for user input.
  • Access Granted. Welcome back. is the success path. This is what you want to trigger.
  • Access Denied. is the failure path.

Notice how few meaningful strings exist compared to an unstripped binary. There are no function names, no debug messages, and no source file references. The validation logic itself is not revealed by strings alone.

Determine binary protections

Knowing what protections a binary has tells you what exploitation and analysis techniques are available. The two protections you care about here are NX (non-executable stack) and PIE (position-independent execution).

  • Check whether the stack is executable by examining the GNU_STACK program header:
readelf -l vault | grep GNU_STACK

What does this output mean?

You should see a line containing flags like RW. The absence of an E (execute) flag means NX is enabled. The stack is not executable, which is standard for modern compiled binaries.

If the flags showed RWE, that would mean the stack is executable. That is rare in modern binaries and would indicate a different class of vulnerability.

Now summarize your full reconnaissance findings. You have confirmed:

  • Architecture: x86-64, dynamically linked against libc.
  • ELF type: EXEC (non-PIE). Addresses in Ghidra will match runtime addresses exactly.
  • Stripped: no .symtab or .debug_* sections. Function names will not appear in analysis tools.
  • NX enabled: stack is not executable.
  • Dynamic symbols (.dynsym): ptrace is imported, which is a strong indicator of anti-debugging.

Why does non-PIE matter for reverse engineering?

A non-PIE binary loads at a fixed address every time it runs. This means the addresses you see in Ghidra's disassembly are the exact same addresses you will see in pwndbg at runtime.

With a PIE binary, the OS randomizes the load address on each execution (ASLR). You would need to compute offsets and add them to a dynamic base. Non-PIE eliminates that complexity entirely.

Commands returning nothing or showing errors?

If readelf or strings are not found, make sure you have binutils installed. On Arch-based systems, it is part of the base-devel group.

If you see No such file, make sure you are running commands from the same directory where vault was compiled.

help me troubleshoot readelf or strings not working on my vault binary

You now have a complete profile of the target. Next, you will load the binary into Ghidra and use its decompiler to reverse engineer the actual validation logic hiding inside.

Static Analysis in Ghidra

Your reconnaissance gave you a high-level map of the binary: architecture, protections, and a handful of readable strings. Now it is time to look inside.

Static analysis means examining the binary without running it. Ghidra will disassemble the machine code, recover function boundaries, and produce a decompiled C-like view of each function. Your job is to find the anti-debug check and the passphrase validation logic, then record the data arrays that hold the encrypted secret.

In this step, get ready to:
  • Import the binary into a new Ghidra project and run auto-analysis.
  • Locate and rename the anti-debug constructor function.
  • Locate and rename the XOR validation function, and record both constant arrays.
Import the binary into Ghidra

Before Ghidra can analyze anything, you need to create a project and import the target file. Ghidra will auto-detect the ELF format, select the correct processor language, and offer to run its suite of analysis passes.

  • Launch Ghidra by running ./ghidraRun from the extracted Ghidra directory (the same way you verified installation earlier).
  • In the Ghidra Project Manager window, click File, then New Project....
  • Select Non-Shared Project and click Next.
  • Choose your working directory as the project location.
  • Enter Vault as the project name and click Finish.
  • Import the binary by clicking File, then Import File....
  • Navigate to and select the vault binary.
  • In the Import dialog, confirm the format is ELF and the language is x86:LE:64:default. Click OK.
  • A results summary dialog appears. Click OK to dismiss it.
  • Double-click the vault file in the project window to open it in the CodeBrowser.
  • Ghidra will prompt you to analyze the binary. Click Yes to run auto-analysis with default options.
  • Click Analyze in the analysis options dialog to start. Wait for the analysis progress bar in the bottom-right to finish.

What does auto-analysis do?

Ghidra's auto-analysis disassembles the binary, identifies function boundaries, resolves cross-references, and builds a decompiler model. For a small binary like vault, this takes only a few seconds.

Once complete, the CodeBrowser shows three key panels: the Listing (assembly), the Decompiler (C pseudocode), and the Symbol Tree (function and label names).

Seeing blank or broken windows?

If you use a non-reparenting window manager (XMonad, Sway, dwm, bspwm, or similar tiling WMs common on Artix), Ghidra may render blank panes. Fix this by uncommenting #ENVVARS_LINUX=_JAVA_AWT_WM_NONREPARENTING=1 in support/launch.properties inside your Ghidra installation directory, then relaunch Ghidra.

Help me fix Ghidra's blank windows on my tiling WM

Find and rename the anti-debug constructor

Stripped binaries have no meaningful function names. Ghidra assigns auto-generated names like FUN_00401xxx. Your job is to identify what each function does and give it a human-readable name.

The __attribute__((constructor)) function runs before main via the .init_array section. It calls ptrace to detect a debugger.

  • In the CodeBrowser, find the Symbol Tree panel on the left side.
  • Expand the Functions folder.
  • Look for functions with addresses in the range that corresponds to the .init_array section. Ghidra will have named them something like FUN_00401xxx.
  • Click through each candidate to view its decompiled output in the Decompiler panel on the right.
  • Find the function that calls ptrace with argument 0 (which is PTRACE_TRACEME), then checks if the return value is -1 and calls exit(1) if so.
  • Right-click the function name in the Decompiler panel.
  • Select Rename Function.
  • Type anti_debug and press Enter.

What is the anti-debug pattern?

The decompiled code should look roughly like this: a call to ptrace(PTRACE_TRACEME, 0, 0, 0) followed by a conditional exit. When a process is already being traced by a debugger (like GDB), ptrace(PTRACE_TRACEME) returns -1. This is a classic anti-debugging trick used in CTF challenges and real-world malware.

Because the function is in .init_array, it executes automatically before main ever runs. A debugger attached at startup will trigger the trap immediately.

After renaming, you should see anti_debug appear in both the Symbol Tree and the Decompiler panel header.

Cannot find a function that calls ptrace?

Because the binary is dynamically linked, ptrace appears in the external symbols. Try this: expand Imports in the Symbol Tree and double-click ptrace, then check its cross-references (XREF) to find calling functions. Alternatively, in the CodeBrowser menu click Search, then For Strings..., and look for references near ptrace.

Help me find the ptrace anti-debug function in Ghidra

Find and rename the validation function

With the anti-debug constructor identified, the next target is the function that decides whether your passphrase is correct. This function implements the XOR cipher check.

  • In the Symbol Tree, find the entry function (Ghidra names the ELF entry point entry).
  • Double-click entry and look at the decompiled output. Find the call to __libc_start_main, whose first argument is the address of main.
  • Double-click that address to navigate to the main function.
  • In the decompiled main, follow the control flow past the printf and fgets calls. You will find a call to another function that receives the user input buffer.
  • Double-click that function call to navigate into it.
  • Examine the decompiled output. You should see a length check (comparing against 14) followed by a for loop that XORs each input byte against one array and compares the result to another array.
  • Right-click the function name in the Decompiler panel and select Rename Function.
  • Type check_pass and press Enter.

What does the XOR loop tell you?

The decompiler will show something like: if ((input[i] ^ key[i]) != encrypted[i]) return 0. This is the core validation logic. It checks every byte of your input against a hardcoded key and an encrypted target.

Since XOR is its own inverse, you can recover the passphrase by computing encrypted[i] ^ key[i] for each position. You will do this in a later step.

  • Now record the addresses of both byte arrays. In the decompiler, click on the first array reference (the key) and note the address shown in the Listing panel.
  • Do the same for the second array (the encrypted target).
  • Verify the key array bytes match: 0x5a, 0x3f, 0x1e, 0x72, 0x4b, 0x6d, 0x11, 0x28, 0x55, 0x3c, 0x7a, 0x09, 0x44, 0x67.
  • Verify the encrypted array bytes match: 0x28, 0x0c, 0x68, 0x17, 0x39, 0x1e, 0x22, 0x77, 0x38, 0x08, 0x09, 0x7d, 0x21, 0x15.

Navigate arrays in the Listing view

When you click an array reference in the Decompiler, Ghidra highlights the corresponding bytes in the Listing panel. If you see the array displayed as db (define byte) directives, you are looking at the raw constant data. Write down the starting address for your dynamic analysis in the next step.

Decompiler shows confusing pointer casts?

With -O2 optimization, the compiler may have inlined or restructured the loop slightly. Focus on finding the XOR operator (^) and the comparison (!=) inside a loop. That is the validation logic regardless of how Ghidra presents the surrounding code.

Help me interpret the decompiler output for the XOR validation loop

You now have a complete picture of the binary's inner workings from static analysis alone. Next, you will fire up pwndbg to bypass the anti-debug check at runtime and confirm these arrays in live memory.

Dynamic Analysis with pwndbg

Your static analysis in Ghidra gave you a hypothesis: the binary XORs each input byte against a key array and compares the result to an encrypted array. You also found an anti-debug constructor that kills the process under a debugger.

Hypotheses from a decompiler can be wrong. Optimized code, misidentified types, or incorrect array boundaries can mislead you. In this step, you will confirm everything at runtime using pwndbg, watching real bytes flow through the actual algorithm.

In this step, get ready to:
  • Bypass the ptrace anti-debug check at runtime.
  • Dump both byte arrays from live memory to confirm your static findings.
  • Single-step through the XOR comparison loop and observe it execute.
Launch pwndbg and bypass the anti-debug check

The anti_debug constructor calls ptrace(PTRACE_TRACEME) before main runs. Under a debugger, this call returns -1 because the process is already being traced. The binary then prints "Access Denied." and exits.

  • Open a terminal and launch the binary under pwndbg by running:
pwndbg ./vault
  • Before running the program, set a breakpoint on ptrace so you can intercept the anti-debug call:
break ptrace
  • Start the program by running:
run

The debugger stops at the ptrace call. The binary has not exited yet because it never got to check the return value.

  • Force a successful return value by setting $rax to 0 (which is what ptrace returns when not being traced):
set $rax = 0
  • Return from the ptrace call immediately, skipping its actual execution:
return

The binary now continues past the anti-debug check. It believes it is not being debugged.

Why does this work?

On x86-64, the return value of a function is stored in the $rax register. By setting it to 0 before the function returns, you are forging a "success" result. The return command then makes execution jump back to the caller, so the real ptrace logic never completes.

The anti-debug code checks if (ptrace(...) == -1). Since you forced 0, the condition is false and execution falls through to main.

Binary still exits immediately?

Make sure you run break ptrace before typing run. If you run first, the constructor executes and kills the process before you can intervene.

If you see "No symbol table" warnings, that is expected for a stripped binary. The breakpoint on ptrace still works because it lives in the dynamic symbol table (.dynsym).

help me bypass the ptrace anti-debug check in pwndbg

Set a breakpoint on the validation function and dump the arrays

With anti-debug bypassed, you can now reach the validation function. Because the binary is non-PIE, the addresses you found in Ghidra match the runtime addresses exactly.

  • Set a breakpoint on the check_pass function using the address you recorded from Ghidra:
break *[[CHECK_PASS_ADDR="address of check_pass from Ghidra, e.g. 0x401200"]]
  • Continue execution so the binary reaches its input prompt:
continue

You should see the banner and input prompt appear in the terminal.

  • Type any 14-character test input (e.g. AAAAAAAAAAAAAA) and press Enter. The breakpoint on check_pass fires.

Now dump the key and encrypted arrays directly from memory to confirm they match your static analysis.

  • Dump the key array (15 bytes to see one past the end) using the address from Ghidra:
x/15bx [[KEY_ADDR="address of key array from Ghidra, e.g. 0x402010"]]
  • Dump the encrypted array (14 bytes):
x/14bx [[ENC_ADDR="address of encrypted array from Ghidra, e.g. 0x402020"]]

You should see the same hex values you identified in Ghidra's decompiler view. The key array should start with 0x5a 0x3f 0x1e 0x72 and the encrypted array should start with 0x28 0x0c 0x68 0x17.

Why confirm what Ghidra already showed you?

Decompilers can misidentify array boundaries, confuse signed and unsigned types, or misattribute data to the wrong variable. Dumping raw memory removes all doubt. If these bytes match, your static analysis is correct.

This is also standard CTF methodology. You form a hypothesis from static analysis, then validate it dynamically before writing a solve script.

Breakpoint not hitting after entering input?

Make sure your test input is exactly 14 characters. The function performs a length check first and returns 0 immediately if the length does not match. With fewer than 14 characters, the XOR loop never runs.

Double-check you are using the correct address from Ghidra. In the CodeBrowser, click on the check_pass function name and look at the address in the bottom-left status bar.

help me set a breakpoint on a function address in pwndbg

Single-step through the XOR comparison loop

With the arrays confirmed, you can now watch the XOR algorithm execute instruction by instruction. This is the final piece of dynamic confirmation.

  • Step one instruction at a time through the XOR loop by running:
ni
  • Keep pressing Enter to repeat the ni command. Watch the registers change as each iteration:

Since your test input (AAAAAAAAAAAAAA) is not the correct passphrase, the comparison fails on the very first byte. You will see the conditional jump taken, branching to the "return 0" path.

What did single-stepping prove?

You watched the CPU execute the exact algorithm the decompiler showed you. The XOR happens in a register, the comparison uses cmp followed by a conditional jump, and a mismatch causes an early return.

This confirms three things: the arrays are correct, the algorithm is XOR (not something more complex the decompiler misrepresented), and a mismatch on any single byte rejects the input immediately.

Speed up stepping with a count

You can step multiple instructions at once with ni 10 to advance 10 instructions. This is useful once you have seen the pattern and want to reach the branch faster.

  • Exit pwndbg when you are done by running:
quit

Not sure which instruction is the XOR?

Look for a xor instruction in the disassembly output as you step. pwndbg highlights the current instruction and shows register values. The XOR result appears in the destination register immediately after the instruction executes.

If you stepped too far past the loop, restart from the breakpoint. Run run again, bypass ptrace the same way, and re-enter your test input.

help me identify the XOR comparison in the pwndbg disassembly

Your dynamic analysis confirms everything the decompiler told you. The arrays are real, the algorithm is a straightforward XOR, and the comparison is byte-by-byte. Next up, you will use this confirmed knowledge to reverse the cipher and extract the actual passphrase.

Solve the Cipher

Your dynamic analysis confirmed exactly what Ghidra's decompiler showed: the binary XORs each byte of your input against a fixed key and compares the result to an encrypted target array.

Now comes the payoff. You have both arrays in hand from your analysis. Since XOR is its own inverse, recovering the passphrase is a single arithmetic operation away.

In this step, get ready to:
  • Confirm your extracted byte arrays from Ghidra and pwndbg.
  • Write a Python solve script that reverses the XOR cipher.
  • Prove the passphrase against the live binary.
Record the byte arrays from your analysis

From your Ghidra decompiler view and your pwndbg memory dumps, you have two 14-byte arrays. The validation logic XORs each input character against the key, then compares the result against the encrypted target.

  • Confirm from your notes that the key array is: 0x5a, 0x3f, 0x1e, 0x72, 0x4b, 0x6d, 0x11, 0x28, 0x55, 0x3c, 0x7a, 0x09, 0x44, 0x67.
  • Confirm the encrypted array is: 0x28, 0x0c, 0x68, 0x17, 0x39, 0x1e, 0x22, 0x77, 0x38, 0x08, 0x09, 0x7d, 0x21, 0x15.

Why does XOR reverse itself?

XOR has a unique mathematical property: applying it twice with the same key returns the original value. If input[i] ^ key[i] == encrypted[i], then encrypted[i] ^ key[i] == input[i].

This means you can recover the plaintext passphrase by XORing the encrypted bytes with the key. No brute force needed.

Write the Python solve script

Rather than doing the math by hand for 14 bytes, a short Python script will XOR the arrays together and print the resulting ASCII passphrase.

  • Create a file called solve.py in your project directory and paste the following code:
key = [0x5a, 0x3f, 0x1e, 0x72, 0x4b, 0x6d, 0x11,
       0x28, 0x55, 0x3c, 0x7a, 0x09, 0x44, 0x67]

encrypted = [0x28, 0x0c, 0x68, 0x17, 0x39, 0x1e, 0x22,
             0x77, 0x38, 0x08, 0x09, 0x7d, 0x21, 0x15]

# XOR each encrypted byte with the corresponding key byte to recover plaintext
passphrase = ''.join(chr(e ^ k) for e, k in zip(encrypted, key))
print(f"Passphrase: {passphrase}")

What does this code do?

  • key and encrypted hold the exact byte arrays you extracted from the binary during static and dynamic analysis.
  • zip(encrypted, key) pairs each encrypted byte with its corresponding key byte at the same index.
  • chr(e ^ k) XORs the pair and converts the resulting integer to an ASCII character.
  • The join() call concatenates all 14 characters into the full passphrase string.
  • Save solve.py.
  • Run the solve script by running this command:
python3 solve.py
  • You should see output in the format Passphrase: <14-character string>. That string is the passphrase the binary expects.

Seeing a syntax error or empty output?

  • Make sure you have Python 3 installed. Run python3 --version to confirm.
  • Check that the hex values in your arrays match exactly. A single wrong byte produces a garbled character.
  • Verify both arrays have exactly 14 elements each. Mismatched lengths cause zip() to silently truncate the output.

Help me debug my solve script for the XOR cipher

✔️ Awesome, I've got everything!

Great. Double check you saved solve.py and you can see the passphrase printed to your terminal.

ⓧ I'd like to double check the full code

Here is the complete solve.py for comparison:

key = [0x5a, 0x3f, 0x1e, 0x72, 0x4b, 0x6d, 0x11,
       0x28, 0x55, 0x3c, 0x7a, 0x09, 0x44, 0x67]

encrypted = [0x28, 0x0c, 0x68, 0x17, 0x39, 0x1e, 0x22,
             0x77, 0x38, 0x08, 0x09, 0x7d, 0x21, 0x15]

passphrase = ''.join(chr(e ^ k) for e, k in zip(encrypted, key))
print(f"Passphrase: {passphrase}")
Prove the passphrase against the live binary

The final proof in any reverse engineering challenge is running the target with your answer. The binary is the judge.

  • Run the vault binary by running this command:
./vault
  • When prompted with Enter passphrase:, type the passphrase that your solve script printed and press Enter.

You should see Access Granted. Welcome back. printed to your terminal. The binary is fully solved.

Still seeing Access Denied?

  • Make sure you are typing the passphrase exactly as printed by your script. Copy-pasting is safest.
  • Check that you did not include a trailing space or newline when pasting.
  • If the binary exits immediately with Access Denied. before you can type anything, the anti-debug check is triggering. Make sure you are not running it under a debugger. Run ./vault directly in a plain terminal.

Help me figure out why my passphrase is being rejected

What just happened?

You followed the same methodology that CTF competitors and professional security researchers use against unknown binaries. Reconnaissance gave you a profile. Static analysis in Ghidra revealed the algorithm. Dynamic analysis in pwndbg confirmed the data. The solve script extracted the answer.

This workflow scales to far more complex targets. The tools change, the ciphers get harder, and the anti-analysis techniques get sneakier. The methodology stays the same.

You have proven the passphrase and fully solved the crackme. But what if you wanted to neutralize the protections permanently, so the binary accepts any input without needing the real passphrase at all? That is exactly what binary patching lets you do.

Secret mission

Binary Patching in Ghidra

You have reversed the vault binary and extracted its passphrase. But what if you could permanently neutralize its protections? In this secret mission, you will modify the binary at the instruction level to disable both the anti-debug check and the passphrase validation entirely.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • Ghidra 12.1.2 installation (extracted directory).
  • Ghidra project "Vault" (project files on disk).
  • Project files: vault.c, vault, vault_patched, solve.py.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the binary, try new Ghidra features, or revisit the project later.

  • Your Ghidra installation stays in place for future RE work.
  • Your project files and Ghidra project remain on disk for reference.

Pause - I'll come back to this later

Keep Ghidra installed and your project files intact. Close any open Ghidra windows to free up memory.

  • Close Ghidra if it is still open.
  • Your Ghidra project and all project files will be waiting for you when you return.

Delete - I don't want to use this again

Remove all project files, the Ghidra project, and optionally the Ghidra installation itself.

  • Close Ghidra if it is still open.
  • Remove your project files by running this command from the directory containing them:
rm vault vault.c vault_patched solve.py
  • Remove the Ghidra project files. Navigate to the directory where you created the "Vault" project and delete the Vault.gpr file and Vault.rep folder:
rm -rf Vault.gpr Vault.rep
  • To remove the Ghidra installation entirely, delete the extracted directory. Replace the path below with where you extracted Ghidra:
rm -rf ~/Desktop/ghidra_12.1.2_PUBLIC
  • Confirm the files are gone by running ls in each location.

Nice Work!

Nice Work!

You just cracked a hardened ELF binary from scratch. You went from a stripped, anti-debug-protected executable to a fully documented solve, extracting a hidden passphrase using the same methodology CTF competitors and security researchers use every day.

You've learned how to:

  • Reverse engineer a stripped binary using Ghidra for static analysis. You identified anti-debug constructors, renamed functions, and extracted XOR logic from decompiled output.
  • Bypass ptrace-based anti-debugging at runtime using pwndbg. You confirmed memory contents and watched the XOR comparison execute instruction by instruction.
  • Write a Python solve script that reverses an XOR cipher to recover a hidden passphrase from extracted byte arrays.
  • Secret Mission: Permanently patched the binary in Ghidra to neutralize both anti-debugging and validation checks, making any input succeed.

Ready to quiz yourself?