На русском языке (и на Хабре, в частности) не так много статей по работе с IDAPython, попытаемся восполнить этот пробел.


Для кого. Для тех, кто уже умеет работать в IDA Pro, но ни разу не писал скрипты на IDAPython. Если вы уже имеете опыт написания скриптов под IDAPython, то вряд ли найдёте здесь что-то новое.


Чего здесь не будет. Мы не будем учить программированию на Python и базовой работе в IDA Pro.


0x00. Знакомство


Как очевидно из названия, IDAPython — это всего-навсего интерпретатор Python, встроенный в дизассемблер IDA как инструмент автоматизации. Функции IDAPython являются "обёртками" для IDC (внутренний С-подобный язык автоматизации IDA Pro).


К сожалению, IDAPython до сих пор имеет скудную документацию, и зачастую ответы на вопросы по API приходится искать в исходниках или получать опытным путём. Модули IDAPython находятся в поддиректории python дизассемблера IDA (обычно это C:\Program Files\IDA 7.0\python).


В этом руководстве мы будем рассматривать IDA Pro версии 7.0. IDAPython является плагином для IDA и идёт сразу в "коробочке" (нет необходимости его устанавливать). В версиях IDA до 7.4 используется Python 2.7 64-bit.


Примечание


Начиная с 7-й версии в IDAPython обновили API, а с версии 7.4 отключили поддержку старого API. Модуль idc_bc695.py обеспечивает обратную совместимость, но в какой-то момент его перестанут поддерживать. В сети в статьях и советах до 2017 года используется старый API, поэтому будьте внимательны и сверяйтесь с текущей документацией.


0x01. В первом приближении


Поскольку IDAPython — это средство автоматизации, то исследователь должен понимать, как совершить ту или иную операцию вручную, чтобы потом ее можно было автоматизировать.


Распространенными задачами для такой автоматизации могут быть:


  • переименование функций,
  • комментирование кода/данных,
  • преобразование кода/данных (изменение типов данных, добавление в код перекрёстных ссылок),
  • поиск каких-нибудь хитрых шаблонов кода/данных,
  • патчинг кода.

Естественно, этими случаями автоматизация не ограничивается — пространство для творчества велико.


Принятые обозначения и соглашения


При работе с IDAPython принимается ряд условных обозначений:


  • ea — Effective Address — адрес в базе IDA, к которому применяется та или иная функция;
  • функция here() возвращает текущий адрес, где установлен курсор в Disassembly-окне. Тип результата функции — long;
  • большинство функций импортируются из модуля idc, этот модуль импортирован по умолчанию. Некоторые функции находятся в других модулях, в этом случае подключение модуля будет указано явно;
  • в качестве "подопытного" будет использоваться файл прошивки Носорога в формате ELF.

Как выполнить код


Выполнить код IDAPython можно несколькими способами:


  • через короткие скрипты в командной строке IDA;
  • запустить скрипт через меню File — Script file (Alt + F7);
  • запустить скрипт через меню File — Script command (Shift + F2);
  • запустить IDA Pro из командной строки с параметром -S.

Рассмотрим особенности каждого способа.


Командная строка IDA CLI


Командная строка расположена внизу главного окна IDA. Слева находится кнопка, позволяющая выбрать язык для вводимых команд, нужно кликнуть по ней и выбрать Python.



Не забудьте выбрать Python как язык для CLI


Особенности IDA CLI


  • Ввод осуществляется построчно как в интерактивном режиме Python.
  • Для блоков кода (функций, циклов, условий) отступы не добавляются автоматически, поэтому не забудьте их добавить вручную.
  • Поддерживается автодополнение вводимых функций при помощи клавиши Tab.
  • Автодополнение работает только для имён функций без указания имени модуля.
  • Функции help и dir работают как в обычном Python и позволяют получить информацию об объекте.
  • Вывод результатов команд осуществляется в окне Output window (Alt + 0).

Пример


Класс Segments из модуля idautils создаёт генератор, выдающий начальные адреса сегментов внутри базы IDA. Вот так будет выглядеть вывод в окне Output window после построчного ввода команд в IDA CLI:


Python>import idautils
Python>help(idautils.Segments)
Help on function Segments in module idautils:
Segments()
Get list of segments (sections) in the binary image
@return: List of segment start addresses.
Python>for ea in idautils.Segments():
Python> print("%08x %s" % (ea, idc.get_segm_name(ea)))
Python>
08000000 .isr_vector
080000c0 .text
08006f9c .rodata
08007a14 .init_array
08007a18 .fini_array
20000000 .data
200001f8 .bss
200006c8 ._user_heap_stack
200017f8 abs

Script File


Для тренировки запуска скриптов через меню File — Script file (Alt + F7) напишем небольшой вспомогательный модуль.


При работе с кодом и данными иногда возникает необходимость преобразовать операнды из одного представления в другое (Hex / Char / Decimal). Для этого есть функции с префиксом op_, вот некоторые из них:


  • op_bin(ea, n) — преобразование операнда в двоичный вид;
  • op_dec(ea, n) — преобразование операнда в десятичный вид;
  • op_oct(ea, n) — преобразование операнда в восьмеричное число;
  • op_hex(ea, n) — представление операнда в hex-виде;
  • op_chr(ea, n) — представление операнда в виде символа;
  • op_seg(ea, n) — представление операнда в виде ссылки на сегмент;
  • op_stkvar(ea, n) — преобразование операнда в стековую переменную;
  • op_stroff(ea, n) — преобразование операнда в поле структуры;
  • op_enum(ea, n) — представление операнда в виде ENUM-константы;
  • op_plain_offset(ea, n, base) — представление операнда в виде ссылки на объект.

Где:


  • ea — адрес, в котором будет применена функция;
  • n — номер операнда в инструкции (нумерация с нуля); то есть в инструкции mov eax, 0x10:
    • eax — 0-й операнд;
    • 0x10 — 1-й операнд;
  • base — базовый адрес для offset-операнда (ссылка) .

Поскольку в IDA 7.0 используется Python 2.7, то для использования кириллических комментариев необходимо указать кодировку файла (или использовать только латиницу).
Тогда можно составить такой модуль для преобразований:


# coding: utf-8
""" 
File: transforms.py
Функции преобразования отображения операндов
"""

START = 0x08000038
END = 0x08000084

def make_offsets32(start_ea, end_ea):
    """Преобразование данных в ссылки на объекты"""
    for ea in xrange(start_ea, end_ea, 4):
        idc.op_plain_offset(ea, 0, 0)

def make_dwords(start_ea, end_ea):
    """Преобразование данных в 32-битные числа в hex-представлении"""
    for ea in xrange(start_ea, end_ea, 4):
        idc.op_hex(ea, 0)

if __name__ == '__main__':
    make_dwords(START, END)

По аналогии с выполнением скрипта в Python, весь код в теле модуля выполняется при запуске в IDA. В данном случае будет выполнена функция make_dwords с адреса START по адрес END. При этом все функции, которые есть в загруженном файле, после выполнения скрипта становятся доступны для запуска через IDA CLI.


Для упрощения доступа к недавним скриптам в IDA есть окно Recent scripts (меню View — Recent scripts Alt + F9):



Важное примечание: если вы разрабатываете и отлаживаете IDAPython-утилиту, которая состоит из нескольких модулей, то при обновлении неглавного модуля придётся перезапускать весь дизассемблер IDA, потому что Python 2 не умеет перезагружать модули, а IDA не умеет отдельно перезагружать свои плагины.


Script Command


Меню File — Script command (Shift + F2) позволяет написать и сохранить скрипт в текущей базе IDA. Это удобно, если предполагается, что некоторые действия могут выполняться по многу раз в текущей базе. В случае если базу нужно передать коллегам, скрипты также будут переданы автоматически.


Код написанный в этом окне сохраняется автоматически. Для выполнения кода нужно нажать кнопку Run.



Запуск IDA c ключом -S


IDA Pro, как и многие приложения, поддерживает запуск с ключами через командную строку. Информацию по всем ключам запуска можно прочитать во встроенной справке (по клавише F1) в разделе Command line switches.


Среди прочих есть ключи, позволяющие выполнить скрипт при запуске:


> C:\Program Files\IDA 7.0\ida.exe" -A -Sscript_name.py rhino.idb


  • -A задаёт режим автономной работы без дополнительных диалоговых окон,
  • -S служит для запуска скрипта script_name.py однократно при открытии IDB-файла.

Ввод/вывод


При взаимодействии с пользователем есть следующие особенности:


  • оператор print выводит результат в окно Output window (Alt + 0);
  • двойной клик по строке в окне Output window позволяет перейти в Disassembly-окно по адресу или имени объекта, если такой существует в базе IDA. Ниже приведён вывод из Output window с указанием случаев, когда будут и не будут выполняться переходы в Disassembly-окне.

