Step 178. CTF Taste Test 3: Reversing — Two crackmes + Ghidra — How to Read Without Source

Step 178. CTF Taste Test 3: Reversing — Two crackmes + Ghidra — How to Read Without Source

Level 3 — Real-World CTF and Advanced Attack Skills | Difficulty ★★★★☆ | Estimated time: 6 hours

Prerequisites: Step 177 (Pwn intro), Steps 62–65 (C and memory). You can use gcc and gdb on WSL Ubuntu.

⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. crackme practice binaries are legal learning material built to be solved.

  • What you need: WSL Ubuntu (measured: Ubuntu 24.04, gcc 13.3.0, gdb 15.1). Ghidra is a tool that needs separate installation, so this chapter shows it as a Screen example.
  • Caution: the crackmes you build today are practice ones you compile yourself. Carelessly applying reversing techniques to other people’s software can become a license or legal problem.

Reversing is the category where you receive only an executable — no source — and dissect "what this program does." If yesterday’s Pwn was "find a gap and attack," reversing’s goal is "understanding the behavior" itself. Today’s practice assignment is a crackme — a practice binary that prints "correct" when you guess the password. The rule of the game is to find the password without looking at the source.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain the difference between static analysis (reading without executing) and dynamic analysis (observing while executing)
  • Find string clues inside a binary with strings
  • Find suspicious operations (compares, XOR) in objdump disassembly
  • Observe "the two strings being compared" mid-execution with a gdb breakpoint
  • Know what a decompiler (Ghidra) shows you

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment C (for building crackmes) + WSL Ubuntu bash (measured: gcc 13.3.0, gdb 15.1, x86-64)
Today’s commands strings, objdump -d, gdb -batch -x command-file, gdb’s break / x/s / continue
Concepts needed Static vs dynamic analysis, decompilers, XOR hiding, function argument registers (rdi, rsi)
Today’s artifact Solution records for two crackmes + a summary of "what found it"

2-1. Static Analysis vs Dynamic Analysis

Reversing tools split in two. Static analysis reads the file itself without running the program — string lists, disassembly, decompilation. Dynamic analysis observes while running — stopping with a debugger, peering into memory and registers.

In the field you alternate between the two. Statically find "suspicious spots," then dynamically confirm actual values at those spots. Today’s second crackme demands exactly that order.

2-2. strings — The Cheapest First Tool

A compiled binary carries string constants inside it verbatim (prompts, error messages, and sometimes passwords). strings scoops out only the "human-readable character sequences" from a binary. Reversing’s first move is always this — if nothing comes out, that’s when you pull out heavier tools.

2-3. Decompilers and Ghidra

A decompiler is a tool that walks machine code backwards and shows it as pseudo-C code. Ghidra is a free decompiler released by the NSA, and it’s the standard tool for CTF reversing. Import a binary (Import → Analyze) and the CodeBrowser window shows the function list (Symbol Tree) and pseudo code (Decompile window) side by side.

Since it’s not installed in this environment, its screen is shown as an example (3-5), and we verify the principles hands-on with the basic tools (objdump, gdb) that do the same job. Even when assembly shows up, don’t be scared — today you need only three words: cmp (compare), call (call), xor (exclusive OR) (the promise from Step 177’s Wall 5).

2-4. XOR Hiding — A One-Byte Padlock

Store a password in plaintext and strings catches it immediately. So the author hides the password with XOR — storing each byte XORed with the same value (the key), then XORing again at runtime to restore it. XOR has the property that applying the same key twice returns the original (A ⊕ K ⊕ K = A), making it perfect for "hide and restore" (the very operation you met in Step 104’s Natas).

strings can’t see it, but the restored moment during execution is visible — the classic case of dynamic analysis filling static analysis’s limit.

2-5. Function Arguments Travel in Registers

