Всем привет. Тут такое дело: ещё одна моя реверсерская мечта сбылась - я написал процессорный модуль для IDA Pro с нуля, за два дня! Если вы когда-то тоже хотели написать свой модуль, но боялись начать - думаю, моя статья сможет помочь. В качестве кода, который требуется дизасемблировать, будет выступать код виртуальной машины из очень крутого хоррора, который выходил сначала на SNES, потом на PS1, PC и Wonderswan - "Clock Tower - The First Fear". В игре имеется 9 концовок (sic!), атмосфера гнетущая, а в качестве главного злодея выступает "Scissorman" (человек с руками-ножницами). Заинтересовал? Тогда добро пожаловать... Собственно, сам код, выполняющий опкоды виртуальной машины, не будет являться предметом данной статьи, но, вот краткая информация о нём: 115 опкодов на все случаи жизни для анализа VM я анализировал исполняемые файлы для PS1 и PC, на SNES виртуальной машины нет уже написан дизассемблер-декомпилятор для Ghidra (ссылки в конце статьи) Итак, приступим к написанию кода. Я писал в Visual Studio на C/C++, но вы можете писать и на Python. Согласно устоявшимся принципам разработки процессорных модулей для IDA Pro (которые можно найти в IDA SDK), нам понадобится создать следующие пока ещё пустые файлы: Инклуды: cpu_name.hpp (где cpu_name - краткое имя вашего процессорного модуля, в моём случае это adc) ins.hpp (будет содержать enum со списком всех опкодов) Файлы с кодом: ana.cpp (здесь будет находиться код анализатора (хорошо, что не как у radare2 - anal) emu.cpp (сюда мы будем писать код эмулятора) ins.cpp (имена опкодов и их feature-флаги - здесь) out.cpp (код, который отвечает за сам вывод мнемоник/операндов и прочей атрибутики, типа запятых, скобок и т.д.) reg.cpp (можно предположить, что это про регистры, но нет - это про регистрацию модуля, его флаги и прочую конфигурацию. Именно здесь начинается модуль) Чтобы проще было изучать готовые процессорные модули и, соответственно, писать свои, стоит сначала понять, что же такое анализатор (ana), эмулятор (emu) и вывод (out). Начнём с ana. Анализатор (ana) Задачи анализатора: чтение входных данных, формирование опкодов и операндов для них. Обычно, это выглядит так: читаем байт/ворд/дворд, находим соответствие одному из наших опкодов, затем читаем данные до тех пор, пока не заполним данные для каждого операнда (индекс регистра/адрес в памяти/смещение для прыжка и т.п.). Пока читаем, формируется итоговая длина инструкции. Эмулятор (emu) После того, как анализатор закончил свою работу, в дело вступает эмулятор. Его задача: формирование ссылок и пост-анализ кода. Ссылки могут быть: на следующую инструкцию, если это не return, jump и другие подобные инструкции на адреса в памяти: чтение/запись/обычное смещение на стек и локальные переменные Под пост-анализом подразумевается, именование каких-то известных встроенных функций, портов, адресов в памяти. Ещё можно добавлять в очередь для анализа адреса из операндов, где также необходимо дизассемблировать/создать функцию. Вывод (out) И, наконец, финальный этап для процессорного модуля - код для вывода накопленной информации об инструкциях (out). Данная часть вызывается Идой только тогда, когда инструкция попадает в область видимости листинга. И именно здесь кроется причина, почему процессорный модуль для Гидры умеет в декомпиляцию из коробки, а модуль из Иды - нет. Выводилка последней - это, фактически, простая printf того, что вы ей укажете, с возможностями вставлять отступы, делать вывод цветным и т.д. Никакого тебе IR (Intermediate Representation). Поэтому, если хочешь декомпилятор для экзотической платформы, у тебя два варианта: Использовать Ghidra Не использовать Ghidra. Вместо этого изощряться через Вывод. Годится лишь для простых виртуальных машин В случае простой виртуальной машины, каковой является VM из Clock Tower, можно извратиться и выводить сразу псевдо-сишный код (ничего страшного в этом нет). Я так и сделал. Хватит разглагольствовать, давайте уже писать код. Открываем Visual Studio, создаём пустой проект DLL, прописываем пути к инклудам и библиотекам IDA SDK. Указываем следующие процессорные директивы (все три являются обязательными): Если необходима поддержка вашего процессорного модуля Идой с 64-битным адресным пространством, также добавляем флаг: Теперь открываем reg.cpp и вставляем следующий шаблон: В файл adc.hpp необходимо прописать некоторые важные инклуды: Ключевых моментов здесь 3: proc ID - идентификатор вашего процессорного модуля. Все пользовательские модули должны иметь номер >= 0x8000 Флаги - их много, и позволяют они достаточно тонко настроить работу процессорного модуля. Список флагов и их описания здесь rVcs/rVds - в моей виртуальной машине регистры не используются, но Иде всё равно требуется указать хотя бы виртуальные регистры. Ими мы займёмся позднее Первым идёт shnames: Здесь мы перечисляем все процессоры, которые поддерживает наш модуль (конечно, их может быть несколько. В конце списка обязательно должен быть NULL. Далее создаём lnames: То же самое, только можно указать более длинное имя. Обратите внимание на саму строку - всё, что в ней расположено до двоеточия, является именем семейства процессоров. Итого, вся эта информация в Иде будет отображаться следующим образом: Следующий кусок кода, на котором многие останавливаются (в том числе и я когда-то) - это описание ассемблера. Что он из себя представляет в понимании Иды? Фактически, по большей части, это - солянка из никем не используемых и никому не нужных элементов синтаксиса ассемблера, которые выводятся в листинг самой Идой (далеко не все) без нашего участия. В основном, это касается нераспознанных данных, массивов и других директив (типа: org, end, equ, public, xref). Нужны они, или нет, а заполнять придётся. Лучшим вариантом будет взять готовый из какого-нибудь SDK-шного модуля и чуть-чуть изменить. У меня получилось следующее: Из всего, что здесь написано, нам нужны только флаги (они отвечают за то, как будут отображаться числа, строки, смещения), и имя для нашего ассемблера. Подробнее про структуру asm_t можно почитать здесь, ну а мы двигаем дальше. Как уже было сказано, notify-колбэк отвечает за создание объекта класса процессорного модуля в момент, когда это нужно Иде. Смысла менять его код нет, поэтому берём как есть: Заменяем только adcvm_t на имя класса вашего будущего модуля. Также, обращаем внимание на макрос SET_MODULE_DATA. Он отвечает за вызов функции set_module_data и создание экземпляра нашего процессора. Требует объявленного выше int data_id;.  Тушкой модуля займёмся чуточку позже, а пока перейдём к регистрам. В случае виртуальной машины из Clock Tower, вместо регистров используются адреса в рабочей (work) памяти. Но Ида, к сожалению, не может избавиться от своего сегментного прошлого и хочет в любом случае указанных cs и ds регистров, даже если они нигде не используются. Поэтому, regnames будет выглядеть следующим образом: Здесь вместо NULL в конце списка, Ида принимает количество регистров, которое можно указать через макрос: qnumber(regnames). Далее необходимо создать enum с самими регистрами (которых нет): Именно на элементы этого enum-а  мы и сослались в структуре. С регистрами покончено, и можно переходить к помощи Иде в распознавании типовых участков кода. Прологов в моём случае не имеется, поэтому указываем NULL, а вот коды возврата есть, поэтому пишем: В разбираемой мной VM, все опкоды имеют минимальную длину два байта, а код инструкции return имеет значение { 0x00, 0xFF }. Итак, мы плавно перешли к самим инструкциям. Как уже было сказано, опкодов в случае Clock Tower VM ровно 115. Их имена были заботливо оставлены разработчиками прямо в исполняемых файлах (для отладочного вывода, который был в итоге выпилен). В общем, берём эти имена, и добавляем к ним префикс в виде имени процессорного модуля: ADCVM, получаем enum, который необходимо расположить в ins.hpp. В итоге имеем вот такую портянку: Теперь нам предстоит наверное самая муторная работёнка, а именно - формирование списка имён опкодов и указание feature-флагов для них (что это - расскажу позднее). Открываем ins.cpp, вставляем в него следующий шаблон, и начинаем заполнять: Каждый элемент списка инструкций представляет из себя структуру instruc_t с полями: имя инструкции, feature-флаги инструкции. Feature-флаги представляют из себя краткое описание того, что представляет из себя конкретная инструкция на совсем примитивном уровне, типа: CF_STOP - инструкция не передаёт управление на следующую CF_USE1-CF_USE8 - используются операнды 1-8 CF_CHG1-CF_CHG8 - изменяются операнды 1-8 CF_CALL - вызов функции CF_JUMP - прыжок на другую инструкцию Самое интересное, что эти флаги не нужны Иде (а зачем, спрашивается, мы их указываем?). Они будут использоваться только вами (далее увидите как именно), тем не менее данную формальность всё равно необходимо соблюсти. Поэтому, вот вам парочка "типичных" инструкций и их флагов: Думаю, флаги в этих примерах говорят сами за себя. В случае, если операндов нет, вместо флагов указываем 0. Покончив с рутиной в виде описания инструкций, настало время заняться ядром нашего процессорного модуля. Поэтому, открываем adc.hpp и вставляем в него после инклудов следующий шаблон: Именно эти функции будут выполнять всё необходимое для того, чтобы превратить набор байтиков входного файла в красивый дизассемблерный (а можно и псевдо-сишный) листинг.  Открываем reg.cpp, и дополняем его следующим шаблоном кода: Имена ивентов, которые перехватывает наш колбэк достаточно говорящие, поэтому я разберу лишь некоторые из них: ev_init - инициализация процессорного модуля. Здесь мы задаём основные свойства, которые нельзя задать флагами. Например, порядок байт (endianness) ev_term - если пришло данное событие, необходимо выполнитьclr_module_dataдля корректной работы в случае, если запущено несколько экземпляров Иды с нашим модулем Не все указанные в этом колбэке методы у нас пока созданы, поэтому их созданием как раз и займёмся. И первым у нас идёт анализатор (ana). Открываем файл ana.cpp, и вставляем в него ещё один шаблон: Началось... Именно здесь мы будем заполнять структуры insn_t (сама инструкция) и op_t (каждый из операндов). Давайте разбираться. Для начала мы должны прочитать необходимый токен, который сможет сказать нам, что за опкод у нас на очереди. В моём случае каждая инструкция имеет переменную длину, но сам опкод без операндов имеет длину 2 байта, т.е. word. Его и читаем. Чтение будет происходить согласно указанному ранее порядку байт. К тому же, чтение с использованием конструкции insn.get_next_xxxx() увеличит значение длины формируемой инструкции на соответствующее значение. Например, инструкция возврата из функции в моём случае имеет значение 0xFF00. Добавим её в наш оператор switch(code), и заполним заодно одно из полей структуры инструкции (insn_t): Поле itype - содержит номер инструкции (который обычно привязан к enum-у со всеми имеющимися опкодами). Именно это поле можно будет использовать, чтобы выполнять разные действия для разных инструкций. Теперь давайте разберём опкод div, который имеет два операнда: dest (делимое) и src (делитель) и сохраняет результат в dest. Так будет выглядеть формирование каждого из операндов (для удобства, были созданы отдельные функции, которые можно использовать в дальнейшем): Пройдёмся по основным полям операндов, которые здесь заполняются: offb - смещение на операнд относительно начала инструкции. Если операнды кодируются битами, или значение не известно, можно использовать значение 0. Данное число будет использоваться эмулятором (т.е. нами), и встроенным hex-редактором при отображении операнда addr - целевой адрес, если данный операнд содержит ссылку value - значение, если данный операнд содержит число. Сюда же можно поместить дополнительное смещение, если, например, требуется дельта для значения в addr dtype - тип данных по целевому адресу в addr, либо значения в value. Все основные типы описаны здесь type - тип собственно операнда. Это может быть: ссылка на данные (o_mem), число (o_imm), ссылка на код (o_near). Другие типы описаны здесь Ещё можно использовать поле reg, если у вас используются регистры. Если их нет, данное поле можно использовать под свои нужды. Ещё под эти самые нужды можно занимать поля: specval, specflag1-specflag4. Отдельная история - когда вам нужно больше чем 8 операндов. У меня было именно так (9). Решил данную проблему как раз использованием полей reg и specval. Ещё у меня прямо в некоторые инструкции могут быть закодированы: строки (не ссылки, а сами символы), массивы. Сохранить их все в указанные поля не выйдет, поэтому заполняем хотя бы основные, а в остальные закидываем всю возможную информацию, которую сможем использовать потом в эмуляторе и выводе: количество элементов массива. длину строки и т.д. На этом работа анализатора завершена и мы переходим к формированию ссылок на код и данные. Ещё один шаблон, который предстоит заполнить (кидать в одноимённый файл): Имеем несколько совершенно непонятных вызовов API-функций из IDA SDK, которые стоит пояснить. set_immd - указание Иде, что в данной инструкции содержится число. Зачем это ей нужно, не знаю op_num - указание Иде на то, что в данном операнде имеется число, что повлияет на его вывод. Возможно, вызов данной функции также активирует возможности по изменению представления числа через горячие клавиши create_op_data - классная функция, которая автоматически устанавливает для целевого адреса тип данных из операнда. Например, если опкод mov копирует word в переменную в памяти, то тип данных для неё будет установлен как word insn.add_dref - добавление ссылки на данные для конкретной инструкции и операнда. Также можно пометить, это ссылка на чтение (dr_R) или на запись (dr_W), либо простая ссылка (dr_O) insn.add_cref - добавление ссылки на код. Это применяется для прыжков (jump) - fl_JN, вызовов (call) - fl_CN, либо обычного потока исполнения (codeflow) - fl_F Как вы могли заметить, именно здесь и используются указанные нами в ins.cpp флаги (больше нигде). Также, инструкции с условными переходами требуют добавления для них второй ссылки (на случай ложности условия, первая - на случай истинности). Осталось дело за малым - вывести инструкции с операндами на экран. Для этого возьмём ещё один шаблон: Не всё понятно в данном шаблоне сразу. Например, что это за класс out_adcvm_t, который наследуется от outctx_t, а за ним обращение к какому-то странному макросу DECLARE_OUT_FUNCS_WITHOUT_OUTMNEM. К тому же, здесь объявлен ещё один метод out_insn(void), без аргументов. Последнее может сбить с толку, т.к. в файле reg.cpp мы видели следующее обращение к методу out_insn: Видим, что здесь у данного метода есть входной аргумент. В общем, если вкратце, то вся магия как раз в макросе DECLARE_OUT_FUNCS_WITHOUT_OUTMNEM, который создаёт для нас вспомагательные методы out_insn и out_opnd для более удобного вывода данных в листинг, открывая при этом для использования дополнительные функции из класса outctx_t. Более подробно изучить данную логику можно в файле idaidp.hpp вашего IDA SDK. Что, уже? Да, действительно, мы только что закончили писать процессорный модуль для IDA Pro для виртуальной машины из игры Clock Tower - The First Fear. На деле, это оказалось не так и сложно. Конечно, есть нюансы, куда без них (например, мне пришлось повозиться  в выводом строк в кодировке SHIFT-JIS), с операндами, которых больше 8, с массивами. Но, всё это уже позади: модуль успешно дизассемблирует весь загруженный бинарь, справляется как VM из версии для PS1, так и для PC. Да, в моём случае архитектура оказалась относительно простой виртуальной машиной и, в вашем случае, это может быть что-то посложнее, для чего захочется иметь декомпилятор. Тогда, вариант (пока) здесь только один - Ghidra (о том, как я создавал процессорный модуль для неё я расскажу в следующий раз). Я же могу себе позволить выводить около-сишный листинг прямо в окно дизассемблера Иды. Спасибо за внимание. Основной репозиторий Документация по IDA SDK Прошу прощения за большие куски кода не под спойлером - Хабр при попытке вставить блок кода в спойлер (на новом редакторе) просто виснет. Реверс-инженер