| name | authoring-gzcli-challenge-dist |
| description | Use this skill when staging files under a challenge's `dist/` directory in a gzcli event. Covers what goes in `dist/`, the gzcli auto-zip behavior (never pre-zip), and decoy-resistance for forensic artifacts (disk images, pcaps, archives) so trivial shortcuts don't yield the flag. |
Challenge dist/ Authoring
For event layout, use authoring-gzcli-ctf-events. For the service / build code that produces dist/, use authoring-gzcli-challenge-src.
What dist/ is
dist/ holds the files a player downloads when they click the challenge's attachment link in the gzctf UI. It is referenced from challenge.yml as provide: "./dist".
gzcli auto-zips — do NOT pre-zip
gzcli automatically zips the contents of dist/ at upload / sync time and serves the resulting archive. Therefore:
- Place raw files directly in
dist/: dist/usb.img, dist/binary, dist/forwarded.eml.
- Do not ship
dist/challenge.zip containing the artifacts — players would receive a double-nested zip.
- The exception is when the artifact itself is a zip with semantic meaning (an evidence ZIP that is part of the scenario, a deflate-compressed payload that is the puzzle). Those are fine; what is not fine is a generic "here are all the files in a zip" archive.
Empty dist/ for compute-only challenges
If the challenge is purely a network service with no downloadable attachment (type: DynamicContainer and provide: unset), dist/ should either not exist or contain only a .gitignore. Do not set provide: "./dist" when dist/ is empty — the upload validator rejects this.
Decoy-resistance for forensics
When dist/ ships disk images, pcaps, memory dumps, or any container that lets a player run trivial scans, the real flag must require the intended technique. Trivial shortcuts (strings, grep, binwalk -e with no further work, read-only mount) must return decoys, not the real flag.
Concrete techniques:
- Real flag in compressed / deleted region. For a FAT/ext disk image: put the real flag in a deflate-compressed ZIP, then delete the ZIP via
mdel / fs primitives so recovery requires fls -f <fs> -r + icat to carve. The compressed bytes do not appear in a flat strings scan.
- Plant decoys in the easy paths. Drop one or two
<PREFIX>{...} strings in the cleartext files (README.txt, grocery_list.txt, etc.) so strings disk.img | grep '<PREFIX>{' returns multiple candidates — none of them real. Decoy strings should be plausible flags, e.g. <PREFIX>{n0t_th3_r34l_fl4g_k33p_d1gg1ng}.
- Padding. Pad with random bytes (
dd if=/dev/urandom) so the deleted entry isn't the only block of non-random data on the disk.
- For pcaps, encode the flag with a non-trivial transform on-wire (base64 of XORed bytes, deflated body) so the printable
<PREFIX>{...} pattern doesn't appear directly in tshark -Y http -T fields -e http.file_data.
Self-check before shipping any forensic dist/ artifact:
strings dist/usb.img | grep -E '<PREFIX>\{[^}]+\}'
If the real flag appears, the challenge is solvable without the intended technique — fix src/build.sh to push the real flag into a non-trivial location and replant decoys.
dist/ hygiene
- No
.env, no plaintext flag.txt (unless the flag is the puzzle, e.g. ciphertext in params.txt), no .git/, no editor swap files (*~, *.swp), no .DS_Store, no __MACOSX.
- No event-prefix flag literals in shipped source. When
dist/ ships source code that contains a local-test flag fallback — e.g. FLAG = os.environ.get("GZCTF_FLAG") or "<PREFIX>{local_test_flag}", GZCTF_FLAG: "<PREFIX>{set-me-locally}" in a docker-compose, etc. — replace the literal with fake{flag} in the shipped copy. The running container reads the real flag from GZCTF_FLAG at runtime, so the fallback never executes in production; shipping the event prefix in it just creates a decoy that confuses player greps (grep -r 'LKS{' dist/ returns a useless hit). The src/ build-input copy may keep an event-prefixed fallback for the maintainer's local-test convenience — only dist/ needs sanitizing. Exceptions: (a) the flag literal is the puzzle artifact (e.g. welcome-flag where dist/flag.txt contains the real flag), and (b) intentional decoys planted per the forensics decoy-resistance rule above — those keep the event prefix on purpose.
- For reverse-engineering challenges: ship the stripped binary (
strip --strip-all); source goes in src/, not dist/.
- For crypto challenges that ship parameters, name files deterministically (
params.txt, output.txt) — players' solvers reference these names.
- For pwn challenges, ship the same binary the container actually runs (do not ship a debug build while running a stripped binary in the container).
Related sub-skills
authoring-gzcli-challenge-src — the build step that produces dist/ artifacts (e.g. src/build.sh, src/generate.py).
authoring-gzcli-challenge-solver — the solver must consume the exact files shipped in dist/.