birmaga.ru
добавить свой файл

1

обфускация и ее преодоление

крис касперски аргентинский болотный бобер nezumi el raton ака жирный нутряк ибн мыщъх
"sorry guys — there was so much fuzz on all message boards about the new Elliptic Curves DSA in latest Armadillo — we just had to do it... Take it as proof of concept, that it is not enough to implement strong crypto, you also should understand it for implementing well... Shouts go out to all, who made this possible especially to clive for donating some CPU power :-)"1

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

введение


Обфускацией (от английского obfuscation – буквально "запутывание") называется совокупность методик и средств, направленных на затруднение анализа программного кода. Существуют различные типы обфускаторов: одни занимаются интерпретируемыми языками типа Perl или PHP и "корежат" исходные тексты (удаляют комментарии, дают переменным бессмысленные имена, шифруют строковые константы etc), другие "перемалывают" байт-код виртуальных машин Java и .NET, что технически сделать намного труднее (РЕДАКТОРУ: уместно дать ссылку на статью по NET). Самые совершенные обфускаторы вламываются непосредственно в машинный код, "разбавляя" его мусорными инструкциями и выполняя целый ряд структурных (реже — математических) преобразований, изменяющих программу до неузнаваемости.

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


Удалять "мусор" в автоматическом режиме дизассемблеры еще не научились, а проанализировать мегабайты кода вручную методом "долота" — нереально. Нужны передовые методики реконструкции потока управления, расплавляющие "замусоренный" код и разделяющие его на "полезные" и "бесполезные" фракции, а их нет даже на уровне "теоретического понимания". И хотя кое-какие идеи на этот счет все же имеются (например, наложение маршрута трассировки на графы зависимостей по данным), до практической реализации еще далеко.


Рисунок 1 nfo от хакерской группы TMG, взломавшей обфускатор Armadillo и открыто выложившей его в осла

Методы обфускации активно используются продвинутыми упаковщиками типа Armadillo (ныне переименованного в Software Passport, который можно скачать с фирменного сайта http://siliconrealms.com/armadillo.shtml), eXtreme Protector (адрес сайта разработчиков http://www.oreans.com/xprotector/) и т. д. Большинство протекторов "запутывают" только свой собственный распаковщик, опасаясь вмешиваться в код защищаемой программы, т. к. это чревато неожиданным появлением глюков в самых различных местах. Какому программисту такая защита понравится? Тем не менее, обфускация процедур проверки серийного номера (ключевого файла) встречается достаточно часто. Обычно она реализуется в полуавтоматическом режиме, когда создатель защиты тем или иным образом взаимодействует с офускатором (например, пишет скрипт, который обфускатор транслирует в замусоренный машинный код, изображая из себя "неэффективный" компилятор).


Рисунок 2 официальный сайт протектора-обфускатора eXtreme Protector

Обфускация конкретно досаждает хакерам, препятствуя реконструкции алгоритмов и быстрому взлому защит, но эти проблемы меркнут перед ситуацией в антивирусной индустрии. Чтобы взломать программу, анализировать ее алгоритм в общем-то и необязательно. А вот обнаружить зловредный код (он же malware) без этого уже не удастся! Но заботы "стражей правопорядка" нас не волнуют, — пускай они сами с ними парятся, в конце концов за это им платят. Мы же сосредоточимся исключительно на методиках взлома "запутанных" программ, с которыми хакерам приходится сталкиваться все чаще и чаще.



Рисунок 3 попытка взлома программы, защищенной Armadill'oй, приводит к жутким ругательствам защиты

Предлагаемые здесь методики нацелены преимущество на программы, защищенные "испытательным сроком", то есть какое-то время программа должна запускаться в полнофункциональном режиме, не требуя ключа (а большинство программ именно на таких условиях и распространяются). Кому-то это условие может показаться излишне жестоким. А как же программы с заблокированными возможностями или программы, вообще не запускающиеся без ключа? Увы! В общем случае их взломать вообще невозможно! Если программист зашифровал часть программы стойким криптографическим алгоритмом, то без знания ключа до заблокированных возможностей хакеру уже будет не дотянуться! Правда, если у него есть хотя бы один-единственный ключ (несколько хакеров купили программу в складчину), то ситуация заметно упрощается, но в этом случае проще распространять сам ключ, чем ковыряться в недрах запутанного кода. Кстати, какими же все-таки приемами запутывания пользуются обфускаторы?

как работает обфускатор


Как говорят медики, СПИД — это еще не приговор. Тоже самое и обфускация. Далеко не каждый обфускатор использует продвинутые методики "запутывания" и высаживаться на измену при этом слове не надо.

В простейшем случае, полиморфный генератор просто "накачивает" программу кучей незначащих команд типа nop, xchg reg,reg, or reg,reg2; никогда не выполняющимися переходами типа xor reg,reg/jnz junk, где xor – значимая команда, а junk – "мертвый код", и т. д. Вот, например:
or ch, ch ; "мусор", не воздействующий на регистр ch,

; но воздействующий на регистр флагов, однако

; это воздействие перекрываются последующим xor

xor eax,eax ; потенциально значимая команда

; (почему "потенциально" будет пояснено ниже)




seto bl ; "мусор", устанавливающий bl в 1, если есть

; переполнение, а после xor его всегда нет

repne jnz short loc_43409A ; "мусор", передающий управление если не ноль,

; но после xor флаг нуля всегда установлен,

; плюс бессмысленный префикс repne

rep jnp short loc_43408D ; "мусор", передающий управление если нечет

; но после xor флаг четности всегда установлен

jo short loc_434094 ; "мусор", передающий управление если флаг

; переполнения установлен, а он сброшен xor

xchg ebx,ebx ; "мусор", обмен регистров ebx местами

Листинг 1 код, замусоренный обфускатором, в котором имеется всего лишь одна потенциально значимая команда — xor eax,eax

Не слишком сложный скрипт для IDA PRO найдет все явно незначимые команды и пометит их как "мусорные" или же вовсе удалит их к едреням. Ильфак уже давно написал "highlighter" — плагин, как раз и предназначенный для этой цели (см. рис. 4) и распространяющийся в исходных текстах на бесплатной основе: http://www.hexblog.com/ida_pro/files/highlighter.zip. Впрочем, бесплатность эта весьма условна. Чтобы скомпилировать плагин нужен IDA SDK, причем не какой-нибудь, а только последней версии. То есть, у большинства пользователей IDA Pro скомпилировать его не получится, но это не повод расстраиваться — ведь точно такую же штуку можно реализовать и самостоятельно, используя встроенный в IDA Pro язык скриптов, затратив на это буквально полчаса (сам язык подробно описан книге "Образ мышления — IDA PRO", электронную версию которой можно бесплатно скачать с моего мыщъхиного сервера ftp://nezumi.org.ru).

Рисунок 4 результат работы плагина highlighter, позволяющего маркировать "мусорные" команды "голубой чертой" (внимание: маркировка производится вручную! никакой автоматизацией тут и не пахнет!)


Более сложные обфускаторы "перемешивают" код, закручивая поток управления в запутанную спираль условных/безусловных переходов, использующих технику "перекрытия" команд: некоторые байты принадлежат сразу двум, а в некоторых случаях и трем (!) машинным инструкциям, что "ослепляет" дизассемблеры, заставляя их генерировать неполный и неправильный листинг!


Рисунок 5 визуализация потока выполнения "запутанной" программы: линейный код превращается в дремучий лес, в котором очень легко заблудиться

Впрочем, в интерактивном режиме (хвала IDA Pro) дизассемблировать код все-таки возможно, но очень уж утомительно. Лучше воспользоваться трассером, генерирующим листинг реально выполняемых машинных команд. Заодно это позволяет избавиться от части мусора и "мертвого" кода, но о трассерах мы еще поговорим, а пока вернемся к дизассемблерам. Рассмотрим фрагмент листинга, сгенерированный IDA Pro:
.adata:0043400E loc_43400E: ; CODE XREF: .adata:00434023j

.adata:0043400E ; .adata:loc_43401Aj

.adata:0043400E mov eax, 0EBB907EBh

.adata:00434013

.adata:00434013 loc_434013: ; CODE XREF: .adata:loc_43401Dj

.adata:00434013 seto bl ;  прыжок в середину команды

.adata:00434016 or ch, bh

.adata:00434018 jmp short loc_434025

.adata:00434018

.adata:0043401A loc_43401A: ; CODE XREF: .adata:00434009j

.adata:0043401A repne jmp short near ptr loc_43400E+4

.adata:0043401D

.adata:0043401D loc_43401D: ; CODE XREF: .adata:loc_43400Cj

.adata:0043401D jmp short near ptr loc_434013+2

Листинг 2 демонстрация техники "перекрытия" машинных команд, используемой обфускаторами

Обратите внимание на команду "043401Dh:jmp short loc_434013+2" (выделена полужирным), прыгающую по адресу 434013h+2h == 434015h, то есть в середину инструкции 434013h:seto bl (так же выделена полужирным). Именно, что в середину! С точки зрения дизассемблера (даже такого продвинутого как IDA Pro), команда является "атомарной" структурной единицей, то есть неделимой. На самом же деле, всякая машинная инструкция состоит из последовательности байт и может быть выполнена с любого места! Во всяком случае x86 процессоры не требуют выравнивания кода. Другими словами, у нас нет "команд" у нас есть только байты! Если начать выполнение инструкции не с первого байта мы получим совсем другую команду! К сожалению, IDA Pro не позволяет узнать какую. Чтобы выполнить переход "043401Dh:jmp short loc_434013+2" мы должны подвести курсор к метке loc_434013 и нажать , чтобы "раскрошить" дизассемблерный код на байты, а после перейти по адресу 434015h и нажать , чтобы превратить байты в дизассемблерный код, в результате чего получится следующее:

.adata:0043400E unk_43400E db 0B8h ; ┐ ; CODE XREF: .adata:loc_434023j

.adata:0043400F db 0EBh ; ы

.adata:00434010 db 7

.adata:00434011 db 0B9h ; │

.adata:00434012 loc_434012: ; CODE XREF: .adata:loc_43401Aj

.adata:00434012 jmp short loc_434023

.adata:00434014

.adata:00434014 nop

.adata:00434015

.adata:00434015 loc_434015: ; CODE XREF: .adata:loc_43401Dj

.adata:00434015 jmp short loc_43401F ;  прыжок сюда

.adata:00434017

.adata:00434017 std

.adata:00434018 jmp short loc_434025

.adata:0043401A

.adata:0043401A loc_43401A: ; CODE XREF: .adata:00434009j

.adata:0043401A repne jmp short loc_434012

Листинг 3 "вскрытие" наложенной команды

Мы видим, что на месте seto bl возникла пара инструкций jmp loc_43401F/std. Какой из двух листингов правильный? Первый или второй? По отдельности — ни тот, ни другой. Они становятся "правильными" только когда их двое! Но удержать эти подробности в голове — нереально, а быстро переключаться между двумя вариантами IDA Pro не позволяет. Остается загонять "альтернативный" листинг в комментарии, но это возможно только тогда их двое. Если же одна и та же машинная команда имеет три и более "точек входа", то комментарии уже не спасают и возникает путаница, вынуждающая вместо дизассемблера использовать трассер (сравнимте дизассемблерный листинг с протоколом трассера, приведенном в листинге 8).

Изощренные обфускаторы отслеживают зависимости по данным, внедряя осмысленные инструкции с "нулевым эффектом". Поясним это на конкретном примере. Допустим, встретилась обфускатору конструкция:
PUSH EAX ; <- последнее обращение к eax

MOV EAX,EBX ; <- реинициализация eax

Листинг 4 оригинальный код до обфускации

Легко показать, что между последним обращением к eax и его реницилизацией можно как угодно модифицировать регистр eax без ущерба для выполнения программы, поскольку любые операции присвоения все равно будут перекрыты командой mov eax,ebx. "Запутанный" код может выглядеть например, так:

push eax ;  последнее значимое обращение к eax

xor eax,eax ; мусор

l1:

inc eax ; мусор

jz l2 ; мусор

cmp eax, ebx ; мусор

jnz l1 ; мусор

cmp eax, ecx ; мусор

jge l1 ; мусор

l2:

sub eax, 666h ; мусор

shl eax, 1 ; мусор

mov eax, ebx ;  значимая реиницилизация eax

Листинг 5 код после обфускации

Еще обфускаторы могут временно сохранять регистр на стеке, а затем вволю "поизмывавшись" над ним, восстанавливать прежнее значение.
001B:0043402C 50 PUSH EAX ; сохраняем eax

001B:0043402D 51 PUSH ECX ; сохраняем ecx

001B:0043402E EB0F JMP 0043403F

001B:0043403F F2EBF5 REPNZ JMP 00434037

001B:00434037 EB0F JMP 00434048

001B:00434048 EBE9 JMP 00434033

001B:00434033 B8EB07B9EB MOV EAX,EBB907EB ; "гадим" в eax

001B:0043403B 08FD OR CH,BH ; "гадим" в ch

001B:0043403D EB0B JMP 0043404A

001B:0043404A F3EBE4 REPZ JMP 00434031

001B:00434031 EB0F JMP 00434042

001B:00434042 EBF6 JMP 0043403A

001B:0043403A EB08 JMP 00434044

001B:00434044 F2EB08 REPNZ JMP 0043404F

001B:0043404F 59 POP ECX ; восстанавливаем ecx

001B:00434050 58 POP EAX ; восстанавливаем eax

Листинг 6 временное сохранение регистров на стеке с последующим восстановлением (для наглядности вместо дизассемблерного текста здесь приведен протокол трассера)

Команда MOV EAX,EBB907EBh на первый взгляд выглядит "значимой", но на самом деле это "мусор", нейтрализуемый командами push eax/pop eax, и по сути весь этот конгломерат имеет нулевой эффект, то есть является совершенно ничего не делающим кодом. Поэтому, делать вывод о "значимости" команд нужно с очень большой осторожностью и до тех пор, пока не будет доказано, что данный кусок кода действительно имеет какой-то эффект, он должен считаться "мусором" по умолчанию.


Некоторые обускаторы любят внедрять подложные расшифровщики, расшифровывающие и тут же зашифровывающие произвольные фрагменты памяти (см. листинг 7).
00434105 83ED 06 SUB EBP,6

00434108 B8 3B010000 MOV EAX,13B

0043410D 03C5 ADD EAX,EBP

0043410F 33DB XOR EBX,EBX

00434111 81C3 01010101 ADD EBX,1010101

00434117 3118 XOR DWORD PTR DS:[EAX],EBX ; <- расшифровываем

00434119 8138 78540000 CMP DWORD PTR DS:[EAX],5478

0043411F 74 04 JE SHORT app_test.00434125

00434121 3118 XOR DWORD PTR DS:[EAX],EBX ; <- зашифровываем

00434123 ^EB EC JMP SHORT app_test.00434111

Листинг 7 "подложный" расшифровщик, внедренный обфускатором

Разумеется, все эти действия вносят побочные эффекты (как минимум воздействуют на флаги) и обфускатору приходится выполнять множество дополнительных проверок, чтобы не убедиться, что эти побочные действия не окажут рокового воздействия на защищаемую программу. Разработка качественного и надежного запутывателя — сложная инженерная задача, но потраченное время стоит того. Бесполезность "инструкций с нулевым эффектом" уже не распознается визуально и обычный трассер тут ничем не поможет. Необходимо трассировать не только поток управления, но и поток данных, то есть отслеживать реальные изменения значений регистров/ячеек памяти. Обычного для этого используется графы, и команда легендарного Володи с не менее легендарного wasm'а вплотную приблизилась к решению этой задачи. Все не так уж и сложно. Как только граф замыкается сам на себя, все "лишние" операции над данными удаляются и остается только суть.

Анализатор "LOCO" (см. рис. 6), созданный тройкой магов по имени Matias Madou, Ludo Van Put и Koen De Bosschere, является практически единственным доступным инструментом, который только есть. К сожалению, для практической работы он все-таки непригоден и больше напоминает игрушку, тем не менее стоящую того, чтобы с ней повозиться. Исходный код (вместе с документацией и кучей интересных статей на тему [де]обфускации) можно бесплатно скачать с официального сайта Diablo (http://www.elis.ugent.be/diablo/?q=obfuscation), однако, работать он будет только под UNIX. Windows-хакеры отдыхают и сосут (лапу).



Рисунок 6 внешний вид анализатора LOCO

Самые извращенные (просите, самые совершенные) обфускаторы выполняют математические преобразования программного кода, а это кранты. В частности, команда "a++" может быть замена на эквивалентную ей конструкцию a += (sin(x)2 + cos(x)2), где sin/cos вычисляются "вручную" посредством самого "тупого" и громоздкого алгоритма, распознать в котором исходную формулу не сможет и академик.

Классические трассеры данных с такой задачей уже не справляются, ведь в этом случае граф не замыкается сам на себя и внесенная обфускатором избыточность не удаляется! Однако, в интерактивном режиме кое-что все-таки можно сделать. Смотрите, на входе мы имеем переменную "a", которая после долгих и загадочных манипуляций, увеличивается на единицу. Если код линеен и инвариантен по отношению к другим данным (то есть не зависит от них), хакер может смело заменить всю эту замутку на "a++". Главное — чтобы исследовательский инструмент обеспечивал удобный, наглядный и непротиворечивый способ визуализации данных. Пока таких инструментов нет, и это — задача будущего! (Ну? У кого чешутся руки? Кто жалуется, что ему не во что вгрызться зубами, что нет задачи достойной его интеллекта? Дерзайте!)

как это ломается


Чтобы ощутить все прелести обфускации на собственной шкуре, достаточно взять Armadillo, упаковать свою собственную программу типа "hello, world!", а затем ковырнуть ее отладчиком или дизассемблером. Мама родная! Это же с ума сойти можно! Сколько ни трассируй программу, а смысла все равно не видно! Нас окружает кромешная тьма, непроглядный мрак диких джунглей запутанного кода. Неужели кому-то под силу это сломать? Поверьте, парни, это возможно!

Рисунок 7 попытка взлома Armadill'ы в hiew'е приводит в ужас даже матерых хакеров, код выглядит совершенной бессмыслицей, которой он по сути и является


Начнем с того, что с работающей программы практически всегда можно снять дамп, как бы этому не сопротивлялся распаковщик. Методики борьбы с распаковщиками довольно разнообразны и заслуживают отдельной статьи, мы же говорим об обускации, вот и будем говорить не отвлекаясь. Отметим лишь механизм динамической расшифровки CopyMem II, используемый Armadillo, при котором память расшифровывается постранично: Armadillo перехватывает обращение к зашифрованной странице через атрибут NO_ACCESS и механизм структурных исключений, расшифровывает ее, а затем зашифровывает вновь. Тем не менее, вполне реально написать драйвер, отслеживающий возникновение исключений и дампящий страницу после завершения ее расшифровки. Анализировать "запутанный" код протектора для этого совсем необязательно, однако, не все и не всегда бывает так радужно…

распутывание кода


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

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

Протокол трассировки программы, уже знакомой нам по листингу 2, будет выглядеть так (если трассер писать лень, можно использовать soft-ice, просто отключив окно кода командной WC, тогда результат трассировки командой T будет "вываливать" в нижнее окно, откуда его можно добыть сохранив историю команд в Symbol Loader'е: File-> Save Soft-Ice History As):
001B:00434001 E800000000 CALL 00434006

001B:00434006 5D POP EBP

001B:00434007 50 PUSH EAX


001B:00434008 51 PUSH ECX

001B:00434009 EB0F JMP 0043401A (JUMP ↓)

001B:0043401A F2EBF5 REPNZ JMP 00434012 (JUMP ↑)

001B:00434012* EB0F JMP 00434023 (JUMP ↓)

001B:00434023 EBE9 JMP 0043400E (JUMP ↑)

001B:0043400E B8EB07B9EB MOV EAX,EBB907EB

001B:00434013* 0F90EB SETO BL

001B:00434016* 08FD OR CH,BH

001B:00434018 EB0B JMP 00434025 (JUMP ↓)

001B:00434025 F3EBE4 REPZ JMP 0043400C (JUMP ↑)

001B:0043400C EB0F JMP 0043401D (JUMP ↓)

001B:0043401D EBF6 JMP 00434015 (JUMP ↑)

001B:00434015 EB08 JMP 0043401F (JUMP ↓)

001B:0043401F F2EB08 REPNZ JMP 0043402A (JUMP ↓)

001B:0043402A 59 POP ECX

001B:0043402B 58 POP EAX

001B:0043402C 50 PUSH EAX

001B:0043402D 51 PUSH ECX

001B:0043402E EB0F JMP 0043403F (JUMP ↓)

001B:0043403F F2EBF5 REPNZ JMP 00434037 (JUMP ↑)

001B:00434037 EB0F JMP 00434048 (JUMP ↓)

001B:00434048 EBE9 JMP 00434033 (JUMP ↑)

001B:00434033 B8EB07B9EB MOV EAX,EBB907EB

001B:00434038 0F90EB SETO BL

001B:0043403B 08FD OR CH,BH

001B:0043403D EB0B JMP 0043404A (JUMP ↓)

001B:0043404A F3EBE4 REPZ JMP 00434031 (JUMP ↑)

001B:00434031 EB0F JMP 00434042 (JUMP ↓)

001B:00434042 EBF6 JMP 0043403A (JUMP ↑)

001B:0043403A EB08 JMP 00434044 (JUMP ↓)

001B:00434044 F2EB08 REPNZ JMP 0043404F (JUMP ↓)

001B:0043404F 59 POP ECX

001B:00434050 58 POP EAX

Листинг 8 протокол трассера

Намного нагляднее дизассемблерного листинга, правда? Теперь не нужно прыгать по условным переходам, гадая какие из них выполняются, а какие нет, к тому же естественным образом исчезает проблема перекрытия машинных команд. Обратите внимание на адреса 434012h, 00434013h и 00434016h. Ба! Так это же наши "перекрытые" команды! То, что дизассемблеру удавалось показать с таким трудом, трассер отдает нам задаром! Это реальный поток выполнения программы, в котором много мусора, но по крайней мере нет скрытых команд, с которыми приходится сталкиваться в дизассемблере.


Полученный протокол трассировки можно (и нужно!) прогонять через различные программы-фильтры (их так же придется написать самостоятельно), распознающие и удаляющие мусорные инструкции. Впрочем, эту операцию можно выполнить и вручную, загрузив протокол в любой редактор (например, в тот, что встроен в FAR). После нескольких минут работы мы получим следующий реально значимый код:
001B:00434001 E800000000 CALL 00434006

001B:00434006 5D POP EBP

001B:00434077 33C9 XOR ECX,ECX

001B:004340C3 33C0 XOR EAX,EAX

001B:004340D3 8B0424 MOV EAX,[ESP]

001B:004340DB C60090 MOV BYTE PTR [EAX],90

001B:00434105 83ED06 SUB EBP,06

Листинг 9 листинг "вычищенный" в ручную

Основную проблему создают циклы. Трассер разворачивает их в длинный многокилометровый многократно повторяющий код. Запаришься его пролистывать! Так что без фильтра, распознающего и "сворачивающего" повторяющие конструкции нам не обойтись.

Хорошая идея — пропустить протокол трассера через оптимизирующий компилятор, использующий системы графов для устранения лишних операций присвоения (внимание! пропускать именно протокол трассера, а не дизассемблерный листинг, поскольку последний неверен, неполон и вообще никуда неканает!). Математических преобразований в стиле sin(x)2+cos(x)2 он, конечно же, распознать не сможет, но выбросит значительную часть "инструкций с нулевым эффектом", а нам не придется реализовывать систему графов и писать то, что было написано задолго до нас. К тому же, превзойти создателей оптимизирующих компиляторов нам все равно не удастся, правда, здесь есть одно "но". Компиляторы с большой осторожностью оптимизируют обращения к памяти, поэтому "ложные" расшифровщики типа листинга 7 компилятором оптимизированы не будут, не смотря на их очевидную "нулевую эффективность". Эту часть работы мы будем должны выполнить самостоятельно или же… просто смириться с тем, что из листинга вычищен не весь мусор. (Но ведь нам и не нужен "идеальный" листинг, правда? Будем есть, что даеют).


За основу лучше всего взять компилятор gcc, поскольку его исходные тексты открыты. Разумеется, просто взять и "оптимизировать" протокол трассера не получится, ведь он "написан" на языке ассемблера! У нас есть два пути: написать сравнительно простой транслятор, превращающий дизассемблерный протокол трассера в программу на Си (и тогда ее будет можно оптимизировать любым компилятором, а не только gcc), но лучше оттранслировать протокол трассера в промежуточный язык gcc (описанный в документации), пропустив его через "гнутый" оптимизатор. В этом случае мы получаем возможность сообщить оптимизатору некоторую дополнительную информацию о структуре программы, выловленную нашим трассером. Эффективность "чистки" кода от этого только повыситься. Короче говоря, наш трассер (и программы-фильтры) будут работать с оптимизатором в связке.

А там уже и до метадо-декомпилятора недалеко, тем более что работы в этом направлении ведутся не только в хакерских, но и "академических" кругах. Так что анализ "запутанного" кода — не такая уж сложная задача.

Кстати говоря, процедуры, обработанные обфускатором, резко отличаются от всех остальных и могут быть найдены простым статистическим анализом процентного содержания различных машинных команд. У "запутанных" процедур оно будет уж очень специфичным. К тому же, такие процедуры как правило до неприличия длинны. Логично, если код процедуры кем-то запутан, то это не просто так! Здесь явно прячется защитный механизм! Процедура проверки регистрационного номера или что-то типа того. Обфускация в этом случае только на пользу хакеру!

черный ящик квадрата Малевича


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

Очень большую информацию несут в себе вызовы API-функций (вместе с аргументами и возвращаемыми значениями), а если хакеру еще удастся перехватить и библиотечные функции вместе с RTL, то картина происходящего будет в общих чертах ясна. По крайней мере хакер сможет выяснить к чему "привязывается" защита и каким образом узнает об окончании испытательного периода. А большего для взлома зачастую и не нужно! Вместо того, чтобы анализировать код самой программы, хакер будет исследовать каким образом она взаимодействует с "внешним миром", то есть с ОС! Тогда на "внутренний" мир защиты можно будет забить. Конечно, не для всех программ это срабатывает, но многие ломаются именно так!

Art.exe|0FF6D4E|GetProcAddress(77F80000,01049A04:"NtContinue") returns: 77F92796

Art.exe|0FF6D4E|GetProcAddress(77F80000,01049A3C:"NtRaiseException") returns: 77F860F2

Art.exe|0FF6D4E|GetProcAddress(77F80000,01049A7C:"KiUserExceptionDispatcher")returns;

Art.exe|0FF6D4E|GetProcAddress(77F80000,01049AC4:"NtQuerySystemInformation") returns;

Art.exe|0FF6D4E|GetProcAddress(77F80000,01049B0C:"NtAllocateVirtualMemory") returns;

Art.exe|0FF6D4E|GetProcAddress(77F80000,01049B50:"NtFreeVirtualMemory") returns;

Art.exe|0FF6D4E|GetProcAddress(77F80000,01049B90:"NtMapViewOfSection") returns;

Art.exe|0FEE7C2|VirtualAlloc(00000000,0000027D,00001000,00000040) returns: 01220000

Art.exe|10000AE|GetModuleFileNameA(00400000, 0012FE61, 000000FF) returns: 0000003B

Art.exe|0FFDA16|CreateFileA(0012FE61:"C:\bin\ElcomSoft\AdvancedRegistryTrace...",,,,)

Art.exe|0FFDBC3|CreateFileMappingA(9Ch,00h,02h,00h,00h,00h) returns: 000000A0

Art.exe|0FFDBD3|CloseHandle(0000009C) returns: 00000001

Art.exe|0FFDBF8|MapViewOfFile(A0h, 04h, 00h, 00h, 00h) returns: 01230000

Art.exe|0FE4EDD|GetActiveWindow() returns: 00000000

Art.exe|0FD5D98|MessageBoxA(0,499DC:"Debugger detected.",,"Protection Error") returns;

Art.exe|FFFFFFF|ExitProcess(72542079)

Листинг 10 шпионаж на API функциями несет в себе очень много информации

Большая ошибка большинства обфускаторов в том, что "запутывая" код, они забывают "запутать" структуру данных (ну разве что только зашифровывают их). Это позволяет использовать классические приемы взлома типа "прямой поиск регистрационных данных в памяти". Хакер вводит произвольный регистрационный номер, находит отладчиком его в памяти, ставит точку останова и всплывает в "запутанной" процедуре, а затем смотрит обстоятельства дел. В половине случаев после серии долгих разбирательств запутанная процедура возвращает TRUE/FALSE (и тогда хакер просто правит условный переход), в другой половине — защита генерирует "эталонный" регистрационный номер, легко обнаруживаемый визуальным осмотром дампа памяти (и в этом случае хакер просто вводит подсмотренный номер в программу). Более сложные защитные механизмы встречаются крайне редко, но и тогда зачастую удается сгенерировать валидный номер "руками" самой защиты, если она построена по схеме if (func_generate_reg_num(user_name) == entered_reg_num) all_ok() else fuck_off(); Как нетрудно догадаться, хакер находит процедуру func_generate_reg_num (а находит от ее по срабатываю точки останова на user_name) и "подсматривает" возвращаемый результат. Данная методика совершенно "прозрачна" и пробивает любые навесные упаковщики, лишний раз подтверждая известный тезис, что грамотно защитить программу — это не грибов надербанить!



Рисунок 8 взлом программы с помощью точек останова в soft-ice и окна memory

В "тяжелых" случаях помогает слежение за данными, то есть опять-таки за дампом памяти. Хакер включает трассер и "втыкает" в окно "memory", анализируя характер изменения переменных. Переменные — это ключ ко всему. Они позволяют реконструировать алгоритм даже без знания кода, точнее говоря, существуют методики реконструкции кода по характеру изменения переменных. На данный момент они еще не очень хорошо отработаны и практически нигде не описаны, но в хакерских кулуарах уже идут оживленные разговоры. Это перспективное направление, в котором стоит копать.


Рисунок 9 утилита LOCO, отслеживающая значение переменных

застенки виртуальной машины


Возвращаясь к разговору о триальных защитах, напомним, что мы имеем программу, которая запускается по меньшей мере один раз, а где один раз, там и два. Если пораскинуть мозгами и пошевелить хвостом, то можно создать такие условия, которые позволяет запускать программу неограниченное множество раз. Грубо говоря, мы как бы помещаем программу "под колпак" и подсовываем ей те данные, в которых она нуждается для продолжения своей жизнедеятельности.

Известно, что виртуальные машины типа VM Ware "автоматически" ломают триальные программы. Если программа введет счетчик запусков или запоминает дату инсталляции где-то внутри компьютера, то после прекращения работы она устанавливается на "чистую" виртуальную машину и продолжает работать как ни в чем ни бывало. Если дата окончания испытательного срока жестко прошита внутри программы, часы виртуальной машины переводятся "назад" и защита даже не подозревают, как жестоко ее обломали. Если программа "стучится" в Интернет, пытаясь подтвердить правоверность своей работы, виртуальная машина просто "отсекается" от Интернета. Короче говоря, виртуальные машины это хорошо, вот только… медленно, неудобно и громоздко.

app.exe|QueryValue|HKLM\Software\Licenses\{I5F218E3F24063708}|SUCCESS|0500000

app.exe|CreateKey |HKLM\Software\Licenses |SUCCESS|Key: 0xE132BB80

app.exe|SetValue |HKLM\Software\Licenses\{I5F218E3F24063708}|SUCCESS|06000000

app.exe|CreateKey |HKLM\Software\Licenses |SUCCESS|Key: 0xE132BB80

app.exe|SetValue |HKLM\Software\Licenses\{05F218E3F24063708}|SUCCESS|563EA80E0BA2A7A6

Листинг 11 виртуальный реестр и слежение за ним

Можно поступить проще — достаточно перехватить базовые API-функции для работы с системным временем, файловой системой, сетью и реестром, не забывая про функции DeviceIoControl и другие подобные ей. Тогда мы сможем организовать "легкую" и весьма быстродействующую виртуальную машину, подсовывающую защите отдельную файловую систему и реестр. Кстати говоря, некоторые протекторы "гадят" в реестре и замуровать их в застенках виртуальной машины сам Джа велел.

Конечно, это не сработает для тех защит, которые, например, работают 30 минут, а затем требуют перезапуска программы, поскольку, существует очень много способов отсчитать эти 30 минут даже без обращения к API. Виртуальная машина бессильна в борьбе с надоедливым NAG-скринами или баннерами, которые крутит бесплатная версия программы, но на универсальность предложенной методики никто и не претендовал. Если программу можно сломать этим путем — хорошо, если нет — значит, используем другие пути, атакуя ее по одному из сценариев, описанных выше.

заключение

Статья в общем-то совсем не про обфускацию, а про методики взлома "запутанных" программ, что совсем не одно и тоже. Будущее обфускации готовит хакерам совсем не радужные перспективы. С ходу можно назвать трансляторы Си-кода в байт-код Машин Тьюринга, Стрелок Пирса, Сетей Петри и многих других примитивных машин. Производительность современных процессоров это уже позволяет. В практическом плане это означает полный мрак стандартным методам анализа кода. Если вычистить мусор и удалить избыточность, внесенную "запутывателями" теоретически вполне возможно (но практически очень и очень сложно), то "распутать" байт-код Сетей Петри уже никак невозможно! Это однонаправленный процесс и развернуть его на 180 градусов не сможет и сам Джа. Написать анализатор байт-кода, повышающий уровень абстракции — вполне возможно, вот только даже на таком уровне придется очень долго разбираться: что, как и куда.


Анализ типа "черного ящика" сулит намного большие перспективы, равно как и создание виртуальной машины, отрезающей защиту от внешнего мира. Дизассемблеры уже остановились в своем развитии и скоро вымрут как мамонты. В последних версиях IDA Pro не появилось ничего радикально нового, хуже того, наметилась признаки явной деградации, превратившие основное окно дизассемблера в… даже не знаю как "это" цензурно назвать. В общем смотрите сами (http://www.datarescue.com/idabase/5preview/index.htm, см. рис. 10).


Рисунок 10 основной режим работы IDA Pro 5.x

Как сказал Юрий Харон "У меня возникает ощущение, что Ильфака зомбировали :)" и мыщъх с ним полностью согласен. Только Ильфак к этому мнению не прислушивается и больше всего озабочен увеличением объемов продаж, чтобы IDA Pro покупали даже те, кто ни хрена не смыслит ни в хакерстве, ни в дизассемблировании, и вместо реально работающего инструмента хочет видеть красивое графическое представление потока выполнения программы, которое можно повесить на стену или показать начальству. Тем же самым занимается F-Secure, пугающая пользователей картинками в стиле "ню" (http://www.f-secure.com/weblog/archives/archive-092005.html, см. рис. 11).


Рисунок 11 трехмерное графическое представление структуры червя W32.Bagle, впечатляет конечно, но годится разве что голивудского ширпотреба, но никак не для реального анализа

Спрашивается — а на хрена такая красивая трехмерная "репрезентация" вообще нужна? Что она реально отображает? С другой стороны, от "низкоуровневого" дизассемблирования на уровне ассемблерных команд тоже не много пользы. Современные программы стали слишком большими, количество уровней абстракций измеряется многими десятками и "плотность" значимого кода неумолимо стремиться нулю. Программа, размером в сто мегабайт реализует простейший алгоритм, в былые времена с легкостью умещающихся в несколько килобайт. Какие там обфускаторы…


Отсюда — многочисленные попытки визуализации потока выполнения программы, поднимающие нас на уровень анализа структуры кода, спускаясь к машинным командам только там, где это действительно необходимо. К сожалению, эта методика работает намного хуже, чем выглядит и только усложняет анализ. Стандартный режим дизассемблирования, к которому мы все привыкли, все еще присутствует в IDA PRO (во всяком случае пока), но уже не является режимом по умолчанию. Вот такая блин тенденция…

>>> врезка ссылки по теме





1 "извините парни — столько было туманных бредней на всех форумах об этих новых Эллиптических Кривых DSA в последнем упаковщике Armadillo — мы просто должны быть сделать это… возьмите наш взлом как демонстрационный пример, доказывающий, что недостаточно просто реализовать сильное крипто, вам бы следовало понять, что необходимо реализовать его правильно… салют всем, кто сделал это возможным, особенном тем, кто пожертвовал немного мощности своего ЦП ;)"

2 команды "семейства" OR REG,REG (TEST REG,REG,ADD REG,0) воздействуют на флаги и к числу "совсем уж не значащих" никак не относятся;