SpecCTF
SpecCTF
Platform: GPN CTF | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-06-05 | Status: Solved Techniques: static_analysis, disassembly, splitmix64_inversion, modular_inverse, xorshift_self_inverse, elf_section_parsing, side_channel_deobfuscation
Summary
Task: a not-stripped x86-64 PIE C++ flag-checker that simulates a Spectre Flush+Reload side channel (clflush/rdtscp/sched_setaffinity/priority_queue). Solution: the cache-timing apparatus is a red herring — the secret-dependent branch reduces to hashy(chunk) == ENC[i] where hashy is the invertible splitmix64 finalizer; invert it (self-inverse xorshift since shift>32 + modular-inverse multiply) over the 6 extracted ENC constants to recover the flag.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
kitctf| ID:20260605_kitctf_specctf - Tags: elf, x86_64, pie, not_stripped, cpp, spectre, flush_reload, speculative_execution, splitmix64, hash_inversion, crackme, flag_checker, side_channel_obfuscation, rdtscp, clflush
- Indicators: clflush + rdtscp + sched_setaffinity in symbols, priority_queue ordered by cache hit count, title/description pun on 'ghost' and 'speculative' (Spectre), hash with two x >> 33 xorshifts and a 64-bit odd multiplier (splitmix64 finalizer), len % 8 == 0 input constraint, processed as little-endian uint64 chunks
- Source:
20260605_kitctf_specctf.md
Foothold
Vulnerability / Misconfiguration
- Static_analysis
- Disassembly
- Splitmix64_inversion
- Modular_inverse
- Xorshift_self_inverse
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- static_analysis
- disassembly
- splitmix64_inversion
- modular_inverse
- xorshift_self_inverse
- elf_section_parsing
- side_channel_deobfuscation
- Tags: elf, x86_64, pie, not_stripped, cpp, spectre, flush_reload, speculative_execution, splitmix64, hash_inversion, crackme, flag_checker, side_channel_obfuscation, rdtscp, clflush
Original Writeup
<details><summary>Click to expand original content</summary>Description
We present for our desert a slightly deranged ghost, even though they may be unstable, they are still delicious. Share a meal and a CPU with this ghost and we're sure you'll be satisfied
English summary: A single 64-bit ELF PIE executable specCTF is given. It is a flag-checker that wraps its secret comparison inside a simulated Spectre / Flush+Reload cache side-channel. The goal is to recover the argv[1] value that the binary accepts as CORRECT.
The title and description are puns pointing directly at the Spectre CPU vulnerability:
- "ghost" = Spectre (a specter / ghost).
- "slightly deranged" / "unstable" = speculative execution and unstable speculation.
- "Share a meal and a CPU" = cache side-channel on a shared CPU core.
- The flag itself confirms the theme:
REDACTED.
Analysis
Recon
file specCTF → ELF 64-bit LSB PIE, x86-64, dynamically linked, not stripped, Debian GCC 12.2.0, C++ / libstdc++.
Demangled symbols of interest:
specEnvTime, specte_byte, spec_func, carrierFunc, readMemoryByte,
get_from_array, init_attack, train, distTrue, distFalse, hashy,
_GLOBAL__sub_I_ENC (static initializer)
Strings include NOPE, CORRECT, and references to
priority_queue<int,...,compareChars>, sched_setaffinity, rdtscp, clflush.
The combination clflush + rdtscp + sched_setaffinity + a cache-hit-count priority queue
is the unmistakable signature of a Spectre / Flush+Reload simulation.
main (offset 0x1d3a)
- The flag is
argv[1]. strlen(argv[1])is taken;len & 7must be0(length must be a multiple of 8), else it prints the error string andexit(0x539).- The input is processed 8 bytes at a time; each 8-byte chunk is read as a little-endian
uint64. - For chunk index
i: it loadsr15 = ENC[i](global arrayENCat vaddr0x70c0),r14 = input_chunk[i], then callsspecte_byte(0x1337, 0x1337). - The return of
specte_byte(a "leaked" byte) is accumulated; main compares the accumulated count tolen/8, adjusting a score, and printsCORRECTwhen all chunks match.
The Spectre machinery is obfuscation
specte_byte→readMemoryByteperforms a classic Flush+Reload: it flushesarr2cache lines withclflush, runsspec_func/carrierFunc(bounds-checked indirect calls that speculatively executeget_from_array), then times reloads withrdtscpagainstCACHE_HIT_THRESHOLD, recording hits inresults[]and apriority_queue(PQ) ordered by hit count.specEnvTime(offset0x1363) is the actual secret-dependent gadget. It returns a "hit" / leak byte whenhashy(r14) == r15, i.e. whenhashy(input_chunk) == ENC[i]. Otherwise it returns a different (miss) byte.- Net effect: the entire cache-timing apparatus is a fancy, side-channel-flavored way to compute
the boolean predicate
hashy(chunk) == ENC[i]. No dynamic execution or real cache timing is needed to solve — it reduces to invertinghashy.
hashy (offset 0x1239) is the splitmix64 finalizer
Disassembly logic on a 64-bit value x (M = 2**64 - 1):
x ^= x >> 33
x *= 0xF451AF97D512CACD ; (-0x0bae5068a2ead353 as unsigned)
x ^= x >> 33
x ^= 0xC2CEAADE1A351C23 ; (-0x3d315521e5cae3dd as unsigned)
x ^= x >> 33
This is the standard splitmix64 finalizer (xorshift–multiply–xorshift–xorconst–xorshift), which is fully invertible:
x ^= x >> 33is self-inverse in a single pass because the shift (33) is greater than half the word size (32) — the top bits are recovered first and never re-collide.- The multiply is inverted by multiplying with the modular inverse of the odd constant:
inv_mul = pow(mul, -1, 2**64). - The xor-const is inverted by xoring the same constant.
Solution
Extracting ENC
ENC is at vaddr 0x70c0 in .data (size 0x38 = 56 bytes). Since this is a PIE and
readelf was unavailable on the host, the ELF section headers were parsed in Python to map
vaddr → file offset (.data vaddr 0x70a0 → file offset 0x60a0), then the 7 little-endian
uint64 values were read:
0xee7590ece97175e5
0x32050538cc51ebea
0xd3a0f1efa162aeed
0x44cde0d9c2d3a245
0x99321a20f8b0a7af
0x72d2aa1cbaabe81f
0x0000000000000000 (null terminator / padding — 6 real chunks)
Inverting hashy on the 6 non-zero values and concatenating the little-endian bytes yields the
48-byte flag (48 = 6 chunks of 8, satisfying the len % 8 == 0 constraint).
Solver
#!/usr/bin/env python3
import struct
M = (1 << 64) - 1
mul = (-0xbae5068a2ead353) & M
xc = (-0x3d315521e5cae3dd) & M
inv_mul = pow(mul, -1, 1 << 64)
def xs33(x): # invert (and apply) x ^= x>>33 (self-inverse, shift>32)
return (x ^ (x >> 33)) & M
def hashy(x):
x &= M
x ^= x >> 33
x = (x * mul) & M
x ^= x >> 33
x ^= xc
x ^= x >> 33
return x
def unhashy(h):
x = h & M
x = xs33(x) # invert last x ^= x>>33
x ^= xc # invert xor const
x = xs33(x) # invert x ^= x>>33
x = (x * inv_mul) & M # invert multiply
x = xs33(x) # invert first x ^= x>>33
return x
enc = [0xee7590ece97175e5, 0x32050538cc51ebea, 0xd3a0f1efa162aeed,
0x44cde0d9c2d3a245, 0x99321a20f8b0a7af, 0x72d2aa1cbaabe81f]
out = b''
for e in enc:
p = unhashy(e)
assert hashy(p) == e
out += p.to_bytes(8, 'little')
print(out.decode()) # GPNCTF{REDACTED}
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR