hex-rays

hex-ray – декомпилятор нового поколения

крис касперски, ака мыщъх, a.k.a. nezumi, a.k.a. souriz, a.k.a. elraton, no-email

начинающие реверсеры, еще не вкурившие в асм, постоянно спрашивают на хакерских формах: где бы им раздобыть декомпилятор для си или паскаля? декомпиляторов-то много, но вот результат… без дизассемблера ни один хвост не разобраться, даже если дунет. и вот, наконец — свершилось! Ильфак (автор легендарного дизассемблера IDA Pro) выпустил декомпилятор нового поколения HexRays, делающий то, что другим не под силу. мир вздрогнул от восторга (реклама вообще обещала чудо), порождая сейсмические волны впечатлений: от оргазма, до полного отвращения. истина же, как водится, где-то посередине и представляет интерес обозначить эту середину, рассмотрев слабые и сильные стороны декомпилятора, а так же области его применения

Почему у Ильфака получилось то, что упорно не желало получаться у других? Начнем с того, что Hex-Ray представляет собой всего лишь plug-in к IDA Pro — интерактивному дизассемблеру более чем с десятилетней историей, первая версия которой увидела свет 6 мая 1991 года. Спустя некоторое время к работе над ней подключился удивительный человек, гениальный программист и необычайно креативный кодер Юрий Харон и они вместе с Ильфаком (не без помощи других талантливых парней, конечно) начали рыть землю в том направлении, куда еще никто не вкладывал деньги. До этого дизассемблеры писались исключительно на «пионерском» энтузиазме параллельно с изучением ассемблера и довольно быстро забрасывались.

IDA Pro — не только основной, но и, пожалуй, единственный продукт фирмы DataRescue (мелкие утилиты и прочие ответвления — не в счет). А Ильфак очень неглупый человек, и Юрий Харон тоже. Так стоит ли удивляться, что им удалось решить практически все фундаментальные проблемы дизассемблирования, над которыми просто не хотели работать остальные разработчики, зная, что быстрой отдачи не будет и проект потребует десятилетий упорного труда.

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

Компиляция — процесс однонаправленный и необратимый, в ходе которого теряется огромное количество «лишней» информации, совершенно ненужной процессору. Это не шутка! Исходный текст, написанный на языке высокого уровня, чрезвычайно избыточен, что с одной стороны упрощает его понимание, а с другой — страхует программиста от ошибок. Взять хотя бы информацию о типах. На низком уровне процессор оперирует базовыми типами: байт, слово, двойное слово. Строки превращаются в последовательности байтов и чтобы догадаться, что это действительно строка, приходится прибегать к эвристическим алгоритмам.

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

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

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

что в имени твоем

Комментарии, имена функций и переменных в процессе компиляции теряются безвозвратно (исключение составляют динамические типы, имена которых в двоичном модуле хранятся «как есть», а потому и тормозят словно динозавры). Ну, с комментариями в большинстве исходных текстов и так небогато, так что их отсутствие еще можно пережить, но вот без имен переменных и функций логика работы даже простейших программ становится совершенно непонятной.

К счастью, современные программы наполовину (а то и более) состоят из библиотечных и API-функций, которые распознаются классическим сигнатурным поиском. Главное — собрать обширную базу библиотек всех популярных (и непопулярных) компиляторов с учетом многообразия их версий.

IDA Pro уже давно поддерживает технологию FLIRT (Fast Library Identification and Recognition Technology), не только распознающую библиотечные функции, но так же восстанавливающую их прототипы вместе с прототипами API-функций, которые кстати говоря, могут импортироваться не только по имени, но еще и по ординалу, то есть по номеру и чтобы превратить бессловесный номер в имя, понятное человеку, опять-таки требуется база.

Почему реконструкция прототипов так важна для декомпилятора? А потому, что она позволяет восстанавливать типы передаваемых функциями переменных, назначая им хоть и обезличенные, но все же осмысленные имена в стиле: hWnd, hModule, X, Y, nWidth, nHeight, hCursor, hMenu, hDlg. Согласитесь, это _намного_ лучше, чем dword_1008030, dword_1008269, dword_1006933 — что характерно для подавляющего большинства остальных декомпиляторов.

Даже если часть переменных распознана, то это уже упрощает анализ программы и реконструкцию оставшихся переменных.

константа или смещения

В силу архитектурных особенностей x86 процессоров, константы и смещения (они же указатели — в терминах языков высокого уровня) синтаксически _абсолютно_ неразличимы и декомпилятру (равно как и дизассемблеру) приходится задействовать мощные эвристические алгоритмы, чтобы не попасть впросак.

