← Back to Writeups
CUSTOMEasyCrypto

Atomic Substitution Theory

XESXOR9/10/20264 min read
#crypto#custom#easy#periodic_table_coordinate_deco#element_symbol_substitution#letter_index_selection

Atomic Substitution Theory

Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: periodic_table_coordinate_decoding, element_symbol_substitution, letter_index_selection

Summary

Walkthrough of the CUSTOM Crypto Atomic Substitution Theory. 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), co


title: "Atomic Substitution Theory" id: 20260711_broncoctf2026_atomic_substitution_theory category: crypto difficulty: easy event: broncoctf2026 tags: ["encoding", "periodic_table", "substitution", "coordinate_cipher", "chemistry"] techniques: ["periodic_table_coordinate_decoding", "element_symbol_substitution", "letter_index_selection"] tools: ["python3"] indicators: ["chemist / atomic / substitution theme", "tuples of (row, column[, k])", "prefix decodes to BrONCo (Br O N Co)", "third tuple element selects a single letter of a 2-char symbol"]

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:

TokenElementLetters
(4,17)BrBr
(2,16)OO
(2,15)NN
(4,9)CoCo

→ 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 single U gives 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
#!/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}

Key Takeaways / Lessons

  • Mastering periodic_table_coordinate_decoding, element_symbol_substitution, letter_index_selection was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-11 • CUSTOM • Atomic Substitution Theory • Difficulty: Easy