Взлом протектора .NET Reactor

Пентест взлом icon

Есть множество различных способов защиты от реверса приложений .NET. Среди них — ком­прес­сия, шиф­рование и протекторы типа Agile.Net и Enigma. Мы уже рассказывали о взломе протектора Enigma. Сегодня поговорим о взломе защиты протектора .NET Reactor.

Еще по теме: Обход защиты протектора Obsidium

Протектор .NET Reactor

Предположим, что у нас есть программа с онлайн‑про­вер­кой лицен­зии при запуске. Ана­лиз приложения с помощью DIE говорит о плат­форме .NET. После загрузки приложения в отладчик dnSpy, мы видим две вещи: хорошую и пло­хую.

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

Плохая заключается в том, что при­ложе­ние надежно обфусци­рова­но, большая часть методов пере­име­нова­на в комбинацию бессмысленных сим­волов, а глав­ное, вмес­то их кода везде пус­тые заг­лушки.

Запуск программы в отладчи­ке dnSpy
Запуск программы в отладчи­ке dnSpy

Взлом приложения с защитой .NET Reactor

Глупая идея, но давайте про­буем сделать дамп при­ложе­ния — это час­то позволяет вос­ста­новить скры­тый код методов. К сожалению, в нашем случае —  это не сработало: дам­пы модулей работос­пособ­ны, но не особо отли­чают­ся от оригинальных. Обфуска­ция осталась, тела методов по прежнему пус­тые.

Вернемся в отладчик dnSpy и попробуем т­расси­ровать работа­ющее приложение. А теперь хорошая новость: в программе нет анти­отладчи­ка и она прек­расно запус­кает­ся и трас­сиру­ется, при­чем при трас­сиров­ке «пус­тых» методов во вклад­ке Call Stack мы видим, что счет­чик команд переме­щает­ся по невиди­мому коду и про­вали­вает­ся в вызовы.

Побродив всле­пую по коду, мы видим еще одну хорошую вещь: не все методы пере­име­нова­ны, некото­рые наз­вания очень даже осмыслен­ны, и можем даже нащупать про­цесс про­вер­ки валид­ности (на скрин­е выше — isValid). Тело дан­ного метода скры­то, однако наз­вание и индекс извес­тны, и это вери гуд.

Теперь пробуем попробовать деоб­фуска­цию по стан­дар­тной схе­ме: в начале подсунем предложение в de4dot. Увы, но в нашем примере данный метод не сработал и de4dot не деоб­фусци­рует. Более ранние вер­сии сра­зу вываливаются с ошиб­кой:

Вер­сии новее фор­мулиру­ют ошиб­ку лаконич­нее:

Ну теперь мы хотя бы понимаем, с каким протектором име­ем дело, — это .NET Reactor пред­положи­тель­но версии 4.8. Это старая версия, но с ней не может справиться даже спе­циаль­но заточенный под .NET Reactor de4dot. Та же ошиб­ка и нам опять пред­лага­ют поис­кать вер­сию поновее.

Открываем нашу зло­получ­ное приложение в отладчике x32dbg. Заг­ружа­ем биб­лиоте­ку cljit.dll, отла­доч­ные сим­волы к ней и уста­новим точ­ку оста­нова на вход JIT-ком­пилято­ра CILJit::compileMethod.

Ука­зан­ный спо­соб работа­ет, то есть при каж­дом вызове ком­пилято­ра в поле ILCode струк­туры CORINFO_METHOD_INFO мы видим рас­шифро­ван­ный IL-код каж­дого метода. В прин­ципе, мож­но ана­лизи­ровать код и даже пат­чить на лету, но это дол­го и уто­митель­но, вдо­бавок нас ждет еще одна лож­ка дег­тя.

Напом­ню, что в пре­дыду­щей статье я опи­сывал слег­ка жуль­ничес­кий спо­соб опре­делить индекс ком­пилиро­ван­ной про­цеду­ры. Суть его сос­тоит в том, что хендл ftn (пер­вое двой­ное сло­во в струк­туре CORINFO_METHOD_INFO), если его исполь­зовать как ука­затель, ука­зыва­ет на оди­нар­ное сло­во — индекс метода в .NET метада­те EXE-модуля.

Так вот, этот хал­турный спо­соб работа­ет не всег­да, в чем мы с огор­чени­ем и убеж­даем­ся. Зная индекс метода isValid 25250 (0x62A2), дела­ем усло­вием оста­нов­ки на брейк‑пой­нте CILJit::compileMethod выраже­ние word:[[[esp+0xc]]]==0x62A2, но точ­ка оста­нова не сра­баты­вает, хотя опре­делен­ные этим спо­собом индексы на дру­гих методах похожи на пра­виль­ные. Что‑то пош­ло не так, надо искать более кор­рек­тный спо­соб иден­тифика­ции метода.

Ины­ми сло­вами, дви­жение по это­му пути, конеч­но, пер­спек­тивно, но тер­нисто, да и сам путь готовит нам мас­су подоб­ных сюр­при­зов. По счастью, для нашего слу­чая есть и более прос­тые спо­собы решения задачи, ибо умные люди, как обыч­но, все при­дума­ли за нас.

