Библиотечный код может составлять львиную долю программы. Ясное дело, ничего интересного этот код не содержит. Поэтому его анализ с полной уверенностью можно опустить. Незачем тратить на него наше драгоценное время. Однако как быть, если дизассемблер неправильно распознал имена функций? Что, придется изучать многотонный листинг самостоятельно? У хакеров есть проверенные методы решения подобных проблем — о них мы сегодня и поговорим.
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Читая текст программы, написанный на языке высокого уровня, мы только в исключительных случаях изучаем реализацию стандартных библиотечных функций, таких, например, как printf
. Да и зачем? Ее назначение хорошо известно, а если и есть какие неясности — всегда можно заглянуть в описание...
Анализ дизассемблерного листинга — дело другое. Имена функций, за редкими исключениями, в нем отсутствуют, и определить, printf
это или что‑то другое, «на взгляд» невозможно. Приходится вникать в алгоритм... Легко сказать — вникать! Та же printf
представляет собой сложный интерпретатор строки спецификаторов — с ходу в нем не разберешься! А ведь есть и более монструозные функции. Самое обидное — алгоритм их работы не имеет никакого отношения к анализу исследуемой программы. Тот же new
может и выделять память из Windows-кучи, и реализовывать собственный менеджер, но нам‑то от этого что? Достаточно знать, что это именно new
, то есть функция выделения памяти, а не free
или, скажем, fopen
.
Доля библиотечных функций в программе в среднем составляет от пятидесяти до девяноста процентов. Особенно она велика у программ, составленных в визуальных средах разработки, использующих автоматическую генерацию кода (например, Microsoft Visual Studio, Delphi). Причем библиотечные функции подчас намного сложнее и запутаннее тривиального кода самой программы. Обидно, львиная доля усилий на анализ тратится впустую... Как бы оптимизировать это?
Уникальная способность IDA различать стандартные библиотечные функции множества компиляторов выгодно отличает ее от большинства других дизассемблеров, этого делать не умеющих. К сожалению, IDA (как и все, созданное человеком) далека от идеала: каким бы обширным ни был список поддерживаемых библиотек, конкретные версии конкретных поставщиков или моделей памяти могут отсутствовать. И даже из тех библиотек, что ей известны, распознаются не все функции (о причинах будет рассказано чуть позже).
Впрочем, нераспознанная функция — это полбеды, неправильно распознанная функция — много хуже, ибо это приводит к ошибкам (иногда трудноуловимым) в анализе исследуемой программы или ставит исследователя в глухой тупик. Например, вызывается fopen
, и возвращенный ею результат спустя некоторое время передается free
— с одной стороны: почему бы и нет? Ведь fopen
возвращает указатель на структуру FILE
, а free
ее удаляет. А если free
никакой не free
, а, скажем, fseek
? Пропустив операцию позиционирования, мы не сможем правильно восстановить структуру файла, с которым работает программа.
Распознать ошибки IDA будет легче, если представлять, как именно она выполняет распознавание. Многие почему‑то считают, что здесь задействован тривиальный подсчет CRC (контрольной суммы). Что ж, подсчет CRC — заманчивый алгоритм, но, увы, для решения данной задачи он непригоден. Основной камень преткновения — наличие непостоянных фрагментов, а именно перемещаемых элементов. И хотя при подсчете CRC перемещаемые элементы можно просто игнорировать (не забывая проделывать ту же операцию и в идентифицируемой функции), разработчик IDA пошел другим, более запутанным и витиеватым, но и более производительным путем.
Ключевая идея заключается в том, что незачем тратить время на вычисление CRC. Для предварительной идентификации функции вполне сойдет и тривиальное посимвольное сравнение, за вычетом перемещаемых элементов (они игнорируются и в сравнении не участвуют). Точнее говоря, не сравнение, а поиск заданной последовательности байтов в эталонной базе, организованной в виде двоичного дерева. Время двоичного поиска, как известно, пропорционально логарифму количества записей в базе. Здравый смысл подсказывает, что длина шаблона (иначе говоря, сигнатуры, то есть сравниваемой последовательности) должна быть достаточной для однозначной идентификации функции. Однако разработчик IDA по непонятным для авторов причинам решил ограничиться только первыми тридцатью двумя байтами, что (особенно если вычесть пролог, который у всех функций практически одинаков) довольно мало.
И верно! Многие функции попадают на один и тот же лист дерева, возникает коллизия — неоднозначность отождествления. Для разрешения ситуации у всех «коллизионных» функций подсчитывается CRC16 с тридцать второго байта до первого перемещаемого элемента и сравнивается с CRC16 эталонных функций. Чаще всего это срабатывает, но, если первый перемещаемый элемент окажется расположенным слишком близко к тридцать второму байту, последовательность подсчета контрольной суммы окажется слишком короткой, а то и вовсе равной нулю (может же быть тридцать второй байт перемещаемым элементом, почему бы и нет?). В случае повторной коллизии находим в функциях байт, в котором все они отличаются, и запоминаем его смещение в базе.
Все это (да простит авторов разработчик IDA!) напоминает следующий анекдот. Поймали туземцы немца, американца и украинца и говорят им: мол, или откупайтесь чем‑нибудь, или съедим. На откуп предлагается: миллион долларов (только не спрашивайте, зачем туземцам миллион долларов, — может, костер жечь), сто щелбанов или съесть мешок соли. Ну, американец достает сотовый, звонит кому‑то... Приплывает катер с миллионом долларов, и американца благополучно отпускают. Немец в это время героически съедает мешок соли, и его полумертвого спускают на воду. Украинец же ел соль, ел‑ел, две трети съел, не выдержал и говорит: а, ладно, черти, бейте щелбаны. Бьет вождь его, и только девяносто ударов отщелкал, тот не выдержал и говорит: да нате миллион, подавитесь! Так и с IDA, посимвольное сравнение не до конца, а только тридцати двух байтов, подсчет CRC не для всей функции, а сколько случай на душу положит, наконец, последний ключевой байт — и тот‑то «ключевой», да не совсем. Дело в том, что многие функции совпадают байт в байт, но совершенно различны по названию и назначению. Не веришь? Тогда как тебе понравится следующее:
read: write: sub rsp, 28h sub rsp, 28h call _read call _write add rsp, 28h add rsp, 28h retn retn
Тут без анализа перемещаемых элементов никак не обойтись! Причем это не какой‑то специально надуманный пример — подобных функций очень много. В частности, библиотеки от Embarcadero (в прошлом от Borland) ими так и кишат. Поэтому в былые времена IDA часто «спотыкалась» и впадала в грубые ошибки. Тем не менее сейчас IDA заметно возмужала и уже не страдает детскими болячками. Для примера скормим компилятору C++Builder такую функцию:
void demo() { printf("DERIVEDn"); }
Последняя версия IDA сейчас 7.4, между тем я использую IDA 7.2, и она чаще всего успешно распознает почти любые функции. В нашем случае результат выглядит следующим образом:
.text:0000000140001000 void demo(void) proc near .text:0000000140001000 sub rsp, 28h .text:0000000140001004 lea rcx, _Format ; "DERIVEDn" .text:000000014000100B call printf .text:0000000140001010 add rsp, 28h .text:0000000140001014 retn .text:0000000140001014 void demo(void) endp
То есть дизассемблер правильно распознал имя функции. Но так бывает далеко не всегда и не со всеми библиотечными функциями. А когда проблемы возникают с ними, кодокопателю анализировать становится сложновато. Бывает, сидишь, тупо уставившись в листинг дизассемблера, и никак не можешь понять: что же этот фрагмент делает? И только потом обнаруживаешь — одна или несколько функций опознаны неправильно!
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
1 год7690 р. |
1 месяц720 р. |
Я уже участник «Xakep.ru»
Читайте также
Последние новости