dualflow
umdctf
Task: submit a float32 window close to a provided reference so it passes three RealNVP-based acceptance checks. Solution: use a white-box adversarial perturbation, changing one coordinate of the reference by +0.03 to push the flow_1 likelihood and margin far above the required thresholds while staying inside the L-infinity budget.
$ ls tags/ techniques/
dualflow — UMDCTF
Description
submit base64(numpy float32 array, shape (5, 64)):
The service loads a reference window and checks whether our submitted float32 array stays within EPS = 0.08 in L-infinity distance. It then evaluates the sample under two RealNVP flows and only prints the flag if the sample looks realistic to flow_1 and strongly prefers class 1 over class 0.
Analysis
challenge.py exposes the full decision rule:
- shape must be
(5, 64) - values are compared to
reference_window.npy linf <= 0.08margin = log q1 - log q0 >= 30log q1 >= 932.8031log_det1must lie in[1367.1451, 1595.0465]
The important point is that this is a white-box setting. We have the reference input, the model code, and both flow checkpoints. The reference sample itself is not close to the target class boundary in the right direction: its margin is about -1.90, so we must meaningfully increase log q1 - log q0 while keeping the perturbation tiny.
In practice, no numerical trick was needed. A simple coordinate search was enough: flatten the reference array and add +0.03 at index 205, which is element [3, 13] in the original (5, 64) layout.
That single change already satisfies every condition:
linf = 0.03margin = 105.70log_q1 = 986.23log_det1 = 1526.48
So the challenge is essentially a straightforward adversarial-example task against a flow-based discriminator.
Solution
Load the reference window, perturb one coordinate, save the result as a NumPy array, then base64-encode the .npy payload for submission.
#!/usr/bin/env python3 import base64 import io import numpy as np ref = np.load("reference_window.npy").astype(np.float32) sub = ref.copy().reshape(-1) sub[205] += np.float32(0.03) # same as ref[3, 13] += 0.03 sub = sub.reshape(5, 64).astype(np.float32) buf = io.BytesIO() np.save(buf, sub) print(base64.b64encode(buf.getvalue()).decode())
Submitting that array to the remote service returns the success line and the flag.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [ml][free]flow— umdctf
- [ml][Pro]leadgate— dicectf2026
- [reverse][free]roulette— umdctf
- [reverse][free]nuclear_codes— umdctf
- [crypto][Pro]Mental flow— duckerz