On x86-64 Linux, when a function is called, its arguments ride in fixed registers. The first argument goes in rdi, the second in rsi. The moment strcmp(a, b) is called, rdi holds a’s address and rsi holds b’s. gdb’s x/s $rdi means "show me the place rdi points at as a string (x/s)" — these two lines are today’s core observation tools.


3. Follow Along

3-1. Lab — Building Two crackmes

Time to be the author. You build structures you know, then become the player and break them. Create a ~/lab178 folder and follow along.

Input (crackme1.c — plaintext password)

#include <stdio.h>
#include <string.h>
int main(void){
    char pw[64];
    printf("password: ");
    scanf("%63s", pw);
    if(strcmp(pw, "sup3r-s3cr3t-pw") == 0)
        puts("correct! FLAG{str1ngs_t0ld_m3_th3_pw}");
    else
        puts("wrong.");
    return 0;
}

Input (crackme2.c — password hidden with XOR)

#include <stdio.h>
#include <string.h>
int main(void){
    char pw[64];
    /* array hiding the "original password" XORed with 0x5A — no plaintext anywhere */
    char secret[] = {0x37, 0x6b, 0x3e, 0x34, 0x6b, 0x3d, 0x32, 0x2e,
                     0x05, 0x28, 0x6e, 0x38, 0x38, 0x6b, 0x2e, 0x00};
    char decoded[64];
    int i;
    for(i = 0; secret[i]; i++) decoded[i] = secret[i] ^ 0x5A;
    decoded[i] = 0;
    printf("password: ");
    scanf("%63s", pw);
    if(strcmp(pw, decoded) == 0)
        puts("correct! FLAG{x0r_c4nnot_h1de_fr0m_gdb}");
    else
        puts("wrong.");
    return 0;
}

Compile

mkdir -p ~/lab178 && cd ~/lab178
gcc -O1 -o crackme1 crackme1.c
gcc -O1 -o crackme2 crackme2.c

(Measured 2026-09-09. A warning about scanf‘s return value may appear, but it doesn’t hinder the exercise.)

From here on, forget the source. You are a player who just received two unfamiliar binaries to solve.

3-2. Solving crackme 1 — Victory for strings

The first tool is the cheapest one, strings:

strings ./crackme1
password: 
sup3r-s3cr3t-pw
wrong.
correct! FLAG{str1ngs_t0ld_m3_th3_pw}

(Measured 2026-09-09. In reality these lines come out mixed in with other system strings — we’ve written down only the suspicious ones. Try filtering like strings ./crackme1 | grep -iE "pw|secret|flag".)

How to read the output: password: is the prompt, wrong. and correct! are verdict messages. And awkwardly wedged between them, sup3r-s3cr3t-pw — a string that is neither prompt nor message is likely "the comparison target." Let’s confirm:

echo sup3r-s3cr3t-pw | ./crackme1
password: correct! FLAG{str1ngs_t0ld_m3_th3_pw}

(Measured 2026-09-09.)

Summary: no source or assembly needed. A binary with the password baked in as plaintext opens to a single line of strings. Half of real introductory crackme challenges are at this level — which is why reversing’s first move is always strings.

3-3. Solving crackme 2, Phase 1 — strings Falls Silent

Point the same tool at the second binary:

strings ./crackme2
password: 
wrong.
correct! FLAG{x0r_c4nnot_h1de_fr0m_gdb}

(Measured 2026-09-09. No password candidate visible — we verified by measurement that strings ./crackme2 | grep -iE "m1dn1ght|r4bb1t" returns nothing.)

How to read the output: the prompt and verdict messages are there, but no comparison target. It means the password does not exist as plaintext inside the file — it’s hidden. Static analysis’s first tool has hit its limit here.

Still, before giving up, one more thing: find "the hiding method" in the disassembly:

objdump -d ./crackme2 | grep "0x5a"
    1219:	83 f2 5a             	xor    $0x5a,%edx

(Measured 2026-09-09.)

How to read the output: xor $0x5a — something is being XORed with 0x5A. XOR is the "hide and restore" operation (2-4). In other words, this program restores something at runtime. If you want to see the restored value, you just stop execution at the moment restoration finishes. That’s where dynamic analysis starts.

