• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Реверсим приложение на Cython

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,167
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Помимо Python, в дикой природе водится несколько производных от него языков программирования, облегчающих написание модулей и приложений с использованием другого синтаксиса. Один из таких проектов — Cython, своеобразный гибрид Python и С. Сегодня мы разберемся, как работают приложения на этом языке, и попробуем взломать одно из них.

warning​

Статья написана в исследовательских целях, имеет ознакомительный характер и предназначена для специалистов по безопасности. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Использование или распространение ПО без лицензии производителя может преследоваться по закону.
Вся история мирового хакерства — это бессмысленная и беспощадная борьба двух противостоящих групп (как ни странно, но зачастую это одни и те же люди). Одни хакеры изо всех сил стараются усложнить задачу анализа исполняемого кода и его реверса другим хакерам: это называется обфускацией кода. В своей статье «Для просмотра ссылки Войди или Зарегистрируйся» я поделился наблюдением, что для приведения стройного кросс‑платформенного байт‑кода в совершенно безумный нечитаемый вид достаточно скомпилировать его в натив c «оптимизацией».


Слово «оптимизация» я взял в кавычки потому, что, как правило, в подобных случаях каждая операция требует обвязки вокруг себя в виде десятков нативных ассемблерных инструкций, что во много раз раздувает скомпилированный код. Остается только верить на слово маркетинговым тестам, рапортующим о среднем увеличении производительности полученного скомпилированного кода.

Впрочем, задачу обфускации подобная «оптимизация» решает на все сто, поскольку анализ полученного кода превращается в жуткий геморрой. В упомянутой выше статье этот подход был описан применительно к байт‑коду JVM, на этот раз объектом нашего исследования будет Python.

В своей статье «Для просмотра ссылки Войди или Зарегистрируйся» я упоминал многочисленные попытки прикрутить к питону более‑менее нормальную компиляцию. Одна из таких попыток — Для просмотра ссылки Войди или Зарегистрируйся.

Особенностью этого проекта, вернее родительского по отношению к нему Для просмотра ссылки Войди или Зарегистрируйся, является промежуточная трансляция скриптового кода в код С или C++, который уже компилируется в платформенно зависимый нативный код. Понятное дело, что Cython — это не совсем чистый Python, а нечто среднее между ним и C (к примеру, в нем можно для оптимизации кода задать строгую типизацию переменных и атрибутов). Большинство статей в сети, посвященных этому чуду враждебной техники, хвалят его за скорость исполнения программ и за удобство совместимости с С. Ну и объясняют тем, у кого ни того ни другого не наблюдается, как и куда именно надо исхитриться поставить костыли, чтобы наступило счастье. Мы же сломаем систему и попробуем поковыряться в его внутренней реализации на примере реверса конкретного приложения, реализованного на Cython.

Поскольку проект кросс‑платформенный, на этот раз мы возьмем некий линуксовый сервер лицензий: нативную x86-библиотеку формата ELF. Она читает параметры лицензии из закодированного текстового файла, и наша задача — смоделировать лицензию или обойти ее проверку.

Начнем с поверхностного анализа нашего модуля .so. Поскольку промежуточный формат кода — файл на языке С, то Detect It Easy нам не сильно поможет — он всего‑навсего определяет компилятор, которым был в итоге скомпилирован этот файл (в нашем случае GCC).

Для просмотра ссылки Войди или Зарегистрируйся
И только открыв модуль в IDA, мы обнаруживаем его родство с Python по импортируемым питоновским библиотечным функциям и, в частности, с Cython по характерным суффиксам pyx у имен.

Для просмотра ссылки Войди или Зарегистрируйся
Вид восстановленного кода с непривычки слегка пугает: даже в псевдокоде логика программы кажется совершенно безумной. Вдобавок напрочь отсутствуют прямые вызовы функций и текстовых строк. C их поиска мы и начнем. Кодировка лицензии сильно напоминает Base64. С учетом того, что строка base64 присутствует в бинарном файле, пробуем раскодировать лицензию этим алгоритмом. На первом этапе нам везет — раскодированная лицензия имеет вполне читаемый JSON-вид, и все ее поля нам знакомы (поля hostid и signature в оригинале намного длиннее):

Код:
{
 "ip_address":"xxx.xxx.xxx.xxx",
 "hostid":"46d0...1605",
 "version":"3.21.11",
 "expiry":"9999-01-01 10:00:00+00",
 "limits":
  {
      "datarig_volume":0,
      "data:quantity":0,
      "eventrig_volume":0,
      "event:quantity":0,
      "timerig_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 setup
from Cython.Build import cythonize

Код:
setup(
    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;
А еще в коде есть описание нужной нам структуры __pyx_string_tab, содержащей все текстовые строки программы:

Код:
__Pyx_StringTabEntry __pyx_string_tab[] = {
    {&__pyx_n_s_, __pyx_k_, sizeof(__pyx_k_), 0, 0, 1, 1},
    {&__pyx_n_s_Hello, __pyx_k_Hello, sizeof(__pyx_k_Hello), 0, 0, 1, 1},
    {&__pyx_n_s_cline_in_traceback, __pyx_k_cline_in_traceback, sizeof(__pyx_k_cline_in_traceback), 0, 0, 1, 1},
    {&__pyx_n_s_helloworld, __pyx_k_helloworld, sizeof(__pyx_k_helloworld), 0, 0, 1, 1},
    {&__pyx_n_s_main, __pyx_k_main, sizeof(__pyx_k_main), 0, 0, 1, 1},
    {&__pyx_n_s_name, __pyx_k_name, sizeof(__pyx_k_name), 0, 0, 1, 1},
    {&__pyx_n_s_string1, __pyx_k_string1, sizeof(__pyx_k_string1), 0, 0, 1, 1},
    {&__pyx_n_s_test, __pyx_k_test, sizeof(__pyx_k_test), 0, 0, 1, 1},
    {&__pyx_n_s_world, __pyx_k_world, sizeof(__pyx_k_world), 0, 0, 1, 1},
    {0, 0, 0, 0, 0, 0, 0}
  };

Для каждой текстовой строки в ней выделена структура __Pyx_StringTabEntry, содержащая ссылку на строку (__pyx_k_??), размер этой строки и ссылку на соответствующий ей питоновский объект PyObject (__pyx_n_s_??). Вообще говоря, аналогичная таблица есть и для методов, но в нашем случае она пуста, поскольку наш пример не содержит скомпилированных методов, весь исполняемый код его содержится в функции static CYTHON_SMALL_CODE int __pyx_pymod_exec_test(PyObject *__pyx_pyinit_module). Организация слотов, модулей и методов в Cython довольно сложная, поэтому не будем подробно разбирать ее — желающие могут сами на досуге поковырять код и поиграть с компилируемыми С файлами и восстановленными IDA исходниками.

Вернемся же к нашему менеджеру лицензий. Как мы видели выше, строка "signature" имеет свое имя идентификатора _pyx_k_signature. Теперь открываем массив __pyx_string_tab и ищем в нем структуру, содержащую ссылку на _pyx_k_signature:

Код:
.data:0000241BE0 00 00 00 00 00 00 00 00+  __Pyx_StringTabEntry <offset __pyx_n_s_signature, \
.data:0000241BE0 01 00 01 00 00 00 00 00+                        offset __pyx_k_signature, 0Ah, 0, 0, 1, 1>
.data:0000241BE0 00 5E 24 00 00 00 00 00+  __Pyx_StringTabEntry <offset __pyx_n_u_signature, \
.data:0000241BE0 42 F0 03 00 00 00 00 00+                        offset __pyx_k_signature, 0Ah, 0, 1, 0, 1>
Как видим, соответствующий строке PyObject называется __pyx_n_s_signature. Автоматическое назначение компилятором имен по содержимому строк весьма полезно при анализе кода приложения. Обрати внимание: строке "signature" соответствуют два элемента __Pyx_StringTabEntry, соответствующие объектам __pyx_n_s_signature и __pyx_n_u_signature. Первый из них — просто текстовая строка, а второй — имя атрибута. Теперь найдем ссылки на них из кода. Наиболее интересна для нас ссылка из метода LicenseChecker.checkSign:

Код:
.text:000025115 48 8B 7C 24 68           mov     rdi, [rsp+148h+arg]
.text:00002511A 48 89 C6                 mov     rsi, rax
.text:00002511D E8 AE 1F FE FF           call    _PyNumber_Add
.text:000025122 48 85 C0                 test    rax, rax
.text:000025125; r9 — результат сложения _PyNumber_Add
.text:000025125 49 89 C1                 mov     r9, rax
.text:000025128 0F 84 CC 0F 00 00        jz      loc_260FA
.text:00002512E 48 8B 7C 24 68           mov     rdi, [rsp+148h+arg]
.text:000025133 48 83 2F 01              sub     qword ptr [rdi], 1
.text:000025137 0F 84 00 06 00 00        jz      loc_2573D
.text:00002513D
.text:00002513D  loc_2513D:
.text:00002513D 48 8B 7C 24 60           mov     rdi, [rsp+148h+func]
.text:000025142 48 C7 44 24 68 00 00 00+ mov     [rsp+148h+arg], 0
.text:000025142 00
.text:00002514B 48 83 2F 01              sub     qword ptr [rdi], 1
.text:00002514F 0F 84 D2 05 00 00        jz      loc_25727
.text:000025155
.text:000025155  loc_25155:
.text:000025155; attr_name ("key")
.text:000025155 48 8B 35 BC 0A 22 00     mov     rsi, cs:__pyx_n_s_key
.text:00002515C; obj ("self")
.text:00002515C 48 8B 7C 24 30           mov     rdi, [rsp+148h+__pyx_v_self]
.text:000025161; var_140 — результат сложения _PyNumber_Add
.text:000025161 4C 89 4C 24 08           mov     [rsp+148h+var_140], r9
.text:000025166 48 C7 44 24 60 00 00 00+ mov     [rsp+148h+func], 0
.text:000025166 00
.text:00002516F E8 CC 85 FE FF           call    __Pyx_PyObject_GetAttrStr
.text:000025174 48 85 C0                 test    rax, rax
.text:000025177; rbp — __Pyx_PyObject_GetAttrStr(__pyx_v_self,__pyx_n_s_key)
.text:000025177 48 89 C5                 mov     rbp, rax
.text:00002517A; r9 — результат сложения _PyNumber_Add
.text:00002517A 4C 8B 4C 24 08           mov     r9, [rsp+148h+var_140]
.text:00002517F 0F 84 6E 0F 00 00        jz      loc_260F3
.text:000025185 48 8B 05 EC BD 21 00     mov     rax, csyDict_Type_ptr
.text:00002518C 48 39 45 08              cmp     [rbp+8], rax
.text:000025190 0F 85 3F 0F 00 00        jnz     loc_260D5
.text:000025196; key ("signature")
.text:000025196 48 8B 35 A3 07 22 00     mov     rsi, cs:__pyx_n_u_signature
.text:00002519D; __Pyx_PyObject_GetAttrStr(__pyx_v_self,__pyx_n_s_key)
.text:00002519D 48 89 EF                 mov     rdi, rbp
.text:0000251A0 E8 AB 93 FE FF           call    __Pyx_PyDict_GetItem
.text:0000251A5 4C 8B 4C 24 08           mov     r9, [rsp+148h+var_140]
.text:0000251AA  loc_251AA:
.text:0000251AA 48 85 C0                 test    rax, rax
.text:0000251AD 48 89 44 24 60           mov     [rsp+148h+func], rax
.text:0000251B2 0F 84 13 0F 00 00        jz      loc_260CB
.text:0000251B8 48 8B 5D 00              mov     rbx, [rbp+0]
.text:0000251BC; rsi — результат __Pyx_PyDict_GetItem(__Pyx_PyObject_GetAttrStr(__pyx_v_self,__pyx_n_s_key),__pyx_n_u_signature)
.text:0000251BC 48 89 C6                 mov     rsi, rax
.text:0000251BF 48 8D 53 FF              lea     rdx, [rbx-1]
.text:0000251C3 48 85 D2                 test    rdx, rdx
.text:0000251C6 48 89 55 00              mov     [rbp+0], rdx
.text:0000251CA 0F 84 39 05 00 00        jz      loc_25709
.text:0000251D0  loc_251D0:
.text:0000251D0; Результат сложения _PyNumber_Add
.text:0000251D0 4C 89 CF                 mov     rdi, r9
.text:0000251D3 BA 03 00 00 00           mov     edx, 3
.text:0000251D8 4C 89 4C 24 08           mov     [rsp+148h+var_140], r9
.text:0000251DD E8 CE 1F FE FF           call    _PyObject_RichCompare
.text:0000251E2 48 85 C0                 test    rax, rax
.text:0000251E5 48 89 C5                 mov     rbp, rax
.text:0000251E8 4C 8B 4C 24 08           mov     r9, [rsp+148h+var_140]
.text:0000251ED 0F 84 BC 0E 00 00        jz      loc_260AF
.text:0000251F3 48 8B 7C 24 60           mov     rdi, [rsp+148h+func]
.text:0000251F8 48 83 2F 01              sub     qword ptr [rdi], 1
.text:0000251FC 0F 84 F6 04 00 00        jz      loc_256F8
.text:000025202
.text:000025202  loc_25202:
.text:000025202 48 3B 2D 57 BD 21 00     cmp     rbp, cs:arg
.text:000025209 48 C7 44 24 60 00 00 00+ mov     [rsp+148h+func], 0
.text:000025209 00
.text:000025212 0F 94 C3                 setz    bl
.text:000025215 48 3B 2D 14 BD 21 00     cmp     rbp, cs:_Py_FalseStruct_ptr
.text:00002521C 0F 94 C0                 setz    al
.text:00002521F 08 D8                    or      al, bl
.text:000025221 0F 84 87 04 00 00        jz      loc_256AE

Попробуем интуитивно понять смысл данной конструкции. Для этого рассмотрим используемые внутри нее внешние функции и методы. _PyNumber_Add, как мы уже поняли из скомпилированного примера, складывает аргументы и возвращает склеенный результат; __Pyx_PyObject_GetAttrStr, как нетрудно догадаться по названию, возвращает атрибут объекта, а __Pyx_PyDict_GetItem — элемент словаря. _PyObject_RichCompare, похоже, сравнивает два объекта. То есть в переводе на человеческий язык в этом участке кода результат сложения двух строк сравнивается со значением self.key["signature"]. Проверим это, попробовав скомпилировать данное выражение через Cython:

Код:
string1="Hello"
helloworld=string1+"world"
self={}
if helloworld==self.key["signature"]:
 result="signature ok!"
Находим результат компиляции в получившемся коде С и видим, что четвертая строка сгенерировала такую конструкцию:

 /* "test.pyx":4
 * helloworld=string1+"world"
 * self={}
 * if helloworld==self.key["signature"]:
 *  result="signature ok!"
 */
  __Pyx_GetModuleGlobalName(__pyx_t_3, __pyx_n_s_helloworld); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 4, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_3);
  __Pyx_GetModuleGlobalName(__pyx_t_2, __pyx_n_s_self); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 4, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_2);
  __pyx_t_4 = __Pyx_PyObject_GetAttrStr(__pyx_t_2, __pyx_n_s_key); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 4, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_4);
  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
  __pyx_t_2 = __Pyx_PyObject_Dict_GetItem(__pyx_t_4, __pyx_n_s_signature); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 4, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_2);
  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
  __pyx_t_4 = PyObject_RichCompare(__pyx_t_3, __pyx_t_2, Py_EQ); __Pyx_XGOTREF(__pyx_t_4); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 4, __pyx_L1_error)
  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
  __pyx_t_5 = __Pyx_PyObject_IsTrue(__pyx_t_4); if (unlikely((__pyx_t_5 < 0))) __PYX_ERR(0, 4, __pyx_L1_error)
  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;

В принципе, это практически соответствует нашему ассемблерному коду. Но если ты думаешь, что для патча проверки сигнатуры достаточно закоротить условный переход jz loc_256AE, то сильно заблуждаешься. Поскольку результат PyObject_RichCompare вовсе не бинарен (это полноценный питоновский объект), то следом нас ждут дальнейшие проверки и нам придется закорачивать их все:

Код:
.text:0000256AE 48 3B 2D A3 B8 21 00  cmp rbp, cs:_Py_NoneStruct_ptr
.text:0000256B5 0F 84 6C FB FF FF     jz  loc_25227  ; Первая
.text:0000256BB 48 89 EF              mov rdi, rbp
.text:0000256BE 4C 89 4C 24 08        mov [rsp+148h+var_140], r9
.text:0000256C3 E8 88 1D FE FF        call_PyObject_IsTrue
.text:0000256C8 85 C0                 testeax, eax
.text:0000256CA 89 C3                 mov ebx, eax
.text:0000256CC 4C 8B 4C 24 08        mov r9, [rsp+148h+var_140]
.text:0000256D1 0F 89 53 FB FF FF     jns loc_2522A  ; Вторая
.text:0000256D7 BE A8 31 00 00        mov esi, 31A8h

В таком виде обход проверки лицензии прекрасно работает, чего мы и добивались. Как видишь, при достаточной сноровке, наличии свободного времени и мотивации вполне реально полностью реверсировать процедуру генерации сигнатуры. Но это ты можешь проделать самостоятельно, используя изложенные в статье принципы.
 
Activity
So far there's no one here
Сверху Снизу