View on GitHub

DEDSEC 2026 — CTF Writeups

Nine CTF challenges I designed for DEDSEC 2026 — forensics, mobile RE, crypto, stego, web. Writeups from the designer's POV.

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:

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:

  1. Rotate each byte right by 3 bits (the log was generated by left-rotating each byte).
  2. 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:

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:

  1. The vocabulary is intentionally wrong. No part of the challenge files mentions RSA, primes, CRT, encryption, or keys. Players have to mentally rename pipeline_modulus to prime before they can attack the structure. Automated solvers that match on crypto vocabulary find nothing.
  2. 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.
  3. 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