Atomic Substitution Theory
Atomic Substitution Theory
Platform: broncoctf2026 | Category: Crypto | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-07-11 | Status: Solved Techniques: periodic_table_coordinate_decoding, element_symbol_substitution, letter_index_selection
Summary
Task: a chemist's secret message given as a list of (row, column[, k]) tuples. Solution: read each tuple as a periodic-table coordinate identifying an element symbol (third value picks one letter), concatenate, and lowercase to get the flag.
Atomic Substitution Theory — broncoctf2026
Description
This text file is what happens when a chemist tries to send you a top secret message.
Hint: all letters in the flag should be lowercase.
We are given secret.txt, a single line of tuples plus the literal characters {, }, and _:
(4, 17), (2, 16), (2, 15), (4, 9), { , (3, 2, 1), (5, 3), _ , (2, 17), (3, 13, 1), (4, 5), (2, 16), (4, 17, 2), (2, 1, 2), (4, 4, 1), (2, 2, 2), _ , ... , (3, 16), (9, 6), (3, 15), (4, 17, 2), (2, 1, 2), (3, 16), (2, 2, 2), }
Goal: decode the message into the bronco{...} flag.
Analysis
The title "Atomic Substitution Theory" and the word "chemist" point directly at the periodic table. "Substitution" means we replace each token with a letter.
Each tuple is a coordinate on the periodic table:
(row, col)→ the element sitting at that period/group → take its full symbol.(row, col, k)→ the k-th letter (1-indexed) of that element's symbol.- Literal
{,},_pass through unchanged;_acts as a word separator (space).
The prefix confirms the scheme immediately:
| Token | Element | Letters |
|---|---|---|
(4,17) | Br | Br |
(2,16) | O | O |
(2,15) | N | N |
(4,9) | Co | Co |
→ BrONCo — the CTF's brand ("Bronco"). Then {, (3,2,1)=Mg→M, (5,3)=Y → {MY.
Three-element tuples slice a single letter of a two-letter symbol, e.g. (4,17,2)=Br→r,
(2,1,2)=Li→i. The detached actinide row is treated as period 9, so (9,6) is Uranium.
Reading everything out and lowercasing yields:
bronco{REDACTED...REDACTED}
Pitfall (lesson learned)
A naive letter-for-letter read produces two plausible-but-wrong words:
- The word decoded from
(3,13,1)(4,4,1)=Al→A +Ti→T = "at", but the intended English (matching the pun "an element of surprise") is "an". - The final word only spells "surprise" if
(9,6)(Uranium) is read as its two-letter form "Ur":S+Ur+P+r+i+S+e=surprise. Reading Uranium as a singleUgives the misspelled "suprise".
So don't trust the first plausible English guess — confirm against the pun's meaning: the message is literally "my favorite messages have an element of surprise."
Solution
Map every coordinate to an element symbol, apply the letter-index for 3-tuples, pass
through the literals, and lowercase. Fix the single ambiguous word at → an to match
the intended phrase.
python read-only • copy allowed#!/usr/bin/env python3 import re # (period, group) -> element symbol, covering every coordinate in secret.txt. PT = { (1, 1): "H", (2, 1): "Li", (2, 2): "Be", (2, 15): "N", (2, 16): "O", (2, 17): "F", (3, 2): "Mg", (3, 13): "Al", (3, 15): "P", (3, 16): "S", (3, 17): "Cl", (4, 4): "Ti", (4, 5): "V", (4, 9): "Co", (4, 13): "Ga", (4, 17): "Br", (5, 3): "Y", (9, 6): "Ur", # Uranium (actinide row), two-letter form so we get "surprise" } def decode(path="secret.txt"): tokens = re.findall(r"\([^)]*\)|[{}_]", open(path).read()) out = [] for tok in tokens: if tok in "{}_": out.append(tok); continue n = [int(x) for x in re.findall(r"-?\d+", tok)] if len(n) == 2: out.append(PT[(n[0], n[1])]) # full symbol elif len(n) == 3: out.append(PT[(n[0], n[1])][n[2] - 1]) # k-th letter return "".join(out) # Naive read gives proper English except one ambiguous word ("at" -> "an"). FIXUPS = {"at": "an"} def to_flag(raw): low = raw.lower() head, _, rest = low.partition("{") content, _, tail = rest.rpartition("}") words = [FIXUPS.get(w, w) for w in content.split("_")] return f"{head}{{{'_'.join(words)}}}{tail}" if __name__ == "__main__": raw = decode() print("raw :", raw) # BrONCo{REDACTED..._AT_..._SUrPriSe} print("naive:", raw.lower()) # ...have_atREDACTED flag = to_flag(raw) print("flag :", flag) assert flag == "bronco{REDACTED}"
Running it:
raw : BrONCo{REDACTEDTREDACTED}
naive: bronco{REDACTEDtREDACTED}
flag : bronco{REDACTED}