Python>here()
134239060         # 1. здесь не будет перехода
Python>hex(here())
0x8005354L        # 2. здесь не будет перехода
Python>"%08x" % here()
08005354          # 3. будет выполнен переход
Python>get_name(here())
aErrorWrongHead   # 4. будет выполнен переход

  • функции input/raw_input не работают;
  • для пользовательского ввода нужно использовать ask_-функции модулей ida_kernwin и idaapi.


Отладка скриптов


Отладка скриптов в привычном понимании под IDAPython невозможна, поскольку скриптам требуется доступ к "внутренностям" IDA Pro. Имеющиеся рецепты неактуальны для IDA 7-ой версии.


Актуальными средствами отладки для IDAPython-скриптов можно назвать логирование и вывод промежуточных значений (в народе — "отладка принтами").


IDAPyHelper


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



IDAPython Cheatsheet


Для упрощения работы с IDAPython мы сделали свою шпаргалку популярных функций. Особенность шпаргалки — цветовое представление типов аргументов и результатов функций.



0x02. Комментирование вызова функции


От общих слов перейдем к конкретным примерам.


Удобным приёмом при исследовании бинарного кода является комментирование отдельных участков. Интересно, что если оставить комментарий в строке с вызовом функции, то этот комментарий будет отображаться в окне ссылок на функцию (Xrefs по клавише X), как это показано на рисунке ниже.



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


Расставим комментарии в местах обращений к функции memcpy. Как видно из рисунка, в текущей базе известно 11 обращений к memcpy.



Общий алгоритм для добавления комментариев будет таким:


  • получить все кросс-ссылки на функцию memcpy;
  • в каждом месте обращения к memcpy составить список аргументов;
  • составить строку комментария;
  • добавить комментарий.

Для выполнения этих действий потребуются следующие функции и классы:


  • idautils.CodeRefsTo(func_ea, flow) — создаёт генератор, который возвращает объекты-ссылки из кода на указанный адрес. Параметр flow (значения 0/1) задаёт необходимость учитывать ссылки, которые сформированы за счёт простого перехода от инструкции к инструкции. В нашем случае flow = 0;
  • idc.prev_head(ea) — возвращает адрес предыдущей инструкции относительно указанного адреса, это необходимо для прохода вверх по коду для поиска аргументов;
  • idc.get_operand_value(ea, n) — возвращает значение n-го операнда по указанному адресу. Если операнд является регистром, то возвращается номер регистра в соответствии с текущей процессорной архитектурой. Для архитектуры ARM номера регистров очевидны:
    • R0 — 0;
    • R1 — 1;
    • R2 — 2 и так далее;
  • idc.get_operand_type(ea, n) — возвращает тип n-го операнда по указанному адресу. Основные типы операндов:
    • o_void = 0 — инструкция без операнда (например, NOP);
    • o_reg = 1 — регистр;
    • o_mem = 2 — адрес в памяти;
    • o_phrase = 3 — составной адрес [Base Reg + Index Reg],
    • o_displ = 4 — составной адрес [Base Reg + Index Reg + Displacement];
    • o_imm = 5 — число-константа;
    • o_far = 6 — FAR-адрес;
    • o_near = 7 — NEAR-адрес;
    • другие типы операндов зависят от процессорной архитектуры;
  • idc.print_operand(ea, n) — возвращает строковое представление операнда;
  • idc.set_cmt(ea, comment, repeat) — добавляет комментарий по указанному адресу (см. ниже).

Аргументы функции


Поскольку в архитектуре ARM аргументы передаются в функцию через регистры (R0-R3), то необходимо:


  • пройти вверх от точки обращения к функции;
  • найти инструкции, где в регистры R0, R1, R2 заносятся данные (функция memcpy принимает 3 аргумента);
  • если в регистры заносится число или адрес, нужно вернуть hex-представление этого числа, для всех остальных случаев вернуть просто текстовое представление операнда.

Комментарии в IDA


Комментарии в базе IDA бывают трех типов:


Простые комментарии — отображаются только в той строке, где они установлены. Добавляются функцией idc.set_cmt(ea, comment, rpt) с аргументом repeat = 0:



Повторяемые комментарии (repeatable) — помимо основной строки отображаются ещё и там, где есть ссылка на строку с комментарием. Добавляются функцией idc.set_cmt(ea, comment, rpt) с аргументом repeat = 1:



Многострочные комментарии в коде () — устанавливаются функцией idc.update_extra_cmt(ea, n, comment):



Для нашего случая подходят простые неповторяемые комментарии.


Код для комментирования вызовов функции


# coding: utf-8
"""
File: funcargs.py
Добавление комментария в места вызова функции с аргументами
"""

