← Back to Writeups
HTBN/AReversing

SpecCTF

XESXOR8/23/20266 min read
#reversing#htb#n/a

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
PortServiceVersionNotes
<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

  1. Static_analysis
  2. Disassembly
  3. Splitmix64_inversion
  4. Modular_inverse
  5. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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 & 7 must be 0 (length must be a multiple of 8), else it prints the error string and exit(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 loads r15 = ENC[i] (global array ENC at vaddr 0x70c0), r14 = input_chunk[i], then calls specte_byte(0x1337, 0x1337).
  • The return of specte_byte (a "leaked" byte) is accumulated; main compares the accumulated count to len/8, adjusting a score, and prints CORRECT when all chunks match.

The Spectre machinery is obfuscation

  • specte_bytereadMemoryByte performs a classic Flush+Reload: it flushes arr2 cache lines with clflush, runs spec_func / carrierFunc (bounds-checked indirect calls that speculatively execute get_from_array), then times reloads with rdtscp against CACHE_HIT_THRESHOLD, recording hits in results[] and a priority_queue (PQ) ordered by hit count.
  • specEnvTime (offset 0x1363) is the actual secret-dependent gadget. It returns a "hit" / leak byte when hashy(r14) == r15, i.e. when hashy(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 inverting hashy.

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 >> 33 is 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-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR