Challenge: Bandaids Help me Heal · CTF: DalCTF 2026 · Category: Pwning · Difficulty: Easy
TL;DR
The “pwn” challenge has no input, no overflow, and nothing to exploit at runtime. It’s a static reversing puzzle wearing a pwn costume. The binary deliberately stalls you inside a usleep loop that, if you actually let it run, would take multiple days to finish. The “security system” is a fake integrity check whose outcome is a foregone conclusion. Once you read the constants out of the disassembly, the flag falls out of a single-byte XOR:
| |
The challenge name is the hint: you’re meant to patch the bandaid off (the sleep loop) or just lift the flag statically — never wait.
1. First contact
We’re handed a single ELF named chall. Standard triage first — what is it, and how is it protected?
| |
Not stripped — that’s a gift. We get real symbol names. Now checksec:
| |
On paper this reads like a textbook buffer-overflow setup: no stack canary, no PIE (fixed addresses, gadgets at known locations). My pwn instincts immediately said “find the overflow, build a ROP chain.” That instinct turned out to be a trap — the challenge is leading you to over-think it.
Because the binary isn’t stripped, listing the symbols tells us almost the whole story before we read a single instruction:
| |
A function literally named decrypt_and_print_flag. The whole game is just figuring out how to make main call it — and what argument it passes.
The strings give us the script of the play:
| |
So the flow is: print “Initializing secure wait system…”, do something, then either “Integrity check failed.” (and exit) or “Access granted.” followed by the flag. The “security system” the prompt mentions is this integrity check.
2. Reading main
Let’s disassemble. I used objdump -d -M intel chall.
| |
Let’s translate this into C in our heads:
| |
Two things jump out.
The “secure wait” is the whole joke
usleep takes microseconds. The argument is 0x3b9aca00 = 1,000,000,000 µs = 1,000 seconds per iteration. The loop runs 255 times.
| |
(usleep with an argument ≥ 1,000,000 is technically out-of-spec on some libcs, but glibc happily sleeps the full duration.) If you naively ./chall and wait, you’ll be staring at “Initializing secure wait system…” for the better part of a long weekend. That is the “bandaid” — an artificial delay slapped over the program. The challenge wants you to rip it off.
The integrity check can never fail
Look closely at what acc actually becomes. The loop XORs acc with every value of counter from 0xff down to 1:
| |
There’s a well-known identity for the running XOR of 1..n:
n mod 4 | XOR(1..n) |
|---|---|
| 0 | n |
| 1 | 1 |
| 2 | n + 1 |
| 3 | 0 |
Here n = 255, and 255 mod 4 == 3, so XOR(1..255) = 0. Then:
| |
The integrity check is always satisfied, and the key handed to decrypt_and_print_flag is always 0x5a. The XOR-loop is pure theater: it exists only to (a) burn three days of wall-clock time and (b) look like it might branch to “Integrity check failed.” It never does.
This is the elegant part of the design — the program genuinely computes the key, it just computes it the slow way on purpose. There’s no patching required to make it “work”; you only patch to avoid the wait, or skip execution entirely.
3. Reading decrypt_and_print_flag
Now the payload function:
| |
In C:
| |
The 22 ciphertext bytes are laid down by three movabs writes. The catch worth noticing: the third write goes to [rbp-0x12], which is only 6 bytes after [rbp-0x18], not 8 — so it overlaps and overwrites the last two bytes of the middle qword. You must replicate that overlap exactly or you’ll get garbage at offsets 14–15.
Reconstructing the buffer (little-endian, with the overlap applied):
| Offset | Source qword | Bytes (LE) |
|---|---|---|
| 0–7 | 0x38213c2e39363b3e | 3e 3b 36 39 2e 3c 21 38 |
| 8–15 | 0x3b2a0523283b3433 | 33 34 3b 28 23 05 2a 3b |
| 14–21 | 0x273e3f32392e3b2a | 2a 3b 2e 39 32 3f 3e 27 (overwrites offsets 14–15) |
4. Recovering the flag (without the 3-day nap)
There are three reasonable ways to skip the wait. Pick your poison.
Option A — Compute it by hand (fastest, no execution)
We know the key is 0x5a and we have the bytes. XOR them:
| |
| |
Done. No binary ever executed.
Option B — Patch the bandaid off (true to the challenge name)
If you want to run it, just neuter the sleep. The usleep argument lives in the instruction at 0x40120c (call usleep@plt). The cleanest patch is to NOP out the call, or zero the argument register. With your editor of choice (radare2 shown):
| |
The loop still runs 255 times, but usleep(0) returns instantly, so the whole thing finishes in microseconds. This is literally “patching the binary” — which, satisfyingly, is exactly what the flag says.
Option C — Just skip the loop in a debugger
Set a breakpoint after the loop and jump the program counter past it:
| |
Or simply set var on the counter to 1 once you’ve broken inside the loop, so it exits after a single iteration.
5. The flag
| |
The flag is self-documenting: the intended solution is to patch the binary (Option B) — peel the bandaid off the artificial delay. Options A and C are equally valid shortcuts that arrive at the same place.
6. Lessons & takeaways
checksecis a starting hint, not a verdict. “No canary + No PIE” screamed overflow at me, but there’s noread/gets/scanfanywhere in the binary — no attacker-controlled input at all. Always confirm an actual input surface exists before committing to a memory-corruption plan.- Not-stripped binaries hand you the plot. A symbol named
decrypt_and_print_flagplus four telltale strings told me the entire control flow before I read one opcode. Runnm/stringsfirst. - “Slow” is a defense mechanism. A
usleepmeasured in billions of microseconds is a deliberate time-wall, not real logic. When a program seems to “hang,” check whether it’s supposed to — and patch the delay rather than wait it out. - Watch overlapping stack writes. The three
movabsconstants overlapped by two bytes. Copy the layout literally; don’t assume eachmovabscontributes a clean 8 bytes. - The
XOR(1..n)identity is worth memorizing.n mod 4instantly tells you the result (0,1,n+1,nfor residues1,2,3,0). It turned a 255-iteration loop into a one-line constant and proved the integrity check was unconditional.
A clean, beginner-friendly reversing challenge dressed up as pwn — the “security system” was never a lock, just a very long, very fake loading bar.