import idautils

MEMCPY = 0x08006B26

def get_function_arg(ea, narg):
    """ Поиск n-го аргумента функции """
    while True:
        ea = idc.prev_head(ea)
        if idc.get_operand_value(ea, 0) == narg:
            break
    if idc.get_operand_type(ea, 1) in (idc.o_imm, idc.o_mem):
        res = "0x%x" % idc.get_operand_value(ea, 1)
    else:
        res = idc.print_operand(ea, 1)  
    return res

def set_comment_by_args(func_ea, nargs):
    """ Добавление комментария к вызову функции """
    for ea in idautils.CodeRefsTo(func_ea, flow=0):
        func_args = []
        for i in range(0, nargs):
            arg = get_function_arg(ea, i)
            func_args.append(arg)
        args = ', '.join(a for a in func_args)  
        comment = "(%s)" % args
        idc.set_cmt(ea, comment, 0)

if __name__ == '__main__':
    set_comment_by_args(MEMCPY, 3)

После выполнения кода комментарии будут отображаться в окне кросс-ссылок:



0x03. Получение аргумента функции


Итак, для создания комментария в месте вызова функции мы сделали функцию get_function_arg:


def get_function_arg(ea, narg):
    """Поиск n-го аргумента функции (нумерация с нуля)"""
    while True:
        ea = idc.prev_head(ea)
        if idc.get_operand_value(ea, 0) == narg:
            break

    if idc.get_operand_type(ea, 1) in (idc.o_imm, idc.o_mem):
        res = "0x%x" % idc.get_operand_value(ea, 1)
    else:        
        res = idc.print_operand(ea, 1)  

    return res

Важно напомнить, что этот вариант доступа к аргументу функции подходит для архитектуры, где аргументы передаются через регистры. Если работа ведётся в рамках архитектуры x86, то в большинстве случаев аргументы передаются в функцию через стек. Тогда для получения n-го аргумента из стека нужно посчитать инструкции push перед вызовом функции:


def get_function_arg_value(ea, narg):
    """ 
    Поиск n-го аргумента функции (нумерация с нуля).
    Аргументы передаются через стек.
    """
    i = 0
    while True:
        ea = idc.prev_head(ea)
        if idc.print_insn_mnem(ea) == "push":
            if i == narg: break
            i += 1

    res = idc.get_operand_value(ea, 0)
    return res

0x04. Переименование функции по строке лога


Рассмотрим часто встречающуюся задачу – переименовать функцию по информации из строки лога (такое может быть, например, при использовании функции assert). Для тренировки возьмем изменённую прошивку Носорога без информации об именах функций. При анализе имеющейся текстовой информации в глаза бросаются строки "sendMsg error %s", "recvMsg error" и "freeMsg error".



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


Если провести дальнейший анализ, то можно установить, что все эти строки передаются в качестве первого аргумента в функцию sub_8006690:



Переименуем ее в x_printf.


В строке по адресу 0x08005034 в регистр R0 помещается адрес строки "sendMsg error %s\r\n". Здесь стоит обратить внимание вот на что: если выполнить запрос значения второго операнда в этой строке, то мы получим адрес 0x08005040:


Python>"%08x" % get_operand_value(here(), 1)
08005040

Связано это с особенностями архитектуры ARM и параметрами при сборке прошивки:


  • длина инструкции фиксирована (в данном случае – 2 байта);
  • чтобы загрузить 32-битное значение в регистр, само значение записывается ниже кода функции, а в инструкции используется короткое смещение относительно счетчика команд (PC).

IDA учитывает эти особенности, анализирует код и смещение, и автоматически подставляет в код ссылку на строку.


Учитывая всё это, алгоритм переименования функций будет таким:


  1. Получить все кодовые ссылки на функцию x_printf функцией idautils.CodeRefsTo
  2. Получить значение первого аргумента функции (регистр R0) при помощи функции idc.get_operand_value (воспользуемся слегка изменённой функцией get_function_arg, которую реализовали ранее).
  3. Получить адрес строки функцией idc.get_wide_dword.
  4. Получить содержимое строки функцией idc.get_strlit_contents.
  5. Проверить, что строка имеет нужный формат.
  6. "Вытащить" из строки имя функции и переименовать функцию, используя idc.set_name.

В итоге получим скрипт:


# coding: utf-8

import idautils

PRINTF = 0x08006690

def get_function_arg(ea, narg):
    """Найти n-й аргумент функции"""
    while True:
        ea = idc.prev_head(ea)
        if idc.get_operand_value(ea, 0) == narg:
            break

    if idc.get_operand_type(ea, 1) == idc.o_mem:
        res = idc.get_operand_value(ea, 1)
    else:        
        res = idc.BADADDR

    return res

def get_func_name(str_log):
    """Получить из строки лога имя функции"""
    words = str_log.split(' ')
    if len(words) > 1 and words[1].startswith("error"):
        return words[0]
    else:
        return ""

def rename_by_log_str(log_func):
    """Переименовать функции, которые вызывают log_func со строкой логирования"""
    for ea in idautils.CodeRefsTo(log_func, 0):
        arg = get_function_arg(ea, 0)
        if arg == idc.BADADDR:
            continue
        str_addr = idc.get_wide_dword(arg)
        str_log = idc.get_strlit_contents(str_addr)
        func_name = get_func_name(str_log)
        if func_name:
            print("0x%08x - 0x%08x - %s" % (arg, str_addr, func_name))

if __name__ == '__main__':
    rename_by_log_str(PRINTF)

Поскольку строка лога в разных приложениях может иметь разный формат, то разумно вынести в отдельную функцию (в нашем случае – get_func_name) проверку формата строки и получение имени функции.


0x05. Раскраска кода и данных


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


Рассмотрим простой пример – установим для кода и данных разные цвета фона.


Основные функции для работы с цветом фона:


  • idc.get_color(ea, what) – получить цвет фона элемента;
  • idc.set_color(ea, what, color) – установить цвет фона элемента;
    • цвет представляется моделью RGB и задаётся hex-числом в формате 0xBBGGRR (голубой-зелёный-красный)
    • аргумент what задаёт что раскрашивать:
      • idc.CIC_ITEM = 1 – отдельная строка листинга;
      • idc.CIC_FUNC = 2 – полностью функция;
      • idc.CIC_SEGM = 3 – полностью сегмент.

Таким образом, код


for i, ea in enumerate(xrange(0x08005196, 0x080051AE, 2)):
    idc.set_color(ea, CIC_ITEM, 0x0f << (2 * i))

раскрасит строки листинга в диапазоне от 0x08005196 до 0x080051AE в разные цвета:



Как упоминалось выше, при работе с ARM-кодом можно увидеть, что ниже кода функции находятся глобальные адреса объектов, если таковые используются в данной функции:



Определим задачу: установить в кодовых сегментах разный цвет для кода и данных.


Для решения задачи нужно выполнить следующие шаги:


  1. Получить список сегментов. Для этого воспользуемся генератором idautils.Segments(), который возвращает стартовые адреса сегментов.
  2. Из всех сегментов получить только сегменты с кодом. Для этого воспользуемся функцией idc.get_segm_attr(segm, attr), которая возвращает атрибут сегмента. Нас интересует тип сегмента SEG_CODE (атрибут — SEGATTR_TYPE) .
  3. Пройти по всем элементам каждого сегмента. Тут воспользуемся генератором idautils.Heads(start, end), который возвращает начальные адреса элементов (инструкций, данных) в интервале адресов от start до end.
  4. С помощью функций idc.get_full_flags(ea) и is_code(flags) проверить, содержится ли код в выбранном адресе ea.
  5. Выполнить раскрашивание сегмента.

Этот алгоритм можно представить следующим кодом:


BLUE = 0xF2D0AF
PINK = 0xAFD0F2

def colored_code():
    """Раскрасить код и данные разными цветами в сегментах кода"""

    code_segmnets = filter(lambda segm:                           idc.get_segm_attr(segm, SEGATTR_TYPE) == SEG_CODE,                          idautils.Segments())

    for segm in code_segmnets:
        end = idc.get_segm_end(segm)
        for ea in idautils.Heads(segm, end):
            flags = idc.get_full_flags(ea)
            if idc.is_code(flags):
                idc.set_color(ea, CIC_ITEM, BLUE)
            else:
                idc.set_color(ea, CIC_ITEM, PINK)

Примечание


Обратите внимание, что функция is_code(flags) принимает битовое поле флагов, а не адрес. Для получения флагов адреса необходимо использовать функцию get_full_flags(ea).


После выполнения функции colored_code сегменты кода примут вид:



Итак, в этом материале мы сперва познакомились с инструментом IDAPython и написали несколько несложных функций, а затем научились перебирать сегменты и элементы кода (функции idautils.Segments и idautils.Heads), раскрашивать код (idc.set_color), а также запрашивать содержимое строки (idc.get_strlit_contents) и числа (idc.get_wide_dword).


Ссылки