А какая между этими двумя сущностями разница? Очень большая!!! Вот, например, в регистр EAX загружается число 106996h. Если это указатель на ячейку памяти или массив данных, то дизассемблер должен воткнуть по этому адресу метку (label) и написать MOV EAX, _offset_ lab_106996 (ес-но, имя метки дано условно и совпадение с ее численным значением абсолютно случайно — при повторной компиляции она может оказаться расположенной по совершенно другому адресу). А вот если 106996h — это константа, выражающая, например, среднюю плотность тушканчиков на один квадратный метр лесополосы или количество полигонов на морде монстра, то ставить offset ни в коем случае нельзя, поскольку, это не только введет нас в заблуждение при анализе, но и, как уже говорилось, при повторной компиляции, смещение может уплыть, превращая число 106996h черт знает во что. Программа либо будет работать неправильно, либо сразу рухнет (особенно, если путаница между константами и смещениями происходит не единожды, а совершается на протяжении всего листинга).

Так как же все-таки определить кто есть ху?! На первый взгляд все достаточно просто. Если данная сущность используется как указатель (то есть, через нее происходит обращение к памяти по заданному адресу: mov eax, 106996h/mov ebx,[eax]), тут и дизассемблеру ясно, что это — смещение. Но стоит лишь немного усложнить пример, например, передать 106996h какой-нибудь функции или начать производить с ней сложные манипуляции, то дизассемблеру потребуется высадится на полную реконструкцию всей цепочки преобразований, но даже тогда его решение может оказаться неверным, поскольку в языке Си индексы (т. е. константы) и указатели (т. е. смещения) _полностью_ взаимозаменяемы и конструкция buf[69] не только равносильна 69[buf], но и транслируется в идентичный машинный код, при реконструкции которого возникает очевидная проблема. У нас есть два числа A и B, _сумма_ которых представляет указатель (то есть, существует возможность, определить, что это указатель — да и то, если попыхтеть), но вот кто из них индекс? Увы. Формальная математика не дает ответа на этот вопрос и дизассемблеру приходится полагаться лишь на свою интуицию да эвристику. Указатели в win32, как правило, велики, поскольку минимальный базовый адрес загрузки файла равен 100000h. Индексы же, как правило (опять! как правило!), малы и много меньше указателей, однако, если у нас имеется массив, расположенный по адресу 200000h и состоящий из 3.145.728 (300000h)элементов (а почему бы, собственно, и нет?!) то тут, индексы старших элементов становятся _больше_ базового указателя на массив, что вводит дизассемблер в заблуждение.

