Урок 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 (в hex 0x41–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, но теперь с кодом:

  1. Посмотрели на строку → определили кодировку по алфавиту.
  2. Раскодировали.
  3. Посмотрели на результат. Опять похоже на 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? · 4 б.
Что отличает base32 от base64? · 4 б.
J1

Разминка: hex → текст

8 б.

Реализуй hex_to_text(s): hex-строка (возможны пробелы/переносы) → текст. Пустая строка → пустая.

Скачать c2_hex.txt
C2

Просто hex

10 б.

В файле — hex. Раскодируй и сдай флаг.

Скачать c3_layers.txt
C3

Матрёшка

14 б.

Флаг обёрнут в несколько слоёв кодировок. Снимай по одному, глядя на алфавит промежутка.