Хакер - Ангард! Реверсим приложение, защищенное DNGuard

Хакер - Ангард! Реверсим приложение, защищенное DNGuard

hacker_frei

https://t.me/hacker_frei

МВК

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

.NET-обфуска­тор DNGuard HVM сущес­тву­ет доволь­но дол­гое вре­мя, за которое он успел обза­вес­тись изрядным количес­твом фич (анти­отладка и анти­дамп, тоталь­ное шиф­рование всей информа­ции в модуле и весь­ма успешное про­тиво­дей­ствие ста­тичес­кому и динами­чес­кому ана­лизу). Нес­мотря на широкую извес­тность и неп­лохую изу­чен­ность (в свое вре­мя CodeCracker запилил мно­жес­тво рас­паков­щиков под раз­ные вер­сии DNGuard и сопутс­тву­ющих фрей­мвор­ков), пос­ледние нес­коль­ко лет средс­тва ана­лиза и рас­паков­ки для акту­аль­ных вер­сий это­го обфуска­тора нап­рочь отсутс­тву­ют. Вплоть до того, что популяр­ные инс­тру­мен­ты (и даже Detect It Easy) до сих пор не научи­лись их даже детек­тировать. Поэто­му, наде­юсь, сегод­няшняя статья поможет читате­лю разоб­рать­ся, как рас­познать эту заразу и бороть­ся с ней под­ручны­ми средс­тва­ми.

WARNING

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

Итак, пред­положим, что в твои заг­ребущие руки попала сбор­ка, защищен­ная DNGuard. Есть два вари­анта. Пер­вый: нам повез­ло, и это одна из ста­рых изу­чен­ных вер­сий, тог­да все ясно и мож­но сме­ло поль­зовать­ся одним из дам­перов DNGuard_HVM_Unpacker от CodeCracker. Воз­можно, я ког­да‑нибудь и рас­ска­жу об осо­бен­ностях и даже прин­ципах работы этих ути­лит. Одна­ко DNGuard совер­шенс­тву­ется стре­митель­но, а про­ект, судя по все­му, дав­но заб­рошен: пос­ледняя вер­сия под­держи­ваемо­го фрей­мвор­ка была 4.2, а физичес­кая при­вяз­ка к кон­крет­ным фай­лам биб­лиотек нас­толь­ко силь­на, что дела­ет исполь­зование дан­ных нарабо­ток малопо­лез­ным в деоб­фуска­ции новых вер­сий.

По­это­му сра­зу перехо­дим к более грус­тно­му вари­анту. Как я уже писал выше, хоть акту­аль­ные вер­сии и не детек­тятся, одна­ко силь­но и не скры­вают­ся. В рабочем катало­ге прог­раммы сра­зу бро­сает­ся в гла­за биб­лиоте­ка runtime.dll (или со схо­жим наз­вани­ем), с навешен­ным на ней VMProtect на минимал­ках.

Внут­ри биб­лиоте­ки обна­ружи­вают­ся две экспор­тиру­емые фун­кции: VMRuntime и GetUserString (или ResolveString).

Еще одна харак­терная чер­та — VMProtect’овская сек­ция hvm0, из‑за которой этот обфуска­тор, нас­коль­ко я понимаю, и получил такое наз­вание.

Впро­чем, наличие этой биб­лиоте­ки в катало­ге в явном виде вов­се необя­затель­но, самые пос­ледние вер­сии научи­лись дер­жать ее в зашиф­рован­ном виде в теле основной прог­раммы, рожая ее при заг­рузке в сис­темные вре­мен­ные катало­ги типа Temp или ProgramData. Cама прог­рамма, защищен­ная этим обфуска­тором, тоже выг­лядит дос­таточ­но харак­терным обра­зом при заг­рузке в отладчик вро­де dnSpy.