Полный перечень эвристических методик, используемых ИДОЙ для распознавания смещений, можно найти в книге «образ мышления — ИДА» (электронную копию которой находится на мыщъхином сервере http://nezumi.org.ru/ida.full.zip), нам же достаточно сказать, что IDA Pro действительно преуспела в этом направлении и ошибается она крайне редко, хотя все-таки ошибается и с ростом размеров дизассемблируемой программы количество ошибок резко нарастает.

код или данные

Вот так проблема! Наверное, даже самый тупой дизассемблер разберется что ему подсунули — код или данные, тем более, что в PE-файлах (основной формат исполняемых файлов под Windows) они разнесены по разным секциям. Ну, это в теории они разнесены, а на практике компиляторы очень любят пихать данные в секцию кода (особенно этим славиться Багдад — в смысле продукция фирмы Borland).

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

Но непосредственные вызовы это — ерунда. Вот косвенные — та да!!! Реконструкция CALL dword ptr DS:[EBX] требует не только эмуляции процессора (т.е. набора машинных команд), но и эмуляции памяти!!! Естественно, это на порядок усложняет дизассемблерный движок, зато нераспознанные массивы данных мгновенно превращаются в функции и, что самое главное, — дизассемблер показывает кто именно и откуда их вызывает!

Наличие эмулятора процессора и памяти позволяет так же отслеживать положение регистра ESP, используемого для адресации локальных переменных и аргументов, передаваемых функциям. Если же эмулятора нет (а в ИДЕ он есть), дизассемблер способен распознавать лишь простейшие формы адресации/передачи аргументов. Механизм, ответственный за распознание и отслеживание аргументов, носит красивое название пива PIT, но расшифровывается эта аббревиатура отнюдь не как «Пивоварный [завод] Ивана Таранова», а происходит от «Parameter Identification and Tracking» — идентификация и отслеживание параметров, в смысле аргументов. Тех, что бывают у функций. А еще бывают функции без аргументов, но это уже другая история.

hex-rays_image_0.jpg

Рисунок 1 PIT – это не только пиво!

реконструкция потока управления

Языки высокого уровня оперируют такими конструкциями как циклы, операторы выбора, ветвления и т. д., что делает программу простой и наглядной. Собственно говоря, именно отсюда и пошло структурное программирование. А ведь когда-то, давным-давно, в Бейсике (и других языках того времени) кроме примитивного GOTO ничего не было и программа, насчитывающая больше сотни строк, превращалась в сплошное «спагетти», лишенное всякой логики, внутренней организации и следов цивилизации.

На низком же, машинном, уровне ситуация осталось той же, что и лет пятьдесят тому назад. И хотя x86 процессоры формально поддерживают инструкцию цикла LOOP, компиляторы ее не используют, довольствуясь одними лишь условными и безусловными переходами, а потому первоочередная задача реверсера — реконструировать циклы, ветвления и другие высокоуровневые сооружения. Подробнее о том, как это делается «руками» читайте в «фундаментальных основах хакерства» (бесплатная электронная копия лежит на http://nezumi.org.ru/phck2.full.zip).

Начиная с пятой версии в IDA Pro появился механизм графов, позволяющий _автоматически_ реконструировать циклы, ветвления и прочие «кирпичи» языков высокого уровня. Впрочем, польза от графов была небольшой и в реальности они только замедляли анализ, поскольку подобрать адекватный механизм визуализации и навигации Ильфаку так и не удалось… Но ведь не пропадать же труду?!

Рисунок 2 графы — как они есть

К пятой версии IDA Pro имела в своем арсенале все необходимое для автоматической декомпиляции, причем, не просто декомпиляции, а _очень_ качественной декомпиляции, декомпиляции принципиально нового уровня, до которого не дотягивает ни один другой существующих декомпилятор.

Вот так и родилась идея — дописать к ИДЕ еще небольшую (на самом деле очень большую) порцию кода, переводящую китайскую ассемблерную грамоту в доступный и понятный листинг на языке Си. Закипела напряженная работа, по ходу которой выявлялись все новые и новые подводные камни, требующие времени, усилий и мозговой активности для поиска тонкой нити фарватера. А всякая (или практически всякая) работа упирается в деньги, тем более, что у Ильфака — фирма!

Включать декомпилиятор в дистрибутив IDA Pro Ильфак не стал. Тому способствовало несколько причин. Первое и главное — основной массе текущих пользователей ИДЫ декомпилятор не сильно нужен, если они им и будет пользоваться, то так — из чистого любопытства, чтобы запустить пару раз, плюнуть и вернуться к привычному стилю жизни — анализу дизассемблерного листинга. Второе — зарабатывать на жить (Ильфаку) и содержать фирму как-то же надо?!

Все это привело к тому, что декомпилятор, получивший название (Hex-Rays), был выпущен отдельным продуктом, но! — внимание на экран! — требующим обязательного присутствия ИДЫ, поскольку Hex-Rays —всего лишь plug-in. Таким образом, реверсеру, желающему упростить свою жизнь за счет автоматической декомпиляции, необходимо прибрести как саму ИДУ, так и Hex-Rays, причем, приобретать этот комплект будет совсем другая пользовательская аудитория, совсем не та, что приобретала ИДУ и почитала ее как самый лучший интерактивный дизассемблер. Интерактивный ‑ значит, тесно взаимодействующий с пользователем (в смысле с хакером). В противовес ей, пакетные дизассемблеры стремятся к максимальной автоматизации реверсинга, лишая пользователя возможности вмешиваться в процесс и отдавать указания.

Рисунок 3 Hex-Rays – это отдельный продукт и в то же время… plug-in для IDA Pro!

Hex-Rays в отличии от ИДЫ интерактивностью не обладает, она у него атрофирована еще в зародыше. Нет даже опций настройки! А там где нет интерактивности, нет и хакеров, а процветает, извините за прямому выражения, пионерия непроходимого ламеризма. Но, тем не менее… пионер это не ругательство, а просто социальный статус. И тут мы плавно переходим к ответу на вопрос кому нужен Hex-Rays.

Цены на продукцию фирмы DataRescue постоянно меняются, причем в очень широких пределах, обусловленных не только спросом, но еще и фазами луны, а потому, перед совершением покупки необходимо зайти на www.idapro.com и взглянуть на прайс.

На момент публикации данной статьи стандартная редакция ИДЫ стоила 515 американских енотов или 360 евриков, но стандартная редакция — это не солидно, да и покоцана она сильно, поэтому, лучше сразу рассчитывать на продвинутую (advanced), готовясь раскошелиться на все 985$/690 EUR.

Сам же Hex-Rays стоит…. всего-навсего каких-то жалких 2.299$ (1.500 EUR), что вместе с ИДОЙ составляет 3.284$, а в евриках — и того меньше. По сравнению с Мерседесом (даже подержанным!) — сущие пустяки, за тем исключением, что люди, занимающиеся реверсингом, если и ездят на Мерседесах, то только взятых на прокат в почасовую оплату.

О том, что в России программное обеспечение не покупается, а экспроприируется и национализируется, переходя в общественное достояние, мы скромно промолчим. И у нас даже в мыслях не было говорить о том, что IDA Pro Advanced последней версии в комплексе с Hex-Rays валяется практически во всех файло-обменных сетях.

Зададимся вопросом: кто же _реально_ готов заплатить три килобакса за декомпилятор? Буржуи, наверное. Фирмы, занимающиеся реверсингом малвари. Да мало ли еще кто! Вот только, среди читательской аудитории таковых скорее всего не окажется, а студенческие скидки отсутствуют. Жаль!

Добыли мы, значит, Hex-Rays (где добыли?! купили конечно! мы же состоятельные хакеры, можем себе позволить!), устанавливаем его туда же, куда IDA Pro ложит все свои plug-in'ы и приступаем к тестированию.

В качестве объекта тестирования используется стандартный «Блокнот» (в данном случае из комплекта поставки W2KSP0), на котором обкатываются все вирусы, все упаковщики исполняемых файлов (вместе с распаковщиками), все протекторы… словом, декомпилятор не станет исключением. А что? Блокнот достаточно простая программа, обуславливающая тот минимум функционала, ниже которого декомпилятор из мощного программного комплекса превращается в дорогостоящую, но абсолютно бесполезную игрушку.

Рисунок 4 «Блокнот» — излюбленный хакерский полигон для всевозможных испытаний

Загрузив notepad.exe в IDA Pro и ответив на ряд несложных вопросов мастера авто-анализа, даем дизассемблеру некоторое время поработать и когда он перейдет в режим ожидания, заходим в меню File  Produce File → Create C file или нажимаем горячую клавишу <Ctrl-F5>.

Рисунок 5 декомпиляция начинается!

Сначала на экране появляется логотип Hex-Rays с предложением провериться на обновления (мы же ведь честные пользователи, правда? так почему мы нам и не провериться?!) и после нажатия на <OK>, Hex-Rays думает некоторое время, а когда он закончит заниматься магической деятельностью, на диске образуется файл notepad.c.

Рисунок 6 кто первый в очереди за обновлениями?!

Ура! Свершилось! Пробуем откомпилировать его компилятором MS VC 6.0, предварительно скопировав в текущий каталог файл defs.h из каталога Hex-Rays и заменив в строке #include <defs.h> угловые скобки на двойные кавычки, иначе компилятор будет искать defs.h в системном каталоге с включаемыми файлами, где, естественно, его не обнаружит. Но это мелочи… А вот уже проблема посерьезнее. Компилятор, выдав более сотни ошибок, просто прерывает компиляцию, поскольку продолжать ее дальше — нет никакого смысла.

Рисунок 7 простыня ошибок компиляции декомпилированного листинга

Может, мы не тот компилятор выбрали для тестирования? Вообще-то, по идее, декомпилятор должен выдавать листинг на ANSI C, поддерживаемый любым ANSI-совместимым C-компилятором, но… все мы хорошо знаем любовь Ильфака к продукции фирмы Borland. На нем написана сама ИДА, на нем же вышло первое SDK…

Хорошо, берем последнюю версию Borland C++ (благо она бесплатна) и что же?! Вновь простыня ошибок и ругательств, приводящих к преждевременному прерыванию компиляции. Оказывается, чтобы откомпилировать декомпилированный файл над ним еще предстоит основательно поработать. Вот тебе, бабушка, и раз…

А вот тебе и два! Декомпилятор выдал чистый *.c файл, проигнорировав тот факт, что notepad.exe содержит еще и секцию ресурсов, но даже если при загрузке «Блокнота» в ИДУ форсировать обработку ресурсов, мы все равно ни хвоста не получим. Ну не умеет Hex-Rays с ними работать. Возможно, когда ни будь он этому и научиться, но сейчас — нет.

Другими словами — Hex-Rays не позволяет компилировать декомпилированные программы без дополнительной ручной работы и реально он пригоден лишь для анализа. Возникает резонный вопрос: и насколько же он упрощает анализ? Чтобы не быть голословным вот два фрагмента кода:

if ( RegOpenKeyExA(

HKEY_CLASSES_ROOT,

«CLSID\\{ADB880A6-D8FF-11CF-9377-00AA003B7A11}\\InprocServer32»,

0,

0x20019u,

&hKey) )

{

result = 0;

}

else

{

cbData = 260;

if ( !RegQueryValueExA(hKey, ValueName, 0, 0, lpData, &cbData) )v1 = 1;

RegCloseKey(hKey);

result = v1;

}

return result;

Листинг 1 фрагмент «Блокнота», декомпилированного Hex-Rays

Просто? Красиво? Понятно? Элегантно? Да!!! А вот как выглядел тот же самый код в чистом дизассемблере:

.text:0100318Fpushebp

.text:01003190movebp, esp

.text:01003192pushecx

.text:01003193pushecx

.text:01003194leaeax, [ebp+hKey]

.text:01003197pushesi

.text:01003198xoresi, esi

.text:0100319Apusheax; phkResult

.text:0100319Bpush20019h; samDesired

.text:010031A0pushesi; ulOptions

.text:010031A1pushoffset SubKey; lpSubKey

.text:010031A6push80000000h; hKey

.text:010031ABcallds:RegOpenKeyExA

.text:010031B1testeax, eax

.text:010031B3jnzshort loc_10031E7

.text:010031B5leaeax, [ebp+cbData]

.text:010031B8mov[ebp+cbData], 104h

.text:010031BFpusheax; lpcbData

.text:010031C0push[ebp+lpData]; lpData

.text:010031C3pushesi; lpType

.text:010031C4pushesi; lpReserved

.text:010031C5pushoffset ValueName; lpValueName

.text:010031CApush[ebp+hKey]; hKey

.text:010031CDcallds:RegQueryValueExA

.text:010031D3testeax, eax

.text:010031D5jnzshort loc_10031DA

.text:010031D7push1

.text:010031D9popesi

.text:010031DA

.text:010031DA loc_10031DA:; CODE XREF: sub_100318F+46↑j

.text:010031DApush[ebp+hKey]; hKey

.text:010031DDcallds:RegCloseKey

.text:010031E3moveax, esi

.text:010031E5jmpshort loc_10031E9

.text:010031E7 ; ────────────────────────────────────────────────────────────────────

.text:010031E7

.text:010031E7 loc_10031E7:; CODE XREF: sub_100318F+24↑j

.text:010031E7 xoreax, eax

.text:010031E9

.text:010031E9 loc_10031E9:; CODE XREF: sub_100318F+56↑j

.text:010031E9popesi

.text:010031EAleave

.text:010031EBretn 4

Листинг 2 тот же код в чистом дизассемблере

Согласитесь, что сравнение отнюдь не в пользу дизассемблера, но не спешите радоваться. Рассмотрим еще один пример:

if ( (unsigned __int16)a2 == 1 )

{

dword_1008BCC = dword_1008028;

if ( !dword_1008014 && sub_10059A3(dword_10087D0, &String2, 0) )

return 1;

}

Листинг 3 еще один пример работы декомпилятора

Что делает этот код? Совершенно непонятно. То есть, не то, чтобы _совсем_ непонятно, но смысл как-то ускользает. Тут требуется проанализировать — что это за переменные такие, кто еще (помимо данной функции) их модифицирует и зачем? В дизассемблере листинг выглядит схожим образом (см. листинг 4), но мощная система навигации по коду вкупе с перекрестными ссылками и подсветкой одноименных переменных/функций сокращает время анализа на несколько порядков, причем, ни в одном текстовом редакторе, ни в одной среде программирования подобной системы навигации по коду нет!!!

.text:01002306 loc_1002306:; CODE XREF: sub_1002239+C4↑j

.text:01002306moveax, dword_1008028

.text:0100230Bpushebx

.text:0100230Cpushedi

.text:0100230Dmovdword_1008BCC, eax

.text:01002312pushdword_10087D0

.text:01002318callsub_10059A3

.text:0100231Dtesteax, eax

.text:0100231Fjzshort loc_1002328

Листинг 4 а это вид в дизассемблере

Hex-Rays – это, безусловно, большой шаг вперед и неплохое подспорье для начинающих, но… он не стоит тех денег, которые за него просят, к тому же, что касается начинающих… однажды потратив время на изучение ассемблера, мы обретаем возможность реверисровать что угодно и в чем угодно (на худой конец с помощью утилиты «DUMPBIN.EXE», входящей в состав SDK), а, обольщенные возможностями автоматической декомпиляции, мы становимся заложниками Hey-Rays, со всеми отсюда вытекающими…