3-4. Solving crackme 2, Phase 2 — Catching the Moment of Comparison with gdb

The restored password sits in memory at the exact moment strcmp is called. Let’s stop at that moment. First, find where strcmp is called inside main:

gdb -batch -ex "disassemble main" ./crackme2 | grep strcmp
   0x000000000000126d <+164>:	call   0x10b0 <strcmp@plt>

(Measured 2026-09-09.)

<+164> — the call is 164 bytes from main’s start. Set a breakpoint there. Prepare the gdb commands as a file (cmds.gdb):

break *main+164
run < in.txt
x/s $rdi
x/s $rsi

$rdi/$rsi are the addresses of the two strings being compared (2-5). Make any input file and run:

echo AAAA > in.txt
gdb -batch -x cmds.gdb ./crackme2
Breakpoint 1 at 0x126d

Breakpoint 1, 0x000055555555526d in main ()
0x7fffffffe5e0:	"AAAA"
0x7fffffffe620:	"m1dn1ght_r4bb1t"

(Measured 2026-09-09. Address values differ per run.)

How to read the output: the decisive scene. In rdi, the "AAAA" we passed in; in rsi, the "m1dn1ght_r4bb1t" the program just restored — standing side by side. We froze a single moment mid-execution and pulled out the password strings couldn’t find. Confirm:

echo m1dn1ght_r4bb1t | ./crackme2
password: correct! FLAG{x0r_c4nnot_h1de_fr0m_gdb}

(Measured 2026-09-09.)

Why: XOR hiding is safe "inside the file" but defenseless "during execution" — to compare, it has to restore the value anyway. This is the division of labor between static and dynamic analysis. If the file won’t talk, ask the execution.

Note: you can also break on the function name directly, like break strcmp. But strcmp gets called several times during the program’s startup preparation (the dynamic loader) — in measurement, four calls in a row were the loader’s. Skipping a few with continue, or breaking precisely on the call address inside main like today, is faster.

3-5. The Same Job in Ghidra — Screen Example

Ghidra isn’t installed in this environment, so we show it as a Screen example. If you want to try it for real, download it from ghidra-sre.org (Java required). The procedure:

# Screen example — the Ghidra workflow
1. Launch Ghidra → File > New Project → create a project
2. Drag the crackme1 file in to Import → double-click → Analyze: Yes
3. In CodeBrowser's left Symbol Tree > Functions, double-click main
4. The right Decompile window shows pseudo-C code:

undefined8 main(void)
{
  char local_48 [64];
  printf("password: ");
  __isoc99_scanf("%63s", local_48);
  iVar1 = strcmp(local_48, "sup3r-s3cr3t-pw");
  if (iVar1 == 0) { puts("correct! ..."); }
  else { puts("wrong."); }
  return 0;
}

How to read it: the flow reads fine even without knowing assembly — it takes input, compares with strcmp, and the comparison-target string shows up verbatim in the decompilation. Cryptic variable names like local_48 and iVar1 are normal. Reading machine-assigned names while inferring "ah, this must be the password buffer" is daily life in reversing.

Drop crackme2 into Ghidra and the XOR restore loop shows as pseudo code, with the key (0x5A) catching your eye — you’d reach the same conclusion as 3-3’s objdump discovery, but via the decompiler. Different tools, same thinking: find the comparison target.

3-6. Organizing the Solution Record

Challenge strings objdump gdb Key
crackme1 Password exposed in plaintext (didn’t look) (not needed) One line of strings
crackme2 Silent (XOR hiding) Found xor $0x5a Observed plaintext at the strcmp moment Static → dynamic handoff

The last column of this table is reversing’s grammar: lightest tool first, and if it fails, one step heavier.


4. Missions & Exercises