Как вид­но, для стар­тап‑кода прог­раммы харак­терно наличие клас­са с метода­ми CheckRuntimeCheckStringGetUserString и так далее и импортом упо­мяну­тых выше фун­кций из биб­лиоте­ки Runtime.dll. Так же как и во всех серь­езных обфуска­торах наз­вания осталь­ных клас­сов, методов, строк и ресур­сов жес­тко пошиф­рованы, а тела методов пус­ты или содер­жат вызов исклю­чения «Error, DNGuard Runtime library not loaded!» (при­чем даже эта стро­ка может быть зашиф­рована).

Собс­твен­но, если поп­робовать запус­тить такое при­ложе­ние из отладчи­ка, мак­симум, что мы смо­жем уви­деть, — это ини­циали­зация стар­тап‑кода DNGuard до натив­ного вызова VMRuntime, пос­ле чего тот перех­ватыва­ет на себя JIT-ком­пилятор и мы про­щаем­ся с отладчи­ком, сло­вив подоб­ное исклю­чение.

По­пыт­ка при­атта­чить­ся к запущен­ному про­цес­су тоже не дает полез­ного резуль­тата, так же как и дамп его все­ми извес­тны­ми дам­перами. Поэто­му зак­рыва­ем dnSpy и начина­ем вспо­минать мат­часть, в том чис­ле информа­цию, изло­жен­ную в моей статье «Ре­вер­синг .NET. Как искать JIT-ком­пилятор в при­ложе­ниях».

Для тех, кто не читал статью, напом­ню ее суть в двух сло­вах. Как извес­тно, любая .NET-сбор­ка устро­ена сле­дующим обра­зом: кросс‑плат­формен­ный IL-код хра­нит­ся в спе­циаль­ных метадан­ных, из которых под­гру­жает­ся по мере исполне­ния каж­дого метода и ком­пилиру­ется в натив­ный код спе­циаль­ной фун­кци­ей. Ука­затель на нее воз­вра­щает фун­кция GetGit биб­лиоте­ки clrjit.dll, одна­ко фрей­мворк любез­но пре­дос­тавля­ет поль­зовате­лю самому уста­нав­ливать адрес на ком­пилятор, чем без­застен­чиво поль­зуют­ся соз­датели все­воз­можных обфуска­торов, под­меняя ее сво­ими про­цеду­рами рас­шифров­ки IL-кода.

Та­ким же обра­зом мож­но менять на свои фун­кции заг­рузки строк, ресур­сов и так далее. В час­тнос­ти, фун­кцию Module::ResolveStringRef(unsigned long,class BaseDomain *,bool), которая воз­вра­щает ука­затель на зап­рашива­емый стро­ковый литерал. По счастью, ни один даже самый прод­винутый обфуска­тор не может пол­ностью под­менить собой .NET-фрей­мворк с JIT-ком­пиляци­ей, хотя бы из сооб­ражений сис­темной сов­мести­мос­ти. Поэто­му, нес­мотря на все под­мены, управле­ние все рав­но в ито­ге переда­ется на ори­гиналь­ные обра­бот­чики. Более того, биб­лиоте­ка mscorlib.ni.dll пре­дос­тавля­ет мас­су воз­можнос­тей для работы с низ­коуров­невыми объ­екта­ми (фай­лы, даты, стро­ки, сло­вари и про­чее), через которые мож­но отсле­живать логику работы прог­раммы уже из откомпи­лиро­ван­ного исполня­емо­го натив­ного кода.

Все это избавля­ет нас от необ­ходимос­ти про­шибать головой железо­бетон­ную сте­ну девир­туали­зации VMProtect, ана­лизи­руя «изнутри» в нашем любимом x64dbg уже рас­шифро­ван­ные биб­лиоте­кой runtime.dll код и стро­ки. Поп­робу­ем рас­смот­реть исполь­зование опи­сан­ного метода на при­мерах.

