From baeb771f4a1298b64d00ee696af64c8fdbd72afe Mon Sep 17 00:00:00 2001 From: eastmadc Date: Tue, 4 Aug 2026 20:49:38 -0600 Subject: [PATCH] feat: add jpeg_comment_steg, policy_derived_credential and decoy_artifact_set atoms Three patterns from a hardware CTF run at DEF CON 34, written to the existing atom format and verified to load through goe.construction_crew.atoms. jpeg_comment_steg hides a route in a JPEG COM segment as base64. Its testing guidance says explicitly that a plaintext search of the image finds nothing on a healthy artifact, because a procedure that greps the raw bytes will otherwise report a false negative. policy_derived_credential publishes a construction rule rather than a password, so the credential exists nowhere as a literal and has to be synthesised from the rule plus a discoverable account. decoy_artifact_set places several convincing artifacts where one is genuine, so the step is discrimination rather than access. Its testing guidance requires asserting that each decoy does NOT satisfy the edge -- the check that decides whether the set works, and the one that fails silently. --- atoms/decoy_artifact_set.md | 43 +++++++++++++++++++++++++ atoms/jpeg_comment_steg.md | 50 ++++++++++++++++++++++++++++++ atoms/policy_derived_credential.md | 43 +++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 atoms/decoy_artifact_set.md create mode 100644 atoms/jpeg_comment_steg.md create mode 100644 atoms/policy_derived_credential.md diff --git a/atoms/decoy_artifact_set.md b/atoms/decoy_artifact_set.md new file mode 100644 index 0000000..5d07180 --- /dev/null +++ b/atoms/decoy_artifact_set.md @@ -0,0 +1,43 @@ +--- +id: decoy_artifact_set +description: Places several near-identical artifacts where only one is genuine and the rest are convincing but inert, so the attacker's task is discrimination rather than access. +required_vars: [artifact_kind, real_index, decoy_count] +--- +# Atom: Decoy Artifact Set +Access alone does not solve this step. The attacker reaches a set of similar artifacts and must work out which one matters, using evidence that lives elsewhere in the scenario. + +### Logic Requirements: + +1. Produce `decoy_count + 1` artifacts of `artifact_kind`; the one at `real_index` is genuine. +2. All of them MUST be reachable by the same method. A decoy the attacker never sees is not a decoy. +3. Decoys MUST be shaped like the answer — well-formed values of the same kind. A decoy that is obviously wrong teaches nothing. +4. Something in the scenario MUST identify which is genuine: a timestamp, a name, a log line, a document. Discrimination MUST be inference, never guessing. +5. Decoys MUST satisfy no edge. A downstream step fed a decoy value MUST fail closed. + +### Common Patterns: + +- Five recorded arrivals at an entry point; only one matches the person named in a separate report. +- Four backup archives; only the one predating the incident still holds the pre-rotation credential. +- Three similar service accounts; only one appears in the access log. +- Several API keys in a leaked bundle; only one is un-revoked, and the revocation list is elsewhere. + +### Testing Guidance: + +**Layer 1 (Internal):** +- Confirm all `decoy_count + 1` artifacts exist and are well-formed +- Confirm the discriminating evidence exists and is reachable on its own + +**Layer 2 (External):** +- Assert every artifact is reachable — a decoy that 404s has silently reduced the difficulty and nothing else reports it +- Assert the genuine artifact satisfies its outgoing edge +- Assert every decoy does NOT: attempt the downstream step with each decoy value and require failure + +The last check is the one that matters and the only one that can fail silently in the direction that hurts. A decoy that happens to work turns an inference step into a guessing game with several winning answers, and a suite that only asks whether the intended path works cannot see it. + +### Synthesis Guidance: + +Difficulty here moves in two independent directions. `decoy_count` raises the cost of a brute-force sweep; the subtlety of the discriminator raises the cost of reasoning. Raising only the first produces tedium, raising only the second produces a step that is either instant or impossible. Move both. + +Where possible put the discriminating evidence on a different system from the artifacts, so the step is a correlation across the graph rather than a careful read of one page. + +Composes with `jpeg_comment_steg` (the note names the index, the index lists the set) and with `policy_derived_credential` (retired accounts in a staff export are this atom applied to identities). diff --git a/atoms/jpeg_comment_steg.md b/atoms/jpeg_comment_steg.md new file mode 100644 index 0000000..f318be7 --- /dev/null +++ b/atoms/jpeg_comment_steg.md @@ -0,0 +1,50 @@ +--- +id: jpeg_comment_steg +description: Hides an operational note (an undocumented route, a hostname, an internal procedure) in a JPEG COM segment as base64, so it is invisible in the rendered image and invisible to a plaintext string search, but trivially recoverable from the file's metadata. +required_vars: [image_path, note_text] +--- +# Atom: JPEG Comment Steganography +An image served by the application carries a note in its JPEG comment segment. The image renders normally and the note never appears on screen. It is recovered by looking at the file rather than the picture, which is a step most attackers reach only after exhausting the obvious surfaces. + +### Logic Requirements: + +1. The note MUST live in a JPEG COM segment (`0xFFFE`), not in EXIF and not in the pixels. +2. The note MUST be base64-encoded, so a plaintext search of the file does not find it. +3. The image MUST still decode and render unchanged. + +Preferred, one line: + +```bash +exiftool -overwrite_original -Comment="$(printf '%s' "$NOTE" | base64 -w0)" "$IMAGE" +``` + +Without exiftool, insert a `0xFFFE` marker after `SOI` with a two-byte big-endian length that includes the length field itself. + +The note MUST read as internal engineering documentation — a service note, a migration reminder — not as a puzzle clue. + +### Common Patterns: + +- A device photo whose comment names the query parameters that expose a recording index. +- A build-server wallpaper carrying the internal artifact repository hostname. +- A scanned network diagram whose comment names the jump host. +- A vendor product image left carrying a support account name by the vendor's asset pipeline. + +### Testing Guidance: + +**Layer 1 (Internal):** +- Confirm the COM segment exists: `exiftool -Comment -b "$IMAGE"` +- Confirm the image still decodes and its dimensions are unchanged + +**Layer 2 (External):** +- Fetch the image over HTTP, then extract and decode: `exiftool -Comment -b img.jpg | base64 -d` +- Assert the decoded text contains the operationally significant token + +Assert on the DECODED value. A procedure that runs `strings` or `body_contains` against the raw image returns nothing on a perfectly healthy artifact, because the note is base64 — that is a false negative from the instrument, not evidence the atom is broken. + +### Synthesis Guidance: + +This atom provides knowledge, not access: afterwards the attacker can do nothing new mechanically, they simply know where to look. Place it upstream of an endpoint that is already reachable but not discoverable — an undocumented parameter, an unlinked path, a second API version. + +Do not pair it with an entity that also leaks the same route in an error message or `robots.txt`; the step collapses to nothing while every test still passes. If a second copy is wanted for realism, make it stale. + +Composes with `policy_derived_credential`: the comment can carry the rule while a separate artifact carries the inputs. diff --git a/atoms/policy_derived_credential.md b/atoms/policy_derived_credential.md new file mode 100644 index 0000000..fdb00c5 --- /dev/null +++ b/atoms/policy_derived_credential.md @@ -0,0 +1,43 @@ +--- +id: policy_derived_credential +description: Publishes a password-construction policy on an internal page so a valid credential can be derived from public rules plus a discoverable account name; the credential is never stored as a literal string anywhere on the system. +required_vars: [policy_path, rule_template, account_source_path, suffix] +--- +# Atom: Policy-Derived Credential +An internal standard documents how staff passwords are built. A separate artifact identifies which account is live. Neither is a secret on its own, and the password exists nowhere on disk — the attacker synthesises it by combining the two. + +### Logic Requirements: + +1. A policy page at `policy_path` MUST state the construction rule as a compliance control would — numbered standard, rationale, effective date. +2. An account source at `account_source_path` MUST make exactly one live account identifiable, with retired entries present so that choosing correctly is a real step. +3. A service MUST accept the derived credential. HTTP Basic auth is sufficient. +4. The password MUST be set by applying `rule_template` at build time. It MUST NOT appear as a literal in any file, environment variable, comment or history — `grep -r` across the built image must not find it. + +### Common Patterns: + +- IT security standard, plus a staff CSV export, plus a departmental portal on Basic auth. +- Onboarding wiki naming the initial-password formula, plus a new-starters list. +- A rotation runbook naming the seasonal pattern, plus a ticket naming who rotated last. +- A vendor integration guide specifying the service-account convention, plus an exposed config listing the integrations in use. + +### Testing Guidance: + +**Layer 1 (Internal):** +- Confirm the password does not appear as a literal anywhere in the image +- Confirm the account source distinguishes live from retired accounts + +**Layer 2 (External):** +- Fetch the policy page and assert it contains the rule +- Fetch the account source and assert it contains the live account +- Derive the credential in the procedure from those two fetched values, then authenticate +- Assert a wrong derivation is rejected — the retired account, or the rule without capitalisation — and that the rejection body carries nothing sensitive + +A procedure that hardcodes the password proves the service authenticates. It does not prove the credential is derivable, and derivability is the whole lesson. + +### Synthesis Guidance: + +This atom consumes two knowledge sources and produces a real `creds_for` edge, which makes it a natural chain hinge: everything before it is reading, everything after it is access. + +Its difficulty is entirely in noticing that the two documents relate. If they are linked from the same index, or the policy page names the account, the step collapses. Put them in different services or behind different earlier steps so that combining them is inference. + +Do not also leak the finished password through `exposed_env_vars`, `bash_history_leak` or a config file — that shortcut makes the chain vestigial while every test still passes. If a leaked copy is wanted for realism, give it a stale value that no longer authenticates.