/Разборы задач
6 ноября 2026
Райтап на задачу «Зона риска»
Разбираем генератор ключа и обращаем шифрование в исполняемом файле, чтобы восстановить флаг.16
11

Rev
Задача «Стружка-код»
«Попробуй разберись…» звучит угрожающе. Скачаем файл и тут же его запустим.
Первым делом ловим DeprecationWarning и кучу страшных символов. На современной версии Python это нормальное явление. Игнорируем и смотрим вниз.
У нас есть проверка флага? При попытке что-то ввести получаем запрос ввести ещё раз.
А потом программа может и просто вылететь, подчёркивая неизвестно что. Видимо, пора открыть Python-файл и разобраться, что к чему.
4 строчки какого-то не пойми чего… Как нам это прочитать? Попробуем просто вбить chr() в IDLE.
Мы получили букву T — первый символ от «The main and the only purpose of this script is to guess the flag», что появляется при запуске программы. Какой-то магической математикой эти операции конвертируются в число, которое chr() превращает в символ. Как нам привести это к нормальному формату?
Для этого можно использовать ast — abstract syntax tree. Этот модуль позволяет анализировать Python-код в виде дерева. Надо вычислить каждое выражение и получить полноценный исходный код. Ссылка на исходники: https://pastebin.com/QXx26xF9.
import ast
# Мы не хотим, чтобы вызов input() сработал
# и заставил программу зависнуть, разрешаем только chr,
# которым делаются константы.
ENV = {"__builtins__": {}, "chr": chr}
class Yo(ast.NodeTransformer):
def visit(self, node):
if isinstance(node, ast.expr): # Проверяем, что узел — выражение.
try:
# Пытаемся вычислить узел целиком.
value = eval(compile(ast.Expression(node), "<x>", "eval"), ENV)
const_node = ast.copy_location(ast.Constant(value), node)
compile(ast.Expression(const_node), "<x>", "eval")
return const_node
except Exception:
pass
return super().generic_visit(node)
tree = Yo().visit(ast.parse(open("pyf_cked.py").read()))
ast.fix_missing_locations(tree)
print(ast.unparse(tree))Наша цель — превратить chr() в человеческий вид, поэтому builtins отключаем: мы не хотим, чтобы input() повесил нашу программу.
Сохраняем вывод и получаем unfucked-версию программы.
Ну как unfucked, ещё надо переименовать переменные и красиво форматировать.
print("The main and the only purpose of this script is to guess the flag")
while True:
booba1 = input("Let's check flag correctness:")
z = "3xtended_"
booba2 = all(map(lambda x: booba1[x] == "u", [1, 9, 17, 24, 34]))
t = "_h3ll0"
booba2 = (booba2 and booba1[3:6] == "CTF" and (
int("".join([i if i.isdigit() or ord("a") <= ord(i) < ord("g") else ""
for i in booba1]), base=16,) == 1515751645894529294289854812)
)
booba2 = (
booba2
and all(map(lambda x: not booba1[x].isalnum(), [6, 10, 15, 28, 40, 45, 52]))
and (booba1[2] == "g")
and (booba1.find("ve_") > booba1.find("_h") > booba1.find("{y") > 0)
)
t = "_th1s_"
booba2 = (
booba2
and booba1[0] == "B"
and ([booba1[i] for i in [18, 19, 36, 47]] == ["c", "c", "c", "c"])
)
booba2 = booba2 and (ord(booba1[37]) == ord(booba1[0].lower()) - 1)
booba2 = booba2 and booba1[-5:][::-1] == "}tpir"
(
print("You win!!!")
if booba2
and booba1.find(t) > 0
and (booba1.find("y_d") == 27)
and booba1[8].isdigit()
else print("Can you give me another flag?")
)Теперь мы пришли к более-менее читаемому виду… Осталось всего лишь полностью всё разобрать.
booba1 — строка, которую ввёл пользователь. booba2 — флаг, но не тот, который надо ввести на борде, а тот, который bool. Если хотя бы одна проверка флага не сработает, то он станет False.
Первым делом откидываем переменные z и первое определение t — они нигде не используются, это ложный след.
Смотрим первую проверку booba2 на строчке 5. Тут из-за функции all оно требует, чтобы на позициях 1, 9, 17, 24 и 34 был символ u. Запомним.
booba1[3:6] должен быть равен CTF. Оно и очевидно, ведь формат флага — BugCTF{}.
Затем идёт интересная и важная часть — hex-фильтр. Он идёт по каждому символу строки booba1 и оставляет только цифры 0–9 и строчные буквы a–f, всё остальное откидывается. Затем эта строка читается как base16 и сравнивается с длинным числом. В hex-форме это число — 0x4e5cce55f11de0bfca7e15c. То есть по порядку все hex-символы в флаге должны быть 4e5cce55f11de0bfca7e15c (23 символа).
Затем идёт снова знакомая нам функция all(), только теперь на индексах [6, 10, 15, 28, 40, 45, 52] символ не должен быть буквой или цифрой, то есть там должны быть знаки вроде _{}.
booba1[2] = "g" (Bug).
find() возвращает индекс, значит, в строке сначала идёт {y, потом _h, потом ve_ (строка 15).
booba1[0] = "B" (Bug).
На позициях [18, 19, 36, 47] должен быть символ c.
Чтобы понять 23-ю строчку, надо сначала вспомнить предыдущие условия. booba1[0] — это «B». "B".lower() — это «b». ord("b") = 98. Минус 1 будет 97. chr(97) будет «a», значит, 37-й символ — a.
В 24-й строчке берутся последние пять символов и переворачиваются, затем сравниваются с }tpir, значит, флаг заканчивается на ript}.
Финальные условия: в флаге должен быть _th1s_ (стр. 28), подстрока y_d начинается с индекса 27 (стр. 29), а 8-й символ должен быть цифрой. Вспомним про hex-проверки, значит, booba1[8] = 4.
Фух. Это было достаточно душно. Успокаиваемся, усваиваем все условия и начинаем потихоньку составлять флаг… Начинаем с BugCTF{ — тут и к гадалке не ходи.
Затем из find("{y") идёт y, 8-й символ = 4, а 9-й = u.
BugCTF{y4u
10 — явно нижнее подчёркивание, потому что 10-й символ должен быть не буквой и не цифрой. Для 11-го символа вспоминаем последовательность и find. Там говорится, что после нижнего подчёркивания идёт h. 11 = h. Может, сразу и добьём эту проверку? ve_ идёт после _h, а что, если это слово have в l33t? Оставим пока как предположение. К 12-му символу вернёмся позже.
BugCTF{y4u_h?ve_
Вспомним про hex-строку: пока что у нас идёт 4e, значит, ? — не hex-символ. Дальше по хексу должен идти 5, допустим. Затем на 17-й позиции — u. 18-й символ — c из списка. 19-й символ — то же самое, c.
BugCTF{y4u_h?ve_5ucc
Смотрим дальше hex. Идёт e55f — что, если наложить как есть?
BugCTF{y4u_h?ve_5ucce55f
— вырисовывается слово successfully, хороший знак.
Следующий символ — u из списка. Затем две hex-единицы. Потом find("y_d") == 27, выходит:
BugCTF{y4u_h?ve_5ucce55fu11y_d
Продолжим мучить hex-поток. e0bf (получается de0bf — вырисовывается deobfuscate в leet-записи). 34-й символ = u. На 35-й символ, по всей видимости, нет проверки, кроме hex-потока. По слову можно догадаться, что этот символ — s. 36-й = c (hex + список), 37-й = a (hex + ord), 38–39 = 7e по хексу.
BugCTF{y4u_h?ve_5ucce55fu11y_de0bfusca7e
Позиция 40 — не буква и не цифра, значит, _. Кстати, надо куда-то впихнуть _th1s_: он хорошо подходит по смыслу.
BugCTF{y4u_h?ve_5ucce55fu11y_de0bfusca7e_th1s_
41-й символ (1) совпадает с hex. Затем по hex идёт 5c и последние символы: ript}.
BugCTF{y4u_h?ve_5ucce55fu11y_de0bfusca7e_th1s_5cript}
You have successfully deobfuscated this script! Ура, мы почти получили флаг. Осталось узнать, что же за 12-й символ такой. Hex-символы не подходят: они нарушат hex-поток.
Можно долго пробовать разные варианты l33t-speak, чтобы угадать 12-й символ, но почему-то флаг упорно будет не подходить. Почему же? Вспомним, как у нас идёт проверка hex-символов — int(string, 16). Если string начинается с 0, то этот 0 просто проигнорируется.
Получается, всё это время 4-ка была не на том месте. Первый hex-символ = 0, а вместо 12-го идёт 4. Почините флаг — и остаётся только submit.