TryHackMe · Reverse Engineering

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.

Room
Compiled
Category
Reverse Engineering / Binary Analysis
Difficulty
Easy
Binary Type
ELF 64-bit
Architecture
x86-64
Answer
DoYouEven_init

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.

Core Lesson

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:

ELF analysis objdump strings / nm x86-64 assembly scanf format strings strcmp control flow .rodata resolution static analysis

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:

Why "not stripped" matters

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:

What a beginner might think

A common first reaction produces several hypotheses:

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.

Key Lesson

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:

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.

nm symbol type codes

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:

Step 5 — Understand the Structure of main

At a high level, main does exactly five things:

  1. Allocates stack space and saves callee-saved registers
  2. Constructs a string in local stack memory by writing byte constants
  3. Prints Password: as a prompt via printf
  4. Reads input from the user via scanf
  5. Compares the input (or a portion of it) using strcmp, then prints Correct! or Try 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:

The only way to determine which role it plays is to follow every reference to that stack buffer in the subsequent code.

Anti-Strings Technique

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:

RegisterArgument position
rdi1st argument
rsi2nd argument
rdx3rd argument
rcx4th argument
r85th argument
r96th 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 valueMeaningCondition flag set
< 0 (negative)a is lexically before bSF=1 (sign flag)
== 0a equals bZF=1 (zero flag)
> 0 (positive)a is lexically after bneither

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:

InstructionJumps whenstrcmp condition
je / jzZF=1strings are equal
jne / jnzZF=0strings are not equal
jsSF=1strcmp returned negative (a < b)
jnsSF=0strcmp returned zero or positive
jle / jngZF=1 or SF≠OFstrcmp returned ≤ 0
jge / jnlSF=OFstrcmp returned ≥ 0
Why this trips up beginners

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:

AddressString contentRole
0x2004Password: Prompt printed to terminal
0x200fDoYouEven%sCTFscanf format string — defines the input pattern
0x201e__dso_handleCompare target 1 (used in the range-check branch)
0x202b_initCompare target 2 (the equality test for "Correct!")
0x2031Correct!Success message
0x203aTry 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:

The Format String Trap

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:

  1. The input must match the scanf format: DoYouEven%sCTF
  2. The captured %s token must equal _init for the "Correct!" branch

Substituting _init into the format: DoYouEven + _init + CTFDoYouEven_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.

RE Crackme Quick-Reference — Compiled
Input functionscanf
Input formatDoYouEven%sCTF
Input buffer[rbp-0x20]
Validation functionstrcmp
Range-gate string__dso_handle
Equality target_init
Success stringCorrect!
Failure stringTry again!
Decoy stringStringsIsForNoobs
Final answerDoYouEven_init

Calling Convention Cheat Sheet #

For any Linux x86-64 binary, argument registers follow the System V AMD64 ABI. Memorize the first six:

RegisterArgumentExample use in this room
rdi1stformat string address for printf / scanf
rsi2ndbuffer address for scanf; second string for strcmp
rdx3rdloaded with .rodata pointer, then moved to rsi before strcmp
rcx4thnot used in this room
r85thnot used in this room
r96thnot 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)MeaningAfter test eax,eax — flagJump instructions that fire
< 0a lexically before bSF=1js, jl, jle, jng, jnge
== 0a equals bZF=1je, jz, jle, jge, jng, jnl
> 0a lexically after bneither SF nor ZFjg, jge, jnl, jnle
Practical Rule

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 #

Answer #

Password
DoYouEven_init

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.