arrow_backUS Cyber Games 2026
Cryptomedium

Vigenère

by Pavel Knespl (capyplivl)

A two-layer classical cipher: a fixed monoalphabetic substitution of the plaintext, then a Vigenere of period 15 to 20 on top. Substitution does not change the index of coincidence and Vigenere only Caesar-shifts each column, so both layers peel off with standard frequency analysis.

Solve

  1. Read encrypt.py. For each alphabetic position i the output is c_i = subst(p_i) + vigkey[i mod L] (mod 26). The counter i only advances on letters, so spaces and punctuation pass through and word boundaries survive.
  2. Recover the period with index of coincidence. The per-column average peaks at L = 20 (about 0.0625, near English 0.066; every other length stays at or below 0.052).
  3. Collapse the Vigenere by aligning each column's letter distribution back to column 0 (mutual IC). Column 0 is left unshifted, so its residual shift is absorbed into the monoalphabetic key in the next step. The result is one monoalphabetic substitution of the plaintext with spaces intact.
  4. Read the collapsed monoalphabetic key straight off recognisable words. The plaintext embeds the pangram "the quick red fox jumped over the lazy brown dog" which fixes the otherwise ambiguous low-frequency letters. Applying the key recovers full English; the flag sits in the second sentence.

Full working solver (saved as solve.py next to ciphertext.txt):

#!/usr/bin/env python3
import re
from collections import Counter

ct = open("ciphertext.txt").read()
nums = [ord(c) - 97 for c in ct.lower() if c.isalpha()]


def ic(s):
    n = len(s)
    return sum(v * (v - 1) for v in Counter(s).values()) / (n * (n - 1)) if n > 1 else 0


L = max(range(1, 26), key=lambda k: sum(ic(nums[i::k]) for i in range(k)) / k)

cols = [nums[i::L] for i in range(L)]
dist = lambda s: [Counter(s).get(i, 0) / len(s) for i in range(26)]
d0 = dist(cols[0])
shifts = [0] * L
for r in range(1, L):
    shifts[r] = max(
        range(26),
        key=lambda s: sum(d0[i] * dist([(x - s) % 26 for x in cols[r]])[i] for i in range(26)),
    )

key = dict(zip("abcdefghijklmnopqrstuvwxyz", "khycpbumosrqetngfvaxidwlzj"))

out = []
ai = 0
for ch in ct:
    if ch.isalpha():
        plain = key[chr((ord(ch.lower()) - 97 - shifts[ai % L]) % 26 + 97)]
        out.append(plain.upper() if ch.isupper() else plain)
        ai += 1
    else:
        out.append(ch)
pt = "".join(out)

print(pt[:240])
m = re.search(r"SVIUSCG\{[^}]*\}", pt)
print("\nflag =", m.group(0) if m else "NOT FOUND")
$ python3 solve.py
So this is a long plaintext.  I'm making it very long in order to help you with the frequency analysis and such.  You'll want the flag of course which is SVIUSCG{those_who_dont_learn_history_alskdfjghmenwncirut}.  But you'll also need some

flag = SVIUSCG{those_who_dont_learn_history_alskdfjghmenwncirut}

Flag: SVIUSCG{those_who_dont_learn_history_alskdfjghmenwncirut}