• XSS.stack #1 – первый литературный журнал от юзеров форума

Мануал/Книга Руководство по IDAPython для новичков

fancier

HDD-drive
Пользователь
Регистрация
06.10.2019
Сообщения
28
Реакции
12
Представляю Вам перевод книги Александра Ханеля "The Beginner’s Guide to IDAPython. Version 6.0".
Перевод не идеален, так что, если есть предложения по перефразированию некоторых частей перевода, то прошу написать мне и прислать правильную, по вашему мнению, версию перевода.

Оглавление
Вступление
Обновления
Предполагаемый читатели и отказ от ответственности
Условные обозначения
Происхождение IDAPython
Старое против нового
Проблемы с Python x86-64
Основы
Сегменты
Функции
Извлечение аргументов функций
Инструкции
Операнды
Базовые блоки
Структуры
Перечисляемые типы
Перекрестные ссылки (xref)
Поиск
Отбор данных
Комментарии и переименования
Раскраска
Доступ к необработыннм данным
Патчинг
PyQt
Создание пакетного файла
Выполнение скриптов
Yara
Unicorn Engine
Заключение
Приложение
Неизменяемые IDC API имена
PeFile


Вступление

Привет!

Эта книга про IDAPython.

Изначально я написал его как справочник для себя – мне нужно было место, где я мог бы найти примеры функций, которые я обычно использую (и забываю) в IDAPython. С тех пор, как я начал эту книгу, я много раз использовал ее в качестве короткого справочника, чтобы понять синтаксис или увидеть пример некоторого кода. Многие скрипты, которые я здесь описываю являются результатом поверхностных экспериментов, которые я задокументировал в интернете.

За прошедшие годы я получил множество писем с вопросами, какое лучшее руководство существует для изучения IDAPython. Обычно я указываю на «Введение в IDAPython» Эры Карреры или примеры скриптов в открытом репозитории IDAPython. Это отличные источники для обучения, но они не охватывают некоторые общие проблемы, с которыми я столкнулся. Я хотел написать книгу, посвященную этим вопросам. Я считаю, что эта книга представляет ценность для всех, кто изучает IDAPython или хочет получить короткий справочник с примерами и фрагментами кода. Будучи электронной книгой, она не будет статичной и будет регулярно обновляться в будущем.

Если Вы столкнетесь с какими-либо проблемами, опечатками или у Вас возникнут вопросы, напишите мне на почту alexander.hanel@gmail.com или напишите мне в Twitter @nullandnull.

Предполагаемые читатели и отказ от ответственности

Эта книга не предназначена для начинающих реверс-инженеров. Она также не является введением в IDA. Если вы новичок в IDA, я бы порекомендовал приобрести книгу Криса Игла «The IDA Pro Book». Для чего-то большего, попробуйте пройти обучение у Криса Игла или Hex-Reys.

Для читателей этой книги есть несколько предварительных условий. Вы должны хорошо разбираться в ассемблере, иметь опыт работы в области реверс-инжиниринга и разбираться в IDA. Если Вы достигли точки, где спросили себя «Как я могу автоматизировать эту задачу с помощью IDAPython?», тогда эта книга для Вас. Если у Вас уже есть опыт программирования на IDAPython, то Вы, вероятно, уже знакомы с материалом. Тем не менее, он может служить удобным справочником для поиска примеров часто используемых функций или может решить проблему, с которой Вы столкнетесь в будущем. Следует отметить, что я занимался реверс-инжинирингом вредоносных файлов в Windows x86. Многие примеры, приведенные в этой книге, получены из общих задач, с которыми я сталкивался при реверсе малварей. После прочтения этой книги читатель сможет самостоятельно копаться в документации и исходном коде IDAPython.

Условные обозначения

Окна вывода IDA (интерфейс командной строки) использовались для большинства примеров и вывода результата. Для краткости, некоторые примеры не содержат присвоения текущего адреса переменной. Обычно он обозначается как ea = here(). Весь код можно вырезать и вставить в командную строку или команду сценариев IDA с опцией Shift+F2. Эту книгу рекомендуется читать от начала и до конца. Есть несколько примеров, которые не объясняются построчно, потому что предполагается, что читатель понимает код из предыдущих примеров. Разные авторы вызывают API IDAPython по-разному. Иногда код вызывается как idc.get_segm_name(ea) или get_segm_name(ea). В этой книге используется первый стиль, так как это соглашение легче читать и отлаживать.

Иногда, при использовании этого соглашения может возникать ошибка.
Код:
Python>DataRefsTo(here()) # нет вопросов
<generator object refs at 0x05247828>
Python>idautils.DataRefsTo(here()) # вызывает исключение
Traceback (most recent call last):
    File "<string>", line 1, in <module>
NameError: name 'idautils' is not defined
Python>import idautils # импорт модуля вручную
Python>idautils.DataRefsTo(here())
<generator object refs at 0x06A398C8>
В этом случае модуль необходимо импортировать вручную.

Происхождение IDAPython

IDAPython был разработан в 2004 году. Это была совместная работа Гергели Эрдели и Эро Карреры. Их цель состояла в том, чтобы объединить мощь Python с автоматизацией анализа С-подобного скриптового языка IDC IDA. В прошлом, IDAPython в основном состоял из трех отдельных модулей. Первый – это idc. Это модуль совместимости для оболочки IDC – функций IDA. Второй модуль – idautils. Это служебная функция высокого уровня для IDA. Третий модуль – idaapi. Она позволяет получить доступ к более низкоуровневым данным. С выходом версии 6.95, IDA начала включать больше модулей, охватывающих функциональность, которая исторически поддерживалась idaapi. Эти новые модули имеют соглашение об именах ida_*. В этой книге есть ссылки на несколько модулей. Один из таких модулей – ida_kernwin.py. После прочтения этой книги я бы порекомендовал изучить эти модули самостоятельно. Они находятся по адресу IDADIR\python\ida_*.py.

Старое против нового

В сентябре 2017 года была выпущена IDA 7.0. Этот выпуск был существенным обновлением для HexRays, поскольку IDA была перенесена с х86 на х86-64. Побочным эффектом данного релиза является необходимость перекомпиляции старых плагинов. Несмотря на то, что внутри IDAPython произошли некоторые важные изменения, старые скрипты выполнялись в версии 7.0. Обратная совместимость с 6.95 по 7.0 обусловлена уровнем совместимости, который находится в IDADIR\python\idc_bc695.py. Следующий код является примером кода уровня совместимости.
def MakeName(ea, name): return set_name(ea, name, SN_CHECK)
Старая функция IDAPython MakeName была переименована в set_name. Если мы хотим быстро распечатать новое имя API из idc_bc695.py с помощью командной строки, мы можем использовать модуль inspect.
Код:
Python>import inspect
Python>inspect.getsource(MakeName)
def MakeName(ea, name): return set_name(ea, name, SN_CHECK)
Для пользователей IDAPython, знакомых со старым соглашением об именах, не все имена API были изменены. Некоторые имена API нельзя переопределить, поэтому они остаются прежними. Список имен API, которые остались как есть, можно найти в Приложении в разделе Неизмененные имена API IDC. В версии IDA 7.4 уровень совместимости был отключен по умолчанию. Это не рекомендуется, но пользователи IDA могут повторно включить его, изменив IDADIR\cfg\python.cfg и убедившись, что AUTOIMPORT_COMPAT_IDA695 равен Yes. Поскольку в следующей версии IDA обратная совместимость не поддерживается, эта книга написана с использованием «новых» имен API. На дату публикации уровень совместимости нацелен только на API внутри idc.py. В октябре 2019 года была выпущена IDA 7.4. Эта версия обеспечивала поддержку Python 3. После выпуска IDA 7.4 поддерживается Python 2 и Python 3, но с окончанием срока службы Python 2.x он не будет поддерживаться в будущих версиях. Поскольку на хосте может быть установлено несколько версий Python, Hex-Rays предоставил инструмент с именем idapyswitch, который находится в IDADIR\idapyswitch.exe. После выполнения, инструмент перечисляет все доступные версии Python и позволяет пользователю выбрать, какую версию Python он хотел бы использовать.

Проблемы с Python-x86-64

Некоторые общие проблемы возникают при обновлении с IDA 6.9 до более новых версий при выполнении старых сценариев, которые полагаются на нестандартные модули. Ранее установленные модули (такие как pefile4) необходимо обновить с x86 до x86_64 для использования в IDA. Самый простой способ обновить их - выполнить следующую команду:
C: \>python%version% \ python.exe -m pip install <package>
Выполнение import sys; print (sys.path) из окна вывода IDA можно использовать для определения пути к папке для версии Python, которую использует IDA. По состоянию на апрель 2020 года, при установке IDAPython с Python 3.8 и 3.81 возникают проблемы. Чтобы решить эту проблему, см. Сообщение в блоге Hex-Rays IDA 7.4 и Python 3.85.

Для многих пользователей обычной практикой является использование функции hex для печати адреса. После обновления до IDA 7+ у пользователей, печатающих адреса в шестнадцатеричном формате, больше не будет интерактивных адресов. Типы адресов теперь длинные, а не целые. Если вам нужно, чтобы напечатанные адреса (через функцию print) были интерактивными, используйте строковое форматирование. Первый адрес для вывода, представленный ниже, является длинным, и на него нельзя нажать. Адреса, выведенные с использованием строкового форматирования, можно вывести в консоль.
Код:
Python>ea = idc.get_screen_ea() # получить адрес команды, где находится курсор
Python>print(hex(ea)) # вывод неинтерактивного адреса
0x407e3bL
Python>print("0x%x" % ea) # вывод интерактивного адреса
0x407e3b
 

Основы

Прежде чем копать слишком глубоко, мы должны определить некоторые ключевые слова и рассмотреть структуру вывода ассемблерного кода IDA. Обычно это можно увидеть в графическом интерфейсе с помощью окна IDA-View. Мы можем использовать следующую строку кода в качестве примера.
.text:00401570 lea eax, [ebp+arg_0]
.text — это название секции, а адрес - 00401570. Отображаемый адрес в шестнадцатеричном формате без префикса 0x. Инструкция lea называется мнемонической. После мнемоники идет первый операнд eax, а вторым операндом является [ebp+arg_0]. При работе с API-интерфейсами IDAPython, наиболее распространенной передаваемой переменной является адрес. В документации IDAPython адрес указан как ea. Доступ к адресу можно получить вручную с помощью нескольких функций. Чаще всего используются функции idc.get_screen_ea() или here(). Эти функции возвращают целочисленное значение, содержащее адрес, по которому установлен курсор. Если мы хотим получить минимальный адрес, который присутствует в IDB, мы можем использовать idc.get_inf_attr (INF_MIN_EA) или, чтобы получить максимальный адрес, мы можем использовать idc.get_inf_attr (INF_MAX_EA).
Код:
Python>ea = idc.get_screen_ea()
Python>print("0x%x %s" % (ea, ea))
0x401570 4199792
Python>ea = here()
Python>print("0x%x %s" % (ea, ea))
0x401570 419972
Python>print("0x%x" % idc.get_inf_attr(INF_MIN_EA))
0x401000
Python>print("0x%x" % idc.get_inf_attr(INF_MAX_EA))
0x41d000
К каждому описанному элементу в ассемблерном коде можно получить доступ с помощью функции в IDAPython. Ниже приведен пример доступа к каждому элементу. Напоминаем, что ранее мы сохраняли адрес в ea.
Код:
Python>idc.get_segm_name(ea) # get text
.text
Python>idc.generate_disasm_line(ea, 0) # get disassembly
lea eax, [ebp+arg_0]
Python>idc.print_insn_mnem(ea) # get mnemonic
lea
Python>idc.print_operand(ea,0) # get first operand
eax
Python>idc.print_operand(ea,1) # get second operand
[ebp+arg_0]
Чтобы получить строковое представление имени сегмента, мы используем idc.get_segm_name(ea), где ea является адресом внутри сегмента. Распечатать строку дизассемблирования можно с помощью idc.generate_disasm_line(ea, 0). Аргументы - это адрес, хранящийся в ea, и флаг 0. Флаг 0 возвращает отображаемую дизассемблированную версию, которую IDA обнаружила во время своего анализа. ea может быть любым адресом в пределах диапазона смещения инструкции, когда передается флаг 0. Чтобы дизассемблировать точное смещение и игнорировать анализ IDA, используется флаг 1. Чтобы получить мнемонику или имя инструкции, мы должны вызвать idc.print_insn_mnem(ea). Чтобы получить операнды мнемоники, мы должны вызвать idc.print_operand(ea, long n). Первый аргумент — это адрес, а вторая long n — это индекс операнда. Первый операнд равен 0, второй - 1, и каждый последующий операнд увеличивается на единицу для n.

В некоторых ситуациях важно убедиться, что адрес существует. idaapi.BADADDR, idc.BADADDR или BADADDR можно использовать для проверки правильности адресов.
Код:
Python>idaapi.BADADDR
4294967295
Python>print("0x%x" % idaapi.BADADDR)
0xffffffff
Python>if BADADDR != here(): print("valid address")
valid address
Пример BADADDR для 64-битного двоичного файла.
Код:
Python>idc.BADADDR
18446744073709551615
Python>print("0x%x" % idaapi.BADADDR)
0xffffffffffffffff
 

Сегменты

Печать одной строки не особенно полезна. Сила IDAPython заключается в переборе всех инструкций, перекрестных ссылках на адреса и поиске кода или данных. Последние два пункта будут более подробно описаны в следующих разделах. Тем не менее, итерация по всем сегментам - хорошее начало.
Код:
Python>for seg in idautils.Segments():\
          print("%s, 0x%x, 0x%x" % (idc.get_segm_name(seg), idc.get_segm_start(seg), idc.get_segm_end(seg)))
Python>
.textbss, 0x401000, 0x411000
.text, 0x411000, 0x418000
.rdata, 0x418000, 0x41b000
.data, 0x41b000, 0x41c000
.idata, 0x41c000, 0x41c228
.00cfg, 0x41d000, 0x41e000
idautils.Segments() возвращает объект типа итератора. Мы можем перебрать объект, используя цикл for. Каждый элемент в списке — это начальный адрес сегмента. Адрес можно использовать для получения имени сегмента, если мы передадим его в качестве аргумента в idc.get_segm_name (ea). Начало и конец сегментов можно найти, вызвав idc.get_segm_start (ea) или idc.get_segm_end (ea). Адрес или ea должен находиться в диапазоне от начала и до конца сегмента. Если мы не хотим перебирать все сегменты, но хотим найти следующий сегмент от смещения, мы могли бы использовать idc.get_next_seg (ea). Переданный адрес может быть любым адресом в диапазоне сегментов, для которого мы хотим найти следующий сегмент. Если бы мы случайно захотели получить начальный адрес сегмента по имени, мы могли бы использовать idc.get_segm_by_sel (idc.selector_by_name (str_SectionName)). Функция idc.selector_by_name (segname) возвращает селектор сегмента и передает единственный строковый аргумент имени сегмента. Селектор сегмента — это целочисленное значение, которое начинается с 1 и увеличивается для каждого сегмента (также известного как раздел) в исполняемом файле. idc.get_segm_by_sel (int) передается селектору сегмента и возвращает начальный адрес сегмента.
 

Функции

Теперь, когда мы знаем, как перебирать все сегменты, мы должны перейти к тому, как перебирать все известные функции.
Код:
Python>for func in idautils.Functions():
          print("0x%x, %s" % (func, idc.get_func_name(func)))
Python>
0x401000, sub_401000
0x401006, w_vfprintf
0x401034, _main
…removed…
0x401c4d, terminate
0x401c53, IsProcessorFeaturePresent
idautils.Functions() возвращает список известных функций. Список содержит начальный адрес каждой функции. idautils.Functions() можно передавать аргументы для поиска в пределах диапазона. Если бы мы хотели это сделать, мы бы передали начальный и конечный адрес idautils.Functions (start_addr, end_addr). Чтобы получить имя функции, мы используем idc.get_func_name (ea). ea может быть любым адресом в пределах функции. IDAPython содержит большой набор API для работы с функциями. Начнем с простой функции. Семантика этой функции не важна, но мы должны создать мысленную запись адресов.
Код:
.text:0045C7C3 sub_45C7C3       proc near
.text:0045C7C3                  mov eax, [ebp-60h]
.text:0045C7C6                  push eax             ; void *
.text:0045C7C7                  call w_delete
.text:0045C7CC                  retn
.text:0045C7CC sub_45C7C3       endp
Чтобы получить границы, мы можем использовать idaapi.get_func (ea).
Код:
Python>func = idaapi.get_func(ea)
Python>type(func)
<class 'ida_funcs.func_t'>
Python>print("Start: 0x%x, End: 0x%x" % (func.start_ea, func.end_ea))
Start: 0x45c7c3, End: 0x45c7cd
idaapi.get_func (ea) возвращает класс ida_funcs.func_t. Иногда не всегда очевидно, как использовать класс, возвращаемый вызовом функции. Полезной командой для изучения классов в Python является функция dir(class).
Код:
Python>dir(func)

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get_points__', '__get_regvars__', '__get_tails__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__',
'__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__swig_destroy__', '__weakref__', '_print', 'analyzed_sp',
'argsize', 'clear', 'color', 'compare', 'contains', 'does_return', 'empty',
'endEA', 'end_ea', 'extend', 'flags', 'fpd', 'frame', 'frregs', 'frsize',
'intersect', 'is_far', 'llabelqty', 'llabels', 'need_prolog_analysis', 'overlaps',
'owner', 'pntqty', 'points', 'referers', 'refqty', 'regargqty', 'regargs',
'regvarqty', 'regvars', 'size', 'startEA', 'start_ea', 'tailqty', 'tails', 'this',
'thisown']
На выходе мы видим функции start_ea и end_ea. Они используются для доступа к началу и концу функции. Конечный адрес — это не последний адрес в последней инструкции, а байт после инструкции. Эти атрибуты применимы только к текущей функции. Если бы мы хотели получить доступ к окружающим функциям, мы могли бы использовать idc.get_next_func (ea) и idc.get_prev_func (ea). Значение ea должно быть адресом только в границах анализируемой функции. Предостережение, связанное с перечислением функций, заключается в том, что он работает только в том случае, если IDA определила блок кода как функцию. Пока блок кода не отмечен как функция, он пропускается в процессе перечисления функций. Код, который не отмечен как функция, помечен красным цветом в легенде в полосе навигации (цветная полоса вверху в графическом интерфейсе IDA). Их можно исправить вручную или автоматизировать с помощью функции idc.create_insn (ea).

IDAPython имеет множество разных способов доступа к одним и тем же данным. Обычный подход для доступа к границам внутри функции — это использование idc.get_func_attr (ea, FUNCATTR_START) и idc.get_func_attr (ea, FUNCATTR_END).
Код:
Python>ea = here()
Python>start = idc.get_func_attr(ea, FUNCATTR_START)
Python>end = idc.get_func_attr(ea, FUNCATTR_END)
Python>cur_addr = start
Python>while cur_addr <= end:
        print("0x%x %s" % (cur_addr, idc.generate_disasm_line(cur_addr, 0)))
        cur_addr = idc.next_head(cur_addr, end)
Python>
0x45c7c3        mov     eax, [ebp-60h]
0x45c7c6        push    eax           ; void *
0x45c7c7        call    w_delete
0x45c7cc        retn
idc.get_func_attr (ea, attr) используется для получения начала и конца функции. Затем мы печатаем текущий адрес и листинг с помощью idc.generate_disasm_line (ea, 0). Мы используем idc.next_head (eax), чтобы получить начало следующей инструкции, и продолжаем, пока не дойдем до конца этой функции. Недостатком этого подхода является то, что он полагается на инструкции, которые должны содержаться в границах начала и конца функции. Если произошел переход к адресу выше, чем конец функции, цикл преждевременно завершится. Эти типы переходов довольно распространены в методах обфускации, таких как преобразование кода. Поскольку границы могут быть ненадежными, лучше всего вызывать idautils.FuncItems (ea) для обхода адресов в функции. Мы рассмотрим этот подход более подробно в следующем разделе.

