Урок 3. Python как рабочий инструмент криптоанализа
К этому моменту вы уже несколько раз написали короткие скрипты и заметили, что все они устроены одинаково: прочитать вход, что-то с ним сделать, напечатать результат. Это не случайность. В олимпиадной криптографии решатель почти всегда — это маленький одноразовый скрипт на 10–20 строк, а не «программа». В этом уроке мы сделаем работу с таким кодом осознанной и быстрой, чтобы инструмент не мешал думать над задачей.
Сразу важная оговорка про стиль. В учебниках код часто пишут «красиво»: с функциями, аннотациями типов, аккуратной архитектурой. Для олимпиады это лишнее. Вы сидите перед задачей с таймером, вам нужно быстро проверить гипотезу — поэтому нормальный боевой код выглядит грязновато: пара переменных, цикл, print. И это правильно. Не тратьте время на оформление; тратьте на идею.
Скелет решателя
Почти любой ваш скрипт в этом курсе — это три части:
# 1. прочитать вход
s = open("task.txt").read().strip()
# 2. преобразовать
# ... тут вся работа ...
# 3. напечатать результат
print(...)
Разберём каждую часть по отдельности — там есть пара приёмов, которые экономят много времени.
Чтение входа
Данные приходят тремя способами, и для каждого — своя строчка.
Из файла как текст (hex, base64, числа — всё, что записано печатаемыми символами):
s = open("task.txt").read().strip()
.strip() здесь обязателен: он убирает перевод строки в конце файла, из-за которого потом падает fromhex или b64decode.
Из файла как байты (когда файл бинарный — дамп, .bin, картинка):
data = open("task.bin", "rb").read()
Разница принципиальная: без "rb" Python попробует прочитать файл как текст в UTF-8 и либо сломается, либо тихо испортит байты. Если сомневаетесь, бинарный файл перед вами или текстовый — открывайте в "rb", это безопаснее.
Прямо в коде (когда данные короткие и даны в условии):
s = "dnNvc2h7ZmxhZ30="
Копировать короткую строку прямо в скрипт — совершенно нормально. Не надо ради одной строки создавать файл.
Работа с байтами
Главные операции вы уже знаете: bytes.fromhex, base64.b64decode, .decode(). Добавим ещё одну очень частую ситуацию — байты, записанные числами.
Иногда данные лежат не в hex и не в base64, а как список десятичных чисел через пробел или запятую:
118 115 111 115 104 123
Каждое число — это значение одного байта. Чтобы собрать из них байты, сначала разбиваем строку на числа, потом превращаем в bytes:
s = open("task.txt").read().strip()
nums = s.split() # ['118', '115', '111', ...]
b = bytes(int(x) for x in nums) # b'vsos...'
print(b.decode())
Конструкция bytes(int(x) for x in nums) — это ваш новый рабочий инструмент. bytes(...) умеет собирать байты из последовательности чисел 0–255. Запомните её: ровно этот приём понадобится в уроке 6, где числа окажутся спрятаны под слоями кодировок.
Если разделителем могут быть и пробелы, и запятые — просто замените запятые на пробелы перед разбиением:
s = s.replace(",", " ")
nums = s.split()
Быстрые циклы над байтами
Когда дойдём до XOR (следующий урок), вам постоянно нужно будет пройтись по всем байтам и что-то с каждым сделать. Два удобных способа:
# способ 1: списковое включение (компактно)
out = bytes(x ^ 55 for x in b)
# способ 2: обычный цикл (нагляднее, когда логика сложнее)
out = []
for x in b:
out.append(x ^ 55)
out = bytes(out)
Оба правильные. Первый короче, второй понятнее при отладке. На олимпиаде пользуйтесь тем, который в данный момент яснее лично вам — читаемость важнее «элегантности».
Печатайте промежуточные результаты
Это, возможно, самый важный совет всего урока, поэтому вынесем его отдельно. Печатайте промежуток после каждого шага. Не пытайтесь написать всю цепочку преобразований сразу и надеяться, что заработает. Пишите по одному шагу и смотрите, что получилось:
s = open("task.txt").read().strip()
b = base64.b64decode(s)
print("после b64:", b[:40]) # что теперь? снова кодировка? числа? текст?
Запустили, посмотрели, поняли, что делать дальше — дописали следующую строку, снова запустили. Так вы всегда видите, на каком слое находитесь, и никогда не «решаете вслепую». Скорость решения на олимпиаде определяется не тем, как быстро вы печатаете, а тем, как быстро вы понимаете, что перед вами — а понимание даёт именно печать промежутка.
Маленькая деталь: печатайте b[:40] (срез), а не весь b. Если данные длинные, полный вывод забьёт экран, и вы не увидите начало — а начало как раз важнее всего (там vsosh{).
Практика урока
- J2 «числа в текст» (тренажёр кода). Автопроверка: по строке чисел (через пробелы или запятые) вернуть текст. Закрепляет
bytes(int(x) for x in ...). - C4 «Числа в строке». В файле — флаг, записанный как список десятичных чисел. Соберите байты и прочитайте флаг. Это прямая репетиция того, что в уроке 6 будет спрятано под кодировками.
Типичные ошибки
- Открывают бинарный файл без
"rb"и получают испорченные данные или падение. - Забывают
.strip()и ловят ошибку на лишнем переводе строки. - Пишут всю цепочку сразу, без печати промежутка, и потом не понимают, на каком шаге всё пошло не так.
- Печатают весь длинный результат вместо среза и не видят начало с
vsosh{. - Путаются в
bytes(...):bytes([118, 115])даётb'vs', аbytes("118")— совсем не то (попытка создать байты из строки). Собирайте из чисел:bytes(int(x) for x in nums).
Что дальше
У вас есть инструмент и есть данные в виде байтов. Теперь введём операцию, вокруг которой строится огромная доля задач начального и среднего уровня, — XOR. В уроке 4 вы поймёте, почему она обратима, научитесь ломать однобайтовый ключ перебором и увидите, где перебор по печатаемости обманывает, а зацепка vsosh{ — нет.
Числа → текст
Реализуй ints_to_text(s): строка чисел 0–255 (разделители — пробелы или запятые) → текст.
Числа в строке
В файле — список десятичных чисел. Собери байты и прочитай флаг.