Урок 2. Байты, строки и кодировки
Теперь мы переходим к тому, без чего криптографические задачи обычно даже не начинают решаться: к представлению данных. В прошлом уроке вы научились узнавать hex, base64 и base32 по внешнему виду. В этом уроке вы научитесь с ними работать — и в конце сдадите свой первый настоящий флаг.
Чтобы всё сложилось, нужно сначала аккуратно разобраться с одной вещью, которая путает почти всех новичков: разницей между строкой и байтами.
Строка и байты — это не одно и то же
В Python есть два разных типа для «текстоподобных» данных:
str— это текст, последовательность символов (букв, цифр, знаков). Например"vsosh".bytes— это сырые байты, последовательность чисел от 0 до 255. Напримерb"vsosh".
Выглядят они похоже, но это принципиально разные вещи. Криптография всегда работает с байтами, а не с текстом. Шифры сдвигают, ксорят и перемешивают байты — числа от 0 до 255, — а не «буквы». Текст — это лишь один из способов посмотреть на байты, причём далеко не всегда применимый (не любой набор байт складывается в осмысленный текст).
Запомните правило: как только речь заходит о преобразованиях данных — думайте про bytes, а не про str. Это избавит вас от половины будущих ошибок.
Перевести одно в другое просто:
s = "vsosh"
b = s.encode() # str -> bytes: b'vsosh'
back = b.decode() # bytes -> str: 'vsosh'
encode() превращает текст в байты (по умолчанию в кодировке UTF-8), decode() — обратно.
ASCII: связь байтов и символов
Откуда байты «знают», какой букве соответствовать? Из таблицы ASCII. Каждому печатаемому символу сопоставлено число:
- заглавные латинские буквы
A–Z— это65–90(в hex0x41–0x5a); - строчные
a–z— это97–122(0x61–0x7a); - цифры
0–9— это48–57(0x30–0x39); - пробел —
32(0x20); - печатаемые символы вообще занимают диапазон
0x20–0x7e.
Отсюда полезная интуиция: если вы расшифровали что-то и получили байты в районе 0x20–0x7e — скорее всего, это осмысленный текст. Если байты «скачут» по всему диапазону 0–255 и много значений вроде 0x00, 0xff, 0x8a — это ещё не текст, работа не закончена. Мы будем пользоваться этим наблюдением в уроке 4, когда начнём перебирать ключи.
Проверьте себя прямо в голове: буква v — это 0x76, значит vsosh в байтах это 76 73 6f 73 68. Узнаёте? Это же hex из прошлого урока.
Hex — человекочитаемая запись байтов
Байты неудобно печатать: многие из них невидимы или ломают терминал. Поэтому их часто записывают в hex — каждый байт превращается ровно в два шестнадцатеричных символа 0–9a–f. Байт 0x76 записывается как 76, байт 0x0a — как 0a. Отсюда и приметы из урока 1: только 0–9a–f, длина чётная.
В Python это две парные операции:
b = bytes.fromhex("76736f736875") # hex-строка -> bytes
print(b) # b'vsoshu'
print(b.hex()) # bytes -> hex-строка: '76736f736875'
bytes.fromhex терпит пробелы внутри, но давайте на всякий случай приучимся их убирать сами — задачи любят подсовывать hex с пробелами и переносами строк:
s = open("task.txt").read()
s = "".join(s.split()) # выкинуть все пробелы и переводы строк
b = bytes.fromhex(s)
print(b.decode())
Вот и весь первый решатель. Три строки: прочитали, почистили, раскодировали. Именно так выглядит боевой код на олимпиаде — коротко и по делу.
Ошибки декодирования — это сигнал, а не беда
Иногда b.decode() падает с ошибкой UnicodeDecodeError. Новички пугаются. А зря: это ценная информация. Ошибка декодирования означает ровно одно — эти байты не являются корректным UTF-8 текстом. То есть либо вы сняли не тот слой, либо перед вами ещё зашифрованные данные, а не готовый текст.
Практический приём: пока вы «нащупываете» задачу, не спешите с .decode(). Печатайте сами байты:
print(b) # напечатает b'...' и покажет непечатаемые байты как \x??
Если видите что-то вроде b'vsosh{...}' — прекрасно, декодируйте. Если видите b'\x8a\x11\x…' — работа не закончена, декодировать пока нечего.
Base64 и base32 — зачем и как
Hex удобен, но раздувает данные ровно вдвое (два символа на байт). Есть представления поэкономнее и «текстовее» — base64 и base32. Их придумали, чтобы передавать произвольные байты по каналам, рассчитанным только на текст (почта, URL, конфиги). Для нас важно другое: это тоже открытые кодировки, снимаются без ключа. Умение выцепить base64 из груды текста нужно и в форензике, например, в задаче «Бэкдор в текстур-кэше» · заключительный этап, 2025–2026 пароль был спрятан именно base64-строкой внутри дампа.
Их приметы вы уже знаете из урока 1. Теперь — код. Всё живёт в стандартном модуле base64:
import base64
s = "dnNvc2h7aGVsbG99"
b = base64.b64decode(s)
print(b) # b'vsosh{hello}'
Для base32 — то же самое, только функция другая:
import base64
b = base64.b32decode("O5SWYZDFOJSXG5BA")
print(b)
Обратите внимание: обе функции возвращают байты, а не строку. Это правильно и это удобно — потому что после декодирования результат может оказаться ещё одной кодировкой, и тогда его снова надо декодировать, а не читать глазами.
Многослойные кодировки
А теперь главная идея урока. Задачи редко ограничиваются одним слоем. Типичный приём — обернуть данные несколько раз: сначала base64, потом ещё раз base64, или base64 поверх base32. Когда вы снимаете один слой и получаете опять строку, похожую на кодировку, — это не ошибка, а следующий слой. Именно такую «матрёшку» base64 → base32 разбирали в задаче «Снежный ком» · муниципальный этап, 2025–2026 (под кодировками там ждал ещё и XOR — до него доберёмся в уроке 4).
Алгоритм ровно такой, как в цикле из урока 1, но теперь с кодом:
- Посмотрели на строку → определили кодировку по алфавиту.
- Раскодировали.
- Посмотрели на результат. Опять похоже на base64/base32/hex? → к шагу 1. Похоже на
vsosh{...}? → готово.
На практике это выглядит как несколько строк, которые вы дописываете по одной и запускаете заново после каждого шага:
import base64
s = open("task.txt").read().strip()
b = base64.b64decode(s) # сняли первый слой
print(b[:40]) # посмотрели — на что похоже?
Запустили, увидели, что внутри снова base32 (сплошные капсы и цифры 2–7) — дописали строку:
b = base64.b32decode(b) # сняли второй слой
print(b[:40])
И так, пока не покажется vsosh{. Не пытайтесь угадать всю цепочку сразу — снимайте по слою и смотрите на промежуточный результат. Это и есть настоящая работа криптоаналитика: не «знать ответ заранее», а идти по слоям, опираясь на то, что видите.
Практика урока
- J1 «hex в текст» (тренажёр кода). Автопроверяемая задача: напишите функцию, которая по hex-строке (возможно с пробелами и переносами) возвращает текст. Это закрепляет
bytes.fromhex(...).decode()и привычку чистить пробелы. - C2 «Просто hex». В файле — hex-представление флага. Прочитать, раскодировать, сдать
vsosh{...}. Ваш первый боевой флаг. - C3 «Матрёшка». В файле — флаг, обёрнутый в несколько слоёв кодировок. Снимайте по слою, каждый раз глядя на алфавит промежуточного результата.
Типичные ошибки
- Путают
strиbytes. Пытаются, например,"...".fromhex(...)или ксорить строку — не работает. Держите данные вbytes. - Читают файл как текст, когда это важно. Для hex/base это не критично, но привыкайте: бинарные данные читаются в режиме
"rb". - Пугаются
UnicodeDecodeError. Это сигнал «декодировать пока рано», а не поражение. - Пытаются снять все слои одной формулой в уме. Снимайте по одному и печатайте промежуток — так вы не заблудитесь.
- Забывают
.strip()/ чистку пробелов. Лишний перевод строки в конце файла ломаетfromhex.
Что дальше
Вы уже пишете короткие решатели почти рефлекторно. Пора превратить это в систему: в уроке 3 мы разберём, как быстро собирать такие скрипты, как удобно читать вход, как готовить данные (в том числе байты, записанные числами) — и почему привычка печатать промежуточные слои экономит больше всего времени.
Разминка: hex → текст
Реализуй hex_to_text(s): hex-строка (возможны пробелы/переносы) → текст. Пустая строка → пустая.
Просто hex
В файле — hex. Раскодируй и сдай флаг.
Матрёшка
Флаг обёрнут в несколько слоёв кодировок. Снимай по одному, глядя на алфавит промежутка.