Подобно idc.get_func_attr (ea, attr), другим полезным средством для сбора информации о функции является idc.get_func_attr (ea, FUNCATTR_FLAGS). FUNCATTR_FLAGS может использоваться для получения информации о функции, например, если это код библиотеки или функция не возвращает значение. Для функции есть девять возможных флагов. Если бы мы хотели перечислить все флаги для всех функций, мы могли бы использовать следующий код.
Код:
Python>import idautils
Python>for func in idautils.Functions():
    flags = idc.get_func_attr(func,FUNCATTR_FLAGS)
    if flags & FUNC_NORET:
        print("0x%x FUNC_NORET" % func)
    if flags & FUNC_FAR:
        print("0x%x FUNC_FAR" % func)
    if flags & FUNC_LIB:
        print("0x%x FUNC_LIB" % func)
    if flags & FUNC_STATIC:
        print("0x%x FUNC_STATIC" % func)
    if flags & FUNC_FRAME:
        print("0x%x FUNC_FRAME" % func)
    if flags & FUNC_USERFAR:
        print("0x%x FUNC_USERFAR" % func)
    if flags & FUNC_HIDDEN:
        print("0x%x FUNC_HIDDEN" % func)
    if flags & FUNC_THUNK:
        print("0x%x FUNC_THUNK" % func)
    if flags & FUNC_LIB:
        print("0x%x FUNC_BOTTOMBP" % func)
Python>
0x401006 FUNC_FRAME
0x40107c FUNC_LIB
0x40107c FUNC_STATIC
…
Мы используем idautils.Functions (), чтобы получить список адресов всех известных функций, а затем мы используем idc.get_func_attr (ea, FUNCATTR_FLAGS), чтобы получить флаги. Мы проверяем значение, используя операцию логического И (&) для возвращаемого значения. Например, чтобы проверить, не имеет ли функция возвращаемого значения, мы могли бы использовать следующее сравнение, if flags & FUNC_NORET. Теперь давайте рассмотрим все флаги функций. Некоторые из этих флагов довольно распространены, а другие - редки.

FUNC_NORET
Этот флаг используется для идентификации функции, которая не выполняет инструкцию возврата. Имеет промежуточное представление 1. Пример функции, не возвращающей значение, можно увидеть ниже.
Код:
CODE:004028F8 sub_4028F8    proc near
CODE:004028F8
CODE:004028F8               and eax, 7Fh
CODE:004028FB               mov edx, [esp+0]
CODE:004028FE               jmp sub_4028AC
CODE:004028FE sub_4028F8    endp
Обратите внимание, что ret или leave - не последняя инструкция.

FUNC_FAR
Этот флаг встречается редко, если не реверсить программное обеспечение, использующее сегментированную память. Промежуточно он представлен как целое число 2.

FUNC_USERFAR
Этот флаг редко встречается, и по нему мало документации. Hex-Rays описывает флаг как «пользователь указал дальность функции». Его промежуточное значение - 32.

FUNC_LIB
Этот флаг используется для поиска кода библиотеки. Идентификация кода библиотеки очень полезна, потому что это код, который обычно можно игнорировать при анализе. Он промежуточно представлен в виде целого числа 4. Ниже приведен пример его использования и функций, которые он идентифицировал.
Код:
Python>for func in idautils.Functions():
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    if flags & FUNC_LIB:
        print("0x%x FUNC_LIB %s" % (func,idc.get_func_name(func)))
Python>
0x40107c FUNC_LIB ?pre_c_initialization@@YAHXZ
0x40113a FUNC_LIB ?__scrt_common_main_seh@@YAHXZ
0x4012b2 FUNC_LIB start
0x4012bc FUNC_LIB ?find_pe_section@@YAPAU_IMAGE_SECTION_HEADER@@QAEI@Z
0x401300 FUNC_LIB ___scrt_acquire_startup_lock
0x401332 FUNC_LIB ___scrt_initialize_crt

FUNC_STATIC
Этот флаг используется для идентификации библиотечных функций со статическим фреймом на основе ebp.

FUNC_FRAME
Этот флаг указывает, что функция использует указатель кадра ebp. Функции, использующие указатели фреймов, обычно начинаются со стандартного пролога функции для установки фрейма стека.
Код:
.text:1A716697      push    ebp
.text:1A716698      mov     ebp, esp
.text:1A71669A      sub     esp, 5Ch

FUNC_BOTTOMBP
Как и FUNC_FRAM, этот флаг используется для отслеживания указателя кадра. Он определяет функции, базовый указатель которых указывает на указатель стека.

FUNC_HIDDEN
Функции с флагом FUNC_HIDDEN означают, что они скрыты и должны быть расширены для просмотра. Если бы мы пошли по адресу функции, которая помечена как скрытая, она автоматически расширилась бы.

FUNC_THUNK
Этот флаг определяет функции, которые являются функциями преобразования. Это простые функции, которые переходят к другой функции.
Код:
.text:1A710606 Process32Next proc near
.text:1A710606      jmp      ds:__imp_Process32Next
.text:1A710606 Process32Next endp
Следует отметить, что функция может состоять из нескольких флагов. Ниже приведен пример функции с несколькими флагами.
Код:
0x1a716697 FUNC_LIB
0x1a716697 FUNC_FRAME
0x1a716697 FUNC_HIDDEN
0x1a716697 FUNC_BOTTOMBP
Иногда часть кода или данных необходимо определить как функцию. Например, следующий код не был определен как функция на этапе анализа или не имеет перекрестных ссылок.
Код:
.text:00407DC1
.text:00407DC1      mov     ebp, esp
.text:00407DC3      sub     esp, 48h
.text:00407DC6      push    ebx
Чтобы определить функцию, мы можем использовать idc.add_func (start, end).
Python>idc.add_func(0x00407DC1, 0x00407E90
Первый аргумент idc.add_func (start, end) — это начальный адрес функции, а второй - конечный адрес функции. Во многих случаях конечный адрес не нужен, и IDA автоматически распознает конец функции. Приведенный ниже листинг является результатом выполнения вышеуказанного кода.
Код:
.text:00407DC1 sub_407DC1 proc near
.text:00407DC1
.text:00407DC1 SystemInfo= _SYSTEM_INFO ptr -48h
.text:00407DC1 Buffer = _MEMORY_BASIC_INFORMATION ptr -24h
.text:00407DC1 flOldProtect= dword ptr -8
.text:00407DC1 dwSize = dword ptr -4
.text:00407DC1
.text:00407DC1      mov         ebp, esp
.text:00407DC3      sub         esp, 48h
.text:00407DC6      push        ebx
 

Извлечение аргументов функций

Извлечение аргументов функций не всегда является простой задачей в IDAPython. Во многих случаях для функции необходимо определить соглашения о вызовах, а аргументы необходимо проанализировать вручную с использованием обратной трассировки или аналогичного метода. Из-за огромного количества соглашений о вызовах это не всегда возможно реализовать в общих чертах. IDAPython действительно содержит функцию с именем idaapi.get_arg_addrs (ea), которую можно использовать для получения адресов аргументов, если IDA смогла идентифицировать прототип для вызываемой функции. Эта идентификация присутствует не всегда, но обычно наблюдается при вызовах API или в 64-битном коде. Например, в следующей сборке мы видим, что API SendMessage имеет четыре аргумента, переданных ему.
Код:
.text:000000014001B5FF      js      loc_14001B72B
.text:000000014001B605      mov     rcx, cs:qword_14002D368 ; hWnd
.text:000000014001B60C      xor     r9d, r9d                ; lParam
.text:000000014001B60F      xor     r8d, r8d                ; wParam
.text:000000014001B612      mov     edx, 0BDh               ; '½' ; Msg
.text:000000014001B617      call    cs:SendMessageW
.text:000000014001B61D      xor     esi, esi
Используя idaapi.get_arg_addrs (ea), где ea является адресом API, мы можем получить список адресов, которым были переданы аргументы.
Код:
Python>ea = 0x00014001B617
Python>idaapi.get_arg_addrs(ea)
[0x14001b605, 0x14001b612, 0x14001b60f, 0x14001b60c]

Инструкции

Теперь, когда мы знаем, как работать с функциями, пришло время перейти к тому, как получить доступ к инструкциям внутри функции. Если у нас есть адрес функции, мы можем использовать idautils.FuncItems (ea), чтобы получить список всех адресов.
Код:
Python>dism_addr = list(idautils.FuncItems(here()))
Python>type(dism_addr)
<type 'list'>
Python>print(dism_addr)
[4573123, 4573126, 4573127, 4573132]
Python>for line in dism_addr: print("0x%x %s" % (line, idc.generate_disasm_line(line, 0)))
0x45c7c3        mov     eax, [ebp-60h]
0x45c7c6        push    eax             ; void *
0x45c7c7        call    w_delete
0x45c7cc        retn
idautils.FuncItems (ea) возвращает тип итератора, но приводится к списку. Список содержит начальный адрес каждой инструкции в последовательном порядке. Теперь, когда у нас есть хорошая база знаний для просмотра сегментов, функций и инструкций, посмотрим полезный пример. Иногда при реверсе упакованного кода полезно знать только то, где происходят динамические вызовы. Динамический вызов — это вызов или переход к операнду, который является регистром, например, call eax или jmp edi.
Код:
Python>
for func in idautils.Functions():
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    if flags & FUNC_LIB or flags & FUNC_THUNK:
        continue
    dism_addr = list(idautils.FuncItems(func))
    for line in dism_addr:
        m = idc.print_insn_mnem(line)
        if m == 'call' or m == 'jmp':
            op = idc.get_operand_type(line, 0)
            if op == o_reg:
                print("0x%x %s" % (line, idc.generate_disasm_line(line, 0)))
Python>
0x43ebde    call    eax         ; VirtualProtect
Мы вызываем idautils.Functions(), чтобы получить список всех известных функций. Для каждой функции мы получаем флаги функций, вызывая idc.get_func_attr (ea, FUNCATTR_FLAGS). Если функция является библиотечным кодом или функцией преобразователя, функция пропускается. Затем мы вызываем idautils.FuncItems (ea), чтобы получить все адреса внутри функции. Мы просматриваем список с помощью цикла for. Поскольку нас интересуют только инструкции call и jmp, нам нужно получить мнемонику, вызвав idc.print_insn_mnem (ea). Затем мы используем простое сравнение строк, чтобы проверить мнемонику. Если мнемоника — это переход или вызов, мы получаем тип операнда, вызывая idc.get_operand_type (ea, n). Эта функция возвращает целое число, которое промежуточное представляется как op_t.type. Это значение можно использовать, чтобы определить, является ли операнд регистром, ссылкой на память и т.д. Затем мы проверяем, является ли op_t.type регистром. Если так, печатаем строку. Приведение результата idautils.FuncItems (ea) в список полезно, потому что итераторы не имеют таких объектов, как len(). Приведя его как список, мы можем легко получить количество строк или инструкций в функции.
Код:
Python>ea = here()
Python>len(idautils.FuncItems(ea))
Traceback (most recent call last):
    File "<string>", line 1, in <module>
TypeError: object of type 'generator' has no len()
Python>len(list(idautils.FuncItems(ea)))
39
В предыдущем примере мы использовали список, содержащий все адреса внутри функции. Мы перебирали каждую сущность, чтобы получить доступ к следующей инструкции. Что, если бы у нас был только адрес и мы хотели получить следующую инструкцию? Чтобы перейти к следующему адресу инструкции, мы можем использовать idc.next_head (ea), а чтобы получить адрес предыдущей инструкции, мы используем idc.prev_head (ea). Эти функции получают начало следующей инструкции, но не получают следующий адрес. Чтобы получить следующий адрес, мы используем idc.next_addr (ea), а чтобы получить предыдущий адрес, мы используем idc.prev_head (ea).
Код:
Python>ea = here()
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x10004f24 call     sub_10004F32
Python>next_instr = idc.next_head(ea)
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(next_instr, 0)))
0x10004f29 mov      [esi], eax
Python>prev_instr = idc.prev_head(ea)
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(prev_instr, 0)))
0x10004f1e mov      [esi+98h], eax
Python>print("0x%x" % idc.next_addr(ea))
0x10004f25
Python>print("0x%x" % idc.prev_head(ea))
0x10004f23
В примере динамического вызова, код IDAPython основан на использовании строкового сравнения jmp и call. Вместо того, чтобы использовать сравнение строк, мы также можем декодировать инструкции с помощью idaapi.decode_insn (insn_t, ea). Первый аргумент - это класс insn_t из ida_ua, который создается путем вызова ida_ua.insn_t(). Этот класс заполняется атрибутами после вызова idaapi.decode_insn. Второй аргумент — это адреса, которые нужно проанализировать. Декодирование инструкции может быть полезным, потому что работа с целочисленным представлением инструкции может быть быстрее и менее подвержена ошибкам. К сожалению, целочисленное представление специфично для IDA и не может быть легко перенесено на другие инструменты дизассемблирования. Ниже приведен тот же пример, но с использованием idaapi.decode_insn (insn_t, ea) и сравнением целочисленного представления.
Код:
Python>JMPS = [idaapi.NN_jmp, idaapi.NN_jmpfi, idaapi.NN_jmpni]
Python>CALLS = [idaapi.NN_call, idaapi.NN_callfi, idaapi.NN_callni]
Python>for func in idautils.Functions():
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    if flags & FUNC_LIB or flags & FUNC_THUNK:
        continue
    dism_addr = list(idautils.FuncItems(func))
    for line in dism_addr:
        ins = ida_ua.insn_t()
        idaapi.decode_insn(ins, line)
        if ins.itype in CALLS or ins.itype in JMPS:
            if ins.Op1.type == o_reg:
                print("0x%x %s" % (line, idc.generate_disasm_line(line, 0)))
Python>
0x43ebde    call    eax           ; VirtualProtect
Результат такой же, как и в предыдущем примере. Первые две строки помещают константы для jmp и call в два списка. Поскольку мы не работаем со строковым представлением мнемоники, нам нужно знать, что мнемоника (например, call или jmp) может иметь несколько значений. Например, jmp может быть представлен как idaapi.NN_jmp для перехода, idaapi.NN_jmpfi для непрямого дальнего перехода или idaapi.NN_jmpni для непрямого перехода вблизи. Все типы инструкций х86 и х64 начинаются с NN. Чтобы изучить все 1700 типов инструкций, мы можем выполнить [name for name in dir(idaapi) if "NN" in name) в командной строке или просмотреть их в файле SDK IDA allins.hpp. Когда у нас есть инструкции в списках, мы используем комбинацию idautils.Functions() и get_func_attr (ea, FUNCATTR_FLAGS), чтобы получить все применимые функции, игнорируя библиотеки и преобразователи. Мы получаем каждую инструкцию в функции, вызывая idautils.FuncItems (ea). Здесь вызывается недавно представленная функция idaapi.decode_insn (ins, ea). Эта функция принимает адрес инструкции, которую мы хотим декодировать. После его декодирования мы можем получить доступ к различным свойствам инструкции, обратившись к классу insn_t в переменной ins.
Код:
Python>dir(ins)

['Op1', 'Op2', 'Op3', 'Op4', 'Op5', 'Op6', 'Operands', '__class__', '__del__',
'__delattr__', '__dict__', '__doc__', '__format__', '__get_auxpref__',
'__get_operand__', '__get_ops__', '__getattribute__', '__getitem__', '__hash__',
'__init__', '__iter__', '__module__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__set_auxpref__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__swig_destroy__', '__weakref__', 'add_cref', 'add_dref',
'add_off_drefs', 'assign', 'auxpref', 'create_op_data', 'create_stkvar', 'cs',
'ea', 'flags', 'get_canon_feature', 'get_canon_mnem', 'get_next_byte',
'get_next_dword', 'get_next_qword', 'get_next_word', 'insnpref', 'ip', 'is_64bit',
'is_canon_insn', 'is_macro', 'itype', 'ops', 'segpref', 'size', 'this', 'thisown']
Как мы видим из команды dir(), ins имеет большое количество атрибутов. Доступ к типу операнда осуществляется с помощью ins.Op1.type. Обратите внимание, что индекс операнда начинается с 1, а не с 0, что отличается от idc.get_operand_type (ea, n).
 

Операнды

Типы операндов часто используются, поэтому полезно перебрать все типы. Как было сказано ранее, мы можем использовать idc.get_operand_type (ea, n) для получения типа операнда. ea — это адрес, а n - индекс. Существует восемь различных типов операндов.

o_void
Если инструкция не имеет операндов, она возвращает 0.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa09166 retn
Python>print(idc.get_operand_type(ea,0))
0

o_reg
Если операнд является регистром общего назначения, он возвращает этот тип. Это значение промежуточно представлено как 1.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa09163 pop edi
Python>print(idc.get_operand_type(ea,0))
1

o_mem
Если операнд является прямой ссылкой на память, он возвращает этот тип. Это значение промежуточно представлено как 2. Этот тип полезен для поиска ссылок на DATA.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa05d86 cmp ds:dword_A152B8, 0
Python>print(idc.get_operand_type(ea,0))
2

o_phrase
Этот операнд возвращается, если операнд состоит из базового регистра и/или индексного регистра. Это значение имеет промежуточное представление 3.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x1000b8c2 mov [edi+ecx], eax
Python>print(idc.get_operand_type(ea,0))
3

o_displ
Этот операнд возвращается, если он состоит из регистров и значения смещения. Смещение — это целое число, такое как 0x18. Обычно это наблюдается, когда инструкция обращается к значениям в структуре. Промежуточно он представлен как 4.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa05dc1 mov eax, [edi+18h]
Python>print(idc.get_operand_type(ea,1))
4

o_imm
Операнды, которые представляют собой значение, такое как целое число 0xC, относятся к этому типу. Промежуточно он представлен как 5.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa05da1 add esp, 0Ch
Python>print(idc.get_operand_type(ea,1))
5

o_far
Этот операнд необычен при реверсе x86 или x86_64. Он используется для поиска операндов, которые обращаются к непосредственно дальним адресам. Промежуточно он представлен как 6.

o_near
Также, как o_far, данный операнд используется для поиска операндов, обращающихся уже непосредственно к ближайшим адресам. Промежуточно он представлен как 7.

Пример
Иногда, при реверсе дампа памяти исполняемого файла, операнды не распознаются как смещение.
Код:
seg000:00BC1388         push    0Ch
seg000:00BC138A         push    0BC10B8h
seg000:00BC138F         push    [esp+10h+arg_0]
seg000:00BC1393         call    ds:_strnicmp
Второе передаваемое значение — это смещение памяти. Если бы мы щелкнули по нему правой кнопкой мыши и изменили его тип данных, мы бы увидели смещение строки. Это можно сделать один или два раза, но, если больше, мы можем автоматизировать этот процесс.
Код:
min = idc.get_inf_attr(INF_MIN_EA)
max = idc.get_inf_attr(INF_MAX_EA)
# для каждой известной функции
for func in idautils.Functions():
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    # пропустить библиотечные и функции преобразования
    if flags & FUNC_LIB or flags & FUNC_THUNK:
        continue
    dism_addr = list(idautils.FuncItems(func))
    for curr_addr in dism_addr:
        if idc.get_operand_type(curr_addr, 0) == 5 and \
            (min < idc.get_operand_value(curr_addr, 0) < max):idc.OpOff(curr_addr, 0, 0)
        if idc.get_operand_type(curr_addr, 1) == 5 and \
            (min < idc.get_operand_value(curr_addr, 1) < max):idc.op_plain_offset(curr_addr, 1, 0)
