Skip to content

patched python

artli edited this page Mar 12, 2018 · 1 revision

Арестован опасный киберпреступник Кеша Миткин

Условие

Вчера вечером агенты ФБР ворвались в убежище русского хакера Кеши Миткина, известного также под псевдонимами 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 станет пригодным к запуску обычным интерпретатором и без дополнительных сложностей выведет флаг.

Clone this wiki locally