На русском языке (и на Хабре, в частности) не так много статей по работе с 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 учитывает эти особенности, анализирует код и смещение, и автоматически подставляет в код ссылку на строку.
Учитывая всё это, алгоритм переименования функций будет таким:
- Получить все кодовые ссылки на функцию
x_printf
функциейidautils.CodeRefsTo
- Получить значение первого аргумента функции (регистр R0) при помощи функции
idc.get_operand_value
(воспользуемся слегка изменённой функциейget_function_arg
, которую реализовали ранее). - Получить адрес строки функцией
idc.get_wide_dword
. - Получить содержимое строки функцией
idc.get_strlit_contents
. - Проверить, что строка имеет нужный формат.
- "Вытащить" из строки имя функции и переименовать функцию, используя
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-кодом можно увидеть, что ниже кода функции находятся глобальные адреса объектов, если таковые используются в данной функции:
Определим задачу: установить в кодовых сегментах разный цвет для кода и данных.
Для решения задачи нужно выполнить следующие шаги:
- Получить список сегментов. Для этого воспользуемся генератором
idautils.Segments()
, который возвращает стартовые адреса сегментов. - Из всех сегментов получить только сегменты с кодом. Для этого воспользуемся функцией
idc.get_segm_attr(segm, attr)
, которая возвращает атрибут сегмента. Нас интересует тип сегментаSEG_CODE
(атрибут —SEGATTR_TYPE
) . - Пройти по всем элементам каждого сегмента. Тут воспользуемся генератором
idautils.Heads(start, end)
, который возвращает начальные адреса элементов (инструкций, данных) в интервале адресов отstart
доend
. - С помощью функций
idc.get_full_flags(ea)
иis_code(flags)
проверить, содержится ли код в выбранном адресеea
. - Выполнить раскрашивание сегмента.
Этот алгоритм можно представить следующим кодом:
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
).
Ссылки
- The Beginner's Guide to IDAPython by Alexander Hanel (актуальная версия на момент написания статьи v.5.0, API для IDA версия 7.x)
- IDA + Python = IDAPython (используется API для IDA версий 6.x)
- Статьи Using IDAPython to Make Your Life Easier в блоге Palo Alto Networks (используется API для IDA версий 6.x)
- Вопросы-ответы по IDAPython на Reverse Engineering StackExchange
- Шпаргалка по IDAPython
WRP
Ильфак молодец. Сразу сделал ставку на интерактивность и в итоге победил.
Помню ещё самую первую версию IDA и Был такой Sourcerer. Он был лучше сначала, но канул в Лету.
Простите за оффтоп…
Ностальгия)
pfemidi
Тоже оффтоп.
[offtopic on]
Насчёт Sourcerer как раз. Меня тогда восхищало то, что он был написан ПОЛНОСТЬЮ на ассемблере! И ASM Tool, и ASM Checker, и всё остальные тулзы от V. Communications, Inc., которые шли в комплекте с Sourcerer (у меня был полный, «фирменный» комплект, предоставленный то ли хакерской группой «The Humble Guys!» (помню как сейчас скаченный с огромными трудами с BBS ещё, на 9600 бод, по ночам и причём не за одну ночь с далёкого американского межгорода), то ли группой «DrinkOrDie», то ли группой «FairLight», то ли ещё какой известной в то время хакерской группой. Я в свои молодые годы ужасно восхищался этим — дизассемлер и полностью на ассемблере!
[offtopic off]
Ни про какую IDA я тогда и не знал и не мог знать, самая первая версия IDA вышла уже тогда, когда вышла вторая или третья версия Sourcerer. Да, и помню тогда я IDA официально купил, лично у Ильфака. Он тогда жил ещё в Москве, а не как сейчас в Бельгии и я лично заплатил ему жуткую по тем временам сумму не то 15, не то целых 20 долларов. За полноценную IDA (в которой никакого декомпилятора в те времена само собой ещё не было, просто чистый интерактивный дизассемблер).
«Но это уже совсем другая история!» © Леонид КаневскийНо это уже совсем оффтопик.