После выполнения, приведенного выше кода, мы увидим строку:
Код:
seg000:00BC1388         push    0Ch
seg000:00BC138A         push    offset aNtoskrnl_exe ; "ntoskrnl.exe"
seg000:00BC138F         push    [esp+10h+arg_0]
seg000:00BC1393         call    ds:_strnicmp
Вначале мы получаем минимальный и максимальный адрес, вызывая idc.get_inf_attr(INF_MIN_EA) и idc.get_inf_attr(INF_MAX_EA). Мы перебираем все функции и инструкции. Для каждой инструкции мы проверяем, является ли тип операнда o_imm и представлен ли внутри как число 5. Типы o_imm — это такие значения, как целое число или смещение. Как только значение найдено, мы читаем его, вызывая idc.get_operand_value (ea, n). Затем, значение проверяется, чтобы увидеть, находится ли оно в диапазоне минимального и максимального адресов. Если это так, мы используем idc.op_plain_offset (ea, n, base) для преобразования операнда в смещение. Первый аргумент ea — это адрес, n - индекс операнда, а base — это базовый адрес. Нашему примеру нужно только иметь нулевую базу.
 

Базовые блоки

Базовый блок — это прямолинейная кодовая последовательность, не имеющая ветвей, состоящая из единственной точки входа и единственной точки выхода. Базовые блоки полезны при анализе потока управления программой. Представление базовых блоков в IDA обычно наблюдается при использовании представления дизассемблированной функции в виде графа. Некоторыми известными примерами использования базовых блоков для анализа является определение циклов или обфускация потока управления. Когда базовый блок передает управление другому блоку, следующий блок называется преемником, а предыдущий блок - предшественником. Следующая блок-схема — это функция, которая расшифровывает строку с помощью однобайтовой операции XOR.
0.png

Поскольку на изображении сложно увидеть код и адреса, листинг можно найти ниже. Функция содержит три блока с началом основного блока по смещению 0x0401034, 0x040104A и 0x0040105E. Цикл XOR начинается с 0x040104A, индекс оценивается по смещению 0x0401059 и продолжается до 0x040105E после завершения цикла XOR.
Код:
.text:00401034          push    esi
.text:00401035          push    edi
.text:00401036          push    0Ah                     ; Size
.text:00401038          call    ds:malloc
.text:0040103E          mov     esi, eax
.text:00401040          mov     edi, offset str_encrypted
.text:00401045          xor     eax, eax                ; eax = 0
.text:00401047          sub     edi, esi
.text:00401049          pop     ecx
.text:0040104A
.text:0040104A loop:                                    ; CODE XREF: _main+28↓j
.text:0040104A          lea     edx, [eax+esi]
.text:0040104D          mov     cl, [edi+edx]
.text:00401050          xor     cl, ds:b_key            ; cl = 0
.text:00401056          inc     eax
.text:00401057          mov     [edx], cl
.text:00401059          cmp     eax, 9                  ; index
.text:0040105C          jb      short loop
.text:0040105E          push    esi
.text:0040105F          push    offset str_format
.text:00401064          mov     byte ptr [esi+9], 0
.text:00401068          call    w_vfprintf
.text:0040106D          push    esi                     ; Memory
.text:0040106E          call    ds:free
.text:00401074          add     esp, 0Ch
.text:00401077          xor     eax, eax                ; eax = 0
.text:00401079          pop     edi
.text:0040107A          pop     esi
.text:0040107B          retn
.text:0040107B _main    endp
Если мы извлекли смещение однобайтового шифрования XOR по адресу 0x0401050, мы используем следующий код, чтобы получить начало и конец базового блока, в котором происходит XOR, и получить базовые блоки преемника и предшественника.
Код:
ea = 0x0401050
f = idaapi.get_func(ea)
fc = idaapi.FlowChart(f, flags=idaapi.FC_PREDS)
for block in fc:
    print("ID: %i Start: 0x%x End: 0x%x" % (block.id, block.start_ea, block.end_ea))
    if block.start_ea <= ea < block.end_ea:
        print(" Basic Block selected")
    successor = block.succs()
    for addr in successor:
        print(" Successor: 0x%x" % addr.start_ea)
    pre = block.preds()
    for addr in pre:
        print(" Predecessor: 0x%x" % addr.end_ea)
    if ida_gdl.is_ret_block(block.type):
        print(" Return Block")
Первая инструкция назначает однобайтовое смещение XOR переменной ea. Функция idaapi.FlowChart (f = None, bounds = None, flags = 0) требует, чтобы в качестве первого аргумента был передан класс func_t. Чтобы получить класс, мы вызываем idaapi.get_func(ea). В границы аргументов можно передать кортеж, в котором первым элементом является начальный адресом, а вторым - конечный адресом. Bounds = (start, end). В IDA 7.4 для третьего аргумента flags должен быть установлен idaapi.FC_PREDS, если должен быть вычислен предшественник. Переменная fc содержит объект ida_gdl.FlowChart, который можно запускать в цикле для перебора всех блоков. Каждый блок содержит следующие атрибуты:
  • id: каждый базовый блок в функции имеет уникальный индекс. Первый блок начинается с идентификатора 0.
  • type: тип описывает базовый блок со следующими типами:
    • fcb_normal представляет собой нормальный блок и имеет промежуточное представление 0
    • fcb_indjump — это блок, который заканчивается непрямым переходом и представляется как 1
  • fcb_retявляется блоком возврата и имеет значение 2. Также может использоваться ida_gdl.is_ret_block (block.type), чтобы определить, имеет ли блок тип fcb_ret
    • fcb_cndret является блоком условного возврата и имеет значение 3
    • fcb_noret — это блок без возврата и имеет значение 4
    • fcb_enoret — это блок без возврата, который не принадлежит функции и имеет значение 5
    • fcb_extern является внешним нормальным блоком и имеет значение 6
    • fcb_error — это блок, который пропускает выполнение после конца функции и имеет значение 7
  • start_ea - начальный адрес базового блока.
  • end_ea - конечный адрес базового блока. Конечный адрес базового блока — это не адрес последней инструкции, а следующее за ним смещение.
  • preds — это функция, которая возвращает генератор, содержащий все адреса предшественников.
  • succs — это функция, которая возвращает генератор, содержащий все адреса преемника.
После вызова idaapi.FlowChart выполняется итерация каждого базового блока. Выводятся идентификатор, начальный и конечный адрес. Чтобы определить местонахождение блока, внутри которого находится ea, происходит сравнение cmp: ea больше или равно началу базового блока, block.start_ea и концу базового блока, block.end_ea. Имя переменной block был выбран произвольно. Чтобы получить генератор всех последующих смещений, мы вызываем block.succs(). Каждый элемент в генераторе succs зацикливается и выводится(print). Чтобы получить генератор всех смещений-предшественников, мы можем вызвать block.preds(). Каждый элемент в генераторе preds зацикливается и выводится(print). Последний оператор if вызывает ida_gdl.is_ret_block(btype), чтобы определить, является ли блок возвращаемым типом. Результат скрипта можно увидеть ниже.
Код:
ID: 0 Start: 0x401034 End: 0x40104a
    Successor: 0x40104a
ID: 1 Start: 0x40104a End: 0x40105e
    Basic Block selected
    Successor: 0x40105e
    Successor: 0x40104a
    Predecessor: 0x40104a
    Predecessor: 0x40105e
ID: 2 Start: 0x40105e End: 0x40107c
    Predecessor: 0x40105e
    Return Block
Базовый блок с идентификатором 1 представляет собой цикл, поэтому у него несколько преемников и предшественников.
 

Структуры

Макет структуры, имена и типы структур удаляются из кода в процессе компиляции. Реконструкция структур и правильная маркировка имен элементов могут значительно помочь в процессе реверс-инжиниринга. Ниже приведен фрагмент листинга, обычно наблюдаемого в шеллкоде x86. Полный код просматривает структуры в блоке среды потока (TEB) и блоке среды процесса (PEB), чтобы найти базовый адрес kernel32.dll.
Код:
seg000:00000000         xor     ecx, ecx
seg000:00000002         mov     eax, fs:[ecx+30h]
seg000:00000006         mov     eax, [eax+0Ch]
seg000:00000009         mov     eax, [eax+14h]
Следующим обычно наблюдаемым шагом является обход PE файла для поиска Windows API. Этот метод был впервые задокументирован The Last Stage of Delirium в их статье Win32 Assembly Components еще в 2002 году. При анализе всех различных структур легко потеряться, если смещения структур не помечены. Как видно из следующего кода, даже пара помеченных структур может оказаться полезной.
Код:
seg000:00000000         xor     ecx, ecx
seg000:00000002         mov     eax, fs:[ecx+_TEB.ProcessEnvironmentBlock]
seg000:00000006         mov     eax, [eax+PEB.Ldr]
seg000:00000009         mov     eax, [eax+PEB_LDR_DATA.InMemoryOrderModuleList.Flink]
seg000:0000000C         mov     eax, [eax+ecx]
Мы можем использовать следующий код для обозначения смещений соответствующими именами структур.
Код:
status = idc.add_default_til("ntapi")
if status:
    idc.import_type(-1, "_TEB")
    idc.import_type(-1, "PEB")
    idc.import_type(-1, "PEB_LDR_DATA")
    ea = 2
    teb_id = idc.get_struc_id("_TEB")
    idc.op_stroff(ea, 1, teb_id, 0)
    ea = idc.next_head(ea)
    peb_ldr_id = idc.get_struc_id("PEB_LDR_DATA")
    idc.op_stroff(ea, 1, peb_ldr_id, 0)
    ea = idc.next_head(ea)
    idc.op_stroff(ea, 1, peb_ldr_id, 0)
Первая строка предназначена для загрузки библиотеки типов (TIL) путем вызова idc.add_default_til (name). Для людей, не знакомых с TIL, они представляют собой собственный формат C/C ++ заголовков файлов в IDA. Они содержат определения структур, перечисления, объединения и другие типы данных. Различные TIL можно изучить вручную, открыв окно библиотеки типов (SHIFT+F11). idc.add_default_til (name) возвращает статус того, может ли библиотека быть загружена или нет. Если TIL удалось загрузить, он возвращает 1 (True) или 0 (False), если библиотека не была загружена. Это хорошая привычка - добавлять эту проверку в свой код. IDA не всегда идентифицирует компилятор для импорта TIL или забывает, что мы вручную загрузили TIL. После загрузки TIL, отдельные определения из TIL необходимо импортировать в IDB. Чтобы импортировать отдельные определения, мы вызываем idc.import_type(idx, type_name). Первый аргумент — это idx, который является индексом типа. У каждого типа есть индекс и идентификатор. Значение idx, равное -1, сигнализирует о том, что тип следует добавить в конец списка импортированных типов IDA. Индекс типа может быть изменен, поэтому полагаться на индекс не всегда надежно. Параметр idx, равный -1, является наиболее часто используемым аргументом. К IDB в приведенном выше коде добавляются три типа: _TEB, PEB и PEB_LDR_DATA.

Переменной ea присваивается значение 2. После присвоения мы получаем идентификатор импортируемого типа, вызывая idc.get_struc_id (string_name). Строка «_TEB» передается в idc.get_struct_id, который возвращает идентификатор структуры в виде целого числа. Идентификатор структуры присваивается teb_id. Чтобы применить имя элемента «ProcessEnvironmentBlock» к смещению структуры (0x30), мы можем использовать idc.op_stroff (ea, n, strid, delta). op_stroff принимает 4 аргумента. Первый аргумент — это адрес (ea) инструкций, содержащих смещение, которое будет помечено. Второй аргумент n - номер операнда. В нашем примере, поскольку мы хотим изменить метку 0x30 в инструкции mov eax, fs: [ecx 30h], нам нужно передать значение 1 для второго операнда. Третий аргумент — это идентификатор типа, который необходимо использовать для преобразования смещения в структуру. Последний аргумент — это дельта между базой структуры и указателем на структуру. Эта дельта обычно имеет значение 0. Функция idc.op_stroff используется для добавления имен структур к смещениям. Затем код вызывает idc.next_head (ea), чтобы получить адрес следующей инструкции, а затем использовать тот же ранее описанный процесс для маркировки других двух структур.

Наряду с использованием встроенного в IDA TIL, для доступа к структурам мы можем создать нашу собственную структуру. В этом примере мы сделаем вид, что в IDA не было определения типа для PEB_LDR_DATA. Вместо использования IDA, нам пришлось сбросить определение типа с в Windbg с помощью команды dt nt! _PEB_LDR_DATA. Результат этой команды можно увидеть ниже.
Код:
0:000> dt nt!_PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
    +0x000 Length                           : Uint4B
    +0x004 Initialized                      : UChar
    +0x008 SsHandle                         : Ptr64 Void
    +0x010 InLoadOrderModuleList            : _LIST_ENTRY
    +0x020 InMemoryOrderModuleList          : _LIST_ENTRY
    +0x030 InInitializationOrderModuleList  : _LIST_ENTRY
    +0x040 EntryInProgress                  : Ptr64 Void
    +0x048 ShutdownInProgress               : UChar
    +0x050 ShutdownThreadId                 : Ptr64 Void
Примечание. Эти поля должны быть статичными на вашем компьютере, но не беспокойтесь, если они отличаются. Это может измениться со временем, если Microsoft добавит новые поля.
Просматривая вывод, мы можем увидеть смещение, имя и тип. Этой информации достаточно, чтобы создать наш собственный тип. Следующий код проверяет, присутствует ли структура с именем my_peb_ldr_data. Если структура присутствует, код удаляет структуру, создает новую, а затем добавляет поле элемента структуры из nt! _PEB_LDR_DATA.
Код:
sid = idc.get_struc_id("my_peb_ldr_data")
if sid != idc.BADADDR:
    idc.del_struc(sid)
sid = idc.add_struc(-1, "my_peb_ldr_data", 0)
idc.add_struc_member(sid, "length", 0, idc.FF_DWORD, -1, 4)
idc.add_struc_member(sid, "initialized", 4, idc.FF_DWORD, -1, 4)
idc.add_struc_member(sid, "ss_handle", -1, idc.FF_WORD, -1, 2)
idc.add_struc_member(sid, "in_load_order_module_list", -1, idc.FF_DATA, -1, 10)
idc.add_struc_member(sid, "in_memory_order_module_list", -1, idc.FF_QWORD + idc.FF_WORD, -1, 10)
idc.add_struc_member(sid, "in_initialization_order_module_list", -1, idc.FF_QWORD + idc.FF_WORD, -1, 10)
idc.add_struc_member(sid, "entry_in_progress", -1, idc.FF_QWORD, -1, 8)
idc.add_struc_member(sid, "shutdown_in_progress", -1, idc.FF_WORD, -1, 2)
idc.add_struc_member(sid, "shutdown_thread_id", -1, idc.FF_QWORD, -1, 8)
Первый шаг в нашем коде вызывает idc.get_struc_id (struct_name) для возврата идентификатора структуры по имени. Если есть структура без имени «my_peb_ldr_data», idc.get_struct_id возвращает idc.BADADDR. Если идентификатор структуры не idc.BADADDR, то мы знаем, что структура с именем «my_peb_ldr_data» уже существует. В этом примере мы удаляем структуру, вызывая idc.del_struc (sid). Требуется единственный аргумент идентификатора структуры. Для создания структуры, код вызывает idc.add_struc (index, name, is_union). Первый аргумент — это индекс новой структуры. Как и в случае с idc.import_type, лучше всего передавать значение -1. Это указывает, что IDA должна использовать следующий по величине индекс для идентификатора. Второй аргумент, переданный в idc.add_struc, — это имя структуры. Третий аргумент is_union — это логическое значение, которое определяет, является ли вновь созданная структура объединением. В приведенном выше коде мы передаем значение 0, чтобы указать, что это не объединение. Элементы структуры можно пометить, вызвав idc.add_struc_member (sid, name, offset, flag, typeid, nbytes).
Примечание: idc.add_struc_member имеет больше аргументов, но поскольку они используются для более сложных определений, мы не будем их рассматривать. Если вас интересует, как создавать более сложные определения, я бы порекомендовал позже покопаться в исходном коде IDAPython.
Первый аргумент — это идентификатор структуры, ранее назначенный переменной sid. Второй аргумент — это строка имени элемента. Третий аргумент - смещение. Смещение может быть -1, чтобы добавить к концу структуры, или целочисленным значением, чтобы указать смещение. Четвертый аргумент — это флаг. Флаг указывает тип данных (слово, число с плавающей запятой и т.д.). Доступные типы данных флага можно увидеть ниже.
Код:
FF_BYTE         0x00000000 // byte
FF_WORD         0x10000000 // word
FF_DWORD        0x20000000 // dword
FF_QWORD        0x30000000 // qword
FF_TBYTE        0x40000000 // tbyte
FF_STRLIT       0x50000000 // ASCII ?
FF_STRUCT       0x60000000 // Struct ?
FF_OWORD        0x70000000 // octaword (16 bytes/128 bits)
FF_FLOAT        0x80000000 // float
FF_DOUBLE       0x90000000 // double
FF_PACKREAL     0xA0000000 // packed decimal real
FF_ALIGN        0xB0000000 // alignment directive
FF_CUSTOM       0xD0000000 // custom data type
FF_YWORD        0xE0000000 // ymm word (32 bytes/256 bits)
FF_ZWORD        0xF0000000 // zmm word (64 bytes/512 bits)
FF_DATA         0x400      // data
Пятый аргумент — это typeid, он используется для более сложных определений. В наших примерах он имеет значение -1. Последний аргумент — это количество байтов (nbyte) для выделения. Важно, чтобы размер флага и nbyte был одинаковым. Если используется двойное слово с флагом idc.FF_DWORD, необходимо указать размер 4. В противном случае IDA не создает элемент структуры. Эту ошибку сложно обнаружить, потому что IDA не выдает никаких предупреждений. Можно использовать комбинацию флагов. Например, idc.FF_QWORD idc.FF_WORD используется для указания размера 10 при создании элемента in_memory_order_module_list. Если передан флаг idc.FF_DATA, можно использовать любой размер без необходимости комбинировать и добавлять другие флаги. Мы увидим следующее, если просмотрим только что созданную структуру в окне структуры IDA.
Код:
00000000 my_peb_ldr_data struc ; (sizeof=0x3A, mappedto_139)
00000000 length dd ?
00000004 initialized dd ?
00000008 ss_handle dw ?
0000000A in_load_order_module_list db 10 dup(?)
00000014 in_memory_order_module_list dt ?
0000001E in_initialization_order_module_list dt ?
00000028 entry_in_progress dq ?
00000030 shutdown_in_progress dw ?
 

Перечисляемые типы

Упрощенное описание перечислимых типов — это способ использования символических констант для представления значимого имени. Перечисляемые типы (также известные как Enums) - обычное дело при вызове системных API. При вызове CreateFileA в Windows желаемый доступ GENERIC_READ представлен как константа 0x80000000. К сожалению, в процессе компиляции имена удаляются. Повторное заполнение констант осмысленными именами помогает в процессе обратного проектирования. При реверс-инжиниринге вредоносных программам нередко можно увидеть константы, представляющие хэши имен API. Этот метод используется для обфускации вызовов API из статического анализа. Следующий код является примером этой техники.
Код:
seg000:00000018     push    0CA2BD06Bh ; ROR 13 hash of CreateThread
seg000:0000001D     push    dword ptr [ebp-4]
seg000:00000020     call    lookup_hash
seg000:00000025     push    0
seg000:00000027     push    0
seg000:00000029     push    0
seg000:0000002B     push    4C30D0h ; StartAddress
seg000:00000030     push    0
seg000:00000032     push    0
seg000:00000034     call    eax     ; CreateThread
Значение 0xCA2BD06B - это хэш CreateThread. Хеширование создается с использованием комбинации циклического перебора каждого символа, сдвига битов байта на 13 с использованием ROR и сохранения результатов для создания хеша. Этот метод обычно называют хешированием z0mbie или ROR-13. Поскольку хеш в некотором роде является символическим именем CreateThread, это практический пример того, когда использовать перечисления.

