Урок 2. Байты, строки и кодировки
До первой функции: где писать код и что сдавать
Для ближайших упражнений устанавливать Python не нужно: редактор и кнопка проверки находятся прямо в уроке. В файловых задачах нужен Python на своём компьютере. Эти два режима отличаются тем, кто вызывает ваш код.
В тренажёре система вызывает функцию с готовыми данными. Не добавляйте input(): ввод уже находится в параметре. Не заменяйте имя функции. Ваша работа — вернуть результат через return.
def echo(text):
return text
def объявляет функцию; echo — её имя; text — параметр, которому проверка передаст строку. После двоеточия начинается тело, с отступом в четыре пробела. return отдаёт значение вызывающему коду. print только выводит его на экран; функция без return вернёт None, и тест не пройдёт.
На входе "кот" функция выше возвращает "кот". Кавычки в примере показывают тип «строка»: они не являются частью слова. Сначала повторите этот работающий пример, затем измените его в следующем упражнении.
Первая функция: вернуть строку
Реализуйте echo(text): верните полученную строку без изменений. Не используйте input() и print().
Изменим работающий пример
Теперь параметр — целое число. В Python 4 + 1 даёт 5, а "4" + "1" склеивает две строки в "41". Тип данных определяет допустимые действия. Сохраните def, имя и отступ; в строке return замените возвращаемое выражение на n + 1.
Изменить пример: число и результат
Реализуйте 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(в 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())
Вот и весь первый решатель. Три строки: прочитали, почистили, раскодировали. Именно так выглядит боевой код на олимпиаде — коротко и по делу.
Разминка: hex → текст
Реализуй hex_to_text(s): hex-строка (возможны пробелы/переносы) → текст. Пустая строка → пустая.
Просто hex
В файле — 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)
Обратите внимание: обе функции возвращают байты, а не строку. Это правильно и это удобно — потому что после декодирования результат может оказаться ещё одной кодировкой, и тогда его снова надо декодировать, а не читать глазами.
Многослойные кодировки
А теперь главная идея урока. Задачи редко ограничиваются одним слоем. Типичный приём — обернуть данные несколько раз: сначала 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{. Не пытайтесь угадать всю цепочку сразу — снимайте по слою и смотрите на промежуточный результат. Это и есть настоящая работа криптоаналитика: не «знать ответ заранее», а идти по слоям, опираясь на то, что видите.
Матрёшка
Флаг обёрнут в несколько слоёв кодировок. Снимай по одному, глядя на алфавит промежутка.
Кодировка из настоящего условия
Примените различение алфавитов и последовательное декодирование. Здесь нет названия кодировки в условии: обоснуйте выбор по данным, затем проверьте результат.
Это исходные задания олимпиады. Они сохраняют свои файлы и правила ответа. Варианты повышенной сложности можно оставить на повторный проход; зачёт уже пройденного урока не отменяется.
ВСОШ: Полимерный контейнер (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 мы разберём, как быстро собирать такие скрипты, как удобно читать вход, как готовить данные (в том числе байты, записанные числами) — и почему привычка печатать промежуточные слои экономит больше всего времени.