Mission — Build Your Own crackme and Hand It to a Friend (or Future You)

  1. Make crackme3.c as a variation of crackme2 — change the XOR key from 0x5A to a different single byte (e.g., 0x37), and change the password too (compute the hidden array in Python: bytes(b ^ 0x37 for b in pw.encode()))
  2. Compile it, then solve it again with today’s three-tool order (strings → objdump → gdb) without looking at the source
  3. Leave a solution record: "what was visible at which tool"

Exercises

Exercise 1. Explain the difference between static and dynamic analysis, citing which scene from today’s crackme2 solve shows it.

Exercise 2. Why did crackme1’s password get caught verbatim by strings? Answer from the perspective of where C source string constants go after compilation.

Exercise 3. When you spotted the single line xor $0x5a,%edx in crackme2, why could you infer "this program restores something at runtime"? Explain using XOR’s property.

Exercise 4. In gdb, x/s $rsi showed the password. Why rdi and rsi of all registers?


5. Model Answers & Completion Criteria

Mission Model Answer

If you built it with key 0x37 and password blu3-m00n:

[build] enc = bytes(b ^ 0x37 for b in "blu3-m00n".encode())
       → secret[] = {0x55, 0x5b, 0x42, 0x04, 0x5a, ...}

[solve 1: strings] ./crackme3 → password not visible (confirms hiding worked)
[solve 2: objdump] grep "0x37" → found xor $0x37 → "so it's XOR hiding"
[solve 3: gdb] break *main+164 (confirm strcmp location with disassembly first)
       → observed "blu3-m00n" in rsi → confirmed with echo → correct!

How to verify: ① does the hidden password escape strings (if it gets caught, the hiding failed — plaintext leaked into the array)? ② is the new key found in objdump? ③ can you also restore it manually with just the key and the array, no gdb (one line of Python: bytes(b ^ 0x37 for b in enc))? All three working means you understand both "the building side and the breaking side."

Exercise Answers

Answer 1. Static analysis reads a file without running it; dynamic analysis observes while running. In crackme2, strings and objdump (static) told us "it’s hidden, it’s XOR" but couldn’t know the password itself. We solved it by stopping at the strcmp call moment with gdb (dynamic) and observing the restored plaintext m1dn1ght_r4bb1t. Asking the execution when the file falls silent — this handoff is reversing’s basic rhythm.

Answer 2. Because C source string constants (literals) get stored verbatim as plaintext in the binary’s data section after compilation. The comparison target of strcmp(pw, "sup3r-s3cr3t-pw") is data, not code, so it never turns into machine code. strings scoops readable character sequences out of this data section, so plaintext constants always get caught.

Answer 3. XOR has the property that applying the same key twice returns the original (A ⊕ K ⊕ K = A). So the combination of "data XORed with a key + code that XORs again at runtime" means precisely "a value hidden away gets restored at runtime." Conversely, if instead of XOR you’d seen only a simple addition loop or a constant comparison, you’d have reasoned differently — which is why we read the kind of operation in disassembly.

Answer 4. Because in x86-64 Linux’s function-calling convention, arguments are passed in fixed registers — the first argument in rdi, the second in rsi. The two arguments of strcmp(input, password) ride in those two registers, so if you stop right before the call and read with x/s, you see the two strings being compared. Know the convention, and you can predict where "the moment of comparison" is loaded.

Completion Criteria Checklist

  • [ ] I can state the difference between static and dynamic analysis in one sentence each
  • [ ] I found crackme1’s password with strings and solved it
  • [ ] I confirmed strings falls silent on crackme2 and know why
  • [ ] I found xor $0x5a in objdump -d and inferred "hiding"
  • [ ] I solved crackme2 by observing the two strings at the strcmp moment with a gdb breakpoint
  • [ ] I know Ghidra’s screen layout (Symbol Tree, Decompile window) and procedure
  • [ ] Mission: I built crackme3 and solved it again with the three-tool order

6. Common Pitfalls & Fixes

Wall 1. There’s too much strings output — I can’t tell what’s a clue