Поскольку мы уже знаем, что хеш 0xCA2BD06B — это строка «CreateThread», мы могли бы просто создать перечисление. Что, если бы мы не знали, какое имя API представляет хэш? Тогда нам понадобится какой-то способ хешировать все имена экспортированных символов в какой-нибудь Windows DLL. Для краткости мы можем обмануть и сказать, что DLL — это kernel32.dll. Чтобы экспортировать имена символов из kernel32.dll, мы можем использовать pefile. В Приложении приведен краткий пример наиболее распространенного варианта использования pefile. Затем нам нужен способ воспроизвести алгоритм хеширования. Для приведенного ниже кода мы будем использовать модифицированную версию Rolf Rolles (см. раздел Что дальше?), реализацию z0mbie hash и pefile. Код был разработан таким образом, чтобы читатель мог легко изменить его, чтобы он соответствовал любому хешу или добавлял все хеши.
Код:
import pefile

def ror32(val, amt):
    return ((val >> amt) & 0xffffffff) | ((val << (32 - amt)) & 0xffffffff)

def add32(val, amt):
    return (val + amt) & 0xffffffff

def z0mbie_hash(name):
    hash = 0
    for char in name:
        hash = add32(ror32(hash, 13), ord(char) & 0xff)
    return hash

def get_name_from_hash(file_name, hash):
    pe = pefile.PE(file_name)
    for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        if z0mbie_hash(exp.name) == hash:
            return exp.name

api_name = get_name_from_hash("kernel32.dll", 0xCA2BD06B)
if api_name:
    id = idc.add_enum(-1, "z0mbie_hashes", idaapi.hexflag())
    idc.add_enum_member(id, api_name, 0xCA2BD06B, -1)
Первая строка импортирует pefile в IDA. Две функции ror32 и add32 отвечают за воспроизведение инструкции ROR. Функция z0mbie_hash (name) принимает единственный аргумент строки, которая должна быть хеширована, и возвращает хеш. Последняя функция get_name_from_hash (file_path, hash) принимает два аргумента. Первый аргумент — это путь к файлу библиотеки DLL, символы которой должны быть хешированы. Второй аргумент — это хеш-значение, имя которого мы ищем. Функция возвращает имя строки. Первая строка в этой функции вызывает pefile.PE (file_path) для загрузки и анализа kernel32.dll. Экземпляр pefile PE сохраняется в переменной pe. Каждый символ в DLL проходит итерацию, перебирая каждый элемент в pe.DIRECTORY_ENTRY_EXPORT.symbols. Это поле содержит имя, адрес и другие атрибуты для каждого экспортируемого символа в DLL. Имя символа хешируется путем вызова z0mbie_hash(exp.name), а затем сравнивается. В случае совпадения возвращается имя символа и присваивается api_name. На этом этапе кода создание и добавление перечисления завершено. Первым шагом при добавлении перечисления является создание идентификатора перечисления. Это делается путем вызова idc.add_enum (idx, name, flag). Первый аргумент — это idx или серийный номер нового перечисления. Значение -1 назначает следующий доступный идентификатор. Второй аргумент — это имя перечисления. Последний аргумент - это флаг idaapi.hexflag(). После выполнения кода, если бы мы нажали M, выделяя значение 0xCA2BD06B в IDA, мы бы увидели строку «CreateThread» как вариант символьной константы. Следующий код — это код, который мы видели ранее, но теперь хэш является символической константой.
Код:
seg000:00000015     mov         [ebp-4], ebx
seg000:00000018     push        CreateThread ; ROR 13 hash of CreateThread
seg000:0000001D     push        dword ptr [ebp-4]
 

Перекрестные ссылки (xref)

Возможность находить перекрестные ссылки (aka xrefs) на данные или код — это обычная задача анализа. Расположение перекрестных ссылок важно, потому что они предоставляют сведения о том, где используются определенные данные или откуда вызывается функция. Например, что, если мы хотим найти весь адрес, с которого был вызван WriteFile. Используя перекрестные ссылки, все, что нам нужно сделать, это найти адрес WriteFile по имени, а затем найти все xref на него.
Код:
Python>wf_addr = idc.get_name_ea_simple("WriteFile")
Python>print("0x%x %s" % (wf_addr, idc.generate_disasm_line(wf_addr, 0)))
0x1000e1b8 extrn WriteFile:dword
Python>for addr in idautils.CodeRefsTo(wf_addr, 0):\
    print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x10004932      call     ds:WriteFile
0x10005c38      call     ds:WriteFile
0x10007458      call     ds:WriteFile
В первой строке мы получаем адрес API WriteFile, используя idc.get_name_ea_simple(str). Эта функция возвращает адрес API. Выводим адрес WriteFile и его строковое представление. Затем перебираем все перекрестные ссылки кода, вызвав idautils.CodeRefsTo (ea, flow). Он возвращает итератор, который можно пройти по циклу. ea — это адрес, на который мы хотели бы сделать перекрестную ссылку. Аргумент flow— это логическое значение. Он используется для указания следовать нормальному потоку кода или нет. Затем отображается каждая перекрестная ссылка на адрес. Небольшое примечание об использовании idc.get_name_ea_simple (str). Ко всем переименованным функциям и API в IDB можно получить доступ, вызвав idautils.Names(). Эта функция возвращает объект-итератор, по которому можно выполнить цикл для печати или доступа к именам. Каждый именованный элемент представляет собой кортеж из (ea, str_name).
Код:
Python>[x for x in Names()]
[(268439552, 'SetEventCreateThread'), (268439615, 'StartAddress'), (268441102,
'SetSleepClose'),....]
Если бы мы хотели узнать, откуда была сделана ссылка на код, мы бы использовали idautisl.CodeRefsFrom (ea, flow). Например, давайте получим адрес, по которому ведется ссылка на 0x10004932.
Код:
Python>ea = 0x10004932
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x10004932 call ds:WriteFile
Python>for addr in idautils.CodeRefsFrom(ea, 0):\
    print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
Python>
0x1000e1b8 extrn WriteFile:dword
Если мы рассмотрим пример idautils.CodeRefsTo (ea, flow), мы увидим, что адрес 0x10004932 предназначен для адресации WriteFile. idautils.CodeRefsTo (ea, flow) и idautils.CodeRefsFrom (ea, flow) используются для поиска перекрестных ссылок на код и из него. Ограничение использования idautils.CodeRefsTo (ea, flow) заключается в том, что API-интерфейсы, которые динамически импортируются, а затем переименовываются вручную, не отображаются как перекрестные ссылки кода. Допустим, мы вручную переименовываем адрес двойного слова в «RtlCompareMemory», используя idc.set_name (ea, name, SN_CHECK).
Код:
Python>print("0x%x" % (ea)
0xa26c78
Python>idc.set_name(ea, "RtlCompareMemory", SN_CHECK)
True
Python>for addr in idautils.CodeRefsTo(ea, 0):\
    print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
IDA не помечает эти API как перекрестные ссылки кода. Чуть позже мы опишем общий метод получения всех перекрестных ссылок. Если мы хотим найти перекрестные ссылки на данные и обратно, мы могли бы использовать idautils.DataRefsTo (e) или idautils.DataRefsFrom (ea).
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x1000e3ec db 'vnc32',0
Python>for addr in idautils.DataRefsTo(ea):\
    print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x100038ac push offset aVnc32   ; "vnc32"
idautils.DataRefsTo (ea) принимает аргумент адреса и возвращает итератор всех адресов, которые перекрестно ссылаются на данные.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x100038ac push offset aVnc32   ; "vnc32"
Python>for addr in idautils.DataRefsFrom(ea):\
    print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x1000e3ec db 'vnc32',0
Чтобы сделать наоборот и показать адрес from, мы вызываем idautils.DataRefsFrom(ea), передаем адрес в качестве аргумента. Что возвращает итератор всех адресов, которые перекрестно ссылаются на данные. Различное использование кода и данных может немного сбивать с толку. Опишем более общий метод. Этот подход можно использовать для получения всех перекрестных ссылок на адрес путем вызова одной функции. Мы можем получить все перекрестные ссылки на адрес с помощью idautils.XrefsTo (ea, flags = 0) и получить все перекрестные ссылки с адреса, вызвав idautils.XrefsFrom (ea, flags = 0).
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x1000eee0 unicode 0, <Path>,0
Python>for xref in idautils.XrefsTo(ea, 1):
    print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), xref.frm, xref.to, xref.iscode))
Python>
1 Data_Offset 0x1000ac0d 0x1000eee0 0
Python>>print("0x%x %s" % (xref.frm, idc.generate_disasm_line(xref.frm, 0))
0x1000ac0d push     offset KeyName      ; "Path"
В первой строке отображается наш адрес и строка с именем «Path». Мы используем idautils.XrefsTo (ea, 1), чтобы получить все перекрестные ссылки на строку. Затем мы используем xref.type для печати значения типа xrefs. idautils.XrefTypeName (xref.type) используется для печати строкового представления этого типа. Существует двенадцать различных задокументированных значений ссылочного типа. Значение можно увидеть слева, а соответствующее имя – справа.
Код:
0 = 'Data_Unknown'
1 = 'Data_Offset'
2 = 'Data_Write'
3 = 'Data_Read'
4 = 'Data_Text'
5 = 'Data_Informational'
16 = 'Code_Far_Call'
17 = 'Code_Near_Call'
18 = 'Code_Far_Jump'
19 = 'Code_Near_Jump'
20 = 'Code_User'
21 = 'Ordinary_Flow'
xref.frm выводит адрес отправителя, а xref.to выводит два адреса. xref.iscode выводится, если внешняя ссылка находится в сегменте кода. В предыдущем примере флаг idautils.XrefsTo (ea, 1) был установлен в значение 1. Если флаг равен нулю, отображается любая перекрестная ссылка. Мы можем использовать следующий блок ассемблерной сборки, чтобы проиллюстрировать этот момент.
Код:
.text:1000AAF6      jnb     short loc_1000AB02  ; XREF
.text:1000AAF8      mov     eax, [ebx+0Ch]
.text:1000AAFB      mov     ecx, [esi]
.text:1000AAFD      sub     eax, edi
.text:1000AAFF      mov     [edi+ecx], eax
.text:1000AB02
.text:1000AB02 loc_1000AB02:                    ; ea is here()
.text:1000AB02      mov     byte ptr [ebx], 1
Курсор расположен по адресу 0x1000AB02. Этот адрес имеет перекрестную ссылку с 0x1000AAF6, но он также имеет вторую перекрестную ссылку с 0x1000AAF6.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x1000ab02 mov byte ptr [ebx], 1
Python>for xref in idautils.XrefsTo(ea, 1):
            print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), xref.frm, xref.to, xref.iscode))
Python>
19 Code_Near_Jump 0x1000aaf6 0x1000ab02 1
Python>for xref in idautils.XrefsTo(ea, 0):
            print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), xref.frm, xref.to, xref.iscode))
Python>
21 Ordinary_Flow 0x1000aaff 0x1000ab02 1
19 Code_Near_Jump 0x1000aaf6 0x1000ab02 1
Вторая перекрестная ссылка - от 0x1000AAFF до 0x1000AB02. Перекрестные ссылки не обязательно должны быть вызваны инструкциями ветвления. Они также могут быть вызваны обычным потоком кода. Если мы установим флаг 1, ссылочные типы Ordinary_Flow добавляться не будут. Теперь вернемся к нашему предыдущему примеру с RtlCompareMemory. Мы можем использовать idautils.XrefsTo (ea, flow), чтобы получить все перекрестные ссылки.
Код:
Python>print("0x%x" % ea)
0xa26c78
Python>idc.set_name(ea, "RtlCompareMemory", SN_CHECK)
True
Python>for xref in idautils.XrefsTo(ea, 1):
            print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), xref.frm, xref.to, xref.iscode))
Python>
3 Data_Read 0xa142a3 0xa26c78 0
3 Data_Read 0xa143e8 0xa26c78 0
3 Data_Read 0xa162da 0xa26c78 0
Получение всех перекрестных ссылок иногда может быть немного подробным.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa21138 extrn GetProcessHeap:dword
Python> for xref in idautils.XrefsTo(ea, 1):
            print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), xref.frm, xref.to, xref.iscode))
Python>
17 Code_Near_Call 0xa143b0 0xa21138 1
17 Code_Near_Call 0xa1bb1b 0xa21138 1
3 Data_Read 0xa143b0 0xa21138 0
3 Data_Read 0xa1bb1b 0xa21138 0
Python>print(idc.generate_disasm_line(0xa143b0, 0))
call    ds:GetProcessHeap
Подробность исходит от Data_Read и Code_Near, добавленных к внешним ссылкам. Получение всех адресов и добавление их в набор может быть полезно для сокращения по всем адресам.
Python:
def get_to_xrefs(ea):
    xref_set = set([])
    for xref in idautils.XrefsTo(ea, 1):
        xref_set.add(xref.frm)
    return xref_set
def get_frm_xrefs(ea):
    xref_set = set([])
    for xref in idautils.XrefsFrom(ea, 1):
        xref_set.add(xref.to)
    return xref_set
Пример сокращения функций в нашем примере GetProcessHeap.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa21138 extrn GetProcessHeap:dword
Python>get_to_xrefs(ea)
set([10568624, 10599195])
Python>[("0x%x" % x) for x in get_to_xrefs(ea)]
['0xa143b0', '0xa1bb1b']
 

Поиск

Мы уже изучили некоторый базовый поиск, перебирая все известные функции или инструкции. Это полезно, но иногда нам нужно искать определенные байты, такие как 0x55 0x8B 0xEC. Этот байтовый шаблон представляет собой классический пролог функции push ebp, mov ebp, esp. Для поиска байтовых или двоичных шаблонов мы можем использовать ida_search.find_binary(start, end, searchstr, radix, sflag). start и end определяют диапазон, в котором мы хотели бы выполнить поиск. searchstr - это шаблон, который мы ищем. radix используется при написании процессорных модулей. Эта тема выходит за рамки данной книги. Я бы порекомендовал прочитать главу 19 книги Криса Игла The IDA Pro Book. На данный момент поле radix заполняется значением 16. sflag — это направление или условие. Есть несколько разных типов флагов. Имена и значения можно увидеть ниже.
Код:
SEARCH_UP = 0
SEARCH_DOWN = 1
SEARCH_NEXT = 2
SEARCH_CASE = 4
SEARCH_REGEX = 8
SEARCH_NOBRK = 16
SEARCH_NOSHOW = 32
SEARCH_IDENT = 128
SEARCH_BRK = 256
Не все эти флаги заслуживают внимания, но мы можем коснуться наиболее часто используемых флагов.
  • SEARCH_UP и SEARCH_DOWN используются для выбора направления, в котором мы хотели бы следовать нашему поиску.
  • SEARCH_NEXT используется для получения следующего найденного объекта.
  • SEARCH_CASE используется для указания чувствительности к регистру.
  • SEARCH_NOSHOW не показывает прогресс поиска.
Предыдущие версии IDA содержали sflag SEARCH_UNICODE для поиска строк Unicode. Этот флаг больше не нужен при поиске символов, поскольку IDA по умолчанию ищет как ASCII, так и Unicode. Давайте быстро пройдемся по поиску шаблона байтов пролога функции, упомянутого ранее.
Код:
Python>pattern = '55 8B EC'
addr = idc.get_inf_attr(INF_MIN_EA)
pattern = '55 8B EC'
addr = idc.get_inf_attr(INF_MIN_EA)
for x in range(0, 5):
    addr = ida_search.find_binary(addr, idc.BADADDR, pattern, 16,ida_search.SEARCH_DOWN)
    if addr != idc.BADADDR:
        print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
Python>
0x401000    push    ebp
0x401000    push    ebp
0x401000    push    ebp
0x401000    push    ebp
0x401000    push    ebp
В первой строке мы определяем наш шаблон поиска. Шаблон поиска может быть в шестнадцатеричном формате, начиная с 0x, как в 0x55 0x8B 0xEC, или как байты, представленные в шестнадцатеричном формате IDA 55 8B EC. Формат \ x55 \ x8B \ xEC нельзя использовать, если мы не использовали ida_search.find_text (ea, y, x, searchstr, sflag). idc.get_inf_attr (INF_MIN_EA) используется для получения первого адреса в исполняемом файле. Затем мы назначаем результат использования ida_search.find_binary (start, end, searchstr, radiux, sflag) переменной с именем addr.

При поиске, важно убедиться, что поиск действительно нашел шаблон. Это проверяется путем сравнения addr с idc.BADADDR. Затем выводим адрес и листинг. Обратили внимание, что адрес не увеличивается? Это потому, что мы не передали флаг SEARCH_NEXT. Если этот флаг не передан, текущий адрес используется для поиска шаблона. Если последний адрес содержал наш байтовый образец, поиск никогда не будет увеличивать его. Ниже представлена исправленная версия с флагом SEARCH_NEXT перед SEARCH_DOWN.
Код:
Python> pattern = '55 8B EC'
addr = idc.get_inf_attr(INF_MIN_EA)
pattern = '55 8B EC'
addr = idc.get_inf_attr(INF_MIN_EA)
for x in range(0, 5):
    addr = ida_search.find_binary(addr, idc.BADADDR, pattern, 16, ida_search.SEARCH_NEXT|ida_search.SEARCH_DOWN)
    if addr != idc.BADADDR:
    print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
Python>
0x401000    push    ebp
0x401040    push    ebp
0x401070    push    ebp
0x4010e0    push    ebp
0x401150    push    ebp
Поиск байтовых шаблонов полезен, но иногда нам может потребоваться поиск таких строк, как «chrome.dll». Мы могли бы преобразовать строки в шестнадцатеричные байты, используя [hex (y) for y in bytearray ("chrome.dll")], но это немного некрасиво. Кроме того, если строка является Unicode, нам придется учитывать эту кодировку. Самый простой подход - использовать ida_search.find_text (ea, y, x, searchstr, sflag). Большинство этих полей должны выглядеть знакомо, потому что они такие же, как ida_search.find_binary. ea - начальный адрес. y - количество строк в точке ea для поиска, а x - координата в строке. Полям y и x обычно присваиваются 0. searchstr — это шаблон для поиска, а sflag определяет направление и типы для поиска. Например, мы можем искать все вхождения строки «Accept». Любая строка из окна строк «Shift+F12» может использоваться для поиска в этом примере.
Код:
Python>cur_addr = idc.get_inf_attr(INF_MIN_EA)
for x in range(0, 5):
    cur_addr = ida_search.find_text(cur_addr, 0, 0, "Accept", ida_search.SEARCH_DOWN)
    if addr == idc.BADADDR:
        break
    print("0x%x %s" % (cur_addr, idc.generate_disasm_line(cur_addr, 0)))
    cur_addr = idc.next_head(cur_addr)
Python>
0x40da72 push offset aAcceptEncoding; "Accept-Encoding:\n"
0x40face push offset aHttp1_1Accept; " HTTP/1.1\r\nAccept: */* \r\n "
0x40fadf push offset aAcceptLanguage; "Accept-Language: ru \r\n"
...
0x423c00 db 'Accept',0
0x423c14 db 'Accept-Language',0
0x423c24 db 'Accept-Encoding',0
0x423ca4 db 'Accept-Ranges',0
Мы используем idc.get_inf_attr (INF_MIN_EA), чтобы получить минимальный адрес и присвоить его переменной с именем cur_addr. Это делается снова для максимального адреса путем вызова idc.get_inf_attr (INF_MAX_EA) и присвоения возврата переменной с именем end. Поскольку мы не знаем, сколько вхождений строки присутствует, нам нужно проверить, что поиск продолжается вниз и она меньше максимального адреса. Затем мы назначаем результат ida_search.find_text текущему адресу. Поскольку мы вручную увеличиваем адрес, вызывая idc.next_head (ea), нам не нужен флаг SEARCH_NEXT. Причина, по которой мы вручную увеличиваем текущий адрес до следующей строки, заключается в том, что шаблон может встречаться несколько раз в одной строке. Это может затруднить получение адреса следующей строки.

