Содержание статьи
IDA — это интерактивный дизассемблер и отладчик одновременно. Не так давно в нем появился еще и декомпилятор Hex-Rays, позволяющий преобразовывать машинный код в читаемый листинг на С. Но если и этого оказывается мало, пора брать ситуацию под своей контроль.
Как ни крути, IDA является дизассемблером №1 в мире и используется хакерами-реверсерами, вирусными аналитиками и много кем еще. Встроенные отладчик и декомпилятор, возможность удаленной отладки, удобный подход к анализу давно сделали этот пакет стандартом де-факто. Для расширения функционала IDA долгое время было два пути: подключаемые плагины (большое количество готовых к употреблению аддонов всегда доступно на www.openrce.org/ downloads/browse/IDA_Plugins), а также скрипты, написанные на собственном интерпретируемом языке IDC. На последних остановимся подробнее.
Основы IDC
С помощью IDC можно быстро разрабатывать сценарии, позволяющие автоматизировать множество рутинных действий. Язык вобрал в себя основные синтаксические элементы традиционного C, поэтому прост для освоения. Полная документация идет вместе с IDA, но для старта вполне достаточно уяснить несколько основных моментов:
- Для объявления переменной используется ключевое слово auto. В отличие от С переменные должны быть объявлены до первого использования, причем при объявлении переменную нельзя инициализировать значением.
- Допускается работа с тремя типами данных: целыми числами (integer), строками (string) и числами с плавающей точкой.
- IDC не поддерживает C’шные массивы, указатели и сложные типы данных вроде структур и объединений.
- Поддерживает практически все операторы языка C. Исключения составляют сокращенные операторы присваивания (+=, -=, *= и так далее), вместо них используется полная форма.
- Крайне просто реализована работа со строками, никаких тебе функций strcpy(), strcat() и прочего. Чтобы скопировать или склеить строки, достаточно воспользоваться соответственно операторами присваивания и сложения.
- Как и в С, все выражения завершаются точкой с запятой, а блоки кода заключаются в фигурные скобки. Внутри этих блоков можно объявлять новые переменные, но делать это нужно в самом начале блока.
- Для задания пользовательской функции используется ключевое слово static. IDC также поддерживает пользовательские функции, выполненные в виде отдельных модулей (.idc-файлов). Входные параметры функции задаются через запятую, без указания типа.
- Все параметры передаются по значению, а не по ссылке (указатели в IDC не поддерживаются).
- В объявлении функции не присутствует никакой информации о том, возвращает ли она какое-то значение, и если возвращает, то какого типа. Для того, чтобы функция возвращала значение, используется старый добрый return. Что примечательно, функция может возвращать значения различных типов.
- Для автоматизации сложной задачи вместо небольшого скрипта целесообразно использовать отдельную программу. Минимальные требования — программа должна подключать директивой #include заголовочный файл idc.idc и иметь функцию main.
Первый сценарий
Чтобы подкрепить краткие теоретические знания практикой, попробуем теперь написать простую программу, которая будет перечислять все присутствующие в программе функции с указанием места, откуда они вызываются.
#include <idc.idc>
static main()
{
auto ea, func, ref;
// получаем текущий адрес курсора
ea = ScreenEA();
// в цикле от начала (SegStart) до
// конца (SegEND) текущего сегмента
for (func=SegStart(ea);
func != BADADDR && func < SegEnd(ea);
func=NextFunction(func))
{
// если текущий адрес является
// адресом функции
if (GetFunctionFlags(func) != -1)
{
Message("Function %s at 0x%xn",
GetFunctionName(func), func);
// находим все ссылки на данную
// функцию и выводим
for (ref=RfirstB(func);
ref != BADADDR;
ref=RnextB(func, ref))
{
Message(" called from %s(0x%x)n",
GetFunctionName(ref), ref);
}
}
}
}
Алгоритм действий простой. Сначала с помощью ScreenEA() получаем адрес, на котором в текущий момент находится курсор, и передаем его функциям SegStart() и SegEnd() для определения границ сегмента. После чего в цикле, с начала и до конца сегмента, перебираем адреса всех функций с помощью NextFunction(). Она принимает на вход адрес текущей функции, возвращая адрес следующей или, если других вызовов не найдено, значение -1 (BADADDR). В цикле с помощью GetFunctionFlags() проверяем, является ли данный адрес адресом функции (если не является, возвращается значение -1). Посредством функции GetFunctionName() получаем имя функции и выводим его. В завершение отображаем также список мест, откуда она вызывается — для этого используется цикл с RfirstB() и RnextB(). Полученный код сохраняем в файл с расширением .idc — теперь им можно пользоваться. Чтобы вызвать скрипт надо перейти в меню «File } Script File...» (используй комбинацию клавиш <Alt+F7>) и выбрать наш сценарий.
В окне «Output Window» появится результат выполнения вроде этого:
Function start at 0x401000
Function sub_401060 at 0x401060
called from start(0x401006)
Function sub_401090 at 0x401090
called from sub_4010E0(0x401185)
Function sub_4010E0 at 0x4010e0
Готово!
IDA + Python = IDAPython
Получилось несложно, но есть еще более простой способ разрабатывать сценарии — использовать для этого плагин IDAPython. О главной фишке несложно догадаться из названия: плагин встраивает в IDA полноценный интерпретатор Python, позволяя при этом создавать скрипты, обладающие полным доступом ко всем возможностям языка сценариев IDC.
Главное приемущество плагина — поддержка родной питоновской обработки данных и возможность использовать весь спектр различных модулей для Python. Также, аддон открывает доступ к значительной части функционала IDA SDK, позволяя тем самым писать сценарии для решения более сложных задач, которые сложно было бы реализовать на IDC. На официальном сайте (code.google.com/p/idapython) можно найти версию под свою операционную систему и версию IDA. Установка проста — достаточно скопировать папки plugins и python из архива в папку IDA. Надо позаботиться о том, чтобы в системе был установлен Python.
Совместимая версия интерпретатора указывается в имени архива IDAPython (например, сборке idapython-1.4.3_ida6.0_py2.6_win32.zip требуется Python 2.6). С дистрибутивом распространяется полезная папка examples, которая поможет быстрее разобраться, что к чему.
После установки IDAPython становятся доступными три модуля:
- idaapi, обеспечивающий связь с IDA API;
- idc, предоставляющий интерфейс к IDC;
- idautils, обеспечивающий доступ к вспомогательным утилитам.
Все три модуля автоматически импортируются во все скрипты.
Немного практики
Для старта нужны базовые знания Python'а и умение подглядывать в документацию по IDAPython. Чтобы ощутить, насколько проще осуществляется обработка данных на питоне, напишем скрипт для решения ранее озвученной задачи — отобразим на экране все вызываемые функции. Воспользуемся низкоуровневыми вызовами из модуля idaapi.
from idaapi import *
получаем текущий адрес курсора
ea = get_screen_ea()
получаем сегмент по адресу
seg = getseg(ea)
ищем в цикле от начала сегмента до конца
func = get_func(seg.startEA)
while func is not None and func.startEA < seg.endEA:
funcea = func.startEA
print "Function %s at 0x%x" %
(GetFunctionName(funcea), funcea)
ref = get_first_cref_to(funcea)
while ref != BADADDR:
print " called from %s(0x%x)" %
(get_func_name(ref), ref)
ref = get_next_cref_to(funcea, ref)
func = get_next_func(funcea)
Как видишь, скрипт получился очень похожим на IDC-реализацию. Но, как говорится, совершенству нет предела: можно написать аналогичный сценарий, который будет еще короче. В этом поможет модуль idautils:
from idautils import *
ea = ScreenEA()
for funcea in Functions(SegStart(ea), SegEnd(ea)):
print "Function %s at 0x%x" %
(GetFunctionName(funcea), funcea)
for ref in CodeRefsTo(funcea, 1):
print " called from %s(0x%x)" %
(GetFunctionName(ref), ref)
Вот где по-настоящему ощущается вся прелесть Python: сценарий вообще не нуждается в комментариях. Код с грамотно обозначенными названиями функций сам предельно ясно говорит, что он делает.
Реальная задача
И все-таки. Посмотреть список функций, вообще говоря, можно и стандартными средствами IDA. Так что мы всего лишь изобрели велосипед (хотя и познакомились с основными понятиями IDC и IDAPython). Но попробуем все-таки решить реально полезную задачу.
Как известно, IDA является основным инструментом вирусных аналитиков, используемым для статического анализа отловленной малвари. Малварь же, в свою очередь, стремится как можно сильнее усложнить процедуру исследования. Очень часто при исследовании вредоносных программ обнаруживается, что приложение не импортирует никаких функций. Или, например, заменяет имена API-вызовов хэшами, определяя адрес нужной функции путем поиска в таблице экспорта, вычисляя хэш для каждого имени и сравнивая с желаемым. Рассмотрим этот прием подробнее.
Программа из PEB (Process Environment Block) получает адрес библиотеки kernel32.dll и в ее таблице экспорта ищет по хэшу адреса LoadLibrary и GetProcAddress. С помощью этих двух вызовов можно подгружать остальные библиотеки и искать в них адреса нужных функций. Вообще функция GetProcAddress даже не нужна, так как, зная адрес библиотеки, можно самостоятельно получить адрес нужной функции из таблицы экспорта. Это популярный механизм, который нередко используется в малвари. Итак, вырисовывается вполне понятная задача — написать скрипт, который автоматизировал бы поиск названий функций и упростил таким образом жизнь реверсера.
Чтобы разговор был предметным, напишем небольшое подопытное приложение, которое будет использовать вышеперечисленные техники маскировки. Весь функционал приложения будет сводиться к выводу сообщения «Hello, world!» при помощи функции MessageBox. Для этого нам потребуется всего три типа вызовов: GetKernelAddress() для получения адреса kernel32.dll, CalcHash() для вычисления хэша по имени функции, а также GetProcAddressEx() для получения адреса функции по хэшу.
Полный исходный код тестового приложения ты найдешь на диске:
.........
int main()
{
HMODULE kernel32, user32;
//получаем адрес kernel32.dll
kernel32 = (HMODULE) GetKernelAddress();
//получаем адрес функции LoadLibraryA
tLoadLibraryA pLoadLibraryA = (tLoadLibraryA)
GetProcAddressEx( kernel32, 0xC8AC8026 );
//загружаем библиотеку user32.dll
user32 = pLoadLibraryA("user32.dll");
//получаем адрес MessageBoxA из user32.dll и вызываем
tMessageBoxA pMessageBoxA = (tMessageBoxA)
GetProcAddressEx( user32, 0xABBC680D );
pMessageBoxA(0, "Hello, world!", 0, 0);
return 0;
}
..........
После компиляции код будет выглядеть примерно так:
00401000 push ebp
00401001 mov ebp, esp
00401003 sub esp, 10h
00401006 call sub_401060 <--- GetKernel()
0040100B mov [ebp+var_8], eax
0040100E push 0C8AC8026h <--- хэш LoadLibraryA
00401013 mov eax, [ebp+var_8]
00401016 push eax
00401017 call sub_4010E0
0040101C add esp, 8
0040101F mov [ebp+var_4], eax
00401022 push offset aUser32_dll ; "user32.dll"
00401027 call [ebp+var_4]
0040102A mov [ebp+var_10], eax
0040102D push 0ABBC680Dh <--- хэш MessageBoxA
00401032 mov ecx, [ebp+var_10]
00401035 push ecx
00401036 call sub_4010E0
0040103B add esp, 8
0040103E mov [ebp+var_C], eax
00401041 push 0
00401043 push 0
00401045 push offset aHelloWorld
; "Hello world!"
0040104A push 0
0040104C call [ebp+var_C]
0040104F xor eax, eax
00401051 mov esp, ebp
00401053 pop ebp
00401054 retn
Как видишь, никаких вызовов API-функций в чистом виде. Обращение к MessageBox превратилось в call [ebp+var_C] (по адресу 0040104C). Причем в случае настоящей малвари были бы зашифрованы еще и строки. На практике такой простой прием может значительно увеличить время анализа приложения. Как быть? Предположим, мы восстановили алгоритм работы программы и поняли, что значения, которые кладутся в стек по адресам 0040100E и 0040102D — это хэши функций. Но как узнать, что за функции скрываются за ними? В нашем текстовом образце — всего несколько вызовов, поэтому нет большой проблемы просто запустить отладчик и посмотреть, куда приведет нас call. Но что делать, когда таких вызовов сотни? Вот тут-то и понадобятся возможности IDAPython.
Наше решение
После беглого анализа кода становится видно, что функция sub_401060 возвращает адрес kernel32.dll. А sub_4010E0, вызываемая в программе два раза, возвращает адрес функции из заданной библиотеки по хэшу. Заглянем внутрь. В теле функции присутствует всего один call (по адресу 00401185), после которого возвращаемое значение сравнивается с переданным хэшем. Очевидно, что это функция вычисления хэша по имени. Переименуем ее в calc_hash. Взглянув на код, становится ясно, что фрагмент является самодостаточным (то есть не вызывает внутри себя других функций) и легко извлекается из общего листинга. Это значит, что его можно «выдернуть» и встроить в нашу программу для подсчета хэшей. Но мы поступим по-другому.
Вместо этого мы задействуем IDAPython и напишем скрипт, который с помощью calc_hash будет вычислять хэши всех экспортируемых имен в рамках текущей отладочной сессии. План таков:
- извлечь тело функции calc_hash и сделать его доступным для использования в скрипте;
- найти все экспортируемые имена;
- с помощью функции calc_hash посчитать хэш для каждого имени и запомнить результаты;
- вывести результаты в отдельном окне.
Итак, этап первый. Подготавливаем функцию calc_hash для использования в скрипте. Так как она является самодостаточной, то мы можем извлечь ее тело из базы данных IDA и вызывать ее при помощи ctypes:
# Извлекаем тело функции из базы IDA
body = idaapi.get_func(idc.LocByName('calc_hash'))
Выделяем буфер при помощи VirtualAlloc, передавая туда значения MEM_COMMIT, PAGE_EXECUTE_READWRITE (указываем, что память исполняемая)
calc_hash_ptr = windll.kernel32.VirtualAlloc(0,
len(body), 0x1000, 0x40)
Копируем тело функции в выделенный буфер
memmove(calc_hash_ptr, body, len(body))
Задаем прототип функции при помощи CFUNCTYPE. Первый параметр — тип возвращаемого значения, второй — тип передаваемого аргумента
proto = CFUNCTYPE(c_uint32, c_char_p)
Создаем экземпляр функции
calc_hash = proto(calc_hash_ptr)
В принципе, реально использовать механизм Appcall для достижения той же самой цели. Выполнив в командной строке питона Python>hex(Appcall.calc_hash("LoadLibraryA")&0xfffffff f) мы могли бы получить значение вычисленного хэша 0x0C8AC8026L. Но так как функция calc_hash будет вызвана по крайней мере 9 тысяч раз (для библиотеки kernel32.dll), то мы будем использовать первый рассмотренный способ как более быстрый.
Следующий этап — поиск экспортируемых имен. Тут я должен сделать небольшое замечание: отладочная сессия для этого должна быть активна. Другими словами, наша программа должна быть запущена под отладчиком. Мы предварительно ставим брейкпоинт, запускаем программу в IDA и, когда она остановится на бряке, запускаем на выполнение наш скрипт. Каждый раз при запуске отладочной сессии дебагер IDA Pro просит отладочный модуль предоставить список отладочных имен. По сути, отладочными именами являются экспортируемые имена всех загруженных модулей. Чтобы получить этот список программным путем, воспользуемся вызовом idaapi.get_debug_names(). Реализуем функцию fetch_debug_names, которая будет возвращать список имен в формате (адрес, имя функции, имя модуля):
def fetch_debug_names():
ret = []
dn = idaapi.get_debug_names(idaapi.cvar.inf.minEA,
idaapi.cvar.inf.maxEA)
for addr in dn:
n = dn[addr]
i = n.find('_')
ret.append((addr, n[i+1:], n[:i]))
return ret
Каждое возвращенное отладочное имя имеет следующий формат: Modulename_ApiName. Вот почему мы разделяем эту строку по символу «_». Теперь у нас есть функция для вычисления хэшей и список всех имен. Можно начинать генерировать хэши. Это уже третий этап:
dn = fetch_debug_names()
cache = {}
for add, name, modname in dn:
hash = calc_hash(name)
if modname not in cache:
cache[modname] = []
cache[modname].append((name, hash, addr))
После этого остается только вывести результат в красивом окне. В IDAPython есть возможность добавлять свои окна со списком, состоящим из чего-либо (как дефолтное окно со списком имен функций). Для этого нужно создать класс-наследник от типа Chooser2. Делается это довольно просто, поэтому подробно останавливаться на этом не буду.
На диске есть пример и полная версия нашего скрипта, которую теперь ты можешь использовать. Хочу заметить, что в сценарии также реализован импорт вычисленных хэшей в Enums (список). Это выглядит так. Допустим, что мы встретили в коде очередной вызов функции по хэшу:
push 3FC1BD8Dh
....
call sub_4010E0
Теперь мы можем нажать <m> и выбрать соответствующую ему функцию, после чего вызов сразу примет удобочитаемый вид:
push hash_kernel32_GetModuleHandleA
....
call sub_4010E0
Удобно? Еще бы! Вот так, единожды решив задачу, можно использовать наработки во время реверсинга вновь и вновь. Конечно, одного прочтения статьи недостаточно, чтобы сходу начать разрабатывать сценарии для IDA. Но наша задача была в том, чтобы показать, как можно быстро и изящно заточить дизассемблер под себя и прокачать его функциональность. Этим постоянно пользуются многие реверсеры и ресечеры, в том числе наши постоянные авторы.
Info
- Поскольку набирать длинные скрипты на Python во встроенной в IDA интерактивной консоли не очень удобно, рекомендую тебе взглянуть на следующий скриптик IPython (http://bit.ly/rl4kK). Он осуществляет интеграцию интерактивной Python-консоли, работать в которой намного удобнее, нежели в дефолтной.
- IDA 6.0 Pro (hexrays.com/products.shtml) — это последняя доступная версия, ее придется покупать за деньги. Релиз 5.0 с недавнего времени стал абсолютно бесплатным, что не может не радовать.
Links
- Подробная информация по модулям IDAPython: hex-rays.com/idapro/idapython_docs;
- Информация том, как работает Appcall: hexblog.com/?p=113.