
Скриптуемый фреймворк для бинарной эмуляции, интегрирующий IDA Pro/Radare2 с движком Unicorn для автоматического анализа вредоносного ПО, дешифрования строк и исследования путей выполнения кода на архитектурах x86, ARM и ARM64.
flare-emu объединяет поддерживаемый фреймворк двоичного анализа, такой как IDA Pro или Radare2, с эмуляционным фреймворком Unicorn, предоставляя пользователю простой и гибкий интерфейс для написания сценариев эмуляции. Он предназначен для выполнения всех рутинных задач по настройке гибкого и надежного эмулятора для поддерживаемых архитектур, чтобы вы могли сосредоточиться на решении задач анализа кода. В настоящее время flare-emu поддерживает архитектуры x86, x86_64, ARM и ARM64.
В настоящее время он предоставляет пять различных интерфейсов для удовлетворения ваших потребностей в эмуляции, а также множество связанных вспомогательных и служебных функций.
emulateRange – Этот API используется для эмуляции диапазона инструкций или функции в заданном пользователем контексте. Он предоставляет опции для пользовательских хуков как для отдельных инструкций, так и для случаев, когда встречаются инструкции «call». Пользователь может решить, будет ли эмулятор пропускать вызовы функций или входить в них. Этот интерфейс предоставляет простой способ для пользователя задать значения для определенных регистров и аргументов стека. Если указан bytestring, он записывается в память эмулятора, а указатель записывается в регистр или переменную стека. После эмуляции пользователь может воспользоваться вспомогательными функциями flare-emu для чтения данных из эмулированной памяти или регистров, либо использовать возвращаемый объект эмуляции Unicorn для прямого изучения. Небольшая обёртка для emulateRange, названная emulateSelection, может использоваться для эмуляции диапазона инструкций, выделенных в данный момент в IDA Pro.
iterate - Этот API используется для принудительной эмуляции по определённым ветвям внутри функции с целью достижения заданной цели. Пользователь может указать список целевых адресов или адрес функции, для которой список перекрёстных ссылок используется в качестве целей, а также обратный вызов, когда цель достигнута. Цели будут достигнуты независимо от условий во время эмуляции, которые могли бы привести к другим ветвям. Как и в API emulateRange, предоставляются опции для пользовательских хуков как для отдельных инструкций, так и для случаев, когда встречаются инструкции «call». Пример использования API iterate — это достижение чего-то подобного тому, что делает наш инструмент argtracker.
iterateAllPaths - Этот API очень похож на iterate, за исключением того, что вместо указания одного или нескольких целевых адресов вы предоставляете целевую функцию, для которой он попытается найти все пути и эмулировать их. Это полезно, когда анализ кода требует достижения каждого базового блока функции.
emulateBytes – Этот API предоставляет способ просто эмулировать фрагмент постороннего шелл-кода. Предоставленные байты не добавляются в IDB и просто эмулируются как есть. Это может быть полезно для подготовки среды эмуляции. Например, flare-emu сам использует этот API для манипулирования специальным регистром модели (MSR) для процессора ARM64, который не предоставляется Unicorn, чтобы включить инструкции с плавающей запятой (VFP) и доступ к регистрам. Объект эмуляции Unicorn возвращается для дальнейшего исследования пользователем.
emulateFrom - Этот API полезен в случаях, когда границы функций нечётко определены, что часто бывает с обфусцированными бинарными файлами или шелл-кодом. Вы предоставляете начальный адрес, и он будет эмулировать, пока не останется ничего для эмуляции или вы не остановите эмуляцию в одном из своих хуков. В IDA Pro это можно вызвать с параметром strict, установленным в False, для включения динамического обнаружения кода; flare-emu заставит IDA Pro создавать инструкции по мере их обнаружения во время эмуляции.
Чтобы установить flare-emu для IDA Pro, просто поместите flare_emu.py, flare_emu_ida.py и flare_emu_hooks.py в каталог python IDA Pro и импортируйте его как модуль в свои скрипты IDAPython.
Чтобы установить flare-emu для Rizin, просто убедитесь, что flare_emu.py, flare_emu_rizin.py и flare_emu_hooks.py находятся в пути поиска Python для импорта модулей. При использовании Rizin в качестве компонента двоичного анализа для flare-emu требуется rzpipe.
Чтобы установить flare-emu для Radare2, просто убедитесь, что flare_emu.py, flare_emu_radare.py и flare_emu_hooks.py находятся в пути поиска Python для импорта модулей. При использовании Radare2 в качестве компонента двоичного анализа для flare-emu требуется r2pipe.
В любом случае, flare-emu зависит от Unicorn и его привязок к Python.
ВАЖНОЕ ПРИМЕЧАНИЕ
flare-emu был написан с использованием нового API IDA Pro 7x и несовместим с предыдущими версиями IDA Pro.
Хотя flare-emu можно использовать для решения множества различных задач анализа кода, одно из его наиболее распространенных применений — помощь в расшифровке строк в вредоносных бинарных файлах. FLOSS — отличный инструмент, который часто может сделать это автоматически, пытаясь идентифицировать функцию(и) расшифровки строк и используя эмуляцию для расшифровки строк, передаваемых в каждой перекрёстной ссылке на неё. Однако FLOSS не всегда может идентифицировать эти функции и правильно эмулировать их с помощью общих подходов. Иногда приходится приложить немного больше усилий, и здесь flare-emu может сэкономить вам много времени, как только вы освоитесь с ним. Давайте рассмотрим типичный сценарий, с которым сталкивается аналитик вредоносных программ при работе с зашифрованными строками.
Вы определили функцию, которая расшифровывает все строки в бинарном файле x86_64. Эта функция вызывается повсеместно и расшифровывает множество различных строк. В IDA Pro вы называете эту функцию decryptString. Вот ваш сценарий flare-emu для расшифровки всех этих строк, размещения комментариев с расшифрованными строками в каждом вызове функции, а также записи каждой расшифрованной строки и адреса, по которому она была расшифрована.```
from future import print_function
import flare_emu
def decrypt(argv): myEH = flare_emu.EmuHelper() myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), registers = {"arg1":argv[0], "arg2":argv[1], "arg3":argv[2], "arg4":argv[3]}) return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData): s = decrypt(argv) print("%s: %s" % (eh.hexString(address), s)) eh.analysisHelper.setComment(address, s, False)
if name == 'main':
eh = flare_emu.EmuHelper()
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
В `__main__` мы начинаем с создания экземпляра класса `EmuHelper` из `flare-emu`. Это класс, который мы используем для выполнения всех действий с `flare-emu`. Затем мы используем API `iterate`, передавая ему адрес нашей функции `decryptString` и имя нашей функции обратного вызова, которую `EmuHelper` будет вызывать для каждой эмулированной перекрестной ссылки.
Функция `iterateCallback` получает экземпляр `EmuHelper`, названный здесь `eh`, вместе с адресом перекрестной ссылки, аргументами, переданными в этот конкретный вызов, и специальным словарем, названным здесь `userData`. `userData` не используется в этом простом примере, но воспринимайте его как постоянный контекст вашего эмулятора, в котором вы можете хранить свои собственные данные. Однако будьте осторожны, поскольку сам `flare-emu` также использует этот словарь для хранения критической информации, необходимой для выполнения своих задач. Одним из таких данных является сам экземпляр `EmuHelper`, хранящийся по ключу `"EmuHelper"`. Если вам интересно, поищите в исходном коде, чтобы узнать больше об этом словаре. Эта функция обратного вызова просто вызывает функцию `decrypt`, выводит расшифрованную строку и создает для нее комментарий по адресу этого вызова `decryptString`.
`decrypt` создает второй экземпляр `EmuHelper`, который используется для эмуляции самой функции `decryptString`, которая расшифрует для нас строку. Прототип этой функции `decryptString` выглядит следующим образом: `char * decryptString(char *text, int textLength, char *key, int keyLength)`. Она просто расшифровывает строку на месте. Наша функция `decrypt` передает аргументы, полученные функцией `iterateCallback`, в наш вызов API `emulateRange` от `EmuHelper`. Поскольку это бинарный файл `x86_64`, соглашение о вызовах использует регистры для передачи аргументов, а не стек. `flare-emu` автоматически определяет, какие регистры представляют какие аргументы, на основе архитектуры и формата файла бинарника, определенных IDA Pro, что позволяет писать хотя бы частично архитектурно-независимый код. Если бы это был 32-битный `x86`, вы бы использовали аргумент `stack` для передачи аргументов следующим образом: `myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), stack = [0, argv[0], argv[1], argv[2], argv[3]])`. Первое значение в стеке — это адрес возврата в `x86`, поэтому мы просто используем `0` в качестве заполнителя. После завершения эмуляции мы вызываем API `getEmuString`, чтобы получить строку с нулевым символом в конце, хранящуюся по адресу памяти, на который указывает первый аргумент, переданный функции.
### flare-emu и idalib
* установите IDA Pro
* установите idalib в соответствии с руководством пользователя Hex-Rays
* (активируйте виртуальное окружение)
* pip install /путь/к/установке/IDA/idalib/python
* python /путь/к/установке/IDA/idalib/python/py-activate-idalib.py [-d /путь/к/активной/установке/IDA]
* импортируйте idapro и напишите свой скрипт
* пример см. в tests/test_flare_emu_idalib.py
### Сценарий легкого расшифрования строк с помощью Rizin
Используя тот же пример, что и выше, при работе с Rizin мало что меняется по сравнению с IDA Pro. Одно из отличий состоит в том, что `flare-emu` в настоящее время предназначен для запуска в качестве скрипта командной строки или в оболочке Python при работе с Rizin. Оболочка Python отлично подходит для решения ad-hoc задач, в то время как скрипт командной строки отлично подходит для пакетной обработки. Версия сценария выше для Rizin выглядит так (вы также можете опустить путь к образцу для запуска внутри rizin):```
from __future__ import print_function
import sys
import flare_emu
def decrypt(argv, eh):
myEH = flare_emu.EmuHelper(samplePath=sys.argv[1], emuHelper=eh, isRizin=True)
myEH.emulateRange(
myEH.analysisHelper.getNameAddr("decryptString"),
registers={
"arg1": argv[0],
"arg2": argv[1],
"arg3": argv[2],
"arg4": argv[3],
},
)
return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData):
s = decrypt(argv, eh)
print("%s: %s" % (eh.hexString(address), s))
eh.analysisHelper.setComment(address, s, False)
if __name__ == "__main__":
eh = flare_emu.EmuHelper(samplePath=sys.argv[1], isRizin=True)
rz = eh.analysisHelper.r
eh.analysisHelper.setName(0x100000D60, "decryptString")
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
Используя тот же пример, что и выше, при работе с Radare2 вместо IDA Pro мало что меняется. Одно отличие заключается в том, что flare-emu в настоящее время рассчитан на запуск в качестве скрипта командной строки или в оболочке Python при работе с Radare2. Оболочка Python отлично подходит для решения ad-hoc-задач, а скрипт командной строки — для пакетной обработки. Версия приведенного выше скрипта для Radare2 выглядит так:```
from future import print_function
import flare_emu
def decrypt(argv, eh): myEH = flare_emu.EmuHelper(samplePath=sys.argv[1], emuHelper=eh) myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), registers = {"arg1":argv[0], "arg2":argv[1], "arg3":argv[2], "arg4":argv[3]}) return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData): s = decrypt(argv, eh) print("%s: %s" % (eh.hexString(address), s)) eh.analysisHelper.setComment(address, s, False)
if name == 'main':
eh = flare_emu.EmuHelper(samplePath=sys.argv[1])
eh.analysisHelper.setName(, "decryptString")
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
В этом скрипте есть два отличия. Во-первых, конструктор `EmuHelper` принимает здесь параметр: `samplePath=sys.argv[1]`. Когда параметр `samplePath` указан, `flare-emu` будет использовать Radare2 с `r2pipe` в качестве движка бинарного анализа. Вы также можете видеть, что второй параметр передается второму экземпляру `EmuHelper`, созданному в функции `decrypt`. Параметр `emuHelper` принимает существующий объект `EmuHelper` и клонирует его память при создании нового объекта. Кроме того, если вы используете Radare2, новый экземпляр повторно использует существующую сессию Radare2 вместо создания новой, что добавило бы дополнительных накладных расходов. Во-вторых, `flare-emu` создает новый экземпляр Radare2 с помощью `r2pipe.open`, поэтому он, скорее всего, не будет иметь имя `decryptString` для интересующей нас функции. Вы можете либо задать имя самостоятельно, используя объект `analysisHelper` из `EmuHelper` следующим образом: `eh.analysisHelper.setName(<some address>, "decryptString")`, либо напрямую указать адрес для вызовов `iterate` и `emulateRange`.
## [Emulation Functions](#emulationfuncs)
`emulateRange(startAddr, endAddr=None, registers=None, stack=None, instructionHook=None, callHook=None, memAccessHook=None, hookData=None, skipCalls=True, hookApis=True, strict=True, count=0)` - Эмулирует диапазон инструкций, начиная с `startAddress` и заканчивая `endAddress`, не включая инструкцию по адресу `endAddress`. Если `endAddress` равен `None`, эмуляция останавливается при встрече инструкции типа "return" в той же функции, с которой началась эмуляция.
* `registers` — это словарь, где ключами являются имена регистров, а значениями — значения регистров. Некоторые специальные имена регистров создаются `flare-emu` и могут использоваться здесь, например `arg1`, `arg2` и т.д., `ret` и `pc`.
* `stack` — это массив значений, которые помещаются в стек в обратном порядке, подобно аргументам функции в `x86`. В `x86` помните, что первое значение в этом массиве используется как адрес возврата при вызове функции, а не как первый аргумент функции. `flare-emu` инициализирует контекст и память эмулируемого потока в соответствии со значениями, указанными в аргументах `registers` и `stack`. Если для любого из этих значений указана строка, она будет записана в определенное место в памяти, а указатель на эту память будет записан в указанный регистр или место в стеке.
* `instructionHook` — это функция, которую вы определяете и которая вызывается перед эмуляцией каждой инструкции. Она имеет следующий прототип: `instructionHook(unicornObject, address, instructionSize, userData)`.
* `callHook` — это функция, которую вы определяете и которая вызывается всякий раз, когда во время эмуляции встречается инструкция типа "call". Она имеет следующий прототип: `callHook(address, arguments, functionName, userData)`.
* `hookData` — это словарь, содержащий пользовательские данные, которые должны быть доступны вашим функциям-хукам. Это способ сохранения данных на протяжении всей эмуляции. `flare-emu` также использует этот словарь для своих целей, поэтому нужно быть осторожным, чтобы не определить уже существующий ключ. Эта переменная часто называется `userData` в пользовательских функциях-хуках из-за ее именования в Unicorn.
* `skipCalls` заставляет эмулятор пропускать инструкции типа "call" и соответствующим образом корректировать стек, по умолчанию `True`.
* `hookApis` заставляет `flare-emu` выполнять наивную реализацию некоторых наиболее распространенных функций библиотеки времени выполнения и ОС, которые он встречает во время эмуляции. Это избавляет вас от необходимости беспокоиться о вызовах таких функций, как `memcpy`, `strcat`, `malloc` и т.д., и по умолчанию `True`.
* `memAccessHook` — это функция, которую вы определяете и которая вызывается всякий раз, когда происходит обращение к памяти для чтения или записи. Она имеет следующий прототип: `memAccessHook(unicornObject, accessType, memAccessAddress, memAccessSize, memValue, userData)`.
* `strict`, если установлено в `True` (по умолчанию), проверяет целевые адреса переходов, чтобы убедиться, что дизассемблер ожидает инструкции. В противном случае он пропускает инструкцию перехода. Если установлено в `False` при использовании IDA Pro, `flare-emu` будет создавать инструкции в IDA Pro по мере их эмуляции **(ОТКЛЮЧАТЬ С ОСТОРОЖНОСТЬЮ)**.
* `count` — максимальное количество инструкций для эмуляции, по умолчанию `0`, что означает отсутствие лимита.
`iterate(target, targetCallback, preEmuCallback=None, callHook=None, instructionHook=None, hookData=None, resetEmuMem=False, hookApis=True, memAccessHook=None)` - Для каждой цели, указанной в `target`, выполняется отдельная эмуляция от начала содержащей функции до целевого адреса. Эмуляция будет принудительно направлена по ветвям, необходимым для достижения каждой цели. `target` может быть адресом функции, и в этом случае список целей заполняется всеми перекрестными ссылками на указанную функцию. Или `target` может быть явным списком целей.
* `targetCallback` — это функция, которую вы создаете, и которая будет вызываться `flare-emu` для каждой цели, достигнутой во время эмуляции. Она имеет следующий прототип: `targetHook(emuHelper, address, arguments, userData)`.
* `preEmuCallback` — это функция, которую вы создаете, и которая будет вызываться перед началом эмуляции для каждой цели. При необходимости вы можете реализовать здесь некоторый код настройки.
* `resetEmuMem` заставляет `flare-emu` сбрасывать память эмуляции перед началом эмуляции каждой цели, по умолчанию `False`.
`iterateAllPaths(target, targetCallback, preEmuCallback=None, callHook=None, instructionHook=None, hookData=None, resetEmuMem=False, hookApis=True, memAccessHook=None, maxPaths=MAXCODEPATHS, maxNodes=MAXNODESEARCH)` - Для функции, содержащей адрес `target`, выполняется отдельная эмуляция для каждого обнаруженного пути через нее, вплоть до `maxPaths`.
* `maxPaths` — максимальное количество путей через функцию, которые будут найдены и эмулированы. Некоторые более сложные функции могут привести к тому, что функция поиска графа будет выполняться очень долго или никогда не завершится; настройте этот параметр в соответствии с вашими потребностями за разумное время.
* `maxNodes` — максимальное количество базовых блоков, которые будут просмотрены при поиске путей через целевую функцию. Это мера безопасности для предотвращения необоснованного времени поиска и зависаний, и, скорее всего, ее не нужно изменять.
`emulateBytes(bytes, registers=None, stack=None, baseAddress=0x400000, instructionHook=None, hookData=None)` - Записывает код, содержащийся в `bytes`, в память эмуляции по адресу `baseAddress`, если это возможно, и эмулирует инструкции от начала до конца `bytes`.
`emulateFrom(startAddr, registers=None, stack=None, instructionHook=None, callHook=None, memAccessHook=None, hookData=None, skipCalls=True, hookApis=True, strict=True, count=0)` - Этот API полезен в случаях, когда границы функций четко не определены, как это часто бывает с обфусцированными бинарными файлами или шелл-кодом. Вы указываете начальный адрес как `startAddr`, и он будет эмулировать до тех пор, пока не останется ничего для эмуляции или пока вы не остановите эмуляцию в одном из своих хуков. Это может быть вызвано с параметром `strict`, установленным в `False`, чтобы включить динамическое обнаружение кода; `flare-emu` заставит IDA Pro создавать инструкции по мере их обнаружения во время эмуляции.
## [Utility Functions](#utility)
Ниже приведен неполный список некоторых полезных утилитных функций, предоставляемых классом `EmuHelper`.
* `hexString(value)` — возвращает строку, отформатированную в шестнадцатеричном виде для указанного значения. Полезно для вывода в журнал и операторов печати.
* `skipInstruction(userData, useAnalysisHelper=False)` — вызывайте это из хука эмуляции, чтобы пропустить текущую инструкцию, переместив программный счетчик на следующую инструкцию. Опция `useAnalysisHelper` была добавлена для обработки случаев, когда фреймворк бинарного анализа объединяет несколько инструкций в одну псевдоинструкцию, и вы хотите пропустить их все. Эту функцию нельзя вызывать несколько раз из одного хука инструкции для пропуска нескольких инструкций. Чтобы пропустить несколько инструкций, рекомендуется не записывать непосредственно в программный счетчик, если вы эмулируете код ARM, так как это может вызвать проблемы с режимом Thumb. Вместо этого попробуйте API `changeProgramCounter` от `EmuHelper` (описан ниже).
* `changeProgramCounter(userData, newAddress)` — вызывайте это из хука эмуляции, чтобы изменить значение регистра программного счетчика. Этот API заботится об отслеживании режима Thumb для архитектуры ARM.
* `getRegVal(registerName)` — извлекает значение указанного регистра, учитывая адресацию подрегистров. Например, "ax" вернет младшие 16 бит регистра EAX/RAX в `x86`.
* `stopEmulation(userData)` — вызывайте это из хука эмуляции, чтобы остановить эмуляцию. Используйте это вместо вызова API `emu_stop` из Unicorn, чтобы объект `EmuHelper` мог выполнить учет, связанный с функцией `iterate`.
* `getEmuString(address)` — возвращает строку символов, расположенную по адресу в эмулируемой памяти, до нулевого терминатора. Символы не обязательно являются печатаемыми.
* `getEmuWideString(address)` — возвращает строку "широких символов", расположенную по адресу в эмулируемой памяти, до нулевого терминатора. "Широкие символы" здесь понимаются в широком смысле как любая последовательность байтов, содержащая нулевой байт через каждый второй байт, как в случае строки ASCII, закодированной в UTF-16 LE. Символы не обязательно печатаемы.
* `getEmuBytes(address, length)` — возвращает строку байтов, расположенную по адресу в эмулируемой памяти.
* `getEmuPtr(address)` — возвращает значение указателя, расположенного по данному адресу.
* `writeEmuPtr(address, value)` — записывает значение указателя по данному адресу в эмулируемой памяти.
* `loadBytes(bytes, address=None)` — выделяет память в эмуляторе и записывает в нее байты.
* `isValidEmuPtr(address)` — возвращает `True`, если указанный адрес указывает на допустимую эмулированную память.
* `getEmuMemRegion(address)` — возвращает кортеж, содержащий начальный и конечный адрес области памяти, содержащей указанный адрес, или `None`, если адрес недействителен.
* `getArgv()` — вызывайте это из хука эмуляции при инструкции типа "call", чтобы получить массив аргументов функции.
* `addApiHook(apiName, hook)` — добавляет новый хук API для этого экземпляра `EmuHelper`. Всякий раз, когда во время эмуляции встречается инструкция вызова `apiName`, `EmuHelper` вызывает функцию, указанную в `hook`. Если `hook` — строка, ожидается, что это имя уже перехваченного API `EmuHelper`, и в этом случае он вызовет существующую функцию-хук. Если `hook` — функция, он вызовет эту функцию.
* `allocEmuMem(size, addr=None)` — выделяет достаточное количество памяти эмулятора для хранения `size` байт. Он пытается соблюсти запрошенный `address`, но если он перекрывается с существующей областью памяти, он выделит в неиспользуемой области памяти и вернет новый адрес. Если `address` не выровнен по странице, он вернет адрес, сохраняющий то же смещение выравнивания по странице в новой области. Например, запрос адреса `0x1234`, когда `0x1000` уже выделен, может привести к выделению по адресу `0x2000` и возврату `0x2234`.
# [Learn More](#learn)
Чтобы узнать больше о **flare-emu**, пожалуйста, прочитайте нашу вводную статью в блоге по адресу https://www.fireeye.com/blog/threat-research/2018/12/automating-objective-c-code-analysis-with-emulation.html.