Наряду с ранее описанным поиском по шаблону, существует пара функций, которые можно использовать для поиска других типов. Соглашения об именах API поиска позволяют легко определить его общую функциональность. Прежде чем мы обсудим поиск различных типов, мы сначала рассмотрим определение типов по их адресу. Существует подмножество API, которые начинаются с "is", которые можно использовать для определения типа адреса. API возвращают логическое значение True или False.

idc.is_code (f)
Возвращает True, если IDA пометила адрес как код.

idc.is_data (f)
Возвращает True, если IDA пометила адрес как данные.

idc.is_tail (f)
Возвращает True, если IDA пометила адрес как хвост функции.

idc.is_unknown (f)
Возвращает True, если IDA пометила адрес как неизвестный. Этот тип используется, когда IDA не определила, является ли адрес кодом или данными.

idc.is_head (f)
Возвращает True, если IDA пометила адрес как заголовок.

Аргумент f для нас в новинку. Вместо того, чтобы передавать адрес, нам сначала нужно получить представление промежуточных флагов, а затем передать его нашему набору функций idc.is_ *. Чтобы получить промежуточные флаги, мы используем idc.get_full_flags (ea). Теперь, когда у нас есть основы того, как можно использовать функцию, и различные типы, давайте сделаем небольшой пример.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x10001000      push     ebp
Python>idc.is_code(idc.get_full_flags(ea))
True

ida_search.find_code (ea, flag)
Он используется для поиска следующего адреса, помеченного как код. Это может быть полезно, если мы хотим найти конец блока данных. Если ea — это адрес, который уже отмечен как код, он возвращает следующий адрес. Флаг используется, как описано в ida_search.find_text.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x4140e8    dd      offset dword_4140EC
Python>addr = ida_search.find_code(ea, SEARCH_DOWN|SEARCH_NEXT)
Python>print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x41410c    push    ebx
Как мы видим, ea — это адрес 0x4140e8 некоторых данных. Мы записываем результат ida_search.find_code (ea, SEARCH_DOWN | SEARCH_NEXT) в addr. Затем печатаем адрес и листинг. Вызывая эту единственную функцию, мы пропустили 36 байтов данных, чтобы получить начало раздела, помеченного как код.

ida_search.find_data (ea, flag)
Он используется точно так же, как ida_search.find_code, за исключением того, что возвращает начало следующего адреса, помеченного как блок данных. Если мы откатим предыдущий сценарий и начнем с адреса кода и выполним поиск, чтобы найти начало данных.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x41410c    push    ebx
Python>addr = ida_search.find_data(ea, SEARCH_UP|SEARCH_NEXT)
Python>print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x4140ec dd 49540E0Eh, 746E6564h, 4570614Dh, 7972746Eh, 8, 1, 4010BCh
Единственное, что немного отличается от предыдущего примера, — это направление SEARCH_UP.

ida_search.find_unknown (ea, flag)
Эта функция используется для поиска адреса байтов, которые IDA не идентифицировала как код или данные. Неизвестный тип требует дальнейшего ручного анализа визуально или с помощью скриптов.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x406a05    jge     short loc_406A3A
Python>addr = ida_search.find_unknown(ea, SEARCH_DOWN)
Python>print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x41b004    db      0DFh ; ?

ida_search.find_defined (ea, flag)
Используется для поиска адреса, который IDA определила как код или данные.
Код:
0x41b900    db      ? ;
Python>addr = ida_search.find_defined(ea, SEARCH_UP)
Python>print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x41b5f4    dd      ?
Это может показаться бессмысленным, но, если бы мы вывели перекрестные ссылки addr, мы бы увидели, что он используется.
Код:
Python>for xref in idautils.XrefsTo(addr, 1):
          print("0x%x %s" % (xref.frm, idc.generate_disasm_line(addr, 0)))
Python>
0x4069c3    mov     eax, dword_41B5F4[ecx*4]

ida_search.find_imm (ea, flag, value)
Вместо поиска типа мы могли бы захотеть найти определенное значение. Cкажем, например, что у нас есть ощущение, что код вызывает rand для генерации случайного числа, но мы не можем найти код. Если бы мы знали, что rand использует значение 0x343FD в качестве начального числа, мы могли бы искать это число с помощью ida_search.find_imm (get_inf_attr (INF_MIN_EA), SEARCH_DOWN, 0x343FD).
Код:
Python>addr = ida_search.find_imm(get_inf_attr(INF_MIN_EA), SEARCH_DOWN, 0x343FD )
Python>addr
[268453092, 0]
Python>print("0x%x %s %x" % (addr[0], idc.generate_disasm_line(addr[0], 0), addr[1]))
0x100044e4      imul    eax, 343FDh 0
В первой строке мы передаем минимальный адрес через get_inf_attr (INF_MIN_EA), ищем вниз, а затем ищем значение 0x343FD. Вместо того, чтобы возвращать адрес, как показано в предыдущих API поиска, ida_search.find_imm возвращает кортеж. Первый элемент в кортеже — это адрес, а второй - операнд. Как и возврат idc.print_operand, первый операнд начинается с нуля. Когда мы выводим адрес и дизассемблируем, мы видим, что значение является вторым операндом. Если бы мы хотели найти все варианты использования ближайшего значения, мы могли бы сделать следующее.
Код:
Python>addr = idc.get_inf_attr(INF_MIN_EA)
while True:
    addr, operand = ida_search.find_imm(addr, SEARCH_DOWN | SEARCH_NEXT, 4)
    if addr == BADADDR:
        break
    print("0x%x %s Operand %i" % (addr, idc.generate_disasm_line(addr, 0), operand))
Python>
0x402434    dd 9, 0FF0Bh, 0Ch, 0FF0Dh, 0Dh, 0FF13h, 13h, 0FF1Bh, 1Bh Operand 0
0x40acee    cmp    eax, 7Ah Operand 1
0x40b943    push   7Ah Operand 0
0x424a91    cmp    eax, 7Ah Operand 1
0x424b3d    cmp    eax, 7Ah Operand 1
0x425507    cmp    eax, 7Ah Operand 1
Большая часть кода должна выглядеть знакомо, но поскольку мы ищем несколько значений, он использует цикл while и SEARCH_DOWN.

В некоторых ситуациях поиск с использованием ida_search.find_ * может быть немного медленным. Для ускорения поиска в IDA можно использовать Yara. Пожалуйста, посмотрите главу Yara, чтобы узнать больше об использовании Yara в IDA для ускорения поиска.
 

Отбор данных

Нам не всегда нужно искать код или данные. В некоторых случаях нам уже известно расположение кода или данных, но мы хотим отобрать их для анализа. В подобных ситуациях мы можем просто выделить код и начать работать с ним в IDAPython. Чтобы получить границы выбранных данных, мы можем использовать idc.read_selection_start(), чтобы получить начало, и idc.read_selection_end(), чтобы получить конец. Допустим, у нас выбран приведенный ниже код.
Код:
.text:00408E46      push        ebp
.text:00408E47      mov         ebp, esp
.text:00408E49      mov         al, byte ptr dword_42A508
.text:00408E4E      sub         esp, 78h
.text:00408E51      test        al, 10h
.text:00408E53      jz          short loc_408E78
.text:00408E55      lea         eax, [ebp+Data]
Мы можем использовать следующий код для вывода адресов.
Код:
Python>start = idc.read_selection_start()
Python>print("0x%x" % start)
0x408e46
Python>end = idc.read_selection_end()
Python>print("0x%x" % end)
0x408e58
Мы назначаем запуск idc.read_selection_start(). Это адрес первого выбранного адреса. Затем мы используем результат idc.read_selection_end () и назначаем его end. Следует отметить, что end — это не последний выбранный адрес, а начало следующего адреса. Если бы мы предпочли сделать только один вызов API, мы могли бы использовать idaapi.read_selection ().

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

Мое личное убеждение: «Если я не описываю, я не исследую». Добавление комментариев, переименование функций и взаимодействие со сборкой - один из лучших способов понять, что делает код. Со временем, часть взаимодействия становится излишней. В подобных ситуациях полезно автоматизировать процесс.

Прежде чем перейти к некоторым примерам, мы должны сначала обсудить основы комментариев и переименования. Есть два типа комментариев. Первый — это обычный комментарий, а второй - повторяющийся. Обычный комментарий появляется по адресу 0x041136B как обычный текстовый комментарий. Повторяющийся комментарий можно увидеть по адресу 0x0411372, 0x0411386 и 0x0411392. Только последний комментарий — это комментарий, введенный вручную. Другие комментарии появляются, когда инструкция ссылается на адрес (например, условие перехода), который содержит повторяющийся комментарий.
Код:
00411365        mov         [ebp+var_214], eax
0041136B        cmp         [ebp+var_214], 0    ; обычный комментарий
00411372        jnz         short loc_411392    ; повторяющийся комментарий
00411374        push        offset sub_4110E0
00411379        call        sub_40D060
0041137E        add         esp, 4
00411381        movzx       edx, al
00411384        test        edx, edx
00411386        jz          short loc_411392    ; повторяющийся комментарий
00411388        mov         dword_436B80, 1
00411392
00411392 loc_411392:
00411392
00411392        mov         dword_436B88, 1     ; повторяющийся комментарий
0041139C        push        offset sub_4112C0
Для добавления комментариев мы используем idc.set_cmt (ea, comment, 0), а для повторяющихся комментариев мы используем idc.set_cmt (ea, comment, 1). ea — это адрес, comment — это строка, которую мы хотели бы добавить, 0 указывает, что комментарий не повторяется, а 1 указывает, что комментарий повторяющийся. Приведенный ниже код добавляет комментарий каждый раз, когда инструкция обнуляет регистр или значение с помощью XOR.
Python:
for func in idautils.Functions():
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    # пропускает библиотечные и функции-преобразователи
    if flags & FUNC_LIB or flags & FUNC_THUNK:
        continue
    dism_addr = list(idautils.FuncItems(func))
    for ea in dism_addr:
        if idc.print_insn_mnem(ea) == "xor":
            if idc.print_operand(ea, 0) == idc.print_operand(ea, 1):
                comment = "%s = 0" % (idc.print_operand(ea, 0))
                idc.set_cmt(ea, comment, 0)
Как описано ранее, мы перебираем все функции, вызывая idautils.Functions(), и перебираем все инструкции, вызывая list(idautils.FuncItems(func)). Мы читаем мнемонику с помощью idc.print_insn_mnem (ea) и проверяем, что она равна xor. Если это так, мы проверяем, что операнды равны idc.print_operand (ea, n). Если они равно, мы создаем строку с операндом, а затем добавляем неповторяющийся комментарий.
Код:
0040B0F7        xor         al, al              ; al = 0
0040B0F9        jmp         short loc_40B163
Чтобы добавить повторяющийся комментарий, мы заменим idc.set_cmt (ea, comment, 0) на idc.set_cmt (ea, comment, 1). Это может быть немного более полезным, потому что мы увидим ссылки на ветки, которые обнуляют значение и, вероятно, возвращают 0. Чтобы получить комментарий, мы просто используем idc.get_cmt (ea, repeatable). ea — это адрес, содержащий комментарий, а repeatable - логическое значение True (1) или False (0). Чтобы получить приведенные выше комментарии, мы будем использовать следующий фрагмент кода.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x40b0f7 xor al, al             ; al = 0
Python>idc.get_cmt(ea, False)
al = 0
Если бы комментарий был повторяющимся, мы бы заменили idc.get_cmt(ea, False) на idc.get_cmt(ea, True). Инструкции - не единственное поле, в которое можно добавлять комментарии. К функциям также можно добавлять комментарии. Чтобы добавить комментарий к функции, мы используем idc.set_func_cmt(ea, cmt, repeatable), а чтобы получить комментарий к функции, мы вызываем idc.get_func_cmt (ea, repeatable). ea может быть любым адресом, который находится в границах начала и конца функции. cmt — это строковый комментарий, который мы хотели бы добавить, а repeatable - это логическое значение, обозначающее комментарий как повторяющийся или нет. Это представлено либо как 0 или False, если комментарий не повторяется, либо как 1 или True, чтобы комментарий был повторяемым. При наличии повторяющегося комментария функции добавляется комментарий всякий раз, когда на функцию делается перекрестная ссылка, она вызывается или просматривается в графическом интерфейсе IDA.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x401040 push ebp
Python>idc.get_func_name(ea)
sub_401040
Python>idc.set_func_cmt(ea, "check out later", 1)
True
В первых двух строках печатаем адрес, листинг и название функции. Затем мы используем idc.set_func_cmt (ea, comment, repeatable), чтобы установить повторяющийся комментарий «check out later». Если мы посмотрим на начало функции, мы увидим наш комментарий.
Код:
00401040 ; check out later
00401040 ; Attributes: bp-based frame
00401040
00401040 sub_401040 proc near
00401040 .
00401040 var_4      = dword ptr -4
00401040 arg_0      = dword ptr 8
00401040
00401040            push    ebp
00401041            mov     ebp, esp
00401043            push    ecx
00401044            push    723EB0D5h
Поскольку комментарий повторяющийся, он отображается при каждом просмотре функции. Это отличное место для добавления напоминаний или заметок о функции.
Код:
00401C07        push        ecx
00401C08        call        sub_401040      ; рассмотреть позже
00401C0D        add         esp, 4
Переименование функций и адресов — это обычно автоматизированная задача, особенно при работе с позиционно-независимым кодом (PIC), упаковщиками или функциями-оболочками. Причина, по которой это часто встречается в PIC или распакованном коде, заключается в том, что таблица импорта может отсутствовать в дампе. В случае функций оболочки, полная функция просто вызывает API.
Код:
10005B3E sub_10005B3E proc near
10005B3E
10005B3E dwBytes = dword ptr 8
10005B3E
10005B3E        push            ebp
10005B3F        mov             ebp, esp
10005B41        push            [ebp+dwBytes]   ; dwBytes
10005B44        push            8               ; dwFlags
10005B46        push            hHeap           ; hHeap
10005B4C        call            ds:HeapAlloc
10005B52        pop             ebp
10005B53        retn
10005B53        sub_10005B3E    endp
В приведенном выше коде функцию можно назвать «w_HeapAlloc». W_ - это сокращение от оболочки(wrapper). Чтобы переименовать адрес, мы можем использовать функцию idc.set_name (ea, name, SN_CHECK). ea — это адрес, а name — это строковое имя, например "w_HeapAlloc". Чтобы переименовать функцию, ea должен быть первым адресом функции. Чтобы переименовать функцию нашей оболочки HeapAlloc, мы должны использовать следующий код.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x10005b3e push ebp
Python>idc.set_name(ea, "w_HeapAlloc", SN_CHECK)
True
ea - это первый адрес в функции, а имя - "w_HeapAlloc".
Код:
10005B3E w_HeapAlloc proc near
10005B3E
10005B3E dwBytes = dword ptr 8
10005B3E
10005B3E        push        ebp
10005B3F        mov         ebp, esp
10005B41        push        [ebp+dwBytes]   ; dwBytes
10005B44        push        8               ; dwFlags
10005B46        push        hHeap           ; hHeap
10005B4C        call        ds:HeapAlloc
10005B52        pop         ebp
10005B53        retn
10005B53 w_HeapAlloc endp
Выше мы видим, что функция была переименована. Чтобы подтвердить, что она была переименована, мы можем использовать idc.get_func_name (ea) для вывода имени новой функции.
Код:
Python>idc.get_func_name(ea)
w_HeapAlloc
Чтобы переименовать операнд, нам сначала нужно получить его адрес. По адресу 0x04047B0 у нас есть двойное слово, которое мы хотим переименовать.
Код:
.text:004047AD      lea     ecx, [ecx+0]
.text:004047B0      mov     eax, dword_41400C
.text:004047B6      mov     ecx, [edi+4BCh]
Чтобы получить значение операнда, мы можем использовать idc.get_operand_value (ea, n).
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x4047b0 mov eax, dword_41400C
Python>op = idc.get_operand_value(ea, 1)
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x41400c dd 2
Python>idc.set_name(op, "BETA", SN_CHECK)
True
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x4047b0 mov eax, BETA[esi]
В первой строке получаем текущий рабочий адрес. Мы присваиваем op значение второго операнда dword_41400C, вызывая idc.get_operand_value (ea, n). Мы передаем адрес операнда в idc.set_name (ea, name, SN_CHECK), а затем выводим только что переименованный операнд.

Теперь, когда у нас есть хорошая база знаний, мы можем использовать то, что мы узнали, для автоматизации именования функций-оберток. Пожалуйста, просмотрите встроенные комментарии, чтобы понять логику.
Python:
import idautils
def rename_wrapper(name, func_addr):
    if idc.set_name(func_addr, name, SN_NOWARN):
        print("Function at 0x%x renamed %s" % (func_addr, idc.get_func_name(func)))
    else:
        print("Rename at 0x%x failed. Function %s is being used." % (func_addr,
name))
    return

def check_for_wrapper(func):
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    # пропускает библиотечные и функции-преобразователи
    if flags & FUNC_LIB or flags & FUNC_THUNK:
        return
    dism_addr = list(idautils.FuncItems(func))
    # получает длину функции
    func_length = len(dism_addr)
    # если инструкция более 32 строк, return
    if func_length > 0x20:
        return
    func_call = 0
    instr_cmp = 0
    op = None
    op_addr = None
    op_type = None
    # для каждой инструкции в функции
    for ea in dism_addr:
        m = idc.print_insn_mnem(ea)
        if m == 'call' or m == 'jmp':
            if m == 'jmp':
                temp = idc.get_operand_value(ea, 0)
                # игнорировать условия перехода в границах функции
                if temp in dism_addr:
                    continue
            func_call += 1
            # функции - обертки не должны содержать несколько вызовов функций
            if func_call == 2:
                return
            op_addr = idc.get_operand_value(ea, 0)
            op_type = idc.get_operand_type(ea, 0)
        elif m == 'cmp' or m == 'test':
            # функции-обертки не должны содержать много логических операций
            instr_cmp += 1
            if instr_cmp == 3:
                return
        else:
            continue
    # все инструкции в функции были проанализированы
    if op_addr == None:
        return
    name = idc.get_name(op_addr, ida_name.GN_VISIBLE)
    # пропустить искаженные имена функций
    if "[" in name or "$" in name or "?" in name or "@" in name or name == "":
        return
    name = "w_" + name
    if op_type == 7:
        if idc.get_func_attr(op_addr, FUNCATTR_FLAGS) & FUNC_THUNK:
            rename_wrapper(name, func)
            return
    if op_type == 2 or op_type == 6:
        rename_wrapper(name, func)
        return

for func in idautils.Functions():
    check_for_wrapper(func)
Пример вывода
Код:
Function at 0xa14040 renamed w_HeapFree
Function at 0xa14060 renamed w_HeapAlloc
Function at 0xa14300 renamed w_HeapReAlloc
Rename at 0xa14330 failed. Function w_HeapAlloc is being used.
Rename at 0xa14360 failed. Function w_HeapFree is being used.
Function at 0xa1b040 renamed w_RtlZeroMemory
Большая часть кода должна быть вам знакома. Одним из заметных отличий является использование idc.set_name (ea, name, flag) из rename_wrapper. Мы используем эту функцию, потому что idc.set_name выдает диалоговое окно с предупреждением, если имя функции уже используется. Передавая значение флага SN_NOWARN или 256, мы избегаем диалогового окна. Мы могли бы применить некоторую логику, чтобы переименовать функцию в w_HeapFree_1, но для краткости мы оставим это.

Чтобы определить, была ли функция переименована, мы можем использовать флаги адресов. Следующий код - это переименованная функция.
Код:
.text:000000014001FF90 func_example     proc near ; CODE XREF: sub_140020B52+3A↓p
.text:000000014001FF90                  ; sub_140020BEF+3A↓p ...
.text:000000014001FF90
.text:000000014001FF90 var_18           = qword ptr -18h
.text:000000014001FF90 var_10           = dword ptr -10h
.text:000000014001FF90
.text:000000014001FF90                  sub     rsp, 38h
.text:000000014001FF94                  mov     eax, cs:dword_1400268D4
.text:000000014001FF9A                  mov     r9, cs:DelayLoadFailureHook
Чтобы получить флаги, мы вызываем ida_bytes.get_flags (ea). Он принимает единственный аргумент адреса, для которого мы хотели бы получить флаги. Результат - это флаги адреса, которые затем передаются в idc.hasUserName (flags), чтобы определить, был ли адрес переименован пользователем.
Код:
Python>here()
0x14001ff90
Python>ida_bytes.get_flags(here())
0x51005600
Python>idc.hasUserName(ida_bytes.get_flags(here()))
True
 

