Back to writeups
CTF writeup/7 min read

Orthogonal Singularity - Author Writeup

The idea behind Orthogonal Singularity was to make a challenge that could not be solved by recognizing one familiar trick.

EventCSS CTF 2026: Return of Nexus
CategoryMisc
DifficultyExpert
Points750
Solves0
AuthorJose

Challenge

Somewhere listening posts along the galactic rim have intercepted a telemetry beacon broadcasting near the event horizon.

To stabilize the link and extract the core telemetry, you must:

1. Pass the handshake gate before the uplink decays.

2. Track the carrier's 16 kHz trajectory through the noise and lock the frequency bounds.

3. Trace the topology of the underlying topological braid to collapse the singularity.

Flag Format: CSSCTF{...}

nc $HOST 7878

Why I Made This

The idea behind Orthogonal Singularity was to make a challenge that could not be solved by recognizing one familiar trick.

I wanted the solve to require an actual pipeline:

  • solve a proof-of-work gate quickly enough,
  • recover a noisy chirp from raw PCM samples,
  • then interpret a small braid word correctly.

The challenge was also written with LLM-assisted solving in mind. A lot of CTF tasks are very good for LLMs because the problem fits into one category. This challenge tries to break that pattern. Even if you know what a PoW is, you still need socket handling. Even if you know DSP, you still need to script the matched filter. Even if you know braid groups, you still need to notice that the server wants a tiny packed register, not a full knot invariant.

The server was strict on purpose. Wrong input meant the connection dropped. That made guessing painful and pushed players toward automating the whole interaction.

Also, sorry for releasing the writeup late. I was pretty tired after the event.

Phase 1: Proof of Work

The first gate is a standard partial SHA-256 inversion puzzle, similar to Hashcash-style proof-of-work schemes [1, 2].

The server gives a prefix and expects a nonce such that:

SHA-256(prefix+nonce)<2256−24\text{SHA-256}(\text{prefix} + \text{nonce}) < 2^{256 - 24}

In practice, this means the digest must start with three zero bytes.

For a 24-bit target, the expected number of tries is:

224=16,777,2162^{24} = 16{,}777{,}216

A single Python process can sometimes finish in time, but it is not reliable enough with the connection timeout. The intended solution was to split the search across multiple processes:

python
def _pow_worker(args):
    prefix_bytes, start, chunk = args
    target = b"\x00\x00\x00"

    for n in range(start, start + chunk):
        h = hashlib.sha256(prefix_bytes + str(n).encode()).digest()
        if h[:3] == target:
            return str(n)

    return None

This part is just a simple PoW. The next one is:

Phase 2: Demodulating the Chirp

After the PoW, the service sends 16,000 signed 16-bit PCM samples as hex.

The sample rate is:

fs=16,000 Hzf_s = 16{,}000\text{ Hz}

and the signal lasts one second.

Here, there is a quadratic chirp. Which is the same signal family that appears in chirp and chirplet analysis [3].

The instantaneous frequency is:

f(t)=f0+(f1−f0)(tT)2f(t) = f_0 + (f_1 - f_0)\left(\frac{t}{T}\right)^2

The two endpoints are quantized from a 16-bit value:

f0=800+8b0f_0 = 800 + 8b_0
f1=3500+8b1f_1 = 3500 + 8b_1

where:

b0=S&0xFF,b1=S≫8b_0 = S \& 0xFF, \qquad b_1 = S \gg 8

The server asks for:

text
f_bounds [hex]>

and expects:

(b1≪8)∣b0(b_1 \ll 8) \mid b_0

as hex.

Recovering the Bounds

The direct way is to use a matched filter, which is the standard tool for detecting a known signal shape in noise [4].

For a candidate chirp, score it with:

∣∑nx[n]e−jϕ[n]∣\left| \sum_n x[n]e^{-j\phi[n]} \right|

The candidate whose phase best matches the hidden chirp gives the largest coherent sum.

Brute forcing all 256×256256 \times 256 pairs is possible, but there is a better way:

  1. Estimate b0b_0 from the start of the signal.
  2. Fix b0b_0, then search all possible b1b_1 values across the full signal.

For the first step, it is almost instaneous. In that short window, the quadratic term has not moved the frequency much yet, so the signal is close enough to a stationary tone.

The score for each possible b0b_0 is:

r0[b0]=∑n=0799x[n]e−j2π(800+8b0)n/fsr_0[b_0] = \sum_{n=0}^{799} x[n]e^{-j2\pi(800 + 8b_0)n/f_s}

Then pick:

b^0=arg⁡max⁡b0∣r0[b0]∣\hat{b}_0 = \arg\max_{b_0}|r_0[b_0]|

After that, fix:

f^0=800+8b^0\hat{f}_0 = 800 + 8\hat{b}_0

