The Elf's Wager: Reverse Engineering CTF Writeup

Barely Tame CTF Player. Debugging Addict. Worshipper of Wi-Fi Signals. Human? Depends on the Ping.
Challenge Description
Category: Reverse engineering
Author: qvipin
The break room buzzes with energy when you walk in. A crowd of elves has gathered around Jingle McSnark's desk, where a holographic scoreboard floats above a plate of half-eaten gingerbread.
"Ah, the human!" Jingle spins around, candy cane tucked behind one pointed ear. "Perfect timing. We were just discussing how long it would take you to fail today's challenge."
He gestures dramatically at his terminal, where green text scrolls across a black screen.
"Every week, I post a little puzzle for the SOC team. Keeps us sharp, you know? Last week, Snowdrift over there" he points at a sheepish-looking elf "took three days to crack my binary. THREE. DAYS."
Snowdrift mutters something about "unfair obfuscation" into his hot cocoa.
"But you," Jingle continues, leaning forward with a grin that's equal parts challenge and condescension, "you're supposed to be some kind of specialist, right? Santa's new secret weapon against the Krampus Syndicate?"
He slides a USB drive across the desk. It's shaped like a tiny Christmas tree.
"Prove it. My mainframe authentication module. Figure out what gets you in. No debuggers and I've made sure of that. Static analysis only, human."
The elves exchange glances. Someone starts a betting pool on a napkin.
"Oh, and one more thing," Jingle adds, spinning back to his monitors. "The Syndicate's been probing our mainframe access systems all week. If you can't figure out how authentication works at the North Pole... well, let's hope they can't either."
The room falls silent, waiting.
Step-by-Step Analysis
In keeping with the challenge constraints, the analysis is performed strictly through static inspection of the binary, avoiding any form of decompilation.
Understanding the Binary
Initial inspection of the binary involves identifying its format and the security hardening features applied to it.
❯ file day4
day4: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b12ceece7b0740e07024986b701c0b5d81aa17f3,
for GNU/Linux 3.2.0, stripped
❯ checksec --file=day4
[*] '/Downloads/AdventOfCTF/Dec04/day4'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
From this we can conclude that the binary is heavily hardened: full RELRO prevents GOT overwrite attacks, stack canaries block stack‑based exploitation, NX disallows code execution on the stack, PIE introduces ASLR across the entire binary, and additional mitigations like FORTIFY, SHSTK, and IBT further reduce the attack surface. Combined with the binary being stripped and the challenge’s explicit restrictions, these protections make runtime exploitation impractical. As a result, the only viable approach is strict static analysis, examining the instructions and data directly without relying on dynamic debugging or code‑execution tricks.
Running the binary with various inputs immediately reveals one key detail: the program is extremely picky about input length.
❯ ./day4
NPLD Mainframe Authentication
Enter access code:
Jingle laughs. Wrong credential length!
❯ ./day4
NPLD Mainframe Authentication
Enter access code: 1234567890
Jingle laughs. Wrong credential length!
❯ ./day4
NPLD Mainframe Authentication
Enter access code: lajkdsfhsadfuueriggvoaiyugrwetkgbiusfgsafyughp
Jingle laughs. Wrong credential length!
Everything fails with the same message, but the error wording strongly suggests that a very specific length is required.
Before diving into disassembly, we check for readable strings:
❯ strings -n 4 day4
... SNIP ...
Nice try, but Santa sees when youre peeking!
Coal for you! Tampering detected.
NPLD Mainframe Authentication
Enter access code:
Jingle laughs. Wrong credential length!
Welcome to the mainframe, Operative. Jingle owes the elves a round.
Access Denied. Jingle smirks.
... SNIP ...
These strings confirm that there is a success path inside the binary.
Finding the Length Check
We dump the disassembly to inspect the binary statically: objdump -d -M intel day4
Inside the disassembly, we encounter:
11e3: e8 e8 fe ff ff call 10d0 <strlen@plt>
11e8: 48 83 f8 17 cmp rax,0x17
11ec: 74 0e je 11fc <exit@plt+0xcc>
At address 0x11e8 the program compares the result of strlen in rax against 0x17 (23 in decimal), and at 0x11ec it performs a conditional jump that proceeds only when the input length matches exactly (23 bytes) —anything else is rejected immediately.
Trying a 23‑char string confirms that this passes the length gate, but is still rejected:
❯ ./temp
NPLD Mainframe Authentication
Enter access code: 11112222333344445555666
Access Denied. Jingle smirks.
Thus, there must be a second validation stage, and this is where things get interesting.
Locating the Real Authentication Logic
A deeper look into the disassembly reveals a tight loop implementing per‑byte validation. The core logic looks like this:
1362: xor eax,eax
1368: lea rcx,[rip+0xda1]
136f: movsx edx,BYTE PTR [rdi+rax*1] ; user[i]
1373: movzx esi,BYTE PTR [rcx+rax*1] ; secret[i]
1377: xor edx,0x42 ; user[i] ^ 0x42
137a: cmp edx,esi ; == secret[i] ?
137c: jne 138d ; fail
137e: inc rax
1381: cmp rax,0x17 ; 23 bytes
1385: jne 136f ; loop
Not hard to translate into logic:
for (i = 0; i < 23; i++) {
if ((input[i] ^ 0x42) != secret[i])
return 0;
}
return 1;
Meaning the correct input is obtained by: input[i] = secret[i] ^ 0x42
All we need now is the 23‑byte secret array at address 0x2110
Extracting the Secret Bytes
❯ hexdump -Cv -s 0x2110 -n 23 day4
00002110 21 31 26 39 73 2c 36 72 1d 36 2a 71 1d 2f 76 73 |!1&9s,6r.6*q./vs|
00002120 2c 24 30 76 2f 71 3f |,$0v/q?|
The 23 bytes are:
21 31 26 39 73 2c 36 72 1d 36 2a 71 1d 2f 76 73 2c 24 30 76 2f 71 3f
Using a tiny Python script to XOR each byte with 0x42:
hexBytes = "21 31 26 39 73 2c 36 72 1d 36 2a 71 1d 2f 76 73 2c 24 30 76 2f 71 3f"
secret = hexBytes.split(" ")
flag = ''.join(chr(int(b, 16) ^ 0x42) for b in secret)
print(flag)
Running it:
❯ python3 sol.py
csd{1nt0_th3_m41nfr4m3}
And that is the valid access code.
Final Flag
csd{1nt0_th3_m41nfr4m3}




