stihl не предоставил(а) никакой дополнительной информации.
Управляющие отладчиком плагины позволяют свалить на машину рутинные задачи отладки и поиска уязвимостей. Мы на примерах рассмотрим обход антиотладки, поиск путей до уязвимых функций и подсветку интересующего кода. Для этого напишем плагин на C++, глянем встроенный язык IDC и попрактикуемся писать скрипты на IDAPython, а также узнаем, как обрабатывать им несколько файлов за раз.
Попробуем собрать и запустить тестовый плагин в Visual Studio 2019 или выше. Создаем пустой проект для Windows (не для консоли), указываем тип конфигурации — динамическая библиотека. В дополнительные каталоги включаемых файлов прописываем путь до папки с инклудами: idasdk90\include. Подключаемые либы из idasdk90\lib\x64_win_vc_64 можно просто перетащить в список файлов проекта, компилятор подхватит их оттуда.
Архитектура стандартная. В экспорте указывается ссылка на структуру PLUGIN. Она содержит описание плагина, желаемую горячую клавишу и ссылки на колбэки. Функция init возвращает ссылку на экземпляр класса MyPlugmod. Он содержит функцию run, которая будет запущена при вызове плагина. Код колбэка получает количество распознанных «Идой» функций через get_func_qty и проходит по каждой, чтобы получить ее имя (через getn_func) и адрес начала. После чего выводит собранную информацию в консоль.
Копируем собранный плагин в папку IDA Professional 9.0 SP1\plugins. Теперь в меню Edit → Plugins появится новый пункт List functions. Как вариант, плагин можно вызывать через указанное в нем сочетание клавиш Ctrl-Q.
У этого подхода есть коммерческий интерес. SDK предоставляет крайне скупую документацию, фактически только имена аргументов и функций. Чтобы узнать детали работы конкретной функции API, надо искать чужой код или обращаться в поддержку, доступ к которой стоит немалых денег. Можно сказать, что потеря совместимости — своего рода защита от пиратства. Жаль, что от нее страдает конечный пользователь.
IDC — это С‑подобный язык без строгой типизации. Но в отличие от С в нем нет ссылок, все аргументы передаются по значению. Язык поддерживает большинство сишных выражений, за исключением +=. Пользовательские функции объявляются через ключевое слово static. Библиотечные функции Для просмотра ссылки Войдиили Зарегистрируйся, но дают меньше контроля, чем C++ SDK. Здесь есть исключения, классы и синтаксический сахар вроде склейки двух строк через + и получения подстроки через срезы str[0:2].
Код аналогичен примеру из прошлого раздела. Он получает ea (Effective Address) первой функции через get_next_func. И продолжает в цикле спрашивать адреса следующих функций, пока API не вернет константу BADADDR. Имя функции возвращает get_func_name, флаги с метаданными берутся из get_func_flags. Тот же msg выводит данные в консоль.
Главное достоинство скриптов на IDC — поддержка «из коробки». В остальном они фатально устарели. Раньше все серьезные решения писались на C++ SDK, однако, судя по публичным репозиториям, сегодня плагины и одноразовые скрипты пишутся преимущественно на Python.
На сегодняшний день IDAPython — основной язык, на котором пишутся новые и на который Для просмотра ссылки Войдиили Зарегистрируйся старые плагины. Свою роль в этом сыграли озвученные выше проблемы обратной совместимости. Да и с Для просмотра ссылки Войди или Зарегистрируйся дела обстоят немного лучше.
Код на IDAPython позволяет делать то же самое, что C++ SDK. Нетрудно заметить, что имена функций совпадают. Фактически IDAPython — это тонкая и удобная обертка для низкоуровневых API. Поэтому на IDAPython можно писать Для просмотра ссылки Войдиили Зарегистрируйся для неизвестных форматов файлов или Для просмотра ссылки Войди или Зарегистрируйся с поддержкой собственных окон в графическом интерфейсе.
По традиции рассмотрим плагин, выводящий список функций. Поместив код в папку IDA Professional 9.0 SP1\plugins и перезапустив «Иду», получаем новый пункт в меню — List Functions v2. Как вариант, плагин можно запустить, нажав Alt-F8.
Получился полный аналог плагина на C++, с той разницей, что в Python есть удобные обертки из библиотеки idautils, которые сообщают нам список адресов функций. Имя get_func_name нам знакомо по прошлому примеру. Вместо структуры PLUGIN здесь используются поля класса, наследуемого от idaapi.plugin_t. В классе три заранее определенных функции: конструктор, деструктор и run — функция, содержащая код, запускаемый при вызове плагина.
После запуска скрипта видим, что все строчки с инструкцией CALL теперь выделены бледно‑синим.
Функция idautils.Segments() возвращает список сегментов — адресов начала каждой секции в PE32. Далее idautils.Heads возвращает все элементы внутри обозначенных адресов. Это может быть код или данные. Проверяем, что элемент является кодом, через idc.is_code. Получаем мнемонику из инструкции через idc.print_insn_mnem и сравниваем ее с искомой CALL. И наконец, idc.set_color раскрашивает элемент заданным цветом. Цвета задаются в формате BBGGRR, то есть, чтобы сделать чисто синий, надо написать FF0000.
Проще всего (через idc.get_name_ea_simple) получить ссылку на IAT_NAME — четыре байта в секции импорта, в которые загрузчик запишет целевой адрес вызываемой функции WinAPI. Далее берем актуальный адрес функции, используя idc.get_wide_dword, и ставим на него точку останова при помощи idc.add_bpt.
Для создания хука используется класс ExitHook, который наследует idaapi.DBG_Hooks. Это современный способ ставить хуки на разные отладочные события, в том числе и на точки останова. Хитрость в том, что экземпляр класса должен быть глобальным, если создать его локально в setup_hook, то сборщик мусора удалит его в момент завершения функции. IDA ничего не скажет, но установленный хук не сработает.
В обработчике точек останова dbg_bpt проверяем, что мы остановились по адресу перехватываемой WinAPI, и получаем из стека адрес возврата. Ставим на него вторую точку останова. Когда она срабатывает, подменяем значение регистра EAX нолем, больше не нужный хук удаляем.
Запустив скрипт при старте тестового приложения, убеждаемся, что хук работает: IsDebuggerPresent возвращает ноль и сообщение не отображается.
Вкратце объясню, что здесь происходит. Функция find_calls_to_import составляет карту вызовов — запоминает все места, откуда вызывается lstrcpyW. Дальше build_call_graph продолжает строить карту, проходя по всем функциям и собирая все вызовы. Наконец, из полученных данных build_paths строит все возможные пути до искомого адреса. Код верхнего уровня фильтрует полученные пути, чтобы выводить только уникальные комбинации.
Запускаем скрипт и смотрим результат его работы:
[+] Found lstrcpyW import at: 0x5A9170
[*] Scanning for calls to imported function...
[+] Found 13 direct callers of lstrcpyW
[*] Building global call graph...
[+] All unique call paths to lstrcpyW:
Ключ -A запускает IDA в автономном режиме, без диалоговых окон. Ключ -S указывает путь до скрипта. Далее идет путь до исследуемого файла.
Запускаемый скрипт должен дождаться конца анализа, для этого используется ida_auto.auto_wait. Результат работы скрипта записывается во внешний файл. После чего завершаем работу IDA.
info
Горячо рекомендую книгу Криса Игла «The IDA Pro Book». Она устарела по части API, но отвечает на большинство вопросов.
Плагин на C++
Первый способ расширить возможности «Иды» — это компилируемые DLL. Для сборки плагина требуется C++ SDK. Легально скачать его можно, только купив лицензию на IDA Pro, о нелегальных способах ты можешь узнать сам.Попробуем собрать и запустить тестовый плагин в Visual Studio 2019 или выше. Создаем пустой проект для Windows (не для консоли), указываем тип конфигурации — динамическая библиотека. В дополнительные каталоги включаемых файлов прописываем путь до папки с инклудами: idasdk90\include. Подключаемые либы из idasdk90\lib\x64_win_vc_64 можно просто перетащить в список файлов проекта, компилятор подхватит их оттуда.
Код:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <ida.hpp>
#include <idp.hpp>
#include <loader.hpp>
#include <funcs.hpp>
class MyPlugmod : public plugmod_t
{
public:
MyPlugmod()
{
msg("MyPlugmod: Constructor called.\n");
}
virtual ~MyPlugmod()
{
msg("MyPlugmod: Destructor called.\n");
}
virtual bool idaapi run(size_t arg) override
{
msg("MyPlugmod.run() called with arg: %d\n", arg);
int n = get_func_qty();
for (int i = 0; i < n; i++) {
func_t* pfn = getn_func(i);
if (pfn == nullptr)
continue;
qstring name;
get_func_name(&name, pfn->start_ea);
msg("Function %s at address 0x%llX\n", name.length() ? name.c_str() : "-UNK-", pfn->start_ea);
}
return true;
}
};
static plugmod_t* idaapi init(void)
{
return new MyPlugmod();
}
static const char comment[] = "The plugin displays a list of functions along with the addresses.";
static const char help[] = "Information is not provided";
static const char wanted_name[] = "List functions";
static const char wanted_hotkey[] = "Ctrl+Q";
plugin_t PLUGIN =
{
IDP_INTERFACE_VERSION,
PLUGIN_MULTI, // flags
init, // initialize
nullptr, // terminate
nullptr, // invoke the plugin
comment,
help,
wanted_name,
wanted_hotkey
};
Архитектура стандартная. В экспорте указывается ссылка на структуру PLUGIN. Она содержит описание плагина, желаемую горячую клавишу и ссылки на колбэки. Функция init возвращает ссылку на экземпляр класса MyPlugmod. Он содержит функцию run, которая будет запущена при вызове плагина. Код колбэка получает количество распознанных «Идой» функций через get_func_qty и проходит по каждой, чтобы получить ее имя (через getn_func) и адрес начала. После чего выводит собранную информацию в консоль.
Копируем собранный плагин в папку IDA Professional 9.0 SP1\plugins. Теперь в меню Edit → Plugins появится новый пункт List functions. Как вариант, плагин можно вызывать через указанное в нем сочетание клавиш Ctrl-Q.
Проблемы совместимости
Ключевая проблема плагинов — крайне низкая совместимость между старыми и новыми версиями SDK. Плагин, собранный с учетом одной версии, скорее всего, не заработает (и не соберется без танцев с бубном) на слегка обновленном SDK. То есть для каждого выпуска IDA Pro надо модифицировать и собирать свою версию плагина. Этим, разумеется, никто не занимается. Поэтому многие плагины со временем стали недоступны.У этого подхода есть коммерческий интерес. SDK предоставляет крайне скупую документацию, фактически только имена аргументов и функций. Чтобы узнать детали работы конкретной функции API, надо искать чужой код или обращаться в поддержку, доступ к которой стоит немалых денег. Можно сказать, что потеря совместимости — своего рода защита от пиратства. Жаль, что от нее страдает конечный пользователь.
IDC
Поддержка скриптов на языке IDC появилась со второй версии IDA Pro, в 1994 году.IDC — это С‑подобный язык без строгой типизации. Но в отличие от С в нем нет ссылок, все аргументы передаются по значению. Язык поддерживает большинство сишных выражений, за исключением +=. Пользовательские функции объявляются через ключевое слово static. Библиотечные функции Для просмотра ссылки Войди
Код:
#include <idc.idc>
static main()
{
auto ea,x;
for ( ea=get_next_func(0); ea != BADADDR; ea=get_next_func(ea) )
{
msg("Function at %08lX: %s", ea, get_func_name(ea));
x = get_func_flags(ea);
if ( x & FUNC_NORET ) msg(" Noret");
if ( x & FUNC_FAR ) msg(" Far");
msg("\n");
}
}
Код аналогичен примеру из прошлого раздела. Он получает ea (Effective Address) первой функции через get_next_func. И продолжает в цикле спрашивать адреса следующих функций, пока API не вернет константу BADADDR. Имя функции возвращает get_func_name, флаги с метаданными берутся из get_func_flags. Тот же msg выводит данные в консоль.
Главное достоинство скриптов на IDC — поддержка «из коробки». В остальном они фатально устарели. Раньше все серьезные решения писались на C++ SDK, однако, судя по публичным репозиториям, сегодня плагины и одноразовые скрипты пишутся преимущественно на Python.
IDAPython
Этот плагин добавляет в IDA Pro поддержку кода на Python. Первый релиз проекта состоялся в 2004 году. Его автор Dyce (Гергей Эрдейи) разработал плагин на деньги своего тогдашнего работодателя — финской компании F-Secure, выпускающей одноименный антивирус. В середине 2010 года из‑за нехватки времени на разработку проект был передан во владение Hex-Rays. Поддержкой IDAPython по сей день занимается их сотрудник 0xeb (Элиас Бачалани). Начиная с версии 5.4, плагин стал частью IDA Pro.На сегодняшний день IDAPython — основной язык, на котором пишутся новые и на который Для просмотра ссылки Войди
Код на IDAPython позволяет делать то же самое, что C++ SDK. Нетрудно заметить, что имена функций совпадают. Фактически IDAPython — это тонкая и удобная обертка для низкоуровневых API. Поэтому на IDAPython можно писать Для просмотра ссылки Войди
Код:
import idaapi
import idautils
import idc
class ListFunctionsPlugin(idaapi.plugin_t):
flags = idaapi.PLUGIN_UNL
comment = "The plugin displays a list of functions along with the addresses."
help = "Information is not provided"
wanted_name = "List Functions v2"
wanted_hotkey = "Alt-F8"
def init(self):
print("[ListFunctionsPlugin] Constructor called.")
return idaapi.PLUGIN_OK
def run(self, arg):
for func_ea in idautils.Functions():
func_name = idc.get_func_name(func_ea)
print(f"0x{func_ea:08X}: {func_name}")
def term(self):
print("[ListFunctionsPlugin] Destructor called.")
def PLUGIN_ENTRY():
return ListFunctionsPlugin()
По традиции рассмотрим плагин, выводящий список функций. Поместив код в папку IDA Professional 9.0 SP1\plugins и перезапустив «Иду», получаем новый пункт в меню — List Functions v2. Как вариант, плагин можно запустить, нажав Alt-F8.
Получился полный аналог плагина на C++, с той разницей, что в Python есть удобные обертки из библиотеки idautils, которые сообщают нам список адресов функций. Имя get_func_name нам знакомо по прошлому примеру. Вместо структуры PLUGIN здесь используются поля класса, наследуемого от idaapi.plugin_t. В классе три заранее определенных функции: конструктор, деструктор и run — функция, содержащая код, запускаемый при вызове плагина.
Пишем скрипты на IDAPython
Покажу несколько примеров того, как можно облегчить себе жизнь, используя IDAPython. Скрипты с диска запускаются через File → Script File, короткие скрипты из буфера обмена можно запустить через File → Script Command.Подсветка CALL
Код:
import idautils
import idc
CALL_COLOR = 0xFFDDCC
for seg_ea in idautils.Segments():
for head in idautils.Heads(seg_ea, idc.get_segm_end(seg_ea)):
if idc.is_code(idc.get_full_flags(head)):
mnem = idc.print_insn_mnem(head)
if mnem.lower() == "call":
idc.set_color(head, idc.CIC_ITEM, CALL_COLOR)
После запуска скрипта видим, что все строчки с инструкцией CALL теперь выделены бледно‑синим.
Функция idautils.Segments() возвращает список сегментов — адресов начала каждой секции в PE32. Далее idautils.Heads возвращает все элементы внутри обозначенных адресов. Это может быть код или данные. Проверяем, что элемент является кодом, через idc.is_code. Получаем мнемонику из инструкции через idc.print_insn_mnem и сравниваем ее с искомой CALL. И наконец, idc.set_color раскрашивает элемент заданным цветом. Цвета задаются в формате BBGGRR, то есть, чтобы сделать чисто синий, надо написать FF0000.
Подмена результатов WinAPI
Попробуем обойти простейшую антиотладку. Допустим, программа сравнивает результат IsDebuggerPresent с единицей. Правильнее всего было бы стереть флаг BeingDebugged из PEB (Process Environment Block), но, чтобы сделать пример интереснее, попробуем подменять результат WinAPI на ходу.
Код:
import idc
import idaapi
IAT_NAME = "__imp__IsDebuggerPresent@0"
RETURN_VALUE = 0
global_hook = None
class ExitHook(idaapi.DBG_Hooks):
def init(self, target_addr):
super().init()
self.target_addr = target_addr
self.ret_addr = None
def dbg_bpt(self, tid, ea):
print(f"[+] Breakpoint hit at 0x{ea:X}")
if ea == self.target_addr:
esp = idc.get_reg_value("esp")
print(f"[+] ESP: 0x{esp:X}")
self.ret_addr = idc.get_wide_dword(esp)
print(f"[+] Captured return address: 0x{self.ret_addr:X}")
idc.add_bpt(self.ret_addr)
elif self.ret_addr and ea == self.ret_addr:
print(f"[+] Return point hit. Overwriting EAX with {RETURN_VALUE}")
idc.set_reg_value(RETURN_VALUE, "EAX")
idc.del_bpt(self.ret_addr)
self.ret_addr = None
return 0
def setup_hook():
global global_hook
imp_ptr = idc.get_name_ea_simple(IAT_NAME)
if imp_ptr == idc.BADADDR:
print(f"[-] Import {IAT_NAME} not found.")
return
target_addr = idc.get_wide_dword(imp_ptr)
print(f"[+] Real ExitProcess address: 0x{target_addr:X}")
idc.add_bpt(target_addr)
global_hook = ExitHook(target_addr)
global_hook.hook()
print("[+] Hook installed. Start or resume process (F9).")
setup_hook()
Проще всего (через idc.get_name_ea_simple) получить ссылку на IAT_NAME — четыре байта в секции импорта, в которые загрузчик запишет целевой адрес вызываемой функции WinAPI. Далее берем актуальный адрес функции, используя idc.get_wide_dword, и ставим на него точку останова при помощи idc.add_bpt.
Для создания хука используется класс ExitHook, который наследует idaapi.DBG_Hooks. Это современный способ ставить хуки на разные отладочные события, в том числе и на точки останова. Хитрость в том, что экземпляр класса должен быть глобальным, если создать его локально в setup_hook, то сборщик мусора удалит его в момент завершения функции. IDA ничего не скажет, но установленный хук не сработает.
В обработчике точек останова dbg_bpt проверяем, что мы остановились по адресу перехватываемой WinAPI, и получаем из стека адрес возврата. Ставим на него вторую точку останова. Когда она срабатывает, подменяем значение регистра EAX нолем, больше не нужный хук удаляем.
Код:
#include <windows.h>
int APIENTRY wWinMain(In HINSTANCE hInstance,
In_opt HINSTANCE hPrevInstance,
In LPWSTR lpCmdLine,
In int nCmdShow)
{
if (IsDebuggerPresent())
{
MessageBoxA(0, "debugger", "!", 0);
}
}
Запустив скрипт при старте тестового приложения, убеждаемся, что хук работает: IsDebuggerPresent возвращает ноль и сообщение не отображается.
Поиск путей до небезопасных функций
Если в импорте обнаружены функции, потенциально ведущие к переполнению буфера, стоит проверить, можно ли передать в них пользовательские данные. Для этого нужно составить граф, пройдя от заданной функции до всех тех, что ее вызывают, всех, которые вызывают их, и так далее до самого верхнего уровня.
Код:
import idautils
import idc
import idaapi
import ida_funcs
from collections import defaultdict
IMPORT_NAME = "lstrcpyW"
callers_map = defaultdict(set)
calls_to_func = []
def get_func_name(ea):
return idc.get_func_name(ea) or f"sub_{ea:X}"
def get_func_start_ea(ea):
f = ida_funcs.get_func(ea)
return f.start_ea if f else ea
def find_import_address():
ea = idc.get_name_ea_simple(IMPORT_NAME)
if ea == idc.BADADDR:
print(f"[-] Import {IMPORT_NAME} not found.")
return None
print(f"[+] Found {IMPORT_NAME} import at: 0x{ea:X}")
return ea
def find_calls_to_import(imp_addr):
print("[*] Scanning for calls to imported function...")
for func_ea in idautils.Functions():
for insn_ea in idautils.FuncItems(func_ea):
if idc.print_insn_mnem(insn_ea).lower() != "call":
continue
op_type = idc.get_operand_type(insn_ea, 0)
if op_type in [idc.o_mem, idc.o_displ]:
target = idc.get_operand_value(insn_ea, 0)
if target == imp_addr:
calls_to_func.append(func_ea)
callers_map[imp_addr].add(func_ea)
print(f"[+] Found {len(calls_to_func)} direct callers of {IMPORT_NAME}")
def build_call_graph():
print("[*] Building global call graph...")
for func_ea in idautils.Functions():
for insn_ea in idautils.FuncItems(func_ea):
if idc.print_insn_mnem(insn_ea).lower() != "call":
continue
target = idc.get_operand_value(insn_ea, 0)
if ida_funcs.get_func(target):
callers_map[target].add(func_ea)
def build_paths(target_ea, path=None, visited=None):
if path is None:
path = []
if visited is None:
visited = set()
path = [target_ea] + path
visited.add(target_ea)
if target_ea not in callers_map or not callers_map[target_ea]:
yield path
else:
for caller in callers_map[target_ea]:
if caller not in visited:
yield from build_paths(caller, path, visited.copy())
def print_path_tree(path):
for depth, ea in enumerate(path):
print(" " * depth + f"- {get_func_name(ea)}")
def main():
imp_addr = find_import_address()
if not imp_addr:
return
find_calls_to_import(imp_addr)
build_call_graph()
print("\n[+] All unique call paths to lstrcpyW:\n")
seen_paths = set()
for caller in calls_to_func:
for path in build_paths(caller):
norm_path = tuple(get_func_start_ea(ea) for ea in path)
if norm_path not in seen_paths:
seen_paths.add(norm_path)
print_path_tree(path)
if not seen_paths:
print("[-] No paths found.")
main()
Вкратце объясню, что здесь происходит. Функция find_calls_to_import составляет карту вызовов — запоминает все места, откуда вызывается lstrcpyW. Дальше build_call_graph продолжает строить карту, проходя по всем функциям и собирая все вызовы. Наконец, из полученных данных build_paths строит все возможные пути до искомого адреса. Код верхнего уровня фильтрует полученные пути, чтобы выводить только уникальные комбинации.
Запускаем скрипт и смотрим результат его работы:
[+] Found lstrcpyW import at: 0x5A9170
[*] Scanning for calls to imported function...
[+] Found 13 direct callers of lstrcpyW
[*] Building global call graph...
[+] All unique call paths to lstrcpyW:
- sub_48CCA0
- sub_490F90
- sub_48D020
- sub_41B820
- sub_48CCA0
- sub_490F90
- sub_48D020
- ?__scrt_common_main_seh@@YAHXZ
- _WinMain@16
- sub_525BB0
Массовое исполнение
IDA Pro имеет встроенное управление из командной строки. Достаточно запустить ее в headless-режиме, то есть без отображения графики, сменив ida64.exe на idat64.exe:idat64.exe -A -S"path\to\script.py" path\to\target.exe
Ключ -A запускает IDA в автономном режиме, без диалоговых окон. Ключ -S указывает путь до скрипта. Далее идет путь до исследуемого файла.
Код:
import ida_auto
import idc
ida_auto.auto_wait()
with open("result.txt", "w") as f:
f.write("something")
idc.qexit(0)
Запускаемый скрипт должен дождаться конца анализа, для этого используется ida_auto.auto_wait. Результат работы скрипта записывается во внешний файл. После чего завершаем работу IDA.