← Back to Writeups
CUSTOMEasyCrypto

Vinegar

XESXOR9/10/20263 min read
#crypto#custom#easy#chi_squared_test#index_of_coincidence#semantic_clue_from_title#vigenere_cryptanalysis

Vinegar

Platform: spbctf | Category: Crypto | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: chi_squared_test, index_of_coincidence, semantic_clue_from_title, vigenere_cryptanalysis

Summary

Task: a long Russian Cyrillic ciphertext with no spaces, titled 'Vinegar'. Solution: recognize the title as a pun on Vigenere, find key length 12 via Index of Coincidence, recover the key 'криптография' by chi-squared frequency analysis over the 33-letter Russian alphabet, and read the flag phrase embedded at the end of the plaintext.

Vinegar — spbctf

Description

Это вообще можно прочитать?!!7!?7!171?!?

(Translation: "Can this even be read?!")

Given a vinegar.txt file containing a 3596-character Russian Cyrillic blob with no spaces or punctuation, using all 33 letters of the Russian alphabet (including ё). Goal: recover the plaintext and extract the flag.

Analysis

The key insight is the task title: "Vinegar" is a pun on "Vigenère" (vinegar ≈ Vigenère). This immediately identifies the cipher as a Vigenère cipher operating over the 33-letter Russian alphabet: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

абвгдеёжзийклмнопрстуфхцчшщъыьэюя   (ё placed after е, index 6)

Because the ciphertext preserves only letters (no spacing/case), classical cryptanalysis applies directly:

  • Key length is found with the Index of Coincidence (IC): splitting the text into klen columns and averaging per-column IC, the value peaks when klen is a multiple of the true key length. Here IC peaked at 6/12/18/24, strongest at 12 (IC ≈ 0.0553), so the key length is 12.
  • Key bytes are recovered per column with a chi-squared goodness-of-fit test against expected Russian letter frequencies: for each column try all 33 shifts and keep the one minimizing chi-square. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The recovered key is криптография (Russian for "cryptography"), and the plaintext is a Russian article about Attack-Defense CTF mechanics. The flag phrase is embedded at the very end, immediately after the word флаг.

Solution

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

d = open('vinegar.txt', encoding='utf-8').read().strip()
ALPH = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'  # 33 letters, ё after е
n = len(ALPH)
idx = {c: i for i, c in enumerate(ALPH)}

def ic(s):
    c = Counter(s); N = len(s)
    return sum(v * (v - 1) for v in c.values()) / (N * (N - 1)) if N > 1 else 0
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# 1) Key-length detection: average per-column IC peaks at multiples of 12
for klen in range(1, 25):
    cols = [''] * klen
    for i, ch in enumerate(d):
        cols[i % klen] += ch
    print(klen, round(sum(ic(c) for c in cols) / klen, 4))

# 2) Russian letter frequencies for chi-squared key recovery
freq = {'о':0.1097,'е':0.0845,'а':0.0801,'и':0.0735,'н':0.0670,'т':0.0626,
'с':0.0547,'р':0.0473,'в':0.0454,'л':0.0440,'к':0.0349,'м':0.0321,'д':0.0298,
'п':0.0281,'у':0.0262,'я':0.0201,'ы':0.0190,'ь':0.0174,'г':0.0170,'з':0.0165,
'б':0.0159,'ч':0.0144,'й':0.0121,'х':0.0097,'ж':0.0094,'ш':0.0073,'ю':0.0064,
'ц':0.0048,'щ':0.0036,'э':0.0032,'ф':0.0026,'ъ':0.0004,'ё':0.0004}
F = [freq.get(c, 0.0001) for c in ALPH]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def best_shift(col):
    best = None; bs = 0
    for s in range(n):
        cnt = Counter(ALPH[(idx[ch] - s) % n] for ch in col)
        N = len(col)
        chi = sum((cnt[c] - F[i] * N) ** 2 / (F[i] * N) for i, c in enumerate(ALPH))
        if best is None or chi < best:
            best = chi; bs = s
    return bs

# 3) Recover key and decrypt
klen = 12
cols = [''] * klen
for i, ch in enumerate(d):
    cols[i % klen] += ch
key = ''.join(ALPH[best_shift(c)] for c in cols)              # -> 'криптография'
plain = ''.join(ALPH[(idx[ch] - idx[key[i % klen]]) % n]
                for i, ch in enumerate(d))
print('key   =', key)
print('tail  =', plain[-60:])
# ...продумывать тактику в соответствии с ним флаг спбцтф проводит лучшие тренировки

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The plaintext ends with ...ним флаг спбцтф проводит лучшие тренировки. The flag is the phrase following флаг, concatenated without spaces: REDACTED ("spbctf conducts the best trainings"). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