Orthogonal Singularity - Author Writeup
The idea behind Orthogonal Singularity was to make a challenge that could not be solved by recognizing one familiar trick.
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:
In practice, this means the digest must start with three zero bytes.
For a 24-bit target, the expected number of tries is:
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:
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 NoneThis 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:
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:
The two endpoints are quantized from a 16-bit value:
where:
The server asks for:
f_bounds [hex]>and expects:
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:
The candidate whose phase best matches the hidden chirp gives the largest coherent sum.
Brute forcing all pairs is possible, but there is a better way:
- Estimate from the start of the signal.
- Fix , then search all possible 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 is:
Then pick:
After that, fix:
and test the 256 possible values of using the full one-second signal.
The server used phase accumulation, so the solver should mirror that style:
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:
[+] 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 , 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:
- the net writhe,
- the endpoint permutation.
Writhe
For this challenge, the writhe is just the signed crossing count:
Positive generators add 1. Negative generators subtract 1.
The response stores this modulo 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:
where:
The sign does not matter for this part, because swapping adjacent endpoints is the same whether the crossing goes over or under.
Start with:
perm = [0, 1, 2, 3]For each generator g, swap:
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:
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:
writhe = 4
perm = [1, 2, 0, 3]then the answer is:
041203Summary
The full solve is:
- First, connect to the service.
- Solve the 24-bit SHA-256 PoW with multiprocessing.
- Parse the PCM samples.
- Estimate from the first 800 samples.
- Estimate using a full matched-filter search.
- Submit
(b1 << 8) | b0as hex. - Parse
B4_GEN. - Compute writhe and endpoint permutation.
- Submit the 6-character topology register.
- 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
- Back, A. (2002). Hashcash - A Denial of Service Counter-Measure. Technical report. Satoshi Nakamoto Institute mirror
- Jakobsson, M., & Juels, A. (1999). Proofs of Work and Bread Pudding Protocols. In Communications and Multimedia Security (pp. 258-272). Springer. dblp
- Mann, S., & Haykin, S. (1995). The chirplet transform: physical considerations. IEEE Transactions on Signal Processing, 43(11), 2745-2761. dblp
- Turin, G. L. (1960). An introduction to matched filters. IRE Transactions on Information Theory, 6(3), 311-329. dblp
- Artin, E. (1947). Theory of Braids. Annals of Mathematics, 48(1), 101-126. JSTOR, scan
- Birman, J. S. (1974). Braids, Links, and Mapping Class Groups (Annals of Mathematics Studies, No. 82). Princeton University Press. JSTOR
- Fuller, F. B. (1971). The writhing number of a space curve. Proceedings of the National Academy of Sciences, 68(4), 815-819. Caltech