Symptom: hundreds of lines come out and your eyes swim.
Cause: system library strings all come out mixed in. Normal.
Fix: filter it — strings ./crackme1 | grep -iE "pass|flag|secret|wrong|correct". Strings near verdict messages ("wrong", "correct") have a high chance of being the comparison target. A length filter, strings -n 6, also cuts noise.

Wall 2. $rdi vanishes into variable expansion in gdb

Symptom: typing x/s $rdi gives Argument required (starting display address). — the $rdi disappears entirely. A problem actually hit while measuring this chapter (when passing through a shell to gdb, $ gets eaten by shell variable expansion).
Cause: the shell interprets $rdi as an environment variable and replaces it with an empty string.
Fix: typing directly inside interactive gdb is fine; when scripting, use a command file (-x cmds.gdb), and take care that $ doesn’t get expanded when creating the file (the author worked around it with Python’s chr(36)). For the same reason, echo $rdi in bash prints a blank line.

Wall 3. I set break strcmp but only weird strings show up

Symptom: you see strings like /lib64/ld-linux-x86-64.so.2 and __vdso_clock_gettime.
Cause: the dynamic loader preparing program startup also uses strcmp. It gets called several times before main runs (four in a row in the 2026-09-09 measurement).
Fix: skip a few with continue, or as in 3-4, find the call location inside main with disassemble main and break precisely on break *main+offset. The latter is the reproducible method.

Wall 4. My addresses differ from the book’s

Symptom: the book says <+164> but your environment shows a different offset.
Cause: machine-code layout varies with compiler version and options (whether -O1 was used). Normal.
Fix: don’t memorize offsets — memorize the procedure: disassemble main → find the call ... strcmp line → break on that line’s offset. The procedure is the same on any binary.

Wall 5. Ghidra’s analysis never finishes / the variable names are weird

Symptom: Analyze takes a long time, and the pseudo code shows only names like iVar1 and local_48.
Cause: both are normal. Analysis can take minutes depending on file size, and the decompiler’s variable names are provisionally assigned by machine.
Fix: just wait for analysis. The variable names reveal their roles as you read — used in a comparison means "password buffer," a return value means "verdict result." Reading while inferring is itself reversing skill. Later you’ll rename them with the L key as you go.


7. Summary

Today’s Concepts

Concept One-line explanation
Reversing The category of dissecting an executable without source to understand its behavior
crackme A practice binary where you guess a password — reversing’s introductory challenge
Static analysis Reading the file without executing (strings, objdump, decompilation)
Dynamic analysis Observing while executing (gdb breakpoints, register watching)
XOR hiding XOR twice with the same key and you’re back — used for hiding and restoring
Decompiler (Ghidra) A tool that restores machine code into pseudo-C code
Calling convention Function arguments are passed in the registers rdi, rsi, … in order

Today’s Commands

Command What it does
strings ./binary Scoop readable strings from a binary — reversing’s first move
strings -n 6 ./binary | grep -iE "pattern" Cut noise with a length filter + pattern
objdump -d ./binary | grep xor Find suspicious operations in disassembly
gdb -batch -ex "disassemble main" ./binary See main’s machine-code layout (find call locations)
break *main+offsetx/s $rdi / x/s $rsi Stop at the comparison moment and observe both argument strings

An Instinct More Important Than Commands

What today’s two crackmes showed is reversing’s entire grammar. Lightest tool first — if strings fails, objdump; if the file falls silent, ask the execution. And conversely, you also saw that "whatever you want to hide must be exposed during execution." To compare, you must restore, and the restored moment is observable.

This instinct goes beyond crackmes. Malware analysis, vulnerability analysis, every question of "what does this program really do" — all start from the same toolbox. Web, Pwn, and now reversing — you’ve tasted three categories. Which tool felt comfortable in your hand? That memory becomes the material for choosing your main field in Step 181.


Once every box is checked, Step 178 is complete. Click the checkbox in the sidebar to save your progress.