Мы неоднократно публиковали развернутые статьи по дизассемблированию приложений и не собираемся останавливаться на достигнутом. Однако многие программы используют различные механизмы антиотладки, из‑за которых подступиться к ним становится непросто. Сегодня мы поговорим о том, как вскрыть популярную у разработчиков защиту Enigma версии 5 и выше, использующую продвинутые инструменты противодействия анализу и взлому.
Бытует распространенное мнение, что к категории крутых, хороших годных защит относятся VMProtect и Themida. В них, мол, и криптование, и виртуальные машины, и антиотладка. Все остальное — так, для лохов и младших школьников. Например, для какой‑нибудь «Энигмы» в сети можно найти кучу туториалов и видосов с инструкциями по взлому, воспользовавшись которыми, любой нуб может почувствовать себя кулхакером.
Отчасти утверждение справедливо: туторов, включая видео, в интернете действительно полно, а при желании можно найти и однокликовые тулзы для взлома приложений. Некоторые из них даже работают. С небольшой оговоркой — это касается старых версий защиты, которая была, мягко говоря, не очень. В последних версиях Enigma создатели постарались как можно сильнее осложнить жизнь хакерам.
Большинство решений они позаимствовали у «взрослых» защит (ниже читатель сможет в этом убедиться). Однако, как известно, в этом мире нет ничего нового, и использование известных решений не делает взлом защиты более простым и приятным. Скорее наоборот. Давай убедимся в этом, хорошенько пощупав Enigma собственными руками.
Ну что ж, думаем мы, не боги горшки обжигают: не могли же они придумать что‑то принципиально новое? Попробуем действовать по образу и подобию, авось получится. И вот тут нас поджидает неприятный сюрприз.
Практически все «кулхакерские» мануалы начинаются с одной и той же неизменной фразы: «загрузите программу в отладчик» или «создайте дамп приложения». Именно на этом шаге исследователя подстерегает первый коварный облом. В любимый всеми x64dbg программа не грузится вообще, дамперы запущенную софтину тоже не желают дампить, а если это и удается, то на выходе получается совершенно неработоспособный (и уж и близко не дотнетовский) кусок памяти. Даже в работающем приложении .NET не распознается от слова «совсем», dnSpy ее родной не признает и аттачить не хочет. Приаттачить программу удается разве что в x64dbg, но при малейшей попытке сдвинуться с точки останова процесс мгновенно закрывается. В общем, налицо все признаки взрослой защиты — шифрование кода, защита от дампа и отладчика.
Придется браться за дело основательно. Для начала установим плагины ScyllaHide и Scylla, чтобы хоть как‑то заработала отладка. Благодаря первой тулзе программа наконец‑то позволяет загрузить себя в отладчик. Однако радость оказывается преждевременной: после прерывания в процессе никакие настройки ScyllaHide не позволяют нам продвинуться дальше, программа захлопывается с завидным упорством. Scylla придет на помощь чуть позже, пока же попробуем извлечь всю выгоду из маленькой победы, которую мы только что одержали.
Итак, программа загружена в отладчик и пошагово трассируется. Потихоньку двигаемся, минуя несколько саморасшифровывающихся участков кода, и обнаруживаем, что они — всего лишь обвязка для огромной упакованной и зашифрованной секции, стартовая точка которой выглядит так:
Вот тут нам и пригодится ранее установленная Scylla — эту секцию можно сдампить в EXE-файл. Толку, правда, от этого мало: модуль неработоспособный. Вдобавок это явно не наш искомый защищенный модуль, дотнетом там по‑прежнему не пахнет, однако пахнет — сюрприз! — Delphi. Оказывается, Enigma написана на дельфи! Но это вовсе не упрощает нам жизнь.
Углубляясь пошагово в последний call, мы довольно быстро вязнем в мешанине безумного кода. Похоже, мы не сильно продвинулись в нашем исследовании: замучаешься пошагово перемещаться до ближайшего осмысленного места, а безболезненно прерваться в процессе выполнения кода антиотладчик по‑прежнему не дает.
Попробуем зайти с другой стороны. Внимательно посмотрев на сдампленный модуль, мы обнаруживаем, что он не такой уж и бесполезный. У него, внезапно, есть экспортируемые символы:
Как нетрудно догадаться, EP — это Enigma Protection, а точки входа указывают на какие‑то очень полезные ништяки, содержащиеся внутри.
Ткнувшись подряд в некоторые из них, мы обнаружим, что через пару длинных jmp все они упираются в конструкцию следующего вида:
Что‑то подобное мы уже наблюдали в VMProtect. Ну конечно: мы таки вышли на виртуальную машину!
Теперь установим брейк‑пойнт на начало интерпретатора и еще раз убедимся — да, так оно и есть, этот фрагмент периодически вызывается среди необозримых просторов безумного кода.
Попробуем извлечь из нашего открытия максимум пользы. Прописываем в свежеиспеченную точку останова логирование — для начала RAX и параметр на стеке {RAX} {[RSP]}, после чего запускаем программу. Она, конечно, мгновенно захлопывается, ибо антиотладчик не дремлет, однако на прощанье она подарит нам развернутый лог вызовов виртуальной машины:
Безумный код потихоньку начинает обретать осмысленность: в частности, мы нашли конкретное место, после которого в программе срабатывает антиотладчик. Ставим условие для бряка (RAX==8 [RSP]==0x5C7C56D) и наблюдаем за поведением программы сразу после данного вызова виртуальной машины. Оказывается, вызов — это часть следующей конструкции:
Соответственно, закоротив переход jz .00007FF7`BA167ADD, можно спокойно двигаться дальше (в смысле, до следующей подобной ловушки!). Чтобы не отнимать у читателя слишком много времени, коснемся принципа действия виртуальной машины, точку входа в интерпретатор которой мы только что нашли.
Вспоминая VMProtect, предположим, что волшебное число на стеке при входе в интерпретатор как‑то связано с адресом начала блока шитого кода, выполняемого интерпретатором при вызове. Вдумчиво трассируя интерпретатор, натыкаемся на такой код:
Предоставляю читателю самому разбираться с системой команд виртуальной машины, ибо она запутана, бессмысленна и беспощадна. При этом необязательно ее полное препарирование — как показал вышеописанный пример с антиотладчиком, местами достаточно знать вызовы отдельных блоков кода, на которые, кстати, указывают торчащие из экспортируемых функций уши. Для особо любопытных отмечу, что попавшая в конец вышеописанного фрагмента команда с опкодом 0x231 — это косвенный вызов процессорного (не виртуального) кода по заданному относительному смещению.
Остановимся подробнее на том, как формируется указанное смещение, — это поможет нам понять принцип взаимодействия отдельных блоков шитого кода друг с другом. Рассмотрим последовательность выхода из интерпретатора:
Мы видим, что из виртуального кода могут выполняться относительные вызовы непосредственно машинного кода (команда 0x631). Кроме того, по возврату из блока виртуального кода (ну, например, команда с опкодом 0x60) происходит возврат на заданный блок машинного кода, выполняющий конкретные действия. Таким образом фрагменты машинного и виртуального кода выполняются в определенной ветвящейся и вложенной последовательности, доступной для исследования пытливому уму при наличии достаточного количества свободного времени и мотивации.
В итоге можно смело утверждать: универсальных защит не существует. Взломать можно все что угодно, было бы свободное время и, безусловно, желание.
Автор @МВК
Бытует распространенное мнение, что к категории крутых, хороших годных защит относятся VMProtect и Themida. В них, мол, и криптование, и виртуальные машины, и антиотладка. Все остальное — так, для лохов и младших школьников. Например, для какой‑нибудь «Энигмы» в сети можно найти кучу туториалов и видосов с инструкциями по взлому, воспользовавшись которыми, любой нуб может почувствовать себя кулхакером.
Отчасти утверждение справедливо: туторов, включая видео, в интернете действительно полно, а при желании можно найти и однокликовые тулзы для взлома приложений. Некоторые из них даже работают. С небольшой оговоркой — это касается старых версий защиты, которая была, мягко говоря, не очень. В последних версиях Enigma создатели постарались как можно сильнее осложнить жизнь хакерам.
Большинство решений они позаимствовали у «взрослых» защит (ниже читатель сможет в этом убедиться). Однако, как известно, в этом мире нет ничего нового, и использование известных решений не делает взлом защиты более простым и приятным. Скорее наоборот. Давай убедимся в этом, хорошенько пощупав Enigma собственными руками.
ЭКСПЕРИМЕНТ
Ради эксперимента скачаем какую‑нибудь программу (для простоты — дотнетовскую) и натравим на нее, например, Exeinfo. Предположим, анализатор опознал упаковщик как Enigma Protector x64 [v.5.0 — 7.0]. Вот и отлично, мысленно выдыхаем мы: не VMProtect и не Themida, а по Enigma точно что‑то можно найти на YouTube. Лезем в гугл — и правда, видосов полно, однако максимальная ломаемая версия — 4, а по x64 нет вообще ничего.Ну что ж, думаем мы, не боги горшки обжигают: не могли же они придумать что‑то принципиально новое? Попробуем действовать по образу и подобию, авось получится. И вот тут нас поджидает неприятный сюрприз.
Практически все «кулхакерские» мануалы начинаются с одной и той же неизменной фразы: «загрузите программу в отладчик» или «создайте дамп приложения». Именно на этом шаге исследователя подстерегает первый коварный облом. В любимый всеми x64dbg программа не грузится вообще, дамперы запущенную софтину тоже не желают дампить, а если это и удается, то на выходе получается совершенно неработоспособный (и уж и близко не дотнетовский) кусок памяти. Даже в работающем приложении .NET не распознается от слова «совсем», dnSpy ее родной не признает и аттачить не хочет. Приаттачить программу удается разве что в x64dbg, но при малейшей попытке сдвинуться с точки останова процесс мгновенно закрывается. В общем, налицо все признаки взрослой защиты — шифрование кода, защита от дампа и отладчика.
Придется браться за дело основательно. Для начала установим плагины ScyllaHide и Scylla, чтобы хоть как‑то заработала отладка. Благодаря первой тулзе программа наконец‑то позволяет загрузить себя в отладчик. Однако радость оказывается преждевременной: после прерывания в процессе никакие настройки ScyllaHide не позволяют нам продвинуться дальше, программа захлопывается с завидным упорством. Scylla придет на помощь чуть позже, пока же попробуем извлечь всю выгоду из маленькой победы, которую мы только что одержали.
Итак, программа загружена в отладчик и пошагово трассируется. Потихоньку двигаемся, минуя несколько саморасшифровывающихся участков кода, и обнаруживаем, что они — всего лишь обвязка для огромной упакованной и зашифрованной секции, стартовая точка которой выглядит так:
Код:
push rcx
push rdx
push r8
push r9
mov r8,000B5ED8E
call .00007FF7`BA4D9610
mov r8,0
mov rdx,1
mov rcx,[rsp][000000018]
call .00007FF7`BA4E49A0
mov r8,0
mov rdx,1
mov rcx,[rsp][000000018]
call .00007FF7`BA13E400
pop r9
pop r8
pop rdx
pop rcx
call .00007FF7`BA12A210
Углубляясь пошагово в последний call, мы довольно быстро вязнем в мешанине безумного кода. Похоже, мы не сильно продвинулись в нашем исследовании: замучаешься пошагово перемещаться до ближайшего осмысленного места, а безболезненно прерваться в процессе выполнения кода антиотладчик по‑прежнему не дает.
Попробуем зайти с другой стороны. Внимательно посмотрев на сдампленный модуль, мы обнаруживаем, что он не такой уж и бесполезный. У него, внезапно, есть экспортируемые символы:
Код:
0 .00007FF7`BABBD06E EP_RegHardwareID
1 .00007FF7`BABBD073 EP_RegHardwareIDA
2 .00007FF7`BABBD078 EP_RegHardwareIDW
3 .00007FF7`BABBD07D EP_RegCheckKey
4 .00007FF7`BABBD082 EP_RegCheckKeyA
5 .00007FF7`BABBD087 EP_RegCheckKeyW
6 .00007FF7`BABBD08C EP_RegSaveKey
7 .00007FF7`BABBD091 EP_RegSaveKeyA
8 .00007FF7`BABBD096 EP_RegSaveKeyW
9 .00007FF7`BABBD09B EP_RegLoadKey
10 .00007FF7`BABBD0A0 EP_RegLoadKeyA
11 .00007FF7`BABBD0A5 EP_RegLoadKeyW
12 .00007FF7`BABBD0AA EP_RegLoadAndCheckKey
13 .00007FF7`BABBD0AF EP_RegCheckAndSaveKey
14 .00007FF7`BABBD0B4 EP_RegCheckAndSaveKeyA
15 .00007FF7`BABBD0B9 EP_RegCheckAndSaveKeyW
16 .00007FF7`BABBD0BE EP_RegDeleteKey
...
Ткнувшись подряд в некоторые из них, мы обнаружим, что через пару длинных jmp все они упираются в конструкцию следующего вида:
Код:
push 005C7AC29
jmp .00007FF7`BA4F3F50
ДИЗАССЕМБЛИРУЕМ
Ищем в распакованном‑расшифрованном коде начало интерпретатора:
Код:
push rsp
push rax
push rcx
push rdx
push rbx
push rbp
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
pushfq
mov dl,1
mov rsi,0
lea rdi,[00007FF7`BA5032F0]
add rdi,rsi
lea rdi,[rdi]
mov rcx,1
...
Попробуем извлечь из нашего открытия максимум пользы. Прописываем в свежеиспеченную точку останова логирование — для начала RAX и параметр на стеке {RAX} {[RSP]}, после чего запускаем программу. Она, конечно, мгновенно захлопывается, ибо антиотладчик не дремлет, однако на прощанье она подарит нам развернутый лог вызовов виртуальной машины:
Код:
610 5C7C535
143336BE190 5C7CACA
143336BE1D0 5C7CACE
14335219760 5C7CAC5
630 5C7C754
7FF8DBCE0000 5C7C76D
143350D0000 5C7C778
7FF8DB160000 5C7C77F
7FF8DB17A360 5C7C770
615FF24EC83C3 5C7C777
7FF8DBD7C8C0 5C7C73F
7FF8D69911B0 5C7C73F
7FF8DBD7CD40 5C7C73F
7FF8DBD7D020 5C7C73F
7FF8DBD7CBE0 5C7C73F
7FF8DBD7CC20 5C7C73F
7FF8DBD7FF90 5C7C73F
...
143352119F0 5C7C549
143336BE928 5C7C544
F12A436D40745745 5C7C55E
15 5C7C56D
63BFFFD10 5C7C5AE
0 5C7C5BB
12 5C7C5BC
143352118F0 5C7C549
14335211910 5C7C549
14335211930 5C7C549
14335211950 5C7C549
14335211970 5C7C549
14335211990 5C7C549
143352119B0 5C7C549
143352119D0 5C7C549
143336BE968 5C7C544
704562405E78BE75 5C7C55E
8 5C7C56D
Код:
mov rcx,[00007FF7`BA49D9F0]
mov r8d,7
call .00007FF7`BA17C750 // вызов виртуальной машины
mov rcx,[rbp][-018]
test rcx,rcx
jnz .00007FF7`BA167AA2
lea rcx,[00007FF7`BA49B470]
call .00007FF7`BA1270C0 // GetModuleHandleA (ntdll.dll)
mov rcx,rax
mov rdx,rbx
call .00007FF7`BA127160 // GetProcAddress (NtSetInformationThread)
mov [rbp][-8],rax
cmp q,[rbp][-8],0
jz .00007FF7`BA167ADD
call .00007FF7`BA1278A0
mov rcx,rax
mov r9d,0
mov r8,0
mov edx,000000011
call q,[rbp][-8]
nop
mov rcx,rbp
call .00007FF7`BA1679D0
...
ПРИНЦИП ДЕЙСТВИЯ
Для начала отмечу, что машина эта мутирующая, то есть код формируется случайным образом, все адреса и ссылки на двух разных защищенных модулях будут отличаться. То есть ловить аналогичные вызовы по смещению или маске довольно неприятно, хотя некоторые блоки кода (например, вышеприведенные) сохраняются и могут быть найдены по маске.Вспоминая VMProtect, предположим, что волшебное число на стеке при входе в интерпретатор как‑то связано с адресом начала блока шитого кода, выполняемого интерпретатором при вызове. Вдумчиво трассируя интерпретатор, натыкаемся на такой код:
Код:
mov [rbp][-8],ecx ; волшебное число на стеке при входе в интерпретатор
mov [rbp][-010],rdx
mov edx,[00007FF7`BA5032B0] ; волшебное число, с которым ксорится эта цифра для дополнительного мутирования кода
mov eax,[rbp][-8]
xor eax,edx ; вот здесь это и происходит
mov [rbp][-020],eax
mov rax,[00007FF7`BA5032C0] ; относительная база шитого кода виртуальной машины — смещение начала шитого кода относительно начала модуля
mov edx,[rbp][-020]
mov edx,[rax][rdx]*4
mov rax,[00007FF7`BA5032B8] ; абсолютная база — адрес начала модуля
lea rax,[rdx][rax]
; итого, адрес первой команды шитого кода виртуальной машины при входе в интерпретатор = [00007FF7`BA5032B8]+ [00007FF7`BA5032C0]+ (ecx xor [00007FF7`BA5032B0])*4 шитый код всегда выровнен на двойное слово и смещение задается именно в этих двойных словах
mov [rbp][-000000088],rax
mov rax,[rbp][-000000088]
cmp d,[rax],000000231 ; а вот и выборка первой команды
jnz .00007FF7`BA4ECB49
...
Остановимся подробнее на том, как формируется указанное смещение, — это поможет нам понять принцип взаимодействия отдельных блоков шитого кода друг с другом. Рассмотрим последовательность выхода из интерпретатора:
Код:
mov rax,[rbp][-000000088] ; текущий указатель команд виртуальной машины
mov rdx,[rax][020] ; относительный адрес машинного (не виртуального) кода, на который будет выполнен переход по выходу из интерпретатора внутри виртуальной команды завершения блока
mov rax,[00007FF7`BA5032D0] ; абсолютная база — адрес начала модуля
lea rax,[rdx][rax] ; адрес машинного кода = [00007FF7`BA5032D0] + относительный адрес внутри виртуальной команды
mov [rbp][-018],rax
jmps .00007FF7`BA4F3E04
jmp .00007FF7`BA4ECAB8
mov rax,[rbp][-018] ; по выходу из интерпретатора RAX=адрес следующей команды для RET
mov rbx,[rbp][-0000000B8]
lea rsp,[rbp][0]
pop rbp
retn
ВЫВОД
Напрашивается вывод: обновленная и усовершенствованная версия Enigma Protector x64 способна доставить изрядную головную боль исследователю, однако на любой хитрый болт всегда отыщется гайка с левой резьбой. Заимствование методов антиотладки у широко известных и популярных протекторов позволяет находить в защите скрытые лазейки, действуя по аналогии.В итоге можно смело утверждать: универсальных защит не существует. Взломать можно все что угодно, было бы свободное время и, безусловно, желание.
Автор @МВК