UMDCTF 2026 - dualflow
We trained two normalizing flow models on similar data distributions. Can you find an input that one model thinks is highly likely and the other thinks is not? Submit a perturbation around the reference window satisfying: L_inf <= 0.08, log q1(x) - log q0(x) >= 30, log q1(x) >= threshold, and log|det J_f1|(x) within a calibrated band.
Files provided: flow_0.pt,
flow_1.pt, reference_window.npy,
check_remote.py
Category: ML / Adversarial
前置知识
本题涉及归一化流(Normalizing Flows)与对抗性攻击(Adversarial ML)的交叉领域。以下概念有助于理解:
| 概念 | 说明 |
|---|---|
| 归一化流 | 通过可逆双射将高斯分布变形为复杂分布,支持精确对数似然计算 |
| RealNVP | 仿射耦合层堆叠,雅可比行列式简化为缩放输出之和 |
| 变量变换公式 | log q(x) = log π(f(x)) + log|det J_f| |
| 对抗扰动 | L∞ 约束限制每个像素最大变化量 |
| 梯度攻击(FGSM) | 沿损失梯度符号方向一步扰动 |
| 线搜索 | 在梯度方向上尝试多个步长,选择最优值 |
| Gram-Schmidt 正交化 | 将梯度方向正交化避免路径退化(flag 暗示) |
参考:RealNVP 论文 arXiv:1605.08803,C&W 攻击 arXiv:1608.04644。
Initial Analysis
We are given two pretrained normalizing flow models
(flow_0.pt and flow_1.pt) along with a
reference input window (reference_window.npy). Normalizing
flows are generative models that define a bijective mapping between a
simple base distribution (usually a standard Gaussian) and a complex
data distribution. They allow exact log-likelihood evaluation via the
change-of-variables formula.
The challenge asks us to find a perturbation \(x_{\text{sub}} = x_{\text{ref}} + \delta\) (with \(\| \delta \|_\infty \le 0.08\)) such that:
- Margin: \(\log q_1(x_{\text{sub}}) - \log q_0(x_{\text{sub}}) \ge 30\)
- Threshold: \(\log q_1(x_{\text{sub}}) \ge \tau\) (some fixed threshold)
- Jacobian constraint: \(\log |\det J_{f_1}|(x_{\text{sub}})\) lies within a calibrated band
- Bounded perturbation: \(\| \delta \|_\infty \le 0.08\)
Both flows are implemented in PyTorch and are fully differentiable.
The reference window is a 2D numpy array of shape
(1, 1, 64, 64) — a single-channel 64x64 image patch.
Understanding the Models
We loaded both .pt files using torch.load()
with weights_only=False and examined their
architectures:
1 | flow_0 = torch.load('flow_0.pt', map_location='cpu') |
Both are RealNVP-style normalizing flows composed of multiple affine coupling layers with alternating checkerboard masking patterns. Internally they use convolutional subnetworks with ActNorm layers for stable training. The architectures are nearly identical — both have the same number of coupling layers and similar parameter counts, but with different learned weights.
The key operations for a coupling layer with mask \(m\):
\[y_1 = x_1\] \[y_2 = x_2 \odot \exp(s(x_1)) + t(x_1)\]
where \(x_1 = x \odot m\), \(x_2 = x \odot (1-m)\), and \(s, t\) are neural networks.
The log-likelihood under the model is computed as:
\[\log q(x) = \log \pi(f(x)) + \sum_{k} \log |\det J_{f_k}|(x)\]
where \(f = f_L \circ \cdots \circ f_1\) and \(\pi\) is the base Gaussian density.
The Jacobian determinant for each affine coupling layer is simply:
\[\log |\det J_{f_k}| = \sum_i s(x_1)_i\]
making it trivial to compute the total log-det-Jacobian — this is just the sum across all coupling layers of the scale outputs.
The check script requires \(\log|\det J_{f_1}|\) to be within a specific band. This constraint prevents trivial solutions where the log-likelihood of model 1 is high purely because of extreme volume distortion.
Checking the Reference
We loaded the reference window and evaluated both models:
1 | x_ref shape: (1, 1, 64, 64) |
The reference is already in-distribution for both models (log-likelihoods around 925) and satisfies the Jacobian constraint. The only problem: the margin is -1.90 — we need it to be at least +30. So we need to find a tiny perturbation that changes the relative log-likelihood by about 32 nats while keeping everything else stable.
The Attack: Gradient-Based Optimization
Since both flows are differentiable, we can compute the gradient of the margin with respect to the input:
\[\nabla_\delta (\log q_1 - \log q_0)\]
The idea is simple: take a step in the direction that maximally increases \(\log q_1\) relative to \(\log q_0\). But there's a critical twist — the gradient magnitudes near the reference point are enormous.
1 | x = torch.tensor(x_ref, requires_grad=True, dtype=torch.float32) |
The gradient \(L_2\) norm was around 700,000. This means even a tiny step in gradient direction yields massive changes in the margin. This makes sense: near the reference, the two models have slightly different density landscapes, and because of the exponential nature of the flow mapping, small changes in input space can produce large changes in log-likelihood.
We normalize the gradient and perform a line search over step sizes:
\[\delta = \alpha \cdot \frac{\nabla_\delta (\log q_1 - \log q_0)}{\|\nabla_\delta (\log q_1 - \log q_0)\|_\infty}\]
clipping to ensure \(\|\delta\|_\infty \le 0.08\).
The step size needed was on the order of \(\alpha \approx 3.1775 \times 10^{-6}\) — extremely tiny. Any larger and \(\log q_1\) would collapse below the required threshold due to the sheer gradient magnitude.
Line Search Results
We scanned \(\alpha\) from \(3.0 \times 10^{-6}\) to \(3.6 \times 10^{-6}\):
1 | alpha=3.00e-06 margin=-1.46 log_q1=924.54 log_det1=1516.90 |
The Jacobian determinant stayed constant at 1516.90 throughout (within the required band) — the perturbation is too small to meaningfully change the volume distortion. The log-likelihood of model 0 actually decreases slightly as we move away from the reference, while model 1's log-likelihood increases, creating the desired margin.
The winning hyperparameter: \(\alpha = 3.40 \times 10^{-6}\) giving:
1 | Margin: 30.01 ✅ |
Submission
The final perturbation was saved, base64-encoded, and submitted via the check script:
1 | # Compute the perturbation |
The remote service confirmed the solution, returning the flag:
1 | UMDCTF{****************************************} |
Key Insight
The challenge name and flag hint at the core idea: Gram-Schmidt orthogonalization and the geometry of adversarial examples in the likelihood space of normalizing flows.
Two models trained on similar data define slightly different density landscapes. In a tiny neighborhood around a point in-distribution, their log-likelihood gradients can point in very different directions (they are not perfectly aligned in function space). By following the difference-of-gradients direction, we exploit the disagreement manifold — the region where model 1 assigns higher likelihood than model 0.
The extreme sensitivity (gradient norms ~700k) arises because:
- Normalizing flows chain many bijective transforms, each amplifying small input changes
- The Jacobian of the flow near the data manifold can have large singular values
- Even though the models are similar, their gradient directions differ enough that a tiny step (\(3.4 \times 10^{-6}\)) suffices to swing the margin by 32 nats
This is a pure white-box adversarial attack on the log-likelihood ratio, analogous to Fast Gradient Sign Method (FGSM) but in log-probability space with a directional objective.
Reflection
This challenge was a beautiful blend of generative modeling and adversarial machine learning. It rewarded understanding:
- How normalizing flows compute exact likelihoods
- That differentiability enables gradient-based input optimization
- That similar models can be teased apart by their gradient disagreement
- That the scale of gradients matters — enormous gradients require nanometer-scale steps
- That Jacobian constraints prevent trivial large-volume-distortion solutions
The "Gram-Schmidt" reference in the flag suggests the intended solution may have involved orthogonalizing the gradients of the two models, but simple directional gradient ascent on the margin works just as well when the step size is chosen carefully.