DEV Community

Yogeshwar Peela
Yogeshwar Peela

Posted on Originally published at exploitnotes.hashnode.dev

TryHackMe : Compiled Writeup

Challenge Summary

A stripped-looking ELF binary (Compiled-1688545393558.Compiled) prompts for a password and prints either Correct! or Try again!. The goal is to reverse-engineer the binary to find the correct input without brute forcing.

Step 1 - Initial Recon

mv /home/kali/Downloads/Compiled-1688545393558.Compiled .

file Compiled-1688545393558.Compiled
Compiled-1688545393558.Compiled: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=06dcfaf13fb76a4b556852c5fbf9725ac21054fd, for GNU/Linux 3.2.0, not stripped
Enter fullscreen mode Exit fullscreen mode

Confirms a 64-bit, dynamically linked, not stripped, PIE ELF executable, meaning symbol names (like main) survive and static analysis tools can resolve them by name.

strings Compiled-1688545393558.Compiled
Enter fullscreen mode Exit fullscreen mode

Relevant strings from the output:

StringsIH
sForNoobH
Password: 
DoYouEven%sCTF
Correct!
Try again!
Enter fullscreen mode Exit fullscreen mode

The string StringsIsForNoob (split across chunks: StringsI + sForNoob + s) stood out immediately as a taunt aimed at anyone assuming the password would be visible in plaintext via strings.

Step 2 - Disassembly

objdump -d Compiled-1688545393558.Compiled
Enter fullscreen mode Exit fullscreen mode

Disassembling main revealed the actual logic:

0000000000001169 <main>:
    1169:       55                      push   %rbp
    116a:       48 89 e5                mov    %rsp,%rbp
    116d:       48 83 ec 40             sub    $0x40,%rsp
    1171:       48 b8 53 74 72 69 6e    movabs $0x4973676e69727453,%rax
    1178:       67 73 49 
    117b:       48 ba 73 46 6f 72 4e    movabs $0x626f6f4e726f4673,%rdx
    1182:       6f 6f 62 
    1185:       48 89 45 c0             mov    %rax,-0x40(%rbp)
    1189:       48 89 55 c8             mov    %rdx,-0x38(%rbp)
    118d:       66 c7 45 d0 73 00       movw   $0x73,-0x30(%rbp)
    1193:       48 8b 05 96 2e 00 00    mov    0x2e96(%rip),%rax        # 4030 <stdout@GLIBC_2.2.5>
    119a:       48 89 c1                mov    %rax,%rcx
    119d:       ba 0a 00 00 00          mov    $0xa,%edx
    11a2:       be 01 00 00 00          mov    $0x1,%esi
    11a7:       48 8d 05 56 0e 00 00    lea    0xe56(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    11ae:       48 89 c7                mov    %rax,%rdi
    11b1:       e8 aa fe ff ff          call   1060 <fwrite@plt>
    11b6:       48 8d 45 e0             lea    -0x20(%rbp),%rax
    11ba:       48 89 c6                mov    %rax,%rsi
    11bd:       48 8d 05 4b 0e 00 00    lea    0xe4b(%rip),%rax        # 200f <_IO_stdin_used+0xf>
    11c4:       48 89 c7                mov    %rax,%rdi
    11c7:       b8 00 00 00 00          mov    $0x0,%eax
    11cc:       e8 7f fe ff ff          call   1050 <__isoc99_scanf@plt>
    11d1:       48 8d 45 e0             lea    -0x20(%rbp),%rax
    11d5:       48 8d 15 42 0e 00 00    lea    0xe42(%rip),%rdx        # 201e <_IO_stdin_used+0x1e>
    11dc:       48 89 d6                mov    %rdx,%rsi
    11df:       48 89 c7                mov    %rax,%rdi
    11e2:       e8 59 fe ff ff          call   1040 <strcmp@plt>
    11e7:       85 c0                   test   %eax,%eax
    11e9:       78 1a                   js     1205 <main+0x9c>
    11eb:       48 8d 45 e0             lea    -0x20(%rbp),%rax
    11ef:       48 8d 15 28 0e 00 00    lea    0xe28(%rip),%rdx        # 201e <_IO_stdin_used+0x1e>
    11f6:       48 89 d6                mov    %rdx,%rsi
    11f9:       48 89 c7                mov    %rax,%rdi
    11fc:       e8 3f fe ff ff          call   1040 <strcmp@plt>
    1201:       85 c0                   test   %eax,%eax
    1203:       7e 46                   jle    124b <main+0xe2>
    1205:       48 8d 45 e0             lea    -0x20(%rbp),%rax
    1209:       48 8d 15 1b 0e 00 00    lea    0xe1b(%rip),%rdx        # 202b <_IO_stdin_used+0x2b>
    1210:       48 89 d6                mov    %rdx,%rsi
    1213:       48 89 c7                mov    %rax,%rdi
    1216:       e8 25 fe ff ff          call   1040 <strcmp@plt>
    121b:       85 c0                   test   %eax,%eax
    121d:       75 16                   jne    1235 <main+0xcc>
    121f:       48 8d 05 0b 0e 00 00    lea    0xe0b(%rip),%rax        # 2031 <_IO_stdin_used+0x31>
    1226:       48 89 c7                mov    %rax,%rdi
    1229:       b8 00 00 00 00          mov    $0x0,%eax
    122e:       e8 fd fd ff ff          call   1030 <printf@plt>
    1233:       eb 2a                   jmp    125f <main+0xf6>
    1235:       48 8d 05 fe 0d 00 00    lea    0xdfe(%rip),%rax        # 203a <_IO_stdin_used+0x3a>
    123c:       48 89 c7                mov    %rax,%rdi
    123f:       b8 00 00 00 00          mov    $0x0,%eax
    1244:       e8 e7 fd ff ff          call   1030 <printf@plt>
    1249:       eb 14                   jmp    125f <main+0xf6>
    124b:       48 8d 05 e8 0d 00 00    lea    0xde8(%rip),%rax        # 203a <_IO_stdin_used+0x3a>
    1252:       48 89 c7                mov    %rax,%rdi
    1255:       b8 00 00 00 00          mov    $0x0,%eax
    125a:       e8 d1 fd ff ff          call   1030 <printf@plt>
    125f:       b8 00 00 00 00          mov    $0x0,%eax
    1264:       c9                      leave
    1265:       c3                      ret
Enter fullscreen mode Exit fullscreen mode

Key observations:

  1. Decoy string construction. The binary manually builds "StringsIsForNoobs" byte-by-byte on the stack using movabs instructions, but this buffer is never referenced again anywhere in the function - it exists purely to bait anyone who runs strings into a dead end.
  2. Prompt + input via fwrite and scanf, using the format string "DoYouEven%sCTF", which visually implies the input has to be typed as DoYouEven<password>CTF.
  3. Two strcmp checks against strings at 0x201e, followed by a third strcmp against a string at 0x202b that actually decides success or failure.

Since this is a PIE binary, the addresses shown (0x201e, 0x202b, etc.) are link-time offsets, not final runtime addresses - they only resolve after ASLR relocation. Rather than deal with that in GDB, the faster path is a static dump of .rodata:

objdump -s -j .rodata Compiled-1688545393558.Compiled

Compiled-1688545393558.Compiled:     file format elf64-x86-64

Contents of section .rodata:
 2000 01000200 50617373 776f7264 3a200044  ....Password: .D
 2010 6f596f75 4576656e 25734354 46005f5f  oYouEven%sCTF.__
 2020 64736f5f 68616e64 6c65005f 696e6974  dso_handle._init
 2030 00436f72 72656374 21005472 79206167  .Correct!.Try ag
 2040 61696e21 00                          ain!.
Enter fullscreen mode Exit fullscreen mode

Decoded offsets:

Address String
0x2004 "Password: "
0x200f "DoYouEven%sCTF"
0x201e "__dso_handle"
0x202b "_init"
0x2031 "Correct!"
0x203a "Try again!"

The two comparison strings - __dso_handle and _init - are themselves a second layer of misdirection: they're genuine internal ELF/glibc symbol names, easy to mistake for debug artifacts rather than an intentional password.

Step 3 - Confirming with Ghidra

To validate the manual disassembly, the binary was also opened in Ghidra. The decompiled main() matched the derived logic exactly:

undefined8 main(void)
{
  int iVar1;
  char local_28 [32];

  fwrite("Password: ",1,10,stdout);
  __isoc99_scanf("DoYouEven%sCTF",local_28);
  iVar1 = strcmp(local_28,"__dso_handle");
  if ((-1 < iVar1) && (iVar1 = strcmp(local_28,"__dso_handle"), iVar1 < 1)) {
    printf("Try again!");
    return 0;
  }
  iVar1 = strcmp(local_28,"_init");
  if (iVar1 == 0) {
    printf("Correct!");
  }
  else {
    printf("Try again!");
  }
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Simplified, the logic is:

  • If local_28 is exactly equal to "__dso_handle", it's an immediate fail.
  • Otherwise, the only thing that actually matters is strcmp(local_28, "_init") == 0, which results in success.

So the real target value to land in local_28 is simply: _init.

Step 4 - The scanf Gotcha

The first attempt to enter the password directly failed:

./Compiled-1688545393558.Compiled
Password: DoYouEven_initCTF
Try again!
Enter fullscreen mode Exit fullscreen mode

Why: %s in scanf is greedy - it reads all non-whitespace characters up to the next whitespace/EOF and does not stop early just because a literal (CTF) appears next in the format string. There's no lookahead or backtracking. Typing DoYouEven_initCTF as one continuous token caused %s to consume _initCTF in its entirety (everything after the DoYouEven literal match), leaving local_28 = "_initCTF" instead of "_init", which fails the strcmp check.

A second attempt using just init also failed:

./Compiled-1688545393558.Compiled
Password: init
Try again!
Enter fullscreen mode Exit fullscreen mode

Here the format string's leading literal DoYouEven never matched the input's first character (i), so %s never even executed and local_28 was left as uninitialized stack garbage.

The fix: since the program never checks scanf's return value, the trailing literal CTF in the format string can be left unmatched without consequence - all that matters is that local_28 gets set correctly. Terminating the input right after _init (pressing Enter, which acts as %s's whitespace boundary) does exactly that:

./Compiled-1688545393558.Compiled
Password: DoYouEven_init
Correct!
Enter fullscreen mode Exit fullscreen mode

Final Answer

Password: DoYouEven_init
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. strings output can be intentionally poisoned. A decoy buffer (StringsIsForNoobs) was constructed on the stack purely to mislead naive analysis - always trace whether a visible string is actually used in the disassembly before trusting it.
  2. PIE binaries need relocation-aware analysis. Static addresses from objdump don't exist at runtime until ASLR is resolved; dumping .rodata statically sidesteps this entirely for read-only string data.
  3. Cross-checking with a decompiler (Ghidra) against manual disassembly is a fast, reliable way to catch mistakes in hand-traced jump logic (js/jle idioms can be easy to misread).
  4. scanf("%s") is greedy and doesn't backtrack. Format strings with literals after a %s conversion are a common trick - understanding this is often the difference between a "logically correct password" and an "actually working input."

Top comments (0)