При поиске "Unicorn Engine" на Хабре, я с удивлением обнаружил, что этот инструмент еще ни разу не попадал в статьи. Я попробую заполнить эту пустоту. Начнем, пожалуй, с азов, и посмотрим на пример использования эмулятора в реальной жизни. Для того, чтобы не изобретать велосипед, я решил просто перевести этот мануал. Перед началом скажу, что все мои комментарии или замечания будут выглядеть так. Сами разработчики пишут о Двигатель Единорога Unicorn Engine так: Это не стандартный эмулятор. Он не эмулирует работу всей программы или целой ОС. Он не поддерживает системные команды (такие как открытие файла, вывод символа в консоль и т. д.). Вам прийдется самостоятельно заниматься разметкой памяти и загрузкой данных в нее, а дальше вы просто запускаете выполнение с какого-то конкретного адреса. Так чем же он полезен? Что вам понадобится? Для примера возьмем задачу с hxp CTF 2017 под именем Fibonacci. Бинарник можно загрузить здесь. Когда вы запускаете программу, она начинает выводить наш флаг в консоль, но очень медленно. Каждый следующий байт флага считается все медленнее и медленнее. Это означает, что для получения флага в разумные сроки, нам необходимо оптимизировать работу данного приложения. С помощью IDA Pro (я лично использовал radare2 + Cutter) мы декомпилировали код в С-подобный псевдокод. Не смотря на то, что код не декомпилировался должным образом, мы все равно можем получить от него информацию о том, что происходит внутри. Вот ассемблерный код main и  fibonacci функций: На данном этапе у нас много возможностей решить эту задачу. К примеру, мы можем восстановить код с использованием одного из языков программирования и применить оптимизацию там, но процесс восстановления кода — это очень сложная задача, во время которой мы можем допустить ошибки. Ну а сравнивать потом код, чтобы найти ошибку, — вообще никуда не годится. Но, если мы используем Unicorn Engine, то мы можем пропустить этап реконструкции кода и избежать проблемы, описанной выше. Мы, конечно, можем избежать этих неприятностей с помощью frida или написанием скриптов для gdb, но речь здесь не о том. Прежде чем начинать оптимизацию, мы запустим эмуляцию в Unicorn Engine без изменений программы. И уже только после успешного запуска, перейдем к оптимизации. Давайте создадим файл fibonacci.py и сохраним рядом с бинарником.  Начнем с импортирования необходимых библиотек: Первая строка загружает основной бинарник и базовые Unicorn константы. Вторая строка загружает константы для двух архитектур x86 и x86_64. Дальше добавим некоторые нужные функции: Здесь мы объявили функции, которые нам понадобятся позже: Примечание: Если у вас есть установленный pwntools, то вам не нужно создавать эти функции, вам достаточно импортировать их:  И так, наконец-то приступим к инициализации нашего Unicorn Engine класса для архитектуры x86_64: Тут мы вызываем функции Uc со следующими параметрами: Вы можете найти все константы в Cheatsheet. Как я писал выше, для использования Unicorn Engine, нам понадобиться инициализировать виртуальную память вручную. Для этого примера нам нужно где-то в памяти разместить код и стек. Базовый адрес (Base addr) бинарника начинается c 0x400000. Давайте разместим наш стек в 0x0 и выделим для него 1024*1024 памяти. Скорей всего, нам не нужно столько места, но это все равно не повредит. Мы можем размечать память вызывая метод mem_map. Добавим эти строки: Теперь нам нужно загрузить бинарник в его основной адрес точно так же, как это делает загрузчик. После этого нам нужно выставить RSP в конец стека. Теперь мы можем начать эмуляцию и запускать код, но нам нужно выяснить, с какого адреса начинать работу и когда эмулятору стоит остановиться. Возьмем адрес первой команды из main(), мы можем начать эмуляцию с 0x004004e0. Концом будет считаться вызов putc("\n"), который находится по адресу 0x00400575, после выведения всего флага. Мы можем начать эмуляцию: Теперь запустим скрипт: Упс, что-то пошло не так, но мы даже не знаем что. Сразу перед вызовом mu.emu_start мы можем добавить: Этот код добавляет хук. Мы объявляем собственную функцию hook_code, которая вызывается эмулятором перед каждой командой. Она принимает такие параметры: По адресу, на котором произошла ошибка, мы можем понять, что наш скрипт не может обработать эту команду:
 Эта инструкция считывает данные с адреса 0x601038 (вы можете увидеть это в IDA Pro). Это .bss секция, которую мы не размечали. Моим решением будет просто пропустить все проблематичные инструкции, если это не влияет на логику программы.
