Ques/Help/Req Гибридная змея. Реверсим приложение на Cython

XakeR

Member
Регистрация
13.05.2006
Сообщения
1 912
Реакции
0
Баллы
16
Местоположение
Ukraine
Помимо 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:eek:rig_volume«:0, «data:quantity«:0, «event:eek:rig_volume«:0, «event:quantity«:0, «time:eek:rig_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»!​


Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

-60%

1 year​


9990 рублей 4000 р.


[TD]

1 month_r​


920 р.
[/TD]

Я уже участник «Xakep.ru»
 
198 237Темы
635 209Сообщения
3 618 425Пользователи
Pandar96Новый пользователь
Верх