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
- Read
encrypt.py. For each alphabetic positionithe output isc_i = subst(p_i) + vigkey[i mod L] (mod 26). The counterionly advances on letters, so spaces and punctuation pass through and word boundaries survive. - 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). - 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.
- 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}