reversefreehard

cf madness

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.

$ ls tags/ techniques/
static_symbolic_solvereturn_address_rewritingfunction_table_dispatchdata_flow_analysis

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

cf madness — pingCTF 2026

Description

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, stripped

The 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.

Analysis

Initial recon

$ 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.

Main at 0x4035cc

The main function:

  1. Prints "Enter the flag: "
  2. Reads input with scanf("%s", 0x41c560)
  3. Stores 726 * (strlen(input)+1) as a long double at 0x41b490
  4. Falls through ~500 bytes of nop sled

The use of x87 long double to store an integer is pure obfuscation — it confuses decompilers that expect floating-point semantics.

The nop sled (0x403651 to 0x415e31)

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.

The trampoline at 0x415e31

Execution falls through the nop sled and reaches the trampoline. This is the core validation loop:

...

$ grep --similar

Similar writeups