leaky-gradient
TJCTF 2026
Task: remote ML oracle accepts a 64-bit value and returns a class plus a partially masked noisy 64-element leak, initially presented as a gradient. Solution: show the leak is actually affine in the input, recover the bias and all basis columns, then threshold column norms to reconstruct the hidden 64-bit secret and hex-decode the flag body.
$ ls tags/ techniques/
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
leaky-gradient — TJCTF 2026
Description
Original organizer description was not preserved in the local task notes.
The service accepts 16 hex characters (64 input bits) and returns a class label together with a partially masked, quantized 64-element leak vector. The goal is to recover the hidden secret encoded by the model and turn it into the final tjctf{...} flag.
Analysis
At first, the leak looked like a gradient of the loss with respect to each input bit. That suggested optimization-style attacks, so I briefly tried gradient descent / simulated annealing on the binary input space, tried fitting a linear softmax classifier to the observations, and even checked whether the raw leak values mapped directly to ASCII. None of those approaches produced a stable recovery.
The real breakthrough was testing whether the leak behaved linearly rather than like a true gradient. Empirically,
leak(e0 + e1) ≈ leak(e0) + leak(e1) - leak(0)
held with RMS error about 0.7, which was consistent with the integer quantization noise already visible in repeated samples. So the exposed signal was effectively an affine map:
leak(x) = W x + b
where x is the 64-bit input vector, W is an internal matrix, and b = leak(0) is the bias.
Once this was clear, the recovery problem became simple:
- query the all-zero input to get
b = leak(0) - query every basis vector
e_i - recover each column with
W[:, i] = leak(e_i) - leak(0)
Because the leak was randomly masked, each logical query had to be repeated several times and merged to reconstruct the full 64-element vector. After recovering all 64 columns, I computed their L2 norms. Those norms separated cleanly into two clusters:
- low cluster around
45 - high cluster around
81
...
$ grep --similar
Similar writeups
- [crypto][free]no-brainrot-allowed— umdctf
- [crypto][free]bit-leak— tjctf
- [ml][Pro]ReLuess Your Inhibitions— kalmarcf
- [crypto][free]Squares— tjctf
- [crypto][free]weave— umdctf