Раскраска

Добавление немного цвета к выходным данным IDA - простой способ ускорить процесс анализа. Цвет можно использовать для визуального добавления контекста к инструкциям, блокам или сегментам. При просмотре больших функций можно легко пропустить инструкцию вызова и, следовательно, упустить функциональность. Если бы мы раскрасили все строки, содержащие инструкцию вызова, было бы намного проще быстро идентифицировать вызовы подфункции. Чтобы изменить цвета, отображаемые в IDB, мы используем функцию idc.set_color (ea, what, color). Первый аргумент, ea — это адрес. Второй аргумент - what. Он используется для обозначения того, что предполагается раскрасить. Это может быть CIC_ITEM для раскраски инструкции, CIC_FUNC для раскраски функционального блока и CIC_SEGM для раскраски сегмента. Аргумент цвета принимает целочисленное значение шестнадцатеричного цветового кода. IDA использует шестнадцатеричный формат цветового кода BGR (0xBBGGRR), а не RGB (0xRRGGBB). Последний шестнадцатеричный цветовой код более распространен из-за его использования в HTML, CSS или SVG. Чтобы раскрасить инструкцию вызова шестнадцатеричным цветовым кодом 0xDFD9F3, мы могли бы использовать следующий код.
Python:
for func in idautils.Functions():
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    # пропускает библиотечные и функции-преобразователи
    if flags & FUNC_LIB or flags & FUNC_THUNK:
        continue
    dism_addr = list(idautils.FuncItems(func))
    for ea in dism_addr:
        if idc.print_insn_mnem(ea) == "call":
            idc.set_color(ea, CIC_ITEM ,0xDFD9F3)
За исключением последней строки, весь код был описан ранее. Код перебирает все функции и все инструкции. Если инструкция содержит команду мнемонического вызова, она изменит цвет адреса. Последняя строка вызывает функцию idc.set_color с текущим адресом в качестве первого аргумента. Поскольку нас интересует только идентификация одной инструкции, мы определяем аргумент what (второй) как CIC_ITEM. Последний аргумент — это шестнадцатеричный цветовой код BGR. Если бы мы просматривали IBD, в котором выполняется наш скрипт вызова цвета, следующие строки 0x0401469 и 0x0401473 изменили бы свой цвет.
Код:
.text:00401468      push    ecx             ; int
.text:00401469      call    __setmode       ; с цветовой кодировкой
.text:0040146E      lea     edx, [esp+40B8h+var_405C]
.text:00401472      push    edx
.text:00401473      call    constants       ; с цветовой кодировкой
.text:00401478      push    esi             ; FILE *
Чтобы получить шестнадцатеричный код цвета для адреса, мы используем функцию idc.get_color (ea, what). Первый аргумент ea — это адрес. Второй аргумент — это тип элемента, для которого мы хотели бы получить цвет. Он использует те же элементы, что описаны ранее (CIC_ITEM, CIC_FUNC & CIC_SEGM). Следующий код получает шестнадцатеричный цветовой код для инструкции, функции и сегмента по адресу 0x0401469.
Код:
Python>"0x%x" % (idc.get_color(0x0401469, CIC_ITEM))
0xdfd9f3
Python>"0x%x" % (idc.get_color(0x0401469, CIC_FUNC))
0xffffffff
Python>"0x%x" % (idc.get_color(0x0401469, CIC_SEGM))
0xffffffff
Шестнадцатеричный код 0xffffffff — это цветовой код по умолчанию, используемый IDA. Если вы заинтересованы в изменении цветовых тем IDA, я бы порекомендовал проверить проект IDASkins.

Доступ к необработанным данным

Возможность доступа к необработанным данным важна при реверс-инжиниринге. Необработанные данные — это двоичное представление кода или данных. Мы можем видеть необработанные данные или байты инструкций слева после адреса.
Код:
00A14380 8B 0D 0C 6D A2 00       mov ecx, hHeap
00A14386 50                      push eax
00A14387 6A 08                   push 8
00A14389 51                      push ecx
00A1438A FF 15 30 11 A2 00       call ds:HeapAlloc
00A14390 C3                      retn
Чтобы получить доступ к данным, нам сначала нужно определиться с размером блока. Соглашение об именах API, используемых для доступа к данным, — это 1 байт. Чтобы получить доступ к байту, мы должны вызвать idc.get_wide_byte (ea) или для доступа к слову мы должны назвать idc.get_wide_word (ea) и т.д.
  • idc.get_wide_byte(ea)
  • idc.get_wide_word(ea)
  • idc.get_wide_dword(ea)
  • idc.get_qword(ea)
  • idc.GetFloat(ea)
  • idc.GetDouble(ea)
Если бы курсор находился на 0x0A14380 в листинге сверху, мы бы получили следующий результат.
Код:
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa14380 mov ecx, hHeap
Python>"0x%x" % idc.get_wide_byte(ea)
0x8b
Python>"0x%x" % idc.get_wide_word(ea)
0xd8b
Python>"0x%x" % idc.get_wide_dword(ea)
0x6d0c0d8b
Python>"0x%x" % idc.get_qword(ea)
0x6a5000a26d0c0d8bL
Python>idc.GetFloat(ea) # пример с плавающей запятой
2.70901711372e+27
Python>idc.GetDouble(ea)
1.25430839165e+204
При написании декодеров не всегда полезно получить один байт или прочитать двойное слово, но полезно прочитать блок необработанных данных. Чтобы прочитать указанный размер байтов по адресу, мы можем использовать idc.get_bytes (ea, size, use_dbg = False). Последний аргумент является необязательным и нужен только в том случае, если нам нужна память отладчика.
Код:
Python>for byte in idc.get_bytes(ea, 6):
    print("0x%X" % byte),
0x8B 0xD 0xC 0x6D 0xA2 0x0
 

Патчинг

Иногда, при реверсе вредоносного ПО, образец содержит закодированные строки. Это сделано для замедления процесса анализа и предотвращения использования программы просмотра строк для восстановления индикаторов. В подобных ситуациях полезен IDB. Мы можем переименовать адрес, но переименование ограничено. Это связано с ограничениями соглашения об именах. Чтобы исправить адрес со значением, мы можем использовать следующие функции.
  • idc.patch_byte(ea, value)
  • idc.patch_word(ea, value)
  • idc.patch_dword(ea, value)
ea - это адрес, а value - это целое число, которым мы хотели бы запатчить в IDB. Размер значения должен соответствовать размеру, заданному выбранным нами именем функции. Один из примеров закодированных строк, которые мы нашли.
Код:
.data:1001ED3C aGcquEUdg_bUfuD  db 'gcqu^E]~UDG_B[uFU^DC',0
.data:1001ED51                  align 8
.data:1001ED58 aGcqs_cuufuD     db 'gcqs\_CUuFU^D',0
.data:1001ED66                  align 4
.data:1001ED68 aWud@uubQU       db 'WUD@UUB^Q]U',0
.data:1001ED74                  align 8
В ходе нашего анализа мы смогли идентифицировать функцию декодера.
Код:
100012A0            push    esi
100012A1            mov     esi, [esp+4+_size]
100012A5            xor     eax, eax
100012A7            test    esi, esi
100012A9            jle     short _ret
100012AB            mov     dl, [esp+4+_key]    ; assign key
100012AF            mov     ecx, [esp+4+_string]
100012B3            push    ebx
100012B4
100012B4 _loop:                                 ;
100012B4            mov     bl, [eax+ecx]
100012B7            xor     bl, dl              ; data ^ key
100012B9            mov     [eax+ecx], bl       ; save off byte
100012BC            inc     eax                 ; index/count
100012BD            cmp     eax, esi
100012BF            jl s    hort _loop
100012C1            pop     ebx
100012C2
100012C2 _ret:                                  ;
100012C2            pop     esi
100012C3            retn
Функция является стандартной функцией декодера XOR с аргументами размера, ключа и расшифрованного буфера.
Код:
Python>start = idc.read_selection_start()
Python>end = idc.read_selection_end()
Python>print hex(start)
0x1001ed3c
Python>print hex(end)
0x1001ed50
Python>def xor(size, key, buff):
    for index in range(0, size):
        cur_addr = buff + index
        temp = idc.get_wide_byte(cur_addr ) ^ key
        idc.patch_byte(cur_addr, temp)
Python>
Python>xor(end - start, 0x30, start)
Python>idc.get_strlit_contents(start)
WSAEnumNetworkEvents
Мы выбираем начало и конец выделенного адреса данных с помощью idc.read_selection_start() и idc.read_selection_end(). Затем у нас есть функция, которая читает байт, вызывая idc.get_wide_byte(ea), выполняет XOR байта с ключом, переданным в функцию, а затем исправляет байт, вызывая idc.patch_byte (ea, value).

Ввод и вывод

Импорт и экспорт файлов в IDAPython могут быть полезны, когда мы не знаем путь к файлу или когда мы не знаем, где пользователь хочет сохранить свои данные. Чтобы импортировать или сохранить файл по имени, мы используем ida_kernwin.ask_file (forsave, mask, prompt). Forsave может иметь значение 0, если мы хотим открыть диалоговое окно, или 1, если мы хотим открыть диалоговое окно сохранения. mask - это расширение файла или шаблон. Если мы хотим открывать только файлы .dll, мы должны использовать маску «*.dll», а prompt — это заголовок окна. Хорошим примером ввода и вывода и выбора данных является следующий класс IO_DATA.
Python:
import sys
import idaapi

class IO_DATA():
    def __init__(self):
        self.start = idc.read_selection_start()
        self.end = idc.read_selection_end()
        self.buffer = ''
        self.ogLen = None
        self.status = True
        self.run()

    def checkBounds(self):
        if self.start is BADADDR or self.end is BADADDR:
            self.status = False

    def getData(self):
        """получить данные между началом и концом, поместить их в object.buffer"""
        self.ogLen = self.end - self.start
        self.buffer = b''
        try:
            self.buffer = idc.get_bytes(self.start, self.ogLen)
        except:
            self.status = False
        return

    def run(self):
        """basically main"""
        self.checkBounds()
        if self.status == False:
            sys.stdout.write('ERROR: Please select valid data\n')
            return
        self.getData()

    def patch(self, temp=None):
        """патч idb с данными в object.buffer"""
        if temp != None:
            self.buffer = temp
            for index, byte in enumerate(self.buffer):
                idc.patch_byte(self.start + index, ord(byte))

    def importb(self):
        '''импортировать файл для сохранения в буфер'''
        fileName = ida_kernwin.ask_file(0, "*.*", 'Import File')
        try:
            self.buffer = open(fileName, 'rb').read()
        except:
            sys.stdout.write('ERROR: Cannot access file')

    def export(self):
        '''сохранить выбранный буфер в файл'''
        exportFile = ida_kernwin.ask_file(1, "*.*", 'Export Buffer')
        f = open(exportFile, 'wb')
        f.write(self.buffer)
        f.close()

    def stats(self):
        print("start: 0x%x" % self.start)
        print("end: 0x%x" % self.end)
        print("len: 0x%x" % len(self.buffer))
С помощью этого класса можно выбрать данные для сохранения в буфер, а затем сохранить в файл. Это полезно для закодированных или зашифрованных данных в IDB. Мы можем использовать IO_DATA, чтобы выбрать данные для декодирования буфера в Python, а затем исправить IDB. Пример использования класса IO_DATA.
Код:
Python>f = IO_DATA()
Python>f.stats()
start: 0x401528
end: 0x401549
len: 0x21
Вместо того, чтобы объяснять каждую строку кода, читателю было бы полезно просмотреть функции одну за другой и посмотреть, как они работают. В нижеследующих пунктах поясняется каждая переменная и то, что делают функции. obj — это любая переменная, которую мы назначаем классу. f - объект в f = IO_DATA ().
  • obj.start
    • содержит адрес начала выбранного смещения
  • obj.end
    • содержит адрес конца выбранного смещения
  • obj.buffer
    • содержит двоичные данные
  • obj.ogLen
    • содержит размер буфера
  • obj.getData()
    • копирует двоичные данные между obj.start и obj.end в obj.buffer
  • obj.run()
    • выбранные данные копируются в буфер в двоичном формате
  • obj.patch()
    • исправляет IDB на obj.start с данными в obj.buffer
  • obj.patch(d)
    • исправляет IDB на obj.start с данными аргумента
  • obj.importb()
    • открывает файл и сохраняет данные в obj.buffer
  • obj.export()
    • экспортирует данные из obj.buffer в файл для сохранения
  • obj.stats()
    • выводит шестнадцатеричный код длины obj.start, obj.end и obj.buffer.
 

PyQt

Большая часть взаимодействия с IDAPython, описанного в этой книге, осуществляется через командную строку. В некоторых случаях может быть полезно взаимодействовать с нашим кодом с помощью графического пользовательского интерфейса, обычно называемого Формой в документации IDAPython. Графический пользовательский интерфейс IDA написан на кроссплатформенной Qt GUI. Чтобы взаимодействовать с этим фреймворком, мы можем использовать привязки Python для Qt под названием PyQt. Углубленный обзор PyQt выходит за рамки этой книги. В этой главе представлен простой скелетный фрагмент, который можно легко модифицировать и использовать для написания форм. Код создает два виджета, первый виджет создает таблицу, а второй виджет является кнопкой. При нажатии кнопки текущий адрес и мнемоника добавляются в строку таблицы. Если щелкнуть строку, IDA перейдет к адресу в строке в представлении дизассемблерного листинга. Думайте об этом коде как о простом маркере адресной книги. На приведенном ниже рисунке показана форма после добавления трех адресов в форму путем нажатия кнопки «Add address» и затем двойного щелчка по первой строке.
1.png

Не все API в следующем коде будут охвачены. Причина такой краткости связана с описательной природой имен API PyQt. Например, функция setColumnCount устанавливает количество столбцов. Если API не подходит, найдите API по имени. Qt и PyQt очень хорошо задокументированы. Как только мы поймем основы приведенного ниже кода, будет легко что-нибудь взломать. Ключевой концепцией PyQt при просмотре приведенного ниже кода является понимание того, что PyQt является объектно-ориентированной структурой.
Python:
from idaapi import PluginForm
from PyQt5 import QtCore, QtGui, QtWidgets

class MyPluginFormClass(PluginForm):
    def OnCreate(self, form):
        # Получает родительский виджет
        self.parent = self.FormToPyQtWidget(form) # IDAPython
        self.PopulateForm()

    def PopulateForm(self):
        # Создает макет
        layout = QtWidgets.QVBoxLayout()
        # Создает виджет таблицы
        self.example_row = QtWidgets.QTableWidget()
        column_names = ["Address", "Mnemonic"]
        self.example_row.setColumnCount(len(column_names))
        self.example_row.setRowCount(0)
        self.example_row.setHorizontalHeaderLabels(column_names)
        self.example_row.doubleClicked.connect(self.JumpSearch)
        layout.addWidget(self.example_row)
        # Создает кнопку
        self.addbtn = QtWidgets.QPushButton("Add Address")
        self.addbtn.clicked.connect(self.AddAddress)
        layout.addWidget(self.addbtn)
        # делает наш созданный макет макетом диалогов
        self.parent.setLayout(layout)

    def AddAddress(self):
        ea = here() # IDAPython
        index = self.example_row.rowCount()
        self.example_row.setRowCount(index + 1)
        h = "0x%x" % ea
        item = QtWidgets.QTableWidgetItem(h)
        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
        self.example_row.setItem(index, 0, item)
        self.example_row.setItem(index, 1,
QtWidgets.QTableWidgetItem(idc.print_insn_mnem(ea))) # IDAPython
        self.example_row.update()

    def JumpSearch(self, item):
        tt = self.example_row.item(item.row(), 0)
        ea = int(tt.text(), 16)
        idaapi.jumpto(ea) # IDAPython
plg = MyPluginFormClass()
plg.Show("Jump Around")
Первые две строки содержат импорт необходимых модулей. Для создания формы в idaapi создается класс, наследующий от PluginForm. В классе MyPluginFormClass есть метод OnCreate. Этот метод вызывается при создании формы плагина. OnClose противоположен и вызывается при закрытии плагина. Функция self.FormToPyQtWidget(form) создает необходимый родительский экземпляр, который используется для заполнения наших виджетов, который хранится в self.parent. В методе PopulateForm (self) происходит проектирование и создание виджетов. Три основных шага, которые важны в этом методе, - это создание экземпляра для макета (layout = QtWidgets.QVBoxLayout()), создание (self.example_row = QtWidgets.QTableWidget()) и добавление виджетов (layout.addWidget (self. example_row)), а затем установка макета (self.parent.setLayout (layout)). Остальная часть кода — это изменение виджетов или добавление действий к виджетам. Один из таких действий вызывает метод self.JumpSearch при двойном щелчке строки. Если пользователь дважды щелкает строку, он читает первую строку, а затем вызывает idaapi.jumpto(ea), чтобы перенаправить представление дизассемблированного листинга на нужный адрес. Когда макет установлен, для отображения формы используется метод Show(str) в экземпляре формы. Show(str) принимает строковый аргумент, который является заголовком формы (например, «Jump Around»).
 

Генерация пакетного файла

Иногда бывает полезно создать IDB или ASM для всех файлов в директории. Это может помочь сэкономить время при анализе набора образцов, принадлежащих к одному семейству вредоносных программ. Создать пакетный файл намного проще, чем делать это вручную на большом наборе. Чтобы выполнить пакетный анализ, нам нужно передать аргумент -B тексту idat.exe. Приведенный ниже код можно скопировать в каталог, содержащий все файлы, для которых мы хотели бы сгенерировать пакетный файл.
Python:
import os
import subprocess
import glob

paths = glob.glob("*")
ida_path = os.path.join(os.environ['PROGRAMFILES'], "IDA Pro 7.5", "idat.exe")

for file_path in paths:
    if file_path.endswith(".py"):
        continue
    subprocess.call([ida_path, "-B", file_path])
Мы используем glob.glob ("*"), чтобы получить список всех файлов в каталоге. Аргумент можно изменить, если мы хотим выбрать только определенный шаблон регулярного выражения или тип файла. Если бы мы хотели получить только файлы с расширением .exe, мы бы использовали glob.glob ("*.exe"). os.path.join(os.environ ['PROGRAMFILES'], «IDA», «idat.exe») используется для получения пути к idat.exe. Некоторые версии IDA имеют имя папки с номером версии. В этом случае аргумент «IDA» необходимо изменить на имя папки. Кроме того, может потребоваться изменить всю команду, если мы решим использовать нестандартное место установки для IDA. А пока предположим, что путь установки IDA - C:\ProgramFiles\IDA. После того, как мы нашли путь, мы перебираем все файлы в каталоге, которые не содержат расширения .py, а затем передаем их в IDA. Для отдельного файла это будет выглядеть так: C:\Program Files\IDA\idat.exe -B bad_file.exe. После запуска он сгенерирует ASM и IDB для файла. Все файлы будут записаны в рабочий каталог. Пример вывода можно увидеть ниже.
Код:
C:\injected>dir

0?/**/____ 09:30 AM <DIR> .
0?/**/____ 09:30 AM <DIR> ..
0?/**/____ 10:48 AM 167,936 bad_file.exe
0?/**/____ 09:29 AM 270 batch_analysis.py
0?/**/____ 06:55 PM 104,889 injected.dll

C:\injected>python batch_analysis.py

Thank you for using IDA. Have a nice day!

C:\injected>dir

