Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Помимо Python, в дикой природе водится несколько производных от него языков программирования, облегчающих написание модулей и приложений с использованием другого синтаксиса. Один из таких проектов — Cython, своеобразный гибрид Python и С. Сегодня мы разберемся, как работают приложения на этом языке, и попробуем взломать одно из них.
warning
Статья написана в исследовательских целях, имеет ознакомительный характер и предназначена для специалистов по безопасности. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Использование или распространение ПО без лицензии производителя может преследоваться по закону.
Вся история мирового хакерства — это бессмысленная и беспощадная борьба двух противостоящих групп (как ни странно, но зачастую это одни и те же люди). Одни хакеры изо всех сил стараются усложнить задачу анализа исполняемого кода и его реверса другим хакерам: это называется обфускацией кода. В своей статье «Суровая жаба. Изучаем защиту Excelsior JET для программ на Java» я поделился наблюдением, что для приведения стройного кросс‑платформенного байт‑кода в совершенно безумный нечитаемый вид достаточно скомпилировать его в натив c «оптимизацией».
Слово «оптимизация» я взял в кавычки потому, что, как правило, в подобных случаях каждая операция требует обвязки вокруг себя в виде десятков нативных ассемблерных инструкций, что во много раз раздувает скомпилированный код. Остается только верить на слово маркетинговым тестам, рапортующим о среднем увеличении производительности полученного скомпилированного кода.
Впрочем, задачу обфускации подобная «оптимизация» решает на все сто, поскольку анализ полученного кода превращается в жуткий геморрой. В упомянутой выше статье этот подход был описан применительно к байт‑коду JVM, на этот раз объектом нашего исследования будет Python.
В своей статье «Змеиная анатомия. Вскрываем и потрошим PyInstaller» я упоминал многочисленные попытки прикрутить к питону более‑менее нормальную компиляцию. Одна из таких попыток — проект Cython.
Особенностью этого проекта, вернее родительского по отношению к нему проекта Pyrex, является промежуточная трансляция скриптового кода в код С или C++, который уже компилируется в платформенно зависимый нативный код. Понятное дело, что Cython — это не совсем чистый Python, а нечто среднее между ним и C (к примеру, в нем можно для оптимизации кода задать строгую типизацию переменных и атрибутов). Большинство статей в сети, посвященных этому чуду враждебной техники, хвалят его за скорость исполнения программ и за удобство совместимости с С. Ну и объясняют тем, у кого ни того ни другого не наблюдается, как и куда именно надо исхитриться поставить костыли, чтобы наступило счастье. Мы же сломаем систему и попробуем поковыряться в его внутренней реализации на примере реверса конкретного приложения, реализованного на Cython.
Поскольку проект кросс‑платформенный, на этот раз мы возьмем некий линуксовый сервер лицензий: нативную x86-библиотеку формата ELF. Она читает параметры лицензии из закодированного текстового файла, и наша задача — смоделировать лицензию или обойти ее проверку.
Начнем с поверхностного анализа нашего модуля .so
. Поскольку промежуточный формат кода — файл на языке С, то Detect It Easy нам не сильно поможет — он всего‑навсего определяет компилятор, которым был в итоге скомпилирован этот файл (в нашем случае GCC).
И только открыв модуль в IDA, мы обнаруживаем его родство с Python по импортируемым питоновским библиотечным функциям и, в частности, с Cython по характерным суффиксам _pyx_
у имен.
Вид восстановленного кода с непривычки слегка пугает: даже в псевдокоде логика программы кажется совершенно безумной. Вдобавок напрочь отсутствуют прямые вызовы функций и текстовых строк. C их поиска мы и начнем. Кодировка лицензии сильно напоминает Base64. С учетом того, что строка "base64"
присутствует в бинарном файле, пробуем раскодировать лицензию этим алгоритмом. На первом этапе нам везет — раскодированная лицензия имеет вполне читаемый JSON-вид, и все ее поля нам знакомы (поля hostid
и signature
в оригинале намного длиннее):
{ "ip_address":"37.60.178.19", "hostid":"46d0...1605", "version":"3.21.11", "expiry":"9999-01-01 10:00:00+00", "limits": { "data:orig_volume":0, "data:quantity":0, "event:orig_volume":0, "event:quantity":0, "time:orig_volume":300000000, "time:volume":-1, "time:quantity":-1, "clients:accounts":5000, "clients:active":-1}, "components":["billing","routing"], "on_exceed":"block", "signature": "2c5b...a107"}
Вызывает сомнение только последнее поле signature
— это явно подпись файла, без которой лицензия считается невалидной. Наше предположение подтверждает и прямой эксперимент: при изменении значения любого параметра (с последующим кодированием Base64) сервер отказывается принимать полученную лицензию с ошибкой License file is incorrect. Итак, наша задача упрощается до поиска алгоритма вычисления сигнатуры файла лицензии. Для начала попробуем поискать ссылки на строку "signature"
в дизассемблированном коде:
...
.rodata:00003EB8E 64 61 74 65 74 69 6D 65+__pyx_k_datetimext db 'datetimext',0
.rodata:00003EB99 ; const char _pyx_k_components[11]
.rodata:00003EB99 63 6F 6D 70 6F 6E 65 6E+__pyx_k_components db 'components',0
.rodata:00003EBA4 ; const char _pyx_k_Got_SIGHUP[12]
.rodata:00003EBA4 47 6F 74 20 53 49 47 48+__pyx_k_Got_SIGHUP db 'Got SIGHUP(',0
.rodata:00003EBB0 ; const char _pyx_k_traceback[10]
.rodata:00003EBB0 74 72 61 63 65 62 61 63+__pyx_k_traceback db 'traceback',0
.rodata:00003EBBA ; const char _pyx_k_tool_name[11]
.rodata:00003EBBA 5F 74 6F 6F 6C 5F 6E 61+__pyx_k_tool_name db '_tool_name',0
.rodata:00003EBC5 ; const char _pyx_k_signature[10]
.rodata:00003EBC5 73 69 67 6E 61 74 75 72+__pyx_k_signature db 'signature',0
.rodata:00003EBCF ; const char _pyx_k_root_path[10]
.rodata:00003EBCF 72 6F 6F 74 5F 70 61 74+__pyx_k_root_path db 'root_path',0
.rodata:00003EBD9 00 00 00 00 00 00 00 align 20h
.rodata:00003EBE0 ; const char _pyx_k_reloading[16]
.rodata:00003EBE0 29 2C 20 72 65 6C 6F 61+__pyx_k_reloading db '), reloading...',0
.rodata:00003EBE0 64 69 6E 67 2E 2E 2E 00
...
Как видим, все текстовые строки программы (включая имена переменных, классов, методов, атрибутов) сосредоточены в одном месте и на каждую строку есть ссылка из некоей глобальной структуры __pyx_string_tab
. Чтобы не гадать на кофейной гуще и разобраться с форматами данных прямым способом, установим себе Cython и попробуем скомпилировать тестовое приложение. Для этого выполним в консоли команду
pip install Cython
После успешной установки пакета попробуем скомпилировать простой файл test.pyx
, суммирующий две строки:
string1="Hello"helloworld=string1+"world"
Для его компиляции создадим еще один питоновский файл setup.py
следующего содержания:
from setuptools import setupfrom Cython.Build import cythonizesetup( ext_modules = cythonize("test.pyx"))
После чего скормим его питону:
python setup.py build_ext --inplace
После компиляции в содержащем исходные файлы каталоге появился скомпилированный бинарный модуль test.cp310-win_amd64.pyd
и исходник на C test.c
, в который был преобразован питоновский файл test.pyx
перед компиляцией в натив. Плата за преобразование в натив ужасно велика, отрицательная оптимизация размера файла поражает воображение: из простого двухстрочного кода, выполняющего единственную операцию суммирования двух строк, получилось более 150 Кбайт «сишного» текста и более 20 Кбайт нативного кода. Зато полученный «сишный» код вполне поддается анализу, безо всех обвязок значимая часть кода (там даже комментарии имеются) выглядит вот так:
/* "test.pyx":1 * string1="Hello" * helloworld=string1+"world" */ if (PyDict_SetItem(__pyx_d, __pyx_n_s_string1, __pyx_n_s_Hello) < 0) __PYX_ERR(0, 1, __pyx_L1_error) /* "test.pyx":2 * string1="Hello" * helloworld=string1+"world" */ __Pyx_GetModuleGlobalName(__pyx_t_2, __pyx_n_s_string1); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 2, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_2); __pyx_t_3 = PyNumber_Add(__pyx_t_2, __pyx_n_s_world); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 2, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_3); __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; if (PyDict_SetItem(__pyx_d, __pyx_n_s_helloworld, __pyx_t_3) < 0) __PYX_ERR(0, 2, __pyx_L1_error) __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
Источник: xakep.ru