and test the 256 possible values of b1b_1 using the full one-second signal.

The server used phase accumulation, so the solver should mirror that style:

ϕ[n]=2πfs∑k=0n(f^0+(3500+8b1−f^0)(kfs)2)\phi[n] = \frac{2\pi}{f_s} \sum_{k=0}^{n} \left( \hat{f}_0 + (3500 + 8b_1 - \hat{f}_0) \left(\frac{k}{f_s}\right)^2 \right)

The matched filter make the correct b1 becomes easy to pick out.

Phase 3: Braid Register

After the chirp is solved, the service prints a braid word:

text
[+] PHASE LOCK CONFIRMED
--- TOPOLOGY REGISTER ---
B4_GEN: [-2, 1, 2, 2, -3, -3, 3, 3, 2, -2, 2, -3, 1, -3, 1, 1]
--- END REGISTER ---
homology_invariant>

This is the part where the challenge tried to bait overthinking.

The prompt says homology_invariant, and the word is in B4B_4, the Artin braid group on 4 strands [5, 6]. You might think about Jones polynomials, Alexander polynomials, Burau representations, Khovanov homology, and so on.

The server did not want any of that.

It wanted two very small invariants:

  1. the net writhe,
  2. the endpoint permutation.

Writhe

For this challenge, the writhe is just the signed crossing count:

w=∑isign(gi)w = \sum_i \text{sign}(g_i)

Positive generators add 1. Negative generators subtract 1.

The response stores this modulo 256:

w mod 256w \bmod 256

This is related to the usual writhing-number idea from space curves, but here it is just the braid word's algebraic crossing count [7].

Permutation

The phrase "trace the braid" was meant literally. If you follow the strands from top to bottom, each generator swaps two adjacent strands.

There is a standard projection:

Bn↠SnB_n \twoheadrightarrow S_n

where:

σi↦(i    i+1)\sigma_i \mapsto (i\;\;i+1)

The sign does not matter for this part, because swapping adjacent endpoints is the same whether the crossing goes over or under.

Start with:

python
perm = [0, 1, 2, 3]

For each generator g, swap:

python
idx = abs(g) - 1
perm[idx], perm[idx + 1] = perm[idx + 1], perm[idx]

The TOPOLOGY REGISTER wording was also meant as a hint. The previous phase already used packed hex values, and this phase does the same:

  • 1 byte for writhe, written as 2 hex characters,
  • 4 strand labels, each packed into a nibble, written as 4 hex characters.

So the final topology answer is:

python
perm = [0, 1, 2, 3]
writhe = 0

for g in gens:
    idx = abs(g) - 1
    writhe += 1 if g > 0 else -1
    perm[idx], perm[idx + 1] = perm[idx + 1], perm[idx]

perm_int = 0
for p in perm:
    perm_int = (perm_int << 4) | p

response = f"{(writhe % 256):02x}{perm_int:04x}"

For example, if:

text
writhe = 4
perm = [1, 2, 0, 3]

then the answer is:

text
041203

Summary

The full solve is:

  1. First, connect to the service.
  2. Solve the 24-bit SHA-256 PoW with multiprocessing.
  3. Parse the PCM samples.
  4. Estimate b0b_0 from the first 800 samples.
  5. Estimate b1b_1 using a full matched-filter search.
  6. Submit (b1 << 8) | b0 as hex.
  7. Parse B4_GEN.
  8. Compute writhe and endpoint permutation.
  9. Submit the 6-character topology register.
  10. Get the flag.

Takeaway

This challenge was not meant to be impossible because each individual part was extremely deep. The difficulty came from chaining skills in the same challenge.

The last phase was especially designed to punish over-reading. The name sounded scary, but the actual invariant was small: signed crossing count plus strand permutation.

That was the whole trick.

References

  1. Back, A. (2002). Hashcash - A Denial of Service Counter-Measure. Technical report. Satoshi Nakamoto Institute mirror
  2. Jakobsson, M., & Juels, A. (1999). Proofs of Work and Bread Pudding Protocols. In Communications and Multimedia Security (pp. 258-272). Springer. dblp
  3. Mann, S., & Haykin, S. (1995). The chirplet transform: physical considerations. IEEE Transactions on Signal Processing, 43(11), 2745-2761. dblp
  4. Turin, G. L. (1960). An introduction to matched filters. IRE Transactions on Information Theory, 6(3), 311-329. dblp
  5. Artin, E. (1947). Theory of Braids. Annals of Mathematics, 48(1), 101-126. JSTOR, scan
  6. Birman, J. S. (1974). Braids, Links, and Mapping Class Groups (Annals of Mathematics Studies, No. 82). Princeton University Press. JSTOR
  7. Fuller, F. B. (1971). The writhing number of a space curve. Proceedings of the National Academy of Sciences, 68(4), 815-819. Caltech