Ниже есть еще одна проблемная инструкция:
 Мы не можем вызывать любые функции с glibc, так как у нас нет загруженных glibc в памяти. В любом случае, нам эта команда не нужна, так что, можем тоже ее пропустить.
Вот полный список команд, которые нужно пропустить:
 Для пропуска команд нам нужно перезаписывать RIP на следующую инструкцию:
 Теперь hook_code должен выглядеть как-то так: Нам также нужно что-то сделать с инструкциями, которые выводят флаг в консоль байт-за-байтом. __IO_putc принимает байты для вывода в качестве первого аргумента (это регистр RDI). Мы можем считывать данные непосредственно с регистра, выводить данные в консоль и пропускать этот набор инструкций. Обновленный hook_code представлен ниже: Мы можем запустить и это все будет работать, но по прежнему медленно. Давайте подумаем про увеличение скорости работы. Почему эта программа такая медленная? Если мы посмотрим на декомпилированный код, то увидим, что main() вызывает  fibonacci() несколько раз и  fibonacci() — это рекурсивная функция. Посмотрим на эту функцию поближе, она принимает и возвращает два аргумента. Первое возвращаемое значение передается через RAX регистр, второе возвращается через ссылку, которую передали через второй аргумент функции. Если мы посмотрим глубже, на связь main() и fibonacci(), то мы увидим, что второй аргумент принимает только два возможных значения: 0 или 1. Если вы этого все еще не видите, то запустите gdb и поставьте точку остановки на начало функции fibonacci(). Для оптимизации работы алгоритма, мы можем воспользоваться динамическим программированием для того, чтобы запомнить возвращаемые значение для входящих параметров. Подумайте сами, второй аргумент может принимать только два возможных значения, так что нам достаточно запомнить лишь  пар. fibonacci — это рекурсивная функция, которая вычисляет следующее значение как сумму двух предыдущих. На каждом шаге она заходит все глубже. Каждый раз, когда она начинает сначала, она проходит тот же путь, что и раньше, плюс одно новое значение.  Пример:
Допустим, глубина = 6, тогда: 1 1 2 3 5 8.
А теперь глубина = 8, тогда: 1 1 2 3 5 8 13 21. Мы бы могли просто запомнить, что первые 6 членов это 1 1 2 3 5 8, и когда нас просят посчитать больше, чем мы запомнили, то мы берем то, что запомнили и считаем только то, чего не хватает. Как только RIP находится в начале fibonacci(), мы можем получить аргументы функции. Мы знаем, что функция возвращает результат, когда выходит из функции. Так как мы не можем оперировать двумя параметрами сразу, нам понадобится стек для возвращения параметров. Во время входа в fibonacci() нам нужно положить аргументы в стек, и забрать их при выходе. Для хранения посчитанных пар мы можем использовать словарь. Как обработать пару значений? Ура, мы наконец-то смогли оптимизировать приложение, используя Unicorn Engine. Хорошая работа! Теперь я решил дать вам небольшое домашнее задание.
Здесь вы можете найти еще три задачи, каждая из которых имеет подсказку и полное решение. Вы можете подглядывать в Cheatsheet во время решение задач.  Одна из самых назойливых проблем — это вспомнить имя нужной константы. С этим легко бороться, если использовать Tab-дополнения в IPython. Когда у вас есть установленный IPython, вы можете написать from unicorn import UC_ARCH_ нажать Tab и вам будет выведены все константы, которые начинаются так же.