До­пус­тим, нам для начала инте­рес­но опре­делить, какие стро­ковые кон­стан­ты были заг­ружены и исполь­зованы на опре­делен­ном учас­тке работы прог­раммы. Заг­ружа­ем прог­рамму в отладчик x32dbg и идем на вклад­ку «Отла­доч­ные сим­волы». В спис­ке заг­ружен­ных биб­лиотек спра­ва находим clr.dll, жмем на пра­вую кноп­ку мыши и заг­ружа­ем отла­доч­ные сим­волы для это­го модуля.

В спис­ке отла­доч­ных сим­волов находим сим­вол public: struct OBJECTHANDLE__ * __thiscall Module::ResolveStringRef(unsigned long,class BaseDomain *,bool) и ста­вим на него точ­ку оста­нова. Запус­каем прог­рамму и тор­мозим­ся на этой точ­ке. Пока ничего инте­рес­ного, она выз­вана из изга­жен­ного VMProtect’oвской вир­туали­заци­ей кода runtime.dll. Запус­каем выпол­нение до воз­вра­та из биб­лиоте­ки. На выходе уже инте­рес­нее — в EAX появи­лась ссыл­ка на ука­затель на рас­шифро­ван­ную стро­ку.

Уби­раем точ­ку оста­нова на вхо­де ResolveStringRef и перено­сим ее на выход, добавив печать воз­вра­щаемой стро­ки.

За­пус­каем прог­рамму и — бин­го! — име­ем в жур­нале отладчи­ка пол­ный спи­сок заг­ружен­ных и рас­шифро­ван­ных в про­цес­се работы при­ложе­ния стро­ковых кон­стант. Теперь нам надо най­ти срав­нение, пос­ле которо­го выпол­нение прог­раммы идет по нуж­ной нам вет­ке. Для это­го точ­но так же заг­ружа­ем отла­доч­ные сим­волы уже для биб­лиоте­ки mscorlib.ni.dll. Мы обна­ружи­ваем уди­витель­ное мно­жес­тво натив­ных методов для низ­коуров­невой работы со стро­ками. Нап­ример, самые полез­ные из них.

Срав­нить две стро­ки:

mscorlib.ni.dll.System.String.Compare(System.String, System.String, Boolean)

Cкле­ить две стро­ки:

mscorlib.ni.dll.System.String.Concat(System.String, System.String)

Заг­рузить стро­ку из сим­воль­ного мас­сива:

mscorlib.ni.dll.System.String.CtorCharArray(Char[])

Заг­рузить стро­ку по ука­зате­лю на сим­воль­ный мас­сив:

mscorlib.ni.dll.System.String.CtorCharPtr(Char*)

Срав­нить две стро­ки:

mscorlib.ni.dll.System.String.Equals(System.String, System.String)

Прис­воить стро­ку:

mscorlib.ni.dll.System.String.Intern(System.String)

Вер­нуть подс­тро­ку:

mscorlib.ni.dll.System.String.Substring(Int32, Int32)

Заг­рузить сим­воль­ный мас­сив из стро­ки:

mscorlib.ni.dll.System.String.ToCharArray()

Пре­обра­зовать стро­ку в ниж­ний регистр:

mscorlib.ni.dll.System.String.ToLower()

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

Срав­нить вре­мен­ные про­межут­ки:

mscorlib.ni.dll.System.DateTimeOffset.CompareTo(System.DateTimeOffset)

Сло­жить вре­мен­ные про­межут­ки:

mscorlib.ni.dll.System.DateTimeOffset.op_Addition(System.DateTimeOffset, System.TimeSpan)

Вы­честь из вре­мени:

mscorlib.ni.dll.System.DateTime.Subtract(System.TimeSpan)

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

mscorlib.ni.dll.System.DateTime.op_Addition(System.DateTime, System.TimeSpan)

Поз­же?

mscorlib.ni.dll.System.DateTime.op_GreaterThan(System.DateTime, System.DateTime)

Рань­ше?

mscorlib.ni.dll.System.DateTime.op_LessThan(System.DateTime, System.DateTime)

Од­новре­мен­но?

