$ cat writeup.md…
$ cat writeup.md…
pingctf2026
Task: ELF crackme with 89KB .text section that breaks decompilers via nop-sled + return-address-rewriting obfuscation. Solution: trace control flow through trampoline mechanism, recover per-character hash formula, extract expected values from .data, and solve symbolically.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Ghidra had a stroke, IDA can't decompile, i don't have Binja. Only :SnowmanDecompiler: survived. (it's a joke, i wouldn't actually make you use snowman)
Files provided:
chall — ELF 64-bit LSB executable, x86-64, strippedThe binary is a classic flag checker: prompts for input, validates it, prints "Correct flag!" or "Wrong flag!". The twist is that the .text section is 89KB — unusually large for a simple crackme — and decompilers struggle to produce meaningful output.
$ file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, stripped
$ checksec chall
No PIE, NX enabled, Partial RELRO
$ objdump -d chall | wc -l
77627
Only 4 imports: puts, printf, scanf, strlen. The entry point at 0x4003b0 calls __libc_start_main(0x4035cc, ...), so the real "main" is at 0x4035cc.
The main function:
"Enter the flag: "scanf("%s", 0x41c560)726 * (strlen(input)+1) as a long double at 0x41b490The use of x87 long double to store an integer is pure obfuscation — it confuses decompilers that expect floating-point semantics.
The code between main and the trampoline is mostly nop instructions. Every ~0x41C bytes there's an opaque predicate block:
mov r10, 0x13 xor r11, r11 test r11, r11 jne junk ; never taken add r11, r10 imul r11, r11, 0x1337 xor r11, r10
These blocks only touch r10/r11 and never alter control flow — pure anti-analysis noise to break linear sweep disassembly.
Execution falls through the nop sled and reaches the trampoline. This is the core validation loop:
...
$ grep --similar