• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Автоматизация IDA Pro. Дорабатываем «Иду» напильником

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,178
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Управляющие отладчиком плагины позволяют свалить на машину рутинные задачи отладки и поиска уязвимостей. Мы на примерах рассмотрим обход антиотладки, поиск путей до уязвимых функций и подсветку интересующего кода. Для этого напишем плагин на C++, глянем встроенный язык IDC и попрактикуемся писать скрипты на IDAPython, а также узнаем, как обрабатывать им несколько файлов за раз.

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. Библиотечные функции Для просмотра ссылки Войди или Зарегистрируйся, но дают меньше контроля, чем C++ SDK. Здесь есть исключения, классы и синтаксический сахар вроде склейки двух строк через + и получения подстроки через срезы str[0:2].

Код:
#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.


Заключение​

Скрипты с доступом к внутренним API значительно расширяют возможности «Иды», превращая ее из декомпилятора в любой необходимый инструмент, будь то анализатор кода или ситуативно улучшенный отладчик. А запуск из командной строки еще и делает ее оружием массового поражения вражеского кода!
 
Activity
So far there's no one here
Сверху Снизу