Echoes of Silicon — DEDSEC CTF Writeup
Category: Cryptography
Difficulty: Hard
Flag: DEDSEC{R3S1DU4L_ENG1N3}
The setup
A hardware accelerator crashed mid-benchmark. Players are handed five files that look exactly like what a hardware engineer would dump after a fault — a config file with numerical parameters, a verbose log, a JSON crash dump, a binary memory fragment, and a stats file.
The catch: there is no mention of “RSA,” “key,” “private,” “CRT,” or anything cryptographic anywhere in the files. The challenge looks like an embedded systems debugging puzzle. It is, in fact, a textbook RSA CRT-exponent leak — but only if you can recognise it through the disguise.
What players are given
| File | Cover story | Real content |
|---|---|---|
challenge.txt |
Accelerator config + register dump | RSA public key (n, e) + ciphertext + decoy registers |
accelerator.log |
Verbose hardware log | Fragment A of dp, base64-chunked across SNAPSHOT_BUF lines |
crash_dump.json |
Crash dump with four large arrays | Fragment B of dp, hidden at every third index in one array |
benchmark.stats |
Benchmark performance counters | Fragment C: dp mod r for cross-validation |
memory_fragment.bin |
Memory dump | Pure red herring — contains a fake “key” marker |
The crypto setup behind the scenes:
p,qare 512-bit safe primes (withp = 2q + 1)n = p × q(1024-bit)e = 65537d = e⁻¹ mod φ(n)dp = d mod (p-1)← the leaked secretct = flag^e mod n
Only n, e, and ct are public. Everything else has to be reconstructed from the “hardware debug” artefacts.
Step 1 — Realise this is RSA
challenge.txt looks like accelerator config. Fields are named modulus, public_exponent, session_ciphertext. They’re not labelled “n,” “e,” “ct” — but the values are unmistakable: a 1024-bit composite, 65537, and a 128-byte hex blob.
Surrounding them are decoy registers (reg_state_alpha, reg_state_beta, reg_state_gamma) with random 1024-bit values. They have no role. They exist to make players who instinctively grep for “big number” waste time.
So: there’s an RSA encryption of the flag, you have the public key, you need the private key. The challenge is in the four supplementary files.
Step 2 — Fragment A: accelerator.log
The log is full of lines like:
[12:33:01] PIPELINE BUSY = 0x4E12
[12:33:02] ENGINE TICK = 0x39
[12:33:02] SNAPSHOT_BUF engine_state=qK7H9w
[12:33:03] WARMUP_OK
[12:33:04] SNAPSHOT_BUF engine_state=I4Lm2v
…
Most lines are noise. The ones that matter all carry engine_state= as a field, and the values look like short base64 chunks. They appear split across multiple lines — naive scrapers that take one line at a time get fragments that don’t decode.
Concatenate every engine_state= value in log order, base64-decode, then:
- Rotate each byte right by 3 bits (the log was generated by left-rotating each byte).
- Reverse the byte order (it was stored little-endian in chunks but represents a big-endian integer).
Result: a 512-bit integer — call it A.
Step 3 — Fragment B: crash_dump.json
The JSON has four arrays. Three are pure noise with very crypto-sounding names:
montgomery_ladder_statebarrel_shift_traceaccumulator_snapshot
If you’re looking for crypto data, you go straight to those names. That’s the trap.
The real fragment lives in the fourth array, named cycle_residues — the most benign-sounding of the four. Inside, the genuine data is only at indices i % 3 == 1. The other two-thirds is random padding to break length-based heuristics.
Extract indices 1, 4, 7, 10, …, pack the resulting 4-byte big-endian ints, then XOR each byte with SHA256("benchmark_cycle_trace")[i % 32]. The XOR-key label is hinted at by the suffix that appears repeatedly in array and key names throughout the dump.
Strip trailing nulls. You get another integer — call it B. It should equal A.
Step 4 — Fragment C: benchmark.stats
This file has two fields that look like internal hardware moduli:
pipeline_modulus = r (a 256-bit prime)
residue_token = dp mod r
This is the cross-validator. If a player has only Fragment A or only Fragment B and isn’t sure which decoding is right, this file lets them check:
candidate_dp mod pipeline_modulus == residue_token ?
If yes, the candidate is correct.
Step 5 — The attack: GCD recovery of p
With dp = d mod (p-1) known, the classic recovery is:
e * dp ≡ 1 (mod p-1)
⇒ 2^(e*dp) ≡ 2 (mod p)
⇒ gcd(2^(e*dp) - 2, n) = p
Code:
from math import gcd
p = gcd(pow(2, e * dp, n) - 2, n)
q = n // p
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
flag = pow(ct, d, n).to_bytes(128, "big").lstrip(b"\x00").decode()
print(flag)
Output:
DEDSEC{R3S1DU4L_ENG1N3}
The traps, in order
I built six layers of anti-automation into this challenge. Each one is calibrated for a specific kind of automated solver:
| Trap | Where | What it does |
|---|---|---|
montgomery_ladder_state |
crash_dump.json |
Sounds like Montgomery ladder; pure random |
barrel_shift_trace |
crash_dump.json |
Sounds like a transformation log; pure random |
reg_state_alpha/beta/gamma |
challenge.txt |
Three 1024-bit decoys |
memory_fragment.bin “key” marker |
binary file | A 256-bit value at offset 0xDEADBEEF. gcd(value, n) = 1 — naive GCD solvers fail silently |
engine_state= split across lines |
accelerator.log |
Single-line scrapers get fragments that don’t decode |
cycle_residues array name |
crash_dump.json |
Sounds like a counter, not data — players ignore it |
The big one is the memory_fragment.bin trap. Many automated solvers extract “the first large integer they find” and try the GCD attack. gcd(fake_key, n) = 1, so they don’t crash — they silently produce garbage and move on. By the time the player realises, they’ve wasted hours.
Why this works as a CTF problem
Three things make Echoes of Silicon hard in a way that’s hard to shortcut:
- The vocabulary is intentionally wrong. No part of the challenge files mentions RSA, primes, CRT, encryption, or keys. Players have to mentally rename
pipeline_modulustoprimebefore they can attack the structure. Automated solvers that match on crypto vocabulary find nothing. - The leaked secret is split three ways with three encodings. Base64 + bit-rotation + endian reversal for A. Array-stride hiding + SHA-XOR for B. Pure modular residue for C. There’s no single decoder that gets all of it.
- The attack itself is classical and short. Once you have
dp, the GCD attack is 4 lines of Python. The whole challenge is “can you recognise that this is the situation you’re in?”
That last point matters because the “real” cryptography is dead easy. The crypto is short. The cryptography is the hidden problem.
— Murugan