-
Notifications
You must be signed in to change notification settings - Fork 1
patched python
- Категория: Reverse
- Стоимость: 500
- Автор: Артур Лисс
- Репозиторий
Вчера вечером агенты ФБР ворвались в убежище русского хакера Кеши Миткина, известного также под псевдонимами black_plague, Vityok и --xxxH4X0Rxxx--. Поиском преступника в американской спецслужбе занимались уже четыре года, и вчерашние события стали кульминацией операции Wormwood, в подготовке к которой был задействован целый отдел.
К сожалению, хакер быстро среагировал на ситуацию, выключив компьютер, и с зашифрованного жёсткого диска не удалось получить никаких данных. «Этот мерзавец оказался быстрее нас!» — комментирует произошедшее агент Мартин Гейл, — «Я лишь заметил краем глаза зелёные буквы на чёрном фоне».
Одновременно с поимкой преступника вторая группа агентов арестовала сервер Миткина в секретном датацентре на Маршалловых островах. На сервере хранился архив электронной почты хакера. Среди спама, подтверждений регистрации и уведомлений о списании денег исследователи обнаружили письмо без текста, но со странным архивом во вложении. В ФБР подозревают, что именно в этом архиве будут найдены главные улики против Кеши, но за прошедшую ночь, несмотря на огромное количество выпитого кофе, экспертам агенства так и не удалось до них добраться.
В приведённом архиве содержатся два файла: 0001.patch
и goldreich.cpython-36.pyc
.
В файлах с расширением .pyc
хранится байт-код программ на языке Python — промежуточное представление, с которым интерпретатору легче работать, чем с исходным кодом. Эти файлы можно запускать с помощью интерпретатора так же, как и обычные файлы .py
.
В данном случае, однако, не выйдет просто запустить программу командой python3 goldreich.cpython-36.pyc
. Даже если на компьютере установлена подходящая версия интерпретатора (3.6.*), запуск закончится ошибкой:
XXX lineno: 60, opcode: 200
Traceback (most recent call last):
File "goldreich.py", line 60, in <module>
SystemError: unknown opcode
Ошибка указывает на то, что одна из инструкций в файле оказалась неизвестной интерпретатору. В этом можно убедиться самостоятельно, воспользовавшись модулем dis, который умеет преобразовывать байт-код в человекочитаемый формат:
>>> import shutil, dis
>>> shutil.copyfile('goldreich.cpython-36.pyc', 'goldreich.pyc')
'goldreich.pyc'
>>> import goldreich
>>> dis.dis(goldreich.main)
55 0 LOAD_GLOBAL 0 (random)
2 LOAD_ATTR 1 (seed)
4 LOAD_FAST 0 (seed)
6 <200> 1
8 POP_TOP
56 10 LOAD_CONST 1 ('QCTF{{{}}}')
12 LOAD_ATTR 2 (format)
14 LOAD_GLOBAL 3 (format_output)
16 LOAD_GLOBAL 4 (generate_owf)
18 LOAD_GLOBAL 5 (N)
20 LOAD_GLOBAL 6 (D)
22 CALL_FUNCTION 2
24 LOAD_GLOBAL 7 (generate_input)
26 LOAD_GLOBAL 5 (N)
28 <200> 4
30 ROT_THREE
32 ROT_THREE
34 ROT_THREE
36 RETURN_VALUE
Видно, что среди типичных инструкций байт-кода Python вроде LOAD_FAST
и CALL_FUNCTION
действительно встречается некая инструкция с опкодом 200, которой нет в списке инструкций в исходном коде интерпретатора.
Здесь нужно вспомнить про второй файл в архиве — 0001.patch
. По расширению и формату файла видно, что это список изменений, которые нужно применить к какому-то исходному коду. Как нетрудно догадаться, эти изменения касаются исходного кода CPython — основного интерпретатора Python. В них как раз определяется недостающая инструкция с опкодом 200:
#define CALL_UNARY_FUNCTIONS 200
TARGET(CALL_UNARY_FUNCTIONS) {
for (Py_ssize_t i = 0; i < oparg; i++) {
PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, 1, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
goto error;
}
}
DISPATCH();
}
Одно из решений задания — просто запустить файл goldreich.cpython-36.pyc
интерпретатором СPython с указанной модификацией. Чтобы применить эту модификацию, можно склонировать себе репозиторий с исходным кодом CPython командой git clone https://github.com/python/cpython.git
, выбрать в нём версию кода, соответствующую Python 3.6.4, командой git checkout v3.6.4
(это последний релиз Python 3.6 и вообще последний стабильный релиз), и применить патч командой git apply path/to/patch/0001.patch
.
После этого надо скомпилировать изменённую версию интерпретатора, следуя инструкциям по сборке для подходящей операционной системы, и свежесобранным интерпретатором запустить файл goldreich.cpython-36.pyc
, который и выведет флаг.
Если заниматься компиляцией страшно или лень, есть и другой путь решения, более близкий по духу к категории Reverse: можно не изменять интерпретатор, чтобы он мог работать с необычным байт-кодом, а изменить байт-код так, чтобы его смог обработать обычный интерпретатор.
Для этого придётся разобраться в том, что именно делает новая инструкция. Это несложно понять, посмотрев на её название — CALL_UNARY_FUNCTIONS
— и сравнив исполняющий её код с кодом для инструкции CALL_FUNCTION. Оказывается, новая инструкция просто n раз подряд вызывает функцию, принимающую один параметр, где n — это аргумент инструкции. В исходном коде CPython аргумент инструкции записан в переменной oparg
, а в байт-коде он следует непосредственно за опкодом инструкции.
Например, если в байт-коде записаны байты C8 03
, это соответствует вызову трёх функций с одним параметром подряд, что эквивалентно трём инструкциям CALL_FUNCTION
с аргументом 1, то есть байтам 83 01 83 01 83 01
. Если заменить C8 03
на 83 01 83 01 83 01
, то смысл программы не поменяется, но в таком виде её сможет исполнить стандартный интерпретатор. (Здесь 83 в шестнадцатеричной системе счисления или 131 в десятеричной — это опкод инструкции CALL_FUNCTION, а C8 или 200 — опкод CALL_UNARY_FUNCTIONS
.)
С заменой инструкций по такому принципу есть одна проблема: длина программы может увеличиться, что сделает файл с байт-кодом некорректным. Чтобы это исправить, можно для каждой изменённой функции найти в файле goldreich.cpython-36.pyc
число, которое отвечает за длину её кода, и написать на месте этого числа новую длину. Этим можно и не заниматься, если вместо добавления новых инструкций вписывать их на место уже существующих. Возьмём, например, фрагменты байт-кода:
C8 02 09 09
C8 03 02 00 02 00
В первом фрагменте после требующей замены инструкции CALL_UNARY_FUNCTIONS
два байта заняты опкодом 09, который соответствует ничего не делающей инструкции NOP
. Во втором фрагменте после CALL_UNARY_FUNCTIONS
стоит две инструкции ROT_TWO
. Каждая из них меняет местами два последних значения в стеке интерпретатора, а в совокупности они тоже ничего не делают.
Такие последовательности никак не влияют на работу программы, и они в достаточном количестве встречаются в байт-коде после любой инструкции CALL_UNARY_FUNCTIONS
. Чтобы не менять длину программы и не разбираться с правкой длины каждой изменённой функции, можно писать байты 83 01
прямо поверх эти «лишних» инструкций.
После тщательной модификации файл goldreich.cpython-36.pyc
станет пригодным к запуску обычным интерпретатором и без дополнительных сложностей выведет флаг.