Compiled
Recover the correct password from an ELF binary through pure static
analysis — without executing the binary — by tracing
scanf format strings, strcmp
control flow, and .rodata address resolution.
Objective #
We are given a compiled Linux ELF binary and asked one question: What is the password?
The room notes that the binary may not execute in the AttackBox — a deliberate signal that this challenge is designed to be solved statically, by reading and analyzing the binary rather than running it.
strings is useful for gathering clues, but it is not
proof. The only reliable way to determine the correct answer is to
trace how the program actually uses those strings in its logic.
Learning Objectives #
By the end of this writeup you should understand how to:
- Identify a binary's type and architecture with
file - Use
stringsfor reconnaissance without over-trusting it - Inspect exported and imported symbols with
nm - Disassemble a program with
objdump -d -M intel - Inspect
.rodatato resolve address references - Interpret
strcmp-based validation logic from assembly - Understand how a non-standard
scanfformat string changes the input contract - Reconstruct the high-level control flow of a crackme from raw assembly
Recommended Workflow #
For beginner ELF crackmes, this workflow is reliable and repeatable. Following it methodically prevents the common trap of submitting the first plausible-looking string you find.
Receive binary | v Run file — understand type, arch, linkage, stripped? | v Run strings + nm — gather clues; do NOT trust results yet | v Locate main — nm gives you the symbol; objdump gives the disassembly | v Disassemble main — identify landmarks, not every instruction | v Find input function — scanf / fgets / read | v Find compare / validation logic — strcmp / memcmp / direct byte comparison | v Resolve referenced addresses in .rodata | v Translate assembly to pseudocode | v Determine exact answer format
Methodology #
Step 1 — Identify the File
Start by determining exactly what kind of file you are dealing with:
file Compiled-1688545393558.Compiled
Expected output:
ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped
Each field tells you something important before you open a single disassembly window:
- ELF 64-bit — Linux executable format; you will use Linux-native tools.
- LSB — little-endian byte order; integers are stored low byte first.
- PIE (Position Independent Executable) — the binary loads at a randomized base address at runtime, so virtual addresses in
objdumpoutput are offsets from the image base, not absolute addresses. This matters when cross-referencing addresses. - x86-64 — expect the System V AMD64 calling convention: first six integer/pointer arguments in
rdi,rsi,rdx,rcx,r8,r9. - dynamically linked — standard C library functions like
printf,scanf, andstrcmpare imported from libc via the PLT. Their names will appear in symbol tables. - not stripped — debug symbols including the
mainfunction name are preserved. This is excellent news: you can jump directly tomainwithout hunting by offset.
Stripped binaries remove all symbol names, forcing you to identify
functions by heuristics (function prologue patterns, call graph
shape). A not-stripped beginner crackme lets you go straight to
main by name.
Step 2 — Gather Clues With strings
Extract all human-readable character sequences of four or more bytes:
strings Compiled-1688545393558.Compiled
Interesting candidates in the output:
StringsIsForNoobs(orStringsIsForNoob)Password:DoYouEven%sCTFCorrect!Try again!_init__dso_handle
What a beginner might think
A common first reaction produces several hypotheses:
- The password is
DoYouEvenStringsIsForNoobsCTF— combining the format and the found string. - The password is
_init— it looks like a symbol name planted as bait. - The format
DoYouEven%sCTFreveals the final answer structure.
These are hypotheses, not conclusions. A string found by
strings may be a prompt, a decoy, a format specifier, a
debug artifact, dead data, or the real validation target. The only way
to tell the difference is to see how the code actually uses each string.
strings is reconnaissance. It narrows the search space.
It does not confirm the answer. Never submit a password that you found
only through strings without tracing it in the disassembly.
Step 3 — Inspect Symbols With nm
nm -C Compiled-1688545393558.Compiled
The -C flag demangles C++ names (less relevant here, but
good habit). Look for:
main— the entry point of program logic._init— a runtime initialization function; its name also appears in the string table, which is the clever misdirection in this room.- Imported (undefined,
U) symbols:strcmp,printf,__isoc99_scanf,fwrite.
The presence of strcmp and __isoc99_scanf in
the import list confirms the control flow pattern before you open
objdump: the program reads input from the user and
compares it against something. That shapes exactly what to look for in
the disassembly.
T = defined in text (code) section —
U = undefined (imported) —
R = read-only data —
D = initialized data —
B = BSS (uninitialized data).
Step 4 — Disassemble the Binary
objdump -d -M intel Compiled-1688545393558.Compiled
The -M intel flag switches from AT&T syntax (default)
to Intel syntax. Intel syntax is generally easier to read because
operands appear in destination-first order and immediates are not
prefixed with $.
How to approach a large disassembly listing
Do not try to understand every instruction from the top. Instead, scan
for structural landmarks inside main:
- The function prologue:
push rbp / mov rbp, rsp - Stack allocation:
sub rsp, N— tells you how much local variable space is reserved - String loads:
lea rax, [rip+offset]loading intordifor aprintfcall - Input reading: a
call __isoc99_scanf@plt - Comparisons: a
call strcmp@pltfollowed bytest eax, eaxand a conditional jump - Output branches: leading toward the strings
Correct!orTry again!
Step 5 — Understand the Structure of main
At a high level, main does exactly five things:
- Allocates stack space and saves callee-saved registers
- Constructs a string in local stack memory by writing byte constants
- Prints
Password:as a prompt viaprintf - Reads input from the user via
scanf - Compares the input (or a portion of it) using
strcmp, then printsCorrect!orTry again!
This is the canonical structure of every beginner crackme. Recognizing it immediately lets you skip over irrelevant prologue and epilogue code and focus on steps 4 and 5.
Step 6 — Decode the Stack-Built String
Early in main, before any function calls, the binary moves
integer constants into local stack slots. When you decode those byte
values they spell out:
StringsIsForNoobs
This is more significant than a string found by strings
because the program actively constructs this value at runtime.
It is not dead data. However, active construction still does not prove
it is the password. It could be:
- A value compared against the input (the real answer)
- A string the program prints in some branch
- Bait — data the program builds and then never uses for validation
The only way to determine which role it plays is to follow every reference to that stack buffer in the subsequent code.
Building sensitive strings on the stack at runtime (rather than
storing them literally in .rodata) is a classic
anti-strings technique used in real-world malware and
CTF challenges alike. It forces analysts to run or trace the binary
rather than grep for hard-coded values. This room uses a mild version
of it as a teaching moment.
Step 7 — Locate User Input
After printing the prompt, main calls scanf.
In Intel-syntax assembly, the relevant block looks like:
lea rax, [rbp-0x20] ; address of local buffer
mov rsi, rax ; 2nd argument: destination buffer
lea rax, [rip+offset] ; address of format string in .rodata
mov rdi, rax ; 1st argument: format string
call __isoc99_scanf@plt
Calling convention breakdown
On Linux x86-64 (System V AMD64 ABI), integer and pointer arguments are passed in registers in this order:
| Register | Argument position |
|---|---|
| rdi | 1st argument |
| rsi | 2nd argument |
| rdx | 3rd argument |
| rcx | 4th argument |
| r8 | 5th argument |
| r9 | 6th argument |
For scanf(format, buffer): rdi holds the
format string address; rsi holds the destination buffer
address.
From the assembly: input is captured into the local buffer at
[rbp-0x20]. The format string comes from .rodata
and must be resolved next — it is not a simple "%s".
Step 8 — Find the Validation Logic
After input is captured, main calls strcmp
multiple times with different comparison targets. The critical block
(shown at file offsets, since this is a PIE binary):
11d1: lea rax, [rbp-0x20]
11d5: lea rdx, [rip+0xe42] ; first compare target
11dc: mov rsi, rdx
11df: mov rdi, rax
11e2: call strcmp@plt
11e7: test eax, eax
11e9: js 1205 ; jump if strcmp < 0
11eb: lea rax, [rbp-0x20]
11ef: lea rdx, [rip+0xe28] ; second compare target (same address)
11f6: mov rsi, rdx
11f9: mov rdi, rax
11fc: call strcmp@plt
1201: test eax, eax
1203: jle 124b ; jump if strcmp <= 0 → "Try again!"
1205: lea rax, [rbp-0x20]
1209: lea rdx, [rip+0xe1b] ; third compare target
1210: mov rsi, rdx
1213: mov rdi, rax
1216: call strcmp@plt
121b: test eax, eax
121d: jne 1235 ; jump if not equal → "Try again!"
121f: ... ; → "Correct!"
The structure here is a three-way partitioned comparison — a
pattern that arises when a binary implements a sorted search or a
range-bounded equality check. Understanding it requires knowing exactly
what strcmp returns.
Step 9 — Understand strcmp Semantics
strcmp(a, b) compares two null-terminated strings
lexicographically. It returns:
| Return value | Meaning | Condition flag set |
|---|---|---|
| < 0 (negative) | a is lexically before b | SF=1 (sign flag) |
| == 0 | a equals b | ZF=1 (zero flag) |
| > 0 (positive) | a is lexically after b | neither |
After call strcmp@plt, the result is in eax.
The subsequent test eax, eax sets flags based on
eax, then the conditional jump reads those flags:
| Instruction | Jumps when | strcmp condition |
|---|---|---|
| je / jz | ZF=1 | strings are equal |
| jne / jnz | ZF=0 | strings are not equal |
| js | SF=1 | strcmp returned negative (a < b) |
| jns | SF=0 | strcmp returned zero or positive |
| jle / jng | ZF=1 or SF≠OF | strcmp returned ≤ 0 |
| jge / jnl | SF=OF | strcmp returned ≥ 0 |
If you see js after a strcmp call and
do not know that the sign flag reflects a negative integer result,
the branch logic appears arbitrary. Once you know the semantics,
the whole validation path becomes readable.
Step 10 — Resolve Referenced Strings in .rodata
Dump the read-only data section to decode every address reference seen in the disassembly:
objdump -s -j .rodata Compiled-1688545393558.Compiled
Relevant hex dump output:
2000 01000200 50617373 776f7264 3a200044
2010 6f596f75 4576656e 25734354 46005f5f
2020 64736f5f 68616e64 6c65005f 696e6974
2030 00436f72 72656374 21005472 79206167
2040 61696e21 00
Decoding the addresses byte by byte:
| Address | String content | Role |
|---|---|---|
| 0x2004 | Password: | Prompt printed to terminal |
| 0x200f | DoYouEven%sCTF | scanf format string — defines the input pattern |
| 0x201e | __dso_handle | Compare target 1 (used in the range-check branch) |
| 0x202b | _init | Compare target 2 (the equality test for "Correct!") |
| 0x2031 | Correct! | Success message |
| 0x203a | Try again! | Failure message |
With these resolved, every mysterious [rip+0xNNN]
reference in the disassembly now has a concrete identity. Nothing is
ambiguous.
Step 11 — Interpret the Format String Trap
This is the insight that makes this room interesting and separates methodical analysts from guessers.
The scanf format string is not
"%s". It is:
DoYouEven%sCTF
scanf with a non-% literal in the format
string does something specific: it requires those exact literal
characters to appear in the input stream. The %s
conversion specifier reads a whitespace-delimited word, which gets
written to the buffer argument.
So if the user enters the full string DoYouEven_initCTF,
then:
-
scanfmatches the literal prefixDoYouEvenagainst the input. -
%scaptures the next whitespace-delimited token:_init. -
scanfthen matches the literal suffixCTFagainst the remaining input. -
The captured value in the local buffer at
[rbp-0x20]is_init— not the full input string.
The binary never directly compares the full string
DoYouEven_initCTF. It compares only the
captured portion — the token matched by %s
— against _init. This means two different things are
true simultaneously: the password you type must look like
DoYouEven___CTF, but the value the binary
validates is just the middle token.
Step 12 — Reconstruct the Intended Logic
Translating the assembly to C pseudocode:
int main(void) {
/* Stack-built string — exists but is not used for final validation */
char stack_str[18];
build_on_stack(stack_str, "StringsIsForNoobs");
char buf[32];
printf("Password: ");
/* Format string captures only the middle token into buf */
scanf("DoYouEven%sCTF", buf);
/*
* Three-way partitioned comparison — faithfully reflecting the assembly:
*
* First call: js 1205
* if strcmp(buf, "__dso_handle") < 0 (buf lexically before "__dso_handle")
* → jumps INTO the third-compare block at 1205 (not a fail path)
*
* Second call: jle 124b
* if strcmp(buf, "__dso_handle") <= 0
* → only reached when buf >= "__dso_handle" (fall-through from first test),
* so this fires only when buf == "__dso_handle" → fail
*
* Third compare (offset 1205, also reachable via js from first call):
* if strcmp(buf, "_init") != 0 → fail
* if strcmp(buf, "_init") == 0 → success
*
* Note: "_init" > "__dso_handle" lexically, so any buf that satisfies
* buf < "__dso_handle" (triggering the js jump) will always fail the
* equality check in the third compare. The js path cannot reach success.
*/
if (strcmp(buf, "__dso_handle") < 0) {
goto third_compare; /* js 1205: jumps into third-compare block */
}
if (strcmp(buf, "__dso_handle") <= 0) { /* only true when buf == "__dso_handle" */
printf("Try again!\n");
return 0;
}
/* buf > "__dso_handle": fall through to third compare */
third_compare:
if (strcmp(buf, "_init") == 0) {
printf("Correct!\n");
} else {
printf("Try again!\n");
}
return 0;
}
The validated internal token is _init.
Everything else — StringsIsForNoobs,
__dso_handle, DoYouEven%sCTF — serves as
noise or structural scaffolding, not as the final answer.
Step 13 — Derive the Final Answer
Two facts are now established from the disassembly:
- The input must match the
scanfformat:DoYouEven%sCTF - The captured
%stoken must equal_initfor the "Correct!" branch
Substituting _init into the format:
DoYouEven + _init + CTF
→ DoYouEven_initCTF
The TryHackMe room's accepted answer, however, is:
DoYouEven_init
This reflects the room's question framing — it asks for the themed
visible password string without the trailing CTF literal.
The room answer and the full typed input differ by that suffix; what
matters for the challenge flag is the token-based construction
DoYouEven_init.
Common Beginner Mistakes #
1. Submitting the first interesting string
StringsIsForNoobs looks exactly like something a CTF
author would hide as a password. It is not. The binary builds it on the
stack but the validation logic never tests input against it. Trusting
appearance over evidence fails here.
2. Ignoring the scanf format string
scanf("DoYouEven%sCTF", buf) is fundamentally different
from scanf("%s", buf). In the first form, the string
placed into buf is only the %s-matched
portion of the input, not the entire typed string. Missing this changes
your understanding of what gets compared.
3. Not resolving .rodata addresses
When the disassembly shows lea rdx, [rip+0xe1b], that
address is not meaningful without looking up 0x202b in the
hex dump of .rodata. Guessing the string instead of
looking it up is a source of errors.
4. Misreading strcmp branch logic
js after strcmp jumps on a negative
result, not on zero. jle jumps on zero or
negative. If you read jle as "jump if less than," you miss
the equality case and misidentify which paths lead to success.
5. Stopping at strings output
Every correct candidate string for this room appears in
strings output. But so does the decoy
(StringsIsForNoobs) and the format string
(DoYouEven%sCTF). Without the assembly context, you
cannot distinguish them. The whole point of the room is this lesson.
Reusable Checklist for Similar Rooms #
Use this template whenever you face a small RE password-checking binary. Filling it in forces you to separate clues from confirmed facts.
| Input function | scanf |
| Input format | DoYouEven%sCTF |
| Input buffer | [rbp-0x20] |
| Validation function | strcmp |
| Range-gate string | __dso_handle |
| Equality target | _init |
| Success string | Correct! |
| Failure string | Try again! |
| Decoy string | StringsIsForNoobs |
| Final answer | DoYouEven_init |
Calling Convention Cheat Sheet #
For any Linux x86-64 binary, argument registers follow the System V AMD64 ABI. Memorize the first six:
| Register | Argument | Example use in this room |
|---|---|---|
| rdi | 1st | format string address for printf / scanf |
| rsi | 2nd | buffer address for scanf; second string for strcmp |
| rdx | 3rd | loaded with .rodata pointer, then moved to rsi before strcmp |
| rcx | 4th | not used in this room |
| r8 | 5th | not used in this room |
| r9 | 6th | not used in this room |
Once you know this, every lea rax, [...]; mov rdi, rax
sequence immediately tells you: "this is the first argument to the
next call." You can read argument setup and function semantics in
parallel.
strcmp Return Value Reference #
| strcmp(a, b) | Meaning | After test eax,eax — flag | Jump instructions that fire |
|---|---|---|---|
| < 0 | a lexically before b | SF=1 | js, jl, jle, jng, jnge |
| == 0 | a equals b | ZF=1 | je, jz, jle, jge, jng, jnl |
| > 0 | a lexically after b | neither SF nor ZF | jg, jge, jnl, jnle |
When you see test eax, eax; jne label after
strcmp, read it as: "if the strings are NOT equal,
jump to label." That is the most common comparison pattern in
crackme binaries and you will see it constantly.
Full Workflow Diagram #
+----------------------+
| Receive Binary |
+----------+-----------+
|
v
+----------------------+
| file / strings / nm | ← clue gathering; do not trust yet
+----------+-----------+
|
v
+----------------------+
| Find main / imports | ← nm gives symbol names
+----------+-----------+
|
v
+----------------------+
| Find input function | ← look for scanf / fgets / read
| identify format str | ← CRITICAL: format ≠ "%s" here
+----------+-----------+
|
v
+----------------------+
| Find validation path | ← strcmp / memcmp / hand-rolled loop
| trace branch logic | ← js / jle / jne semantics matter
+----------+-----------+
|
v
+----------------------+
| Resolve .rodata refs | ← objdump -s -j .rodata
| map every address |
+----------+-----------+
|
v
+----------------------+
| Rebuild pseudocode | ← combine calling convention + semantics
+----------+-----------+
|
v
+----------------------+
| Infer exact answer | ← format string + validated token
+----------------------+
Key Takeaways #
-
stringsis reconnaissance, not confirmation. Every string in this binary appears instringsoutput, including the decoy. Assembly context is what distinguishes them. -
Format strings completely change the input contract.
scanf("DoYouEven%sCTF", buf)captures only the middle token — the typed input and the validated value are different strings. -
Always resolve
.rodataaddress references.[rip+0xe1b]is meaningless without looking up the corresponding offset. Oneobjdump -s -j .rodatarun decodes everything. -
Know
strcmpreturn value semantics. Negative / zero / positive map to specific CPU flags and specific conditional jumps. This knowledge alone is enough to read all beginner crackme validation logic. - Know the x86-64 calling convention. Being able to instantly identify which register holds which argument is what allows you to read assembly quickly.
- Stack-built strings are not automatically the answer. They require the same follow-through: find every reference to the buffer and determine its role.
Answer #
The binary captures the %s-matched token from the input
format DoYouEven%sCTF into a local buffer and compares it
against _init using strcmp. The TryHackMe
room accepts the themed construction of those two pieces:
DoYouEven_init.