0?/**/____ 09:30 AM <DIR> .
0?/**/____ 09:30 AM <DIR> ..
0?/**/____ 09:30 AM 506,142 bad_file.asm
0?/**/____ 10:48 AM 167,936 bad_file.exe
0?/**/____ 09:30 AM 1,884,601 bad_file.idb
0?/**/____ 09:29 AM 270 batch_analysis.py
0?/**/____ 09:30 AM 682,602 injected.asm
0?/**/____ 06:55 PM 104,889 injected.dll
0?/**/____ 09:30 AM 1,384,765 injected.idb
bad_file.asm, bad_file.idb, injected.asm и injected.idb являются сгенерированными файлами.

Выполнение скриптов

Скрипты IDAPython можно запускать из командной строки. Мы можем использовать следующий код для подсчета каждой инструкции в IDB, а затем записать ее в файл с именем Instru_count.txt.
Python:
import idc
import idaapi
import idautils

idaapi.auto_wait()

count = 0
for func in idautils.Functions():
    # Игнорирует библиотечный код
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    if flags & FUNC_LIB:
        continue
    for instru in idautils.FuncItems(func):
        count += 1
f = open("instru_count.txt", 'w')
print_me = "Instruction Count is %d" % (count)
f.write(print_me)
f.close()
idc.qexit(0)
С точки зрения командной строки две наиболее важные функции - это idaapi.auto_wait() и idc.qexit(0). Когда IDA открывает файл, важно дождаться завершения анализа. Это позволяет IDA заполнять все функции, структуры или другие значения, основанные на механизме анализа IDA. Чтобы дождаться завершения анализа, мы вызываем idaapi.auto_wait(). Он будет ждать/останавливаться, пока IDA не завершит свой анализ. После завершения анализа, управление возвращается скрипту. Важно сделать это в начале скрипта, прежде чем мы вызовем какие-либо функции IDAPython, которые полагаются на завершение анализа. После выполнения нашего скрипта, нам нужно вызвать idc.qexit(0). Это останавливает выполнение нашего скрипта, закрывает базу данных и возвращается к вызывающему скрипту. В противном случае, наш IDB не закроется должным образом.

Если бы мы хотели запустить IDAPython для подсчета всех строк, в IDB мы бы выполнили следующую команду в командной строке:
"C:\Program Files\IDA Pro 7.5\ida.exe" -Scount.py example.idb
-S сигнализирует IDA запустить скрипт на IDB после его открытия. В рабочем каталоге мы увидим файл с именем Instru_count.txt, содержащий счетчик всех инструкций. Если бы мы хотели выполнить наш скрипт для исполняемого файла, нам нужно было бы, чтобы IDA работала в автономном режиме, передав -A.
"C:\Program Files\IDA Pro 7.5\ida.exe" -A -Scount.py example.exe
 

Yara

Yara - это программное обеспечение и библиотека для сопоставления шаблонов на основе правил, которые можно использовать для поиска файлов. Он был написан и поддерживается Виктором М. Альваресом. Правила Yara определяются с использованием шаблонов, основанных на строках («foo»), байтах ({66 6f 6f}), размерах файлов (размер файла < 37) или других условных атрибутах файла. Благодаря своим мощным и гибким правилам, Yara по праву называют «швейцарским ножом для поиска образцов для исследователей вредоносных программ». С точки зрения IDAPython, Yara - отличная библиотека, которую можно добавить в свой инструментарий по нескольким причинам. Во-первых, Yara значительно быстрее поиска IDAPython, его правила можно использовать для автоматизации процесса анализа, и существует множество общедоступных сигнатур Yara. Один из моих любимых примеров поиска для автоматизации процесса анализа — это поиск констант, используемых криптографическими функциями. Путем поиска байтовых шаблонов мы можем сделать перекрестную ссылку на совпадение и сделать вывод, что функция, ссылающаяся на байты, связана с криптографическим алгоритмом. Например, поиск константы 0x67452301 может использоваться для поиска функций, связанных с алгоритмами хеширования MD4, MD5 и SHA1.

