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

По­мимо 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

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *