stihl не предоставил(а) никакой дополнительной информации.
Помимо Python, в дикой природе водится несколько производных от него языков программирования, облегчающих написание модулей и приложений с использованием другого синтаксиса. Один из таких проектов — Cython, своеобразный гибрид Python и С. Сегодня мы разберемся, как работают приложения на этом языке, и попробуем взломать одно из них.
Слово «оптимизация» я взял в кавычки потому, что, как правило, в подобных случаях каждая операция требует обвязки вокруг себя в виде десятков нативных ассемблерных инструкций, что во много раз раздувает скомпилированный код. Остается только верить на слово маркетинговым тестам, рапортующим о среднем увеличении производительности полученного скомпилированного кода.
Впрочем, задачу обфускации подобная «оптимизация» решает на все сто, поскольку анализ полученного кода превращается в жуткий геморрой. В упомянутой выше статье этот подход был описан применительно к байт‑коду JVM, на этот раз объектом нашего исследования будет Python.
В своей статье «Для просмотра ссылки Войдиили Зарегистрируйся» я упоминал многочисленные попытки прикрутить к питону более‑менее нормальную компиляцию. Одна из таких попыток — Для просмотра ссылки Войди или Зарегистрируйся.
Особенностью этого проекта, вернее родительского по отношению к нему Для просмотра ссылки Войдиили Зарегистрируйся, является промежуточная трансляция скриптового кода в код С или C++, который уже компилируется в платформенно зависимый нативный код. Понятное дело, что Cython — это не совсем чистый Python, а нечто среднее между ним и C (к примеру, в нем можно для оптимизации кода задать строгую типизацию переменных и атрибутов). Большинство статей в сети, посвященных этому чуду враждебной техники, хвалят его за скорость исполнения программ и за удобство совместимости с С. Ну и объясняют тем, у кого ни того ни другого не наблюдается, как и куда именно надо исхитриться поставить костыли, чтобы наступило счастье. Мы же сломаем систему и попробуем поковыряться в его внутренней реализации на примере реверса конкретного приложения, реализованного на Cython.
Поскольку проект кросс‑платформенный, на этот раз мы возьмем некий линуксовый сервер лицензий: нативную x86-библиотеку формата ELF. Она читает параметры лицензии из закодированного текстового файла, и наша задача — смоделировать лицензию или обойти ее проверку.
Начнем с поверхностного анализа нашего модуля .so. Поскольку промежуточный формат кода — файл на языке С, то Detect It Easy нам не сильно поможет — он всего‑навсего определяет компилятор, которым был в итоге скомпилирован этот файл (в нашем случае GCC).
Для просмотра ссылки Войдиили Зарегистрируйся
И только открыв модуль в IDA, мы обнаруживаем его родство с Python по импортируемым питоновским библиотечным функциям и, в частности, с Cython по характерным суффиксам pyx у имен.
Для просмотра ссылки Войдиили Зарегистрируйся
Вид восстановленного кода с непривычки слегка пугает: даже в псевдокоде логика программы кажется совершенно безумной. Вдобавок напрочь отсутствуют прямые вызовы функций и текстовых строк. C их поиска мы и начнем. Кодировка лицензии сильно напоминает Base64. С учетом того, что строка base64 присутствует в бинарном файле, пробуем раскодировать лицензию этим алгоритмом. На первом этапе нам везет — раскодированная лицензия имеет вполне читаемый JSON-вид, и все ее поля нам знакомы (поля hostid и signature в оригинале намного длиннее):
Вызывает сомнение только последнее поле signature — это явно подпись файла, без которой лицензия считается невалидной. Наше предположение подтверждает и прямой эксперимент: при изменении значения любого параметра (с последующим кодированием Base64) сервер отказывается принимать полученную лицензию с ошибкой License file is incorrect. Итак, наша задача упрощается до поиска алгоритма вычисления сигнатуры файла лицензии. Для начала попробуем поискать ссылки на строку signature в дизассемблированном коде:
Как видим, все текстовые строки программы (включая имена переменных, классов, методов, атрибутов) сосредоточены в одном месте и на каждую строку есть ссылка из некоей глобальной структуры __pyx_string_tab. Чтобы не гадать на кофейной гуще и разобраться с форматами данных прямым способом, установим себе Cython и попробуем скомпилировать тестовое приложение. Для этого выполним в консоли команду
После успешной установки пакета попробуем скомпилировать простой файл test.pyx, суммирующий две строки:
Для его компиляции создадим еще один питоновский файл setup.py следующего содержания:
После чего скормим его питону:
После компиляции в содержащем исходные файлы каталоге появился скомпилированный бинарный модуль test.cp310-win_amd64.pyd и исходник на C test.c, в который был преобразован питоновский файл test.pyx перед компиляцией в натив. Плата за преобразование в натив ужасно велика, отрицательная оптимизация размера файла поражает воображение: из простого двухстрочного кода, выполняющего единственную операцию суммирования двух строк, получилось более 150 Кбайт «сишного» текста и более 20 Кбайт нативного кода. Зато полученный «сишный» код вполне поддается анализу, безо всех обвязок значимая часть кода (там даже комментарии имеются) выглядит вот так:
А еще в коде есть описание нужной нам структуры __pyx_string_tab, содержащей все текстовые строки программы:
Для каждой текстовой строки в ней выделена структура __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:
Как видим, соответствующий строке PyObject называется __pyx_n_s_signature. Автоматическое назначение компилятором имен по содержимому строк весьма полезно при анализе кода приложения. Обрати внимание: строке "signature" соответствуют два элемента __Pyx_StringTabEntry, соответствующие объектам __pyx_n_s_signature и __pyx_n_u_signature. Первый из них — просто текстовая строка, а второй — имя атрибута. Теперь найдем ссылки на них из кода. Наиболее интересна для нас ссылка из метода LicenseChecker.checkSign:
Попробуем интуитивно понять смысл данной конструкции. Для этого рассмотрим используемые внутри нее внешние функции и методы. _PyNumber_Add, как мы уже поняли из скомпилированного примера, складывает аргументы и возвращает склеенный результат; __Pyx_PyObject_GetAttrStr, как нетрудно догадаться по названию, возвращает атрибут объекта, а __Pyx_PyDict_GetItem — элемент словаря. _PyObject_RichCompare, похоже, сравнивает два объекта. То есть в переводе на человеческий язык в этом участке кода результат сложения двух строк сравнивается со значением self.key["signature"]. Проверим это, попробовав скомпилировать данное выражение через Cython:
В принципе, это практически соответствует нашему ассемблерному коду. Но если ты думаешь, что для патча проверки сигнатуры достаточно закоротить условный переход jz loc_256AE, то сильно заблуждаешься. Поскольку результат PyObject_RichCompare вовсе не бинарен (это полноценный питоновский объект), то следом нас ждут дальнейшие проверки и нам придется закорачивать их все:
В таком виде обход проверки лицензии прекрасно работает, чего мы и добивались. Как видишь, при достаточной сноровке, наличии свободного времени и мотивации вполне реально полностью реверсировать процедуру генерации сигнатуры. Но это ты можешь проделать самостоятельно, используя изложенные в статье принципы.
warning
Статья написана в исследовательских целях, имеет ознакомительный характер и предназначена для специалистов по безопасности. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Использование или распространение ПО без лицензии производителя может преследоваться по закону.
Вся история мирового хакерства — это бессмысленная и беспощадная борьба двух противостоящих групп (как ни странно, но зачастую это одни и те же люди). Одни хакеры изо всех сил стараются усложнить задачу анализа исполняемого кода и его реверса другим хакерам: это называется обфускацией кода. В своей статье «Для просмотра ссылки Войдиили Зарегистрируйся» я поделился наблюдением, что для приведения стройного кросс‑платформенного байт‑кода в совершенно безумный нечитаемый вид достаточно скомпилировать его в натив c «оптимизацией».
Слово «оптимизация» я взял в кавычки потому, что, как правило, в подобных случаях каждая операция требует обвязки вокруг себя в виде десятков нативных ассемблерных инструкций, что во много раз раздувает скомпилированный код. Остается только верить на слово маркетинговым тестам, рапортующим о среднем увеличении производительности полученного скомпилированного кода.
Впрочем, задачу обфускации подобная «оптимизация» решает на все сто, поскольку анализ полученного кода превращается в жуткий геморрой. В упомянутой выше статье этот подход был описан применительно к байт‑коду JVM, на этот раз объектом нашего исследования будет Python.
В своей статье «Для просмотра ссылки Войди
Особенностью этого проекта, вернее родительского по отношению к нему Для просмотра ссылки Войди
Поскольку проект кросс‑платформенный, на этот раз мы возьмем некий линуксовый сервер лицензий: нативную 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"
}
Код:
...
.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"
Код:
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_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>
Код:
.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
В таком виде обход проверки лицензии прекрасно работает, чего мы и добивались. Как видишь, при достаточной сноровке, наличии свободного времени и мотивации вполне реально полностью реверсировать процедуру генерации сигнатуры. Но это ты можешь проделать самостоятельно, используя изложенные в статье принципы.