Первый шаг в процессе использования Yara — это создание правила. Правила Yara следуют простому синтаксису, подобному языку C. Правило состоит из его имени, шаблона соответствия (также известного как определение строк в документации Yara) и условия. Приведенный ниже текст представляет собой простое правило Yara. Это не практическое правило Yara, но оно полезно для демонстрации синтаксиса Yara-правил.
Код:
/*
        Example Yara Rule
*/
rule pe_md5_constant
{
    strings:
        $pe_header = "MZ"
        $hex_constant = { 01 23 45 67 } // byte pattern
    condition:
        $pe_header at 0 and $hex_constant
}
Первая пара строк — это многострочный комментарий. Как и в случае с C и другими языками, комментарий начинается с /* и заканчивается */. Правила Yara имеют синтаксис, аналогичный синтаксису структур в C. Правило Yara начинается с ключевого слова rule, за которым следует имя правила (также называемое идентификатором правила). После определения имени правила - открытая фигурная скобка {. За открывающей фигурной скобкой следует определение строки, которое начинается с ключевого слова strings, за которым следует двоеточие. Определение строк используется для определения правила, которому соответствует Yara. Каждая строка имеет идентификатор, который начинается с символа $, за которым следуют символы и цифры, составляющие имя определения строки. Определение строки может состоять из символов (например, MZ) или шестнадцатеричных чисел (например, {01 23 45 67}). После определения строки идет условие, которому соответствует правило Yara. Условия начинаются с ключевого слова condition, за которым следует двоеточие. В приведенном выше примере правила, условие, которое ищется, является верным, если определения строки $ pe_header расположены по смещению 0 и файл содержит шаблон байтов, которые определены в $ hex_constant, тогда Yara имеет совпадение. Поскольку для $ hex_constant не было определено смещение, для совпадения байтовый шаблон должен присутствовать в любом месте файла. Yara поддерживает широкий спектр ключевых слов, которые можно использовать для определения различных символов, точки входа, размера и других условий. Рекомендуется прочитать правила написания Yara в документации Yara, чтобы узнать обо всех различных ключевых словах, поддерживаемых ими параметрах и различных способах сканирования или сопоставления файлов.

Интерфейс python для Yara можно легко установить с помощью pip, выполнив команду pip install yara-python. Следующие шаги необходимы для сканирования файла с помощью Yara в Python.

1. Yara нужно импортировать
  • import yara
2. Yara необходимо скомпилировать правило Yara с помощью yara.compile
  • rules = yara.compile(source=signature)
3. Откройте файл или сохраните данные в буфере, чтобы Yara могла сопоставить их
  • data = open(scan_me, "rb").read()
4. Отсканируйте файл, используя скомпилированное правило Yara, используя yara.match
  • matches = rules.match(data=self.mem_results)
5. Получите совпадения или примените логику на основе совпадений

Это, конечно, упрощенное описание необходимых шагов. Yara содержит несколько методов и настроек, которые можно использовать для расширенных параметров сканирования. Примерами этих функций являются обратные вызовы функций, сканирование запущенных процессов и тайм-ауты для файлов большего размера. Полный список этих методов и конфигураций смотрите в документации Yara. В контексте использования Yara в IDA, те же шаги необходимы для сканирования двоичных данных в IDB. За исключением того, что требуется один дополнительный шаг - преобразовать смещение файла соответствия Yara в исполняемый виртуальный адрес, для чего IDA ссылается на адреса. Если переносимый исполняемый файл сканируется с помощью Yara и соответствует шаблону со смещением файла 0x1000, это может быть представлено как виртуальный адрес 0x0401000 в IDA. Следующий код — это класс, который считывает двоичные данные из IDB, а затем сканирует данные с помощью Yara.
Python:
import yara
import idautils

SEARCH_CASE = 4
SEARCH_REGEX = 8
SEARCH_NOBRK = 16
SEARCH_NOSHOW = 32
SEARCH_UNICODE = 64
SEARCH_IDENT = 128
SEARCH_BRK = 256

class YaraIDASearch:
    def __init__(self):
        self.mem_results = ""
        self.mem_offsets = []
        if not self.mem_results:
            self._get_memory()

    def _get_memory(self):
        print("Status: Loading memory for Yara.")
        result = b""
        segments_starts = [ea for ea in idautils.Segments()]
        offsets = []
        start_len = 0
        for start in segments_starts:
            end = idc.get_segm_end(start)
            result += idc.get_bytes(start, end - start)
            offsets.append((start, start_len, len(result)))
            start_len = len(result)
        print("Status: Memory has been loaded.")
        self.mem_results = result
        self.mem_offsets = offsets

    def _to_virtual_address(self, offset, segments):
        va_offset = 0
        for seg in segments:
            if seg[1] <= offset < seg[2]:
                va_offset = seg[0] + (offset - seg[1])
        return va_offset

    def _init_sig(self, sig_type, pattern, sflag):
        if SEARCH_REGEX & sflag:
            signature = "/%s/" % pattern
            if SEARCH_CASE & sflag:
                # ida по умолчанию не чувствительна к регистру, но yara чувствительна
                pass
            else:
                signature += " nocase"
            if SEARCH_UNICODE & sflag:
                signature += " wide"
        elif sig_type == "binary":
            signature = "{ %s }" % pattern
        elif sig_type == "text" and (SEARCH_REGEX & sflag) == False:
            signature = '"%s"' % pattern
            if SEARCH_CASE & sflag:
                pass
            else:
                signature += " nocase"
            signature += " wide ascii"
        yara_rule = "rule foo : bar { strings: $a = %s condition: $a }" % signature
        return yara_rule

    def _compile_rule(self, signature):
        try:
            rules = yara.compile(source=signature)
        except Exception as e:
            print("ERROR: Cannot compile Yara rule %s" % e)
            return False, None
        return True, rules

    def _search(self, signature):
        status, rules = self._compile_rule(signature)
        if not status:
            return False, None
        values = []
        matches = rules.match(data=self.mem_results)
        if not matches:
            return False, None
        for rule_match in matches:
            for match in rule_match.strings:
                match_offset = match[0]
                values.append(self._to_virtual_address(match_offset, self.mem_offsets))
        return values

    def find_binary(self, bin_str, sflag=0):
        yara_sig = self._init_sig("binary", bin_str, sflag)
        offset_matches = self._search(yara_sig)
        return offset_matches

    def find_text(self, q_str, sflag=0):
        yara_sig = self._init_sig("text", q_str, sflag)
        offset_matches = self._search(yara_sig)
        return offset_matches

    def find_sig(self, yara_rule):
        offset_matches = self._search(yara_rule)
        return offset_matches

    def reload_scan_memory(self):
        self._get_memory()
Все API с предыдущего кода были рассмотрены ранее. Функция _to_virtual_address может использоваться для преобразования совпадения смещения файла Yara в адрес IDA внутри правильного адреса. Ниже приведен пример создания экземпляра YaraIDASearch (), сканирующего IDB с помощью сигнатуры Yara и возвращающего смещение, по которому соответствует правило. Следует отметить, что это правило было изменено по сравнению с предыдущим правилом. IDA не всегда загружает MZ-заголовок исполняемого файла как сегмент.
Код:
Python>ys = YaraIDASearch()
Status: Loading memory for Yara.
Status: Memory has been loaded.
Python>example_rule = """rule md5_constant
{
    strings:
        $hex_constant = { 01 23 45 67 } // byte pattern
    condition:
        $hex_constant
}"""
Python>
Python>ys.find_sig(example_rule)
[4199976L]
Первая строка создает экземпляр YaraIDASearch и назначает его ys. Правило Yara сохраняется в виде строки и присваивается переменной example_rule. Правило передается как аргумент методу ys.find_sig (yara_rule). Метод поиска возвращает список всех смещений, соответствующих правилу Yara. Если бы мы хотели найти двоичный шаблон, мы могли бы использовать ys.find_binary(bytes). Поиск ys.find_binary (01 23 45 67) вернет те же результаты, что и пользовательское правило Yara. YaraIDASearch также поддерживает поиск строк с помощью ys.find_text (string).
 

Unicorn Engine

Unicorn Engine — это облегченный кроссплатформенный мультиархитектурный фреймворк эмулятора ЦП, построенная на основе модифицированной версии Qemu. Unicorn написан на C, но содержит привязки для многих языков, включая Python. Unicorn — это мощный инструмент, который может помочь в процессе обратного проектирования, поскольку он, по сути, позволяет эмулировать код в настраиваемом, управляемом и специфичном состоянии. В последнем, специфическом состоянии, в игру вступает Unicorn Engine. Выполнение кода и получение конкретных результатов без полного понимания ассемблерного кода может сэкономить время и помочь в автоматизации процесса анализа. Например, использование Unicorn для выполнения сотен встроенных подпрограмм дешифрования строк с различными алгоритмами битового сдвига и / или ключами XOR, а затем запись дешифрованного ключа в виде строки в качестве комментария - лишь одно из таких его применений.

Чтобы получить хорошее представление об использовании Unicorn Engine, полезно рассмотреть некоторые основные концепции и способы их реализации с помощью Unicorn API.

Инициализация экземпляра Unicorn

Для инициализации класса Unicorn используется API Uc (UC_ARCH, UC_MODE). Uc определяет особенности эмуляции кода. Например, должны ли двоичные данные выполняться как MIPS-32 или как X86-64. Первый аргумент — это тип архитектуры оборудования. Второй аргумент — это тип аппаратного режима и / или порядок байтов. Ниже перечислены поддерживаемые в настоящее время типы архитектуры.
  • UC_ARCH_ARM
    • Архитектура ARM (включая Thumb, Thumb-2)
  • UC_ARCH_ARM64
    • ARM-64, также называемую AArch64
  • UC_ARCH_MIPS
    • Архитектура MIPS
  • UC_ARCH_X86
    • Архитектура x86 (включая x86-64)
  • UC_ARCH_PPC
    • Архитектура PowerPC (в настоящее время не поддерживается)
  • UC_ARCH_SPARC
    • Архитектура Sparc
  • UC_ARCH_M68K
    • Архитектура M68K
Ниже перечислены доступные типы оборудования. Комментарии с сайта unicorn.h.
Порядок байтов
  • UC_MODE_LITTLE_ENDIAN
    • режим little-endian (по умолчанию)
  • UC_MODE_BIG_ENDIAN
    • режим big-endian
Arm
  • UC_MODE_ARM
    • режим ARM
  • UC_MODE_THUMB
    • режим Thumb (включая Thumb-2)
Mips
  • UC_MODE_MIPS32
    • Mips32 ISA
  • UC_MODE_MIPS64
    • Mips64 ISA
x86 / x64
  • UC_MODE_16
    • 16-битный режим
  • UC_MODE_32
    • 32-битный режим
  • UC_MODE_64
    • 64-битный режим
Sparc
  • UC_MODE_SPARC32
    • 32-битный режим
  • UC_MODE_SPARC64
    • 64-битный режим
Существует множество различных комбинаций архитектур и типов оборудования. Каталог привязок Unicorn Engine Python содержит несколько примеров скриптов. Во всех примерах есть шаблонный образец _.*. py.

Чтение и запись в память

Прежде чем память сможет выполнять чтение или запись, необходимо отобразить память. Для отображения памяти используются API-интерфейсы uc.mem_map (address, size, perms = uc.UC_PROT_ALL) и uc.mem_map_ptr (address, size, perms, ptr). Доступны следующие средства защиты памяти.
  • UC_PROT_NONE
  • UC_PROT_READ
  • UC_PROT_WRITE
  • UC_PROT_EXEC
  • UC_PROT_ALL
Для защиты диапазона памяти используется API uc.mem_protect (address, size, perms = uc.UC_PROT_ALL). Для отмены отображения памяти используется API uc.mem_unmap (address, size). После отображения памяти в нее можно производить запись, вызывая uc.mem_write (address, data). Для чтения из выделенной памяти используется uc.mem_read (address, size).

Чтение и запись регистров

Регистры можно прочитать, вызвав uc.reg_read (reg_id, opt = None). reg_id определяется в соответствующем файле константы архитектуры Python в каталоге привязок Python.
  • ARM-64 в arm64_const.py
  • ARM в arm_const.py
  • M68K в m68k_const.py
  • MIPS в mips_const.py
  • SPARC в sparc_const.py
  • X86 в x86_const.py
Чтобы ссылаться на константы, они должны быть сначала импортированы. Константы импортируются путем вызова from unicorn.x86_const import * для x86. Для записи содержимого регистра используется uc.reg_write (reg_id, value).

Запуск и остановка эмуляции

Чтобы запустить Unicorn Engine, эмулирующий API, вызывается uc.emu_start (begin, until, timeout = 0, count = 0). Первый запуск функции — это первый эмулируемый адрес. Второй аргумент until — это адрес, по которому Unicorn Engine прекращает эмуляцию. Аргумент timeout используется для определения количества миллисекунд, которое Unicorn Engine выполняется до истечения времени ожидания. UC_SECOND_SCALE * n может использоваться для ожидания n секунд. Последний аргумент count может использоваться для определения количества инструкций, которые выполнятся до того, как Unicorn Engine прекратит выполнение. Если count = 0 или меньше, Unicorn Engine отключен. Чтобы остановить эмуляцию API, используется uc.emu_stop ().

Управление памятью и хуками с помощью определяемых пользователем обратных вызовов

Unicorn Engine поддерживает большое количество хуков (перехват). Ниже описывается подмножество этих перехватов. Перехватчики вводятся перед вызовом для запуска эмуляции. Чтобы добавить перехват используется API uc.hook_add (UC_HOOK_ *, callback, user_data, begin, end, ...). Первые два аргумента обязательны. Последние три являются необязательными и обычно заполняются значениями по умолчанию None, 1, 0, UC_INS. Для удаления перехватчика используется API emu.hook_del (hook). Чтобы удалить прехватчик, ее необходимо присвоить переменной. Например, в следующем фрагменте показано, как удалить хук.
Код:
i = emu.hook_add(UC_HOOK_CODE, hook_code, None)
emu.hook_del(i)
Хуки и соответствующие им обратные вызовы позволяют инструментировать эмулируемый код. В этих обратных вызовах мы можем применить логику для анализа, изменить код или просто вывести значения. Эти обратные вызовы чрезвычайно полезны при отладке ошибок или обеспечении правильных значений инициализации. Некоторые из приведенных ниже примеров взяты из репозитория Unicorn Engine.

UC_HOOK_INTR
UC_HOOK_INTR используется для перехвата всех событий прерывания и системных вызовов. Первым аргументом обратного вызова hook_intr является экземпляр Unicorn. Экземпляр можно использовать для вызова ранее описанного Unicorn API. Второй аргумент intno — это номер прерывания. Третий аргумент user_data — это переменная, которую можно передать от ловушки к обратному вызову. В следующем примере выводится номер прерывания (если он не равен 0x80), и эмуляция останавливается путем вызова uc.emu_stop ().
Python:
def hook_intr(uc, intno, user_data):
    # обрабатывает только системные вызовы Linux
    if intno != 0x80:
        print("got interrupt %x ???" %intno);
        uc.emu_stop()
        return
uc.hook_add(UC_HOOK_INTR, hook_intr)

UC_HOOK_INSN
UC_HOOK_INSN добавляет перехватчик, когда выполняются инструкции x86 IN, OUT или SYSCALL. Следующий фрагмент добавляет UC_HOOK_INSN и вызывает функцию обратного вызова hook_syscall всякий раз, когда выполняется UC_X86_INS_SYSCALL. Обратный вызов считывает регистр RAX, если RAX равен 0x100, он меняется на 0x200, и Unicorn Engine продолжает эмулировать код.
Python:
def hook_syscall(uc, user_data):
    rax = uc.reg_read(UC_X86_REG_RAX)
    if rax == 0x100:
        uc.reg_write(UC_X86_REG_RAX, 0x200)
uc.hook_add(UC_HOOK_INSN, hook_syscall, None,1, 0, UC_X86_INS_SYSCALL)

UC_HOOK_CODE
UC_HOOK_CODE может перехватить диапазон кода. Перехватчик вызывается перед выполнением каждой инструкции. Код обратного вызова hook_code содержит четыре аргумента. Следующий фрагмент кода реализует перехватчик UC_HOOK_CODE и выводит эмулируемый адрес и размер. Первый аргумент uc — это экземпляр Unicorn, address — это адрес кода, который должен быть выполнен, size — это размер эмулируемой инструкции, а user_data уже рассматривалась ранее.
Код:
def hook_code(uc, address, size, user_data):
    print("Tracing instruction at 0x%x, instruction size = 0x%x" %(address, size))
uc.hook_add(UC_HOOK_CODE, hook_code)

UC_HOOK_BLOCK
UC_HOOK_BLOCK - это перехватчик, который может реализовать обратный вызов для отслеживания базовых блоков. Аргументы такие же, как описано в UC_HOOK_CODE.
Код:
def hook_block(uc, address, size, user_data):
    print("Tracing basic block at 0x%x, block size = 0x%x" %(address, size)
uc.hook_add(UC_HOOK_BLOCK, hook_block)

UC_HOOK_MEM_ *
Unicorn Engine имеет несколько хуков специально для чтения, выборки, записи и доступа к памяти. Все они начинаются с UC_HOOK_MEM_ *. Все их обратные вызовы имеют такие же аргументы, как показано ниже.
Python:
def hook_mem_example(uc, access, address, size, value, user_data):
    pass
Первый аргумент — это экземпляр Unicorn, а второй аргумент — это доступ. Их значения можно увидеть ниже:
Код:
UC_MEM_READ = 16
UC_MEM_WRITE = 17
UC_MEM_FETCH = 18
UC_MEM_READ_UNMAPPED = 19
UC_MEM_WRITE_UNMAPPED = 20
UC_MEM_FETCH_UNMAPPED = 21
UC_MEM_WRITE_PROT = 22
UC_MEM_READ_PROT = 23
UC_MEM_FETCH_PROT = 24
UC_MEM_READ_AFTER = 25

UC_HOOK_MEM_INVALID
Пример кода UC_HOOK_MEM_INVALID содержит пример сравнения ошибки доступа. Обратный вызов hook_mem_invalid выполняется, когда происходит недопустимый доступ к памяти.
Python:
def hook_mem_invalid(uc, access, address, size, value, user_data):
    eip = uc.reg_read(UC_X86_REG_EIP)
    if access == UC_MEM_WRITE:
        print("invalid WRITE of 0x%x at 0x%X, data size = %u, data value = 0x%x" % (address, eip, size, value))
    if access == UC_MEM_READ:
        print("invalid READ of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    if access == UC_MEM_FETCH:
        print("UC_MEM_FETCH of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    if access == UC_MEM_READ_UNMAPPED:
        print("UC_MEM_READ_UNMAPPED of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    if access == UC_MEM_WRITE_UNMAPPED:
        print("UC_MEM_WRITE_UNMAPPED of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    if access == UC_MEM_FETCH_UNMAPPED:
        print("UC_MEM_FETCH_UNMAPPED of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    if access == UC_MEM_WRITE_PROT:
        print("UC_MEM_WRITE_PROT of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    if access == UC_MEM_FETCH_PROT:
        print("UC_MEM_FETCH_PROT of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    if access == UC_MEM_FETCH_PROT:
        print("UC_MEM_FETCH_PROT of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    if access == UC_MEM_READ_AFTER:
        print("UC_MEM_READ_AFTER of 0x%x at 0x%X, data size = %u" % (address, eip, size))
    return False
uc.hook_add(UC_HOOK_MEM_INVALID, hook_mem_invalid)

UC_HOOK_MEM_READ_UNMAPPED
UC_HOOK_MEM_READ_UNMAPPED - это перехватчик, который выполняет обратный вызов, когда эмулируемый код пытается прочитать неотображенную память. Следующий фрагмент один из примеров.
Python:
def hook_mem_read_unmapped(uc, access, address, size, value, user_data):
    pass
uc.hook_add(UC_HOOK_MEM_READ_UNMAPPED, hook_mem_read_unmapped, None)
Ниже приводится список других перехватчиков памяти с минимальным описанием. Отрывки из предыдущих примеров можно изменить, чтобы использовать указанные ниже хуки.
  • UC_HOOK_MEM_WRITE_UNMAPPED
    • Выполняет обратный вызов при возникновении недопустимого события записи в память
  • UC_HOOK_MEM_FETCH_UNMAPPED
    • Выполняет обратный вызов при недопустимой выборке из памяти для события выполнения
  • UC_HOOK_MEM_READ_PROT
    • Выполняет обратный вызов, когда происходит чтение памяти из защищенной от чтения памяти области
  • UC_HOOK_MEM_WRITE_PROT
    • Выполняет обратный вызов, когда происходит запись в защищенную от записи область памяти
  • UC_HOOK_MEM_FETCH_PROT
    • Выполняет обратный вызов, когда происходит выборка из неисполняемой памяти
  • UC_HOOK_MEM_READ
    • Выполняет обратный вызов, когда происходит событие чтения памяти
  • UC_HOOK_MEM_WRITE
    • Выполняет обратный вызов при событиях записи в память
  • UC_HOOK_MEM_FETCH
    • Выполняет обратный вызов, когда происходит выборка памяти для события выполнения
  • UC_HOOK_MEM_READ_AFTER
    • Выполняет обратный вызов, когда происходит успешное событие чтения памяти.
Теперь, когда мы понимаем, как работает Unicorn Engine, мы используем его в контексте IDA. Дизассемблерный листинг ниже выделяет память путем вызова malloc, копирует смещение зашифрованной строки, затем выполняет операцию XOR для каждого байта строки с ключом и сохраняет результаты в выделенной памяти.
Код:
.text:00401034      push    esi
.text:00401035      push    edi
.text:00401036      push    0Ah                         ; Size
.text:00401038      call    ds:malloc
.text:0040103E      mov     esi, eax
.text:00401040      mov     edi, offset str_encrypted
.text:00401045      xor     eax, eax                    ; eax = 0
.text:00401047      sub     edi, esi
.text:00401049      pop     ecx
.text:0040104A
.text:0040104A loop:                                    ; CODE XREF: _main+28↓j
.text:0040104A      lea     edx, [eax+esi]
.text:0040104D      mov     cl, [edi+edx]
.text:00401050      xor     cl, ds:b_key
.text:00401056      inc     eax
.text:00401057      mov     [edx], cl
.text:00401059      cmp     eax, 9                      ; index
.text:0040105C      jb      short loop
.text:0040105E      push    esi
Приведенный выше код прост, но содержит несколько нюансов, которые необходимо учитывать при эмуляции кода. Первая проблема связана с вызовом malloc по смещению 0x0401038. Unicorn Engine эмулирует инструкции, как если бы он был процессором, но не как если бы он был эмулятором операционной системы. Он не работает так, как загрузчик Windows. Он не инициализирует память для исполняемого файла, чтобы он мог выполняться. Отображение памяти, загрузка библиотек динамической компоновки или заполнение таблицы импорта не обрабатываются Unicorn Engine. Он может выполнять независимый от позиции код, если он самодостаточен и, следовательно, не полагается на структуры памяти, заполненные операционной системой (например, блок среды процесса). Если это атрибуты, которые необходимы для успешного выполнения, то эти атрибуты необходимо вручную создать и сопоставить или вручную обработать с помощью перехватчиков и обратного вызова. Вторая проблема связана со смещением 0x0401040 с перемещением смещения зашифрованной строки. Смещение строки соответствует виртуальному смещению 0x0402108. Если код в исполняемом файле обрабатывался как необработанные данные без сопоставлений памяти, то выполненное смещение было бы 0x440, но попытка чтения виртуального адреса вернула бы неверное чтение памяти, потому что память не была сопоставлена. Исполняемый файл или исполняемые данные в IDB IDA должны быть отображены на правильный адрес. Последняя проблема заключается в выполнении только цикла XOR и игнорировании другого кода и исключений. В следующем коде предполагается, что пользователь выделил ассемблерный код сверху от 0x401034 до 0x40105e.
Python:
from unicorn import *
from unicorn.x86_const import *
import idautils
import math

VIRT_MEM = 0x4000

def roundup(x):
    return int(math.ceil(x / 1024.0)) * 1024

def hook_mem_invalid(uc, access, address, size, value, user_data):
    if uc._arch == UC_ARCH_X86:
        eip = uc.reg_read(UC_X86_REG_EIP)
    else:
        eip = uc.reg_read(UC_X86_REG_RIP)
    bb = uc.mem_read(eip, 2)
    if bb != b"\xFF\x15":
        return
    if idc.get_name(address) == "malloc":
        uc.mem_map(VIRT_MEM, 8 * 1024)
    if uc._arch == UC_ARCH_X86:
        uc.reg_write(UC_X86_REG_EAX, VIRT_MEM)
        cur_addr = uc.reg_read(UC_X86_REG_EIP)
        uc.reg_write(UC_X86_REG_EIP, cur_addr + 6)
    else:
        cur_addr = uc.reg_read(UC_X86_REG_RIP)
        uc.reg_write(UC_X86_REG_RIP, cur_addr + 6)

def hook_code(uc, address, size, user_data):
    """Только для отладки"""
    print('Tracing instruction at 0x%x, instruction size = 0x%x' % (address, size))

def emulate():
    try:
        # получить начальный и конечный адрес сегмента
        segments = []
        for seg in idautils.Segments():
            segments.append((idc.get_segm_start(seg), idc.get_segm_end(seg)))

        # получить базовый адрес
        BASE_ADDRESS = idaapi.get_imagebase()

        # получить бит
        info = idaapi.get_inf_structure()
        if info.is_64bit():
            mu = Uc(UC_ARCH_X86, UC_MODE_64)
        elif info.is_32bit():
            mu = Uc(UC_ARCH_X86, UC_MODE_32)

        # выделяется 8MB памяти для этой эмуляции
        mu.mem_map(BASE_ADDRESS - 0x1000, 8 * 1024 * 1024)

        # записать сегменты в память
        for seg in segments:
            temp_seg = idc.get_bytes(seg[0], seg[1] - seg[0])
            mu.mem_write(seg[0], temp_seg)

        # инициализация стека
        stack_size = 1024 * 1024
        if info.is_64bit():
            stack_base = roundup(seg[1])
            mu.reg_write(UC_X86_REG_RSP, stack_base + stack_size - 0x1000)
            mu.reg_write(UC_X86_REG_RBP, stack_base + stack_size)
        elif info.is_32bit():
            stack_base = roundup(seg[1])
            mu.reg_write(UC_X86_REG_ESP, stack_base + stack_size - 0x1000)
            mu.reg_write(UC_X86_REG_EBP, stack_base + stack_size)

        # записать нулевые байты в стек
        mu.mem_write(stack_base, b"\x00" * stack_size)

        # получить выбранный диапазон адресов
        start = idc.read_selection_start()
        end = idc.read_selection_end()
        if start == idc.BADADDR:
            return

        # добавить перехватчик
        mu.hook_add(UC_HOOK_MEM_READ, hook_mem_invalid)
        mu.hook_add(UC_HOOK_CODE, hook_code)

        mu.emu_start(start, end)
        decoded = mu.mem_read(VIRT_MEM, 0x0A)
        print(decoded)

    except UcError as e:
        print("ERROR: %s" % e)
        return None
    return mu

emulate()
Начиная с функции эмуляции, он выполняет итерацию по всем сегментам в IDB, вызывает idautils.Segments() и извлекает начальный адрес сегментов, вызывает idc.get_segm_start (seg), а затем получает конечный адрес с помощью idc.get_segm_end (seg). Базовый адрес получается путем вызова idaapi.get_imagebase(). После получения базового адреса мы вызываем idaapi.get_inf_structure(), чтобы получить экземпляр структуры idainfo. Используя структуру idainfo, хранящуюся в info, мы вызываем info.is_64bit() или info.is_32bit(), чтобы определить бит IDB. Поскольку это 32-битный исполняемый файл, вызывается Uc (UC_ARCH_X86, UC_MODE_32). Это настраивает экземпляр Unicorn для выполнения кода как X86 в 32-битном режиме и сохраняет экземпляр в переменной mu. 8 МБ памяти выделяются по базовому адресу минус 1000 путем вызова mu.mem_map (BASE_ADDRESS - 0x1000, 8 * 1024 * 1024). После выделения памяти в нее можно записывать данные. При попытке записи, чтения или доступа к памяти, которая не отображается, произойдет ошибка. Данные сегмента записываются в соответствующие смещения памяти. После записи сегментов, выделяется стековая память. Регистры базового указателя и указателя стека записываются путем вызова mu.reg_write (UC_X86_REG_ESP, stack_base stack_size - 0x1000) и mu.reg_write (UC_X86_REG_EBP, stack_base stack_size). Первый аргумент — это регистрационный идентификатор (например, UC_X86_REG_ESP), а второе значение — это значение, которое должно быть записано в регистр. Стек инициализируется нулевыми байтами («\0x00»), и выбранные адреса извлекаются. Перехватчик UC_HOOK_MEM_READ с обратным вызовом hook_mem_invalid и перехватчик UC_HOOK_CODE с обратным вызовом hook_code. Код эмулируется путем вызова mu.emu_start (start, end), где start и end заполняются выбранными смещениями. После завершения эмуляции он считывает строку, обработанную XOR, и печатает ее.

Первым срабатывающим перехватчиком является UC_HOOK_MEM_READ, который срабатывает, когда Unicorn пытается прочитать адрес, на который должен быть отображен malloc. Как только перехватчик срабатывает, выполняется функция обратного вызова hook_mem_invalid. В этом обратном вызове мы пишем свой собственный malloc, выделяя память, записывая смещение в EAX или RAX и затем возвращаясь. Чтобы определить, следует ли записывать EAX или RAX, мы можем получить архитектуру, хранящуюся в экземпляре Unicorn, сравнив uc._arch с UC_ARCH_X86. Другой вариант - передать результаты info.is_32bit() в качестве необязательного аргумента в user_data. EIP читается путем вызова uc.reg_read (reg_id) с аргументом UC_X86_REG_EIP. Затем мы читаем два байта, вызывая uc.mem_read (int, size), причем первый аргумент — это смещение, хранящееся в EIP, а второй аргумент - размер данных для чтения. Два байта сравниваются с "\xFF\x15", чтобы убедиться, что исключение произошло в инструкции вызова. Если используется инструкция вызова, имя адреса извлекается с помощью idc.get_name (ea) и проверяется, что имя адреса API - malloc. Если malloc, память выделяется с помощью uc.mem_map (адрес, размер), а затем мы записываем адрес в регистр EAX с помощью uc.reg_write (UC_X86_REG_EAX, VIRT_MEM). Чтобы обойти исключение памяти, нам нужно установить EIP на адрес вызова malloc (0x40103E). Для записи в EIP мы используем uc.reg_write (reg_id, value) со значением, являющимся адресом EIP+6. Второй перехватчик UC_HOOK_CODE с обратным вызовом hook_code выводит текущий адрес, который эмулируется, и его размер. Ниже представлен результат эмуляции Unicorn, запущенной в IDA. Последняя строка содержит расшифрованную строку.
Код:
Tracing instruction at 0x401034, instruction size = 0x1
Tracing instruction at 0x401035, instruction size = 0x1
Tracing instruction at 0x401036, instruction size = 0x2
..removed..
Tracing instruction at 0x401059, instruction size = 0x3
Tracing instruction at 0x40105c, instruction size = 0x2
Tracing instruction at 0x40105e, instruction size = 0x1
bytearray(b'test mess\x00')
 

Заключение

Надеюсь, вы получили немного знаний о том, как использовать IDAPython или подсказку для решения проблемы, над которой вы работаете. Как я сказал в начале этой книги, я часто забываю вызовы API IDA. Что-то, что помогло мне (наряду с написанием этой книги) вспомнить API-вызовы это копировать и вставлять почти все мои скрипты IDAPython в GitHub Gist. Вы будете удивлены, как часто вы можете писать одни и те же функции снова и снова, если знаете, насколько они полезны. Быстрый доступ к ним экономит много времени.

Если у вас есть какие-либо вопросы, комментарии или отзывы, пришлите мне электронное письмо. Я планирую продолжить редактирование книги. Обратите внимание на номер версии и проверьте его снова в будущем.

Будущие главы

Это список будущих глав, которые я планирую добавить.

• Декомпилятор HexRays
• Взаимодействие с IDA
• Отладчик…
• Переопределение Pintools

Приложение

Неизменяемые IDC API имена

Код:
GetLocalType
AddSeg
SetType
GetDisasm
SetPrcsr
GetFloat
GetDouble
AutoMark
is_pack_real
set_local_type
WriteMap
WriteTxt
WriteExe
CompileEx
uprint
form
Appcall
ApplyType
GetManyBytes
GetString
ClearTraceFile
FindBinary
FindText
NextHead
ParseTypes
PrevHead
ProcessUiAction
SaveBase
eval
MakeStr
GetProcessorName
SegStart
SegEnd
SetSegmentType
CleanupAppcall
DelUserInfo

PeFile

Pefile - это кроссплатформенный модуль Python для анализа Portable executable файлов. Его написал и поддерживает Эро Каррера. Следующий код Python содержит некоторые из наиболее распространенных способов использования и вывода pefile. Пожалуйста, смотрите репозиторий pefile GitHub для получения дополнительной информации.
Python:
import pefile
import sys
import datetime
import zlib

"""
        Author: Alexander Hanel
        Summary: Most common pefile usage examples
"""

def pefile_example(_file, file_path=True):
    try:
        if file_path:
            # загрузить исполняемый файл из пути к файлу для создания класса PE
            pe = pefile.PE(_file)
        else:
            # загрузить исполняемый файл из буфера/строки для создания класса PE
            pe = pefile.PE(data=_file)
    except Exception as e:
        print("pefile load error: %s" % e)
        return
    print("IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint=0x%x" % pe.OPTIONAL_HEADER.AddressOfEntryPoint)
    print("IMAGE_OPTIONAL_HEADER32.ImageBase=0x%x" % pe.OPTIONAL_HEADER.ImageBase)

    # Теперь используйте AddressOfEntryPoint, чтобы получить предпочтительный виртуальный адрес точки входа.
    print("RVA (preferred) Entry Point=0x%x" % (pe.OPTIONAL_HEADER.ImageBase + pe.OPTIONAL_HEADER.AddressOfEntryPoint))
    print("CPU TYPE=%s" % pefile.MACHINE_TYPE[pe.FILE_HEADER.Machine])
    print("Subsystem=%s" % pefile.SUBSYSTEM_TYPE[pe.OPTIONAL_HEADER.Subsystem])
    print("Compile Time=%s" % datetime.datetime.fromtimestamp(pe.FILE_HEADER.TimeDateStamp))
    ext = ""
    if pe.is_dll():
        ext = ".dll"
    elif pe.is_driver():
        ext = '.sys'
    elif pe.is_exe():
        ext = '.exe'
    if ext:
        print("FileExt=%s" % ext)
    # парсинг секций
    print("Number of Sections=%s" % pe.FILE_HEADER.NumberOfSections)
    print("Section VirtualAddress VirtualSize SizeofRawData CRC Hash")
    for index, section in enumerate(pe.sections):
        # как читать данные секции
        sec_data = pe.sections[index].get_data()
        # simple usage
        crc_hash = zlib.crc32(sec_data) & 0xffffffff
        print("%s 0x%x 0x%x 0x%x 0x%x" % (section.Name, section.VirtualAddress, section.Misc_VirtualSize, section.SizeOfRawData, crc_hash))
    print("Imported DLLs")
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        # вывести имя dll
        print(entry.dll)
        print("\tImport Address, Name, File Offset")
        for imp in entry.imports:
            # вычислить виртуальный адрес по смещению файла
            file_offset = pe.get_offset_from_rva(imp.address - pe.OPTIONAL_HEADER.ImageBase)
            # вывести имя символа
            print("\t0x%x %s 0x%x" % (imp.address, imp.name, file_offset))

path = sys.argv[1]
pefile_example(path)
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх