Урок 2. Байты, строки и кодировки

До первой функции: где писать код и что сдавать

Для ближайших упражнений устанавливать Python не нужно: редактор и кнопка проверки находятся прямо в уроке. В файловых задачах нужен Python на своём компьютере. Эти два режима отличаются тем, кто вызывает ваш код.

В тренажёре система вызывает функцию с готовыми данными. Не добавляйте input(): ввод уже находится в параметре. Не заменяйте имя функции. Ваша работа — вернуть результат через return.

def echo(text):
    return text

def объявляет функцию; echo — её имя; text — параметр, которому проверка передаст строку. После двоеточия начинается тело, с отступом в четыре пробела. return отдаёт значение вызывающему коду. print только выводит его на экран; функция без return вернёт None, и тест не пройдёт.

На входе "кот" функция выше возвращает "кот". Кавычки в примере показывают тип «строка»: они не являются частью слова. Сначала повторите этот работающий пример, затем измените его в следующем упражнении.

PYTHON-ECHO

Первая функция: вернуть строку

Реализуйте echo(text): верните полученную строку без изменений. Не используйте input() и print().

Изменим работающий пример

Теперь параметр — целое число. В Python 4 + 1 даёт 5, а "4" + "1" склеивает две строки в "41". Тип данных определяет допустимые действия. Сохраните def, имя и отступ; в строке return замените возвращаемое выражение на n + 1.

PYTHON-ADD

Изменить пример: число и результат

Реализуйте add_one(n): верните число n + 1. Здесь n — число, а не текст.

Текст, байты и список чисел

Значение Тип Для чего нужно
"vs" str Читаемый текст
b"vs" bytes Последовательность байтов: 118, 115
[118, 115] list[int] Список целых чисел, удобный для возврата в тренажёре
"7673" str Текстовая hex-запись двух байтов
text = "vs"
data = text.encode("utf-8")          # str -> bytes
numbers = list(data)                  # bytes -> [118, 115]
restored = bytes(numbers).decode()    # список -> bytes -> str
assert restored == text

.encode() переводит текст в байты, .decode() переводит байты в текст. Это не шифрование. У числа 118 нет метода .decode(): сначала соберите последовательность bytes([118]).

bytes.fromhex("76 73") читает шестнадцатеричную запись и даёт b"vs". Последующий .decode() даст строку "vs". ASCII-пробелы и переносы fromhex уже умеет пропускать; .strip() допустим для внешних пробелов, но не является обязательным условием работы этой функции.

Когда ниже появится for b in data, при data типа bytes переменная b будет целым числом одного байта, не всей строкой. Если пройти циклом по str, получите символы — их нельзя XOR-ить с числом.

Как запустить файл у себя

Сохраните скачанный файл и solve.py в одной папке. В терминале перейдите в эту папку командой cd путь_к_папке. Проверьте python3 --version (Windows: py -3 --version). Если команды нет, установите Python 3 с python.org, затем снова откройте терминал.

Файл solve.py для первого знакомства:

from pathlib import Path
s = Path("c2_hex.txt").read_text(encoding="utf-8")
data = bytes.fromhex(s)
print(data.decode("utf-8"))

Запуск: python3 solve.py (Windows: py -3 solve.py). Здесь print уместен: вы хотите увидеть ответ в терминале. В поле флага вставляется напечатанная строка, без кода, кавычек и префикса b. В редактор функции вставляется код с def и return.

FileNotFoundError означает, что программа не нашла файл в текущей папке. Проверьте имя, расширение и папку; содержимое шифра к этой ошибке отношения не имеет.

Как читать сообщение об ошибке

Читайте последнюю строку traceback, затем указанную строку своего кода.

  • IndentationError: проверьте двоеточие и одинаковые отступы пробелами внутри функции.
  • KeyError: 'hex_to_text': проверка не нашла функцию с ожидаемым именем.
  • TypeError: сравните фактические типы с таблицей выше; str, bytes и list[int] не взаимозаменяемы.
  • WA: код выполнился, но ответ отличается. Проверьте тип результата, порядок преобразований и return вместо print.
  • Ошибка сериализации bytes: условие требует строку или список, а вы вернули объект bytes. Верните .decode() для текста или list(data) для списка — смотря что просит условие.

Проверяйте по одному шагу. Исправление случайных символов без понимания последней ошибки обычно создаёт новую ошибку вместо решения старой.

Урок 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())

Вот и весь первый решатель. Три строки: прочитали, почистили, раскодировали. Именно так выглядит боевой код на олимпиаде — коротко и по делу.

Какой набор признаков точнее всего указывает на hex? · 4 б.
J1

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

8 б.

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

Скачать c2_hex.txt
C2

Просто hex

10 б.

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

Скачайте c2_hex.txt, прочитайте его как текст, примените bytes.fromhex, затем .decode(). Сдайте только полученный флаг: не hex, не Python-код и не строку с кавычками. Полный пример запуска находится выше в этом уроке.

Ошибки декодирования — это сигнал, а не беда

Иногда 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)

Обратите внимание: обе функции возвращают байты, а не строку. Это правильно и это удобно — потому что после декодирования результат может оказаться ещё одной кодировкой, и тогда его снова надо декодировать, а не читать глазами.

Что отличает base32 от base64? · 4 б.

Многослойные кодировки

А теперь главная идея урока. Задачи редко ограничиваются одним слоем. Типичный приём — обернуть данные несколько раз: сначала 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{. Не пытайтесь угадать всю цепочку сразу — снимайте по слою и смотрите на промежуточный результат. Это и есть настоящая работа криптоаналитика: не «знать ответ заранее», а идти по слоям, опираясь на то, что видите.

Скачать c3_layers.txt
C3

Матрёшка

14 б.

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

Кодировка из настоящего условия

Примените различение алфавитов и последовательное декодирование. Здесь нет названия кодировки в условии: обоснуйте выбор по данным, затем проверьте результат.

Это исходные задания олимпиады. Они сохраняют свои файлы и правила ответа. Варианты повышенной сложности можно оставить на повторный проход; зачёт уже пройденного урока не отменяется.

ВСОШ-204

ВСОШ: Полимерный контейнер (10-11 класс ВСОШ ШЭ 2025-2026)

Рекомендуемые утилиты: Python

В лабораториях комплекса «Завод-3826» найден подозрительный файл с ключом к роботу-кодировщику. Исследователи подозревают, что данные закодированы каким-то необычным способом, возможно, кратность длины кодирования как-то с этим связана. Ваша задача — извлечь секретную информацию.

Файлы оригинала:

Открыть оригинальную карточку. Флаг можно сдать в поле ниже. Решение оригинала учитывается и в каталоге задач платформы. Дополнительная практика: 0 баллов курса, порог урока не повышается.

Практика урока

  • J1 «hex в текст» (тренажёр кода). Автопроверяемая задача: напишите функцию, которая по hex-строке (возможно с пробелами и переносами) возвращает текст. Это закрепляет bytes.fromhex(...).decode() и привычку чистить пробелы.
  • C2 «Просто hex». В файле — hex-представление флага. Прочитать, раскодировать, сдать vsosh{...}. Ваш первый боевой флаг.
  • C3 «Матрёшка». В файле — флаг, обёрнутый в несколько слоёв кодировок. Снимайте по слою, каждый раз глядя на алфавит промежуточного результата.

Типичные ошибки

  • Путают str и bytes. Пытаются, например, "...".fromhex(...) или ксорить строку — не работает. Держите данные в bytes.
  • Читают файл как текст, когда это важно. Для hex/base это не критично, но привыкайте: бинарные данные читаются в режиме "rb".
  • Пугаются UnicodeDecodeError. Это сигнал «декодировать пока рано», а не поражение.
  • Пытаются снять все слои одной формулой в уме. Снимайте по одному и печатайте промежуток — так вы не заблудитесь.
  • Путают представление и содержимое. fromhex пропускает ASCII-пробелы и переносы, но не понимает префикс 0x, запятые или не-hex символы. Сначала посмотрите на формат файла.

Что дальше

Вы уже пишете короткие решатели почти рефлекторно. Пора превратить это в систему: в уроке 3 мы разберём, как быстро собирать такие скрипты, как удобно читать вход, как готовить данные (в том числе байты, записанные числами) — и почему привычка печатать промежуточные слои экономит больше всего времени.