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
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
Relevant strings from the output:
StringsIH
sForNoobH
Password:
DoYouEven%sCTF
Correct!
Try again!
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
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
Key observations:
-
Decoy string construction. The binary manually builds
"StringsIsForNoobs"byte-by-byte on the stack usingmovabsinstructions, but this buffer is never referenced again anywhere in the function - it exists purely to bait anyone who runsstringsinto a dead end. -
Prompt + input via
fwriteandscanf, using the format string"DoYouEven%sCTF", which visually implies the input has to be typed asDoYouEven<password>CTF. -
Two
strcmpchecks against strings at0x201e, followed by a thirdstrcmpagainst a string at0x202bthat 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!.
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;
}
Simplified, the logic is:
- If
local_28is 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!
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!
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!
Final Answer
Password: DoYouEven_init
Key Takeaways
-
stringsoutput 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. -
PIE binaries need relocation-aware analysis. Static addresses from
objdumpdon't exist at runtime until ASLR is resolved; dumping.rodatastatically sidesteps this entirely for read-only string data. -
Cross-checking with a decompiler (Ghidra) against manual disassembly is a fast, reliable way to catch mistakes in hand-traced jump logic (
js/jleidioms can be easy to misread). -
scanf("%s")is greedy and doesn't backtrack. Format strings with literals after a%sconversion are a common trick - understanding this is often the difference between a "logically correct password" and an "actually working input."
Top comments (0)