А при­дума­ли они про­ект под наз­вани­ем NetReactorSlayer. Так же как и упо­мяну­тый выше мод de4dot, он заточен под рас­шифров­ку и деоб­фуска­цию .NET Reactor, но в отли­чие от пре­дыду­щего он не валит­ся с ошиб­кой, а впол­не себе успешно соз­дает деоб­фусци­рован­ный модуль, в котором методы уже не скры­ты от дизас­сем­бли­рова­ния в dnSpy.

Мо­дуль пос­ле NetReactorSlayer
Мо­дуль пос­ле NetReactorSlayer

При­чем деоб­фуска­цией мож­но управлять: у прог­раммы есть клю­чи коман­дной стро­ки. К при­меру, при исполь­зовании клю­ча --no-deob (Don’t deobfuscate methods) мы получа­ем исходный обфусци­рован­ный байт‑код метода в том виде, в котором он хра­нит­ся в фай­ле.

При отсутс­твии же это­го клю­ча по умол­чанию NetReactorSlayer пыта­ется отфиль­тро­вать код от паразит­ных инс­трук­ций в исходный вид до обфуска­ции. Нап­ример, обфусци­рован­ный код иско­мого метода isValid выг­лядит вот так:

А пос­ле деоб­фуска­ции сво­рачи­вает­ся в коротень­кое однос­троч­ное выраже­ние:

То есть мы фак­тичес­ки добились сво­ей цели — откры­ли код и даже деоб­фусци­рова­ли его, но, к сожале­нию, столь близ­кое счастье сно­ва усколь­зает от нас. Деоб­фусци­рован­ный модуль нап­рочь нерабо­тос­пособен: при запус­ке прог­раммы ошиб­ки сып­лются в самых неожи­дан­ных мес­тах, и никакие ком­бинации клю­чей NetReactorSlayer не реша­ют эту проб­лему.

При бли­жай­шем рас­смот­рении мы понима­ем и ее суть: нес­мотря на всю свою полез­ность, NetReactorSlayer — не вол­шебная кноп­ка, а раз­вива­ющий­ся про­ект, к сожале­нию далекий от совер­шенс­тва. В некото­рых методах наз­вания потеря­ны, код так и не открыт, и деоб­фуска­ция оставля­ет желать луч­шего.

Ес­ли у вас мно­го вре­мени и тер­пения, мож­но вдум­чиво и кро­пот­ливо пофик­сить каж­дый проб­лемный метод, но мы, как обыч­но, поп­робу­ем най­ти более корот­кий путь. По счастью, у нас есть исходный код NetReactorSlayer, поп­робу­ем его про­ана­лизи­ровать. Сно­ва не буду вда­вать­ся в под­робнос­ти, жела­ющие могут открыть про­ект и под­робно в нем разоб­рать­ся. Вмес­то это­го я заос­трю вни­мание на опре­делен­ных момен­тах.

Рас­шифров­кой кода в про­екте занима­ется модуль NecroBit.cs, а кон­крет­но метод Execute. Этот метод счи­тыва­ет из обфусци­рован­ного модуля блок зашиф­рован­ных дан­ных и в два при­ема рас­шифро­выва­ет его. Пос­ле стро­ки:

мас­сив methodsData содер­жит рас­шифро­ван­ный код методов обфусци­рован­ного модуля. Давайте пос­мотрим, что NetReactorSlayer про­делы­вает с этим мас­сивом даль­ше, и выяс­ним при­мер­ный фор­мат хра­нения дан­ных в нем. Цикл чте­ния и ана­лиза всех рас­шифро­ван­ных методов выг­лядит так:

Вот сра­зу за этим мес­том мы уже зна­ем сме­щение до рас­шифро­ван­ного IL-кода метода нуж­ного нам индекса и сам рас­шифро­ван­ный код. В нашем слу­чае IL-код метода isValid выг­лядит так:

Что­бы любая лицен­зия ста­ла валид­ной, нам дос­таточ­но поменять в этом коде два пер­вых бай­та на сле­дующие:

В исходном (нерас­шифро­ван­ном) модуле по это­му RVA зна­чения двух бай­тов соот­ветс­твен­но рав­ны 9E F4. По счастью, метод шиф­рования дан­ных — обыч­ный XOR по клю­чу.

Счи­таем новые зна­чения этих двух бай­тов:

Ме­няем эти два бай­та на новые зна­чения и на вся­кий слу­чай про­веря­ем пра­виль­ность замены, еще раз нат­равив на исправ­ленный модуль NetReactorSlayer.

Теперь и вправ­ду декоди­рован­ное тело метода содер­жит толь­ко return true, что и под­твержда­ет запуск прог­раммы — лицен­зия под­ходит! Таким обра­зом, мы получи­ли не толь­ко полез­ный рас­ширя­емый и совер­шенс­тву­емый инс­тру­мент для ревер­са при­ложе­ний, защищен­ных .NET Reactor (в том чис­ле нес­тандар­тных), но и быс­трый спо­соб пат­ча подоб­ных при­ложе­ний без пол­ного ревер­са и перес­борки прог­раммы.

Полезные ссылки:

Дима (Kozhuh)

Эксперт в кибербезопасности. Работал в ведущих компаниях занимающихся аналитикой компьютерных угроз. Анонсы новых статей в Телеграме.

Добавить комментарий