BroncoCTF : Atomic Substitution Theory Writeup
DEV Community

BroncoCTF : Atomic Substitution Theory Writeup

Challenge

We're given secret.txt, a single line of comma-separated tuples wrapped in what looks like braces and underscores:

(4, 17), (2, 16), (2, 15), (4, 9), { , (3, 2, 1), (5, 3), _ , (2, 17), ...
$ file secret.txt
secret.txt: ASCII text, with very long lines (479), with no line terminators

No further context is given - the challenge is to figure out the encoding scheme from the data alone.

Recon

The tuples are almost all 2- or 3-element groups of small integers, plus three literal tokens scattered through the stream: {, }, and _. Small integer pairs, a {/} wrapper, and an obvious "flag format" hint (bronco{...}) strongly suggested a coordinate lookup cipher - each tuple indexes into some 2D reference grid, and the underscore/braces are literal characters meant to survive the decode untouched.

The first four tuples are the key to spotting the scheme:

(4, 17), (2, 16), (2, 15), (4, 9)

Reading these as (period, group) coordinates on the periodic table:

Tuple Period Group Element Symbol
(4, 17) 4 17 Bromine Br
(2, 16) 2 16 Oxygen O
(2, 15) 2 15 Nitrogen N
(4, 9) 4 9 Cobalt Co

Concatenating the symbols: Br + O + N + Co = BrONCo โ†’ "Bronco" - matching the expected flag prefix exactly. Scheme confirmed.

Refining the scheme

Most tuples are 2-element (period, group) pairs and decode to a full element symbol (1 or 2 letters). But a number of tuples have a third element, e.g. (4, 17, 2). Decoding these as full symbols produced garbled output, so the third number was tested as a letter index into the 2-letter symbol:

  • (4, 17, 2) โ†’ Br, take letter #2 โ†’ "r"
  • (2, 1, 2) โ†’ Li, take letter #2 โ†’ "i"
  • (3, 13, 1) โ†’ Al, take letter #1 โ†’ "A"

This let single letters be pulled out of two-letter element symbols - necessary because plain English text needs individual letters, not 2-character blocks, at most positions.

The literal tokens map directly:

  • _ โ†’ space
  • { / } โ†’ literal brace (flag wrapper)

Decoding

Running the full tuple stream through this scheme (see decode.py) produces:

BrONCo{MY FAVOriTe MeSSAGeS HAVe AT eleMeNT OF S[?9,6?]PriSe}

Nearly the entire message resolves cleanly into a coherent, on-theme sentence - fitting, since the challenge itself hinges on the "surprise" of finding a message hidden in periodic table coordinates:

MY FAVORITE MESSAGES HAVE AN ELEMENT OF SURPRISE

Two positions didn't resolve automatically:

  • (9, 6) - there is no period 9 on the periodic table (periods only go 1โ€“7), so this tuple has no valid lookup. Context makes the intended letter obvious: the word is S_PRISE, which can only sensibly be SURPRISE, so the missing letter is U.
  • The word decoded as AT reads awkwardly; grammatically the sentence wants AN ("have an element of surprise"). This points to a likely transcription slip in the source tuple for that word (a digit that should differ from what was copied), rather than an error in the decoding scheme itself - every other tuple in the file resolves without issue.

Both anomalies are localized to specific known tuples and don't affect confidence in the rest of the decode, which is fully self-consistent across ~50 other tuples.

Final Flag

bronco{MY_FAVORITE_MESSAGES_HAVE_AN_ELEMENT_OF_SURPRISE}

(Spaces converted to underscores to match standard flag formatting conventions.)

Tools

decode.py - standalone Python decoder implementing the scheme above.

Usage: python3 decode.py secret.txt

Full source:

#!/usr/bin/env python3
"""
Periodic Table Cipher Decoder
==============================

Encoding scheme discovered from secret.txt:
- Each token is either:
  * A literal character: { } _
  * A tuple (period, group) -> full element symbol (1 or 2 letters)
  * A tuple (period, group, index) -> a single letter from that symbol,
    where index is 1-based (1st or 2nd letter)
- "_" represents a space / underscore separator in the final message
- "{" and "}" are literal brace characters (used for the flag wrapper)

Example:
  (4,17) (2,16) (2,15) (4,9) -> Br O N Co -> "BrONCo" -> "Bronco"

Usage: python3 decode.py secret.txt
"""

import re
import sys

# Standard periodic table symbols by (period, group), IUPAC 1-18 group numbering.
# Only positions that are actually populated on a real periodic table are included.
PERIODIC_TABLE = {
    (1, 1): 'H', (1, 18): 'He',
    (2, 1): 'Li', (2, 2): 'Be', (2, 13): 'B', (2, 14): 'C', (2, 15): 'N',
    (2, 16): 'O', (2, 17): 'F', (2, 18): 'Ne',
    (3, 1): 'Na', (3, 2): 'Mg', (3, 13): 'Al', (3, 14): 'Si', (3, 15): 'P',
    (3, 16): 'S', (3, 17): 'Cl', (3, 18): 'Ar',
    (4, 1): 'K', (4, 2): 'Ca', (4, 3): 'Sc', (4, 4): 'Ti', (4, 5): 'V',
    (4, 6): 'Cr', (4, 7): 'Mn', (4, 8): 'Fe', (4, 9): 'Co', (4, 10): 'Ni',
    (4, 11): 'Cu', (4, 12): 'Zn', (4, 13): 'Ga', (4, 14): 'Ge', (4, 15): 'As',
    (4, 16): 'Se', (4, 17): 'Br', (4, 18): 'Kr',
    (5, 1): 'Rb', (5, 2): 'Sr', (5, 3): 'Y', (5, 4): 'Zr', (5, 5): 'Nb',
    (5, 6): 'Mo', (5, 7): 'Tc', (5, 8): 'Ru', (5, 9): 'Rh', (5, 10): 'Pd',
    (5, 11): 'Ag', (5, 12): 'Cd', (5, 13): 'In', (5, 14): 'Sn', (5, 15): 'Sb',
    (5, 16): 'Te', (5, 17): 'I', (5, 18): 'Xe',
    (6, 1): 'Cs', (6, 2): 'Ba', (6, 3): 'La', (6, 4): 'Hf', (6, 5): 'Ta',
    (6, 6): 'W', (6, 7): 'Re', (6, 8): 'Os', (6, 9): 'Ir', (6, 10): 'Pt',
    (6, 11): 'Au', (6, 12): 'Hg', (6, 13): 'Tl', (6, 14): 'Pb', (6, 15): 'Bi',
    (6, 16): 'Po', (6, 17): 'At', (6, 18): 'Rn',
    (7, 1): 'Fr', (7, 2): 'Ra', (7, 3): 'Ac', (7, 4): 'Rf', (7, 5): 'Db',
    (7, 6): 'Sg', (7, 7): 'Bh', (7, 8): 'Hs', (7, 9): 'Mt', (7, 10): 'Ds',
    (7, 11): 'Rg', (7, 12): 'Cn', (7, 13): 'Nh', (7, 14): 'Fl', (7, 15): 'Mc',
    (7, 16): 'Lv', (7, 17): 'Ts', (7, 18): 'Og',
}

# Matches: {, }, _, or a tuple like (4, 17) / (4, 17, 2)
TOKEN_RE = re.compile(r'\{|\}|_|\(\s*\d+\s*,\s*\d+\s*(?:,\s*\d+\s*)?\)')
TUPLE_RE = re.compile(r'\(\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d+)\s*)?\)')


def decode(text: str) -> str:
    output = []
    for match in TOKEN_RE.finditer(text):
        token = match.group(0)
        if token in ('{', '}'):
            output.append(token)
            continue
        if token == '_':
            output.append(' ')
            continue
        tm = TUPLE_RE.match(token)
        period, group, index = tm.groups()
        period, group = int(period), int(group)
        symbol = PERIODIC_TABLE.get((period, group))
        if symbol is None:
            output.append(f'[?{period},{group}?]')
            continue
        if index is None:
            output.append(symbol)  # full symbol
        else:
            idx = int(index)
            if 1 <= idx <= len(symbol):
                output.append(symbol[idx - 1])  # single letter
            else:
                output.append(f'[?{symbol}#{idx}?]')
    return ''.join(output)


def main():
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <secret.txt>")
        sys.exit(1)
    with open(sys.argv[1], 'r') as f:
        raw = f.read()
    result = decode(raw)
    print("Decoded message:")
    print(result)


if __name__ == '__main__':
    main()

Key Takeaways

  • When a cipher's tuples don't line up with an obvious 1:1 substitution, try mapping them onto a real-world reference table (periodic table, keyboard layout, ASCII table, book/page/line, etc.) rather than assuming a purely mathematical transform.
  • Confirming a scheme against a small, checkable prefix (here, the bronco{ flag wrapper) before decoding the whole message saves a lot of wasted effort chasing the wrong theory.
  • When a decode is 95% clean and self-consistent, isolated garbage characters are usually a transcription/OCR artifact in the source data rather than a flaw in the scheme - worth verifying against the raw file before assuming the cipher logic itself is wrong.

Comments

No comments yet. Start the discussion.