mscorlib.ni.dll.System.DateTime.op_Equality(System.DateTime, System.DateTime)

От­сле­див нуж­ное срав­нение и подоб­рав для него под­ходящее усло­вие, мож­но даже не заботить­ся о том, как имен­но и куда ском­пилиро­вал­ся IL код, — при работе в отладчи­ке управле­ние все рав­но при­дет в это мес­то и оста­новит­ся в нуж­ный момент.

Итак, мы разоб­рались, какую про­вер­ку и на что нуж­но менять, но что делать даль­ше? Мы же не можем каж­дый раз запус­кать прог­рамму в отладчи­ке, что­бы руками под­ска­зывать ей, как даль­ше работать. По сути, мы не зна­ем даже наз­вания клас­са и метода, в котором про­исхо­дит про­вер­ка. Да даже если бы и зна­ли, тол­ку все рав­но мало — код по‑взрос­лому зашиф­рован, а дек­риптор вир­туали­зиро­ван самим VMProtect’ом, пус­кай на минимал­ках.

В подоб­ных слу­чаях, как я уже писал, мож­но сурово идти до кон­ца, как делал в свое вре­мя CodeCracer, и, пол­ностью или час­тично про­бив­шись сквозь вир­туали­зиро­ван­ный код, написать свой деоб­фуска­тор. Или же пой­ти по более лег­кому пути, соз­дав лоадер, пат­чащий IL-код в момент, ког­да он уже рас­шифро­ван и пода­ется на вход JIT-ком­пилято­ра. Суть этих двух спо­собов я объ­яснял в статье, поэто­му не буду пов­торять­ся, а в зак­лючение прос­то при­веду нес­коль­ко советов и пред­ложений, как реали­зовать эти идеи.

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

Не буду их дуб­лировать, что­бы не перег­ружать собс­твен­ный текст, прос­то обри­сую прин­цип в двух сло­вах. Есть заг­рузчик обфусци­рован­ной прог­раммы на C# (rbloader), который дела­ет инъ­екцию кода, содер­жащего­ся в натив­ной биб­лиоте­ке. К статье при­лага­ются при­меры про­ектов инъ­екто­ров, которые демонс­три­руют реаль­ные име­на ком­пилиру­емых клас­сов и методов исполня­емой прог­раммы, и даже выпол­няет­ся дизас­сем­бли­рова­ние их рас­шифро­ван­ного IL-кода.

Ко­неч­но, эта статья тоже уста­рела, и при­веден­ный там код тре­бует допили­вания под сов­ремен­ные фрей­мвор­ки. К при­меру, исполь­зуемая для получе­ния реаль­ного адре­са JIT-ком­пилято­ра фун­кция getJit сей­час содер­жится не в упо­мина­емой там биб­лиоте­ке mscorjit.dll, а в биб­лиоте­ке clrjit.dll. Но это мелочи, впол­не пре­одо­лимые, если не замахи­вать­ся сра­зу на соз­дание пол­ного деком­пилято­ра или дам­пера, а начать с прос­тых, но полез­ных вещей.

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

Пос­тавив условную точ­ку оста­нова на JIT-ком­пилятор c логиро­вани­ем, мож­но через ftn при­вязать рас­шифро­ван­ный IL-код к откомпи­лиро­ван­ному натив­ному коду метода, одна­ко его имя и имя его клас­са на момент ком­пиляции из отладчи­ка получить зат­рудни­тель­но. Хал­турный спо­соб, пред­ложен­ный мной в пре­дыду­щей статье, здесь не работа­ет.

Вот тут нам и при­годит­ся заг­рузчик с инъ­екто­ром Дани­еля Пис­телли. Из кода, инъ­екти­руемо­го в CompileMethod, получить имя ком­пилиру­емо­го метода и его клас­са не прос­то, а очень прос­то. Более того, пря­мо в тело обра­бот­чика мож­но встро­ить авто­патч све­жерас­шифро­ван­ного IL-кода метода.

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

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei



Report Page