Когда мы разрабатывали модуль ghidra nodejs для инструмента Ghidra, мы поняли, что не всегда получается корректно реализовать опкод V8 (движка JavaScript, используемого Node.js) на языке описания ассемблерных инструкций SLEIGH. В таких средах исполнения, как V8, JVM и прочие, один опкод может выполнять достаточно сложные действия. Для решения этой проблемы в Ghidra предусмотрен механизм динамической инъекции конструкций P-code — языка промежуточного представления Ghidra. Используя этот механизм, нам удалось превратить вывод декомпилятора из такого: В такой: Рассмотрим пример с опкодом CallRuntime. Он вызывает одну функцию из списка т. н. Runtime-функций V8 по индексу (kRuntimeId). Также данная инструкция имеет переменное число аргументов (range — номер начального регистра-аргумента, rangedst — число аргументов). Описание инструкции на языке SLEIGH, который Ghidra использует для определения ассемблерных инструкций, выглядит так: Итого для, казалось бы, не очень сложной операции необходимо проделать целую кучу работы. Поиск нужного названия функции в массиве Runtime-функций по индексу kRuntimeId. Поскольку аргументы передаются через регистры, необходимо сохранить их предыдущее состояние. Передача в функцию переменного количества аргументов. Вызов функции и сохранение результата вызова в аккумулятор. Восстановление предыдущего состояния регистров. Если вы знаете, как сделать такое на SLEIGH, пожалуйста, напишите комментарий. А мы решили, что все это (а особенно работу с переменным количеством аргументов-регистров) не очень удобно (если возможно) реализовывать на языке описания процессорных инструкций, и применили механизм динамических инъекций p-code, который как раз для таких случаев реализовали разработчики Ghidra. Что это за механизм? Можно создать в файле описания ассемблерных инструкций (slaspec) специальную пользовательскую операцию, например CallRuntimeCallOther. Далее, изменив конфигурацию вашего модуля (подробнее об этом — ниже), вы можете сделать так, чтобы при нахождении в коде данной инструкции Ghidra передавала бы обработку в Java динамически, и уже на языке Java написать обработчик, который будет динамически формировать p-code для инструкции, пользуясь всей гибкостью Java. Рассмотрим подробно, как это сделать. Опишем опкод CallRuntime следующим образом. Подробнее об описании процессорных инструкций на языке SLEIGH все можете узнать из статьи Создаем процессорный модуль под Ghidra на примере байткода v8. Определим служебную операцию: И опишем саму инструкцию: Таким образом, любой опкод, начинающийся с байта 0x53, будет расшифрован как CallRuntime При попытке его декомпиляции будет вызываться обработчик операции CallRuntimeCallOtherс аргументами 2 и 0. Эти аргументы описывают тип инструкции (CallRuntime) и позволят нам написать один обработчик для нескольких похожих инструкций (CallWithSpread, CallUndefinedReceiverи т. п.). Добавим класс, через который будет проходить инъекция кода: V8_PcodeInjectLibrary. Этот класс мы унаследуем от ghidra.program.model.lang.PcodeInjectLibrary который реализует большую часть необходимых для инъекции p-code методов. Начнем написание класса V8_PcodeInjectLibraryс такого шаблона: V8_PcodeInjectLibraryбудет использоваться не пользовательским кодом, а движком Ghidra, поэтому нам необходимо задать значение параметра pcodeInjectLibraryClassв файле pspec, чтобы движок Ghidra знал, какой класс задействовать для инъекции p-code. Также нам понадобится добавить нашу инструкцию CallRuntimeCallOtherв файл cspec. Ghidra будет вызывать V8_PcodeInjectLibraryтолько для инструкций, определенных таким образом в cspec-файле. После всех этих нехитрых процедур (которые, к слову, на момент создания нашего модуля почти не были описаны в документации) можно перейти к написанию кода. Создадим HashSet, в котором будем хранить реализованные нами инструкции. Также мы создадим и проинициализируем член нашего класса — переменную language. Данный код сохраняет операцию CallRuntimeCallOtherв наборе поддерживаемых операций, а также выполняет ряд служебных действий, в которые мы не будем подробно вдаваться. Благодаря внесенным нами изменениям Ghidra будет вызывать метод getPayloadнашего класса V8_PcodeInjectLibraryкаждый раз при попытке декомпиляции инструкции CallRuntimeCallOther Создадим данный метод, который при наличии инструкции в списке реализованных операций будет создавать объект класса V8_InjectCallVariadic(этот класс мы реализуем чуть позже) и возвращать его. Основная работа по динамическому созданию p-code будет происходить в классе V8_InjectCallVariadic. Давайте его создадим и опишем типы операций. Как нетрудно догадаться, нам необходимо разработать нашу реализацию метода getPcode Для начала создадим объект pCode класса V8_PcodeOpEmitter Этот класс будет помогать нам создавать инструкции pCode (позже мы ознакомимся с ним подробнее). Далее из аргумента context (контекст инъекции кода) мы можем получить адрес инструкции, который нам пригодится в дальнейшем. С помощью данного адреса мы получим объект текущей инструкции: Также с помощью аргумента contextмы получим значения аргументов, которые ранее описывали на языке SLEIGH. Реализуем обработку инструкции и генерации Pcode. Теперь рассмотрим логику работы класса V8_PcodeOpEmitter (https://github.com/PositiveTechnologies/ghidra_nodejs/blob/main/src/main/java/v8_bytecode/V8_PcodeOpEmitter.java), который во многом основан на аналогичном классе модуля для JVM. Данный класс генерирует p-code операции с помощью ряда методов. Рассмотрим их в порядке обращения к ним в нашем коде. emitAssignVarnodeFromPcodeOpCall(String varnodeName, int size, String pcodeop, String... args) Для понимания работы данного метода сначала рассмотрим понятие Varnode —один из основных элементов p-code, по сути представляющий собой любую переменную, задействованную в p-code. Регистры, локальные переменные — всё это Varnode. Вернемся к методу. Данный метод генерирует p-code для вызова функции pcodeopс аргументами argsи сохраняет результат работы функции в varnodeName То есть в итоге получается такая конструкция: emitPushCat1Value(String valueName) и emitPopCat1Value (String valueName) Генерирует p-code для аналогов ассемблерных операций push и pop соответственно с Varnode valueName. emitAssignVarnodeFromVarnode (String varnodeOutName, String varnodeInName, int size) Генерирует p-code для операции присвоения значения varnodeOutName = varnodeInName emitVarnodeCall (String target, int size) Генерирует P-Code для вызова функции target. Благодаря вышеизложенному механизму у нас получилось значительно улучшить вывод декомплилятора Ghidra. В итоге динамическая генерация p-code стала еще одним кирпичиком в нашем большом инструменте — модуле для анализа скомпилированного bytenode скриптов Node.JS. Исходный код модуля доступен в нашем репозитории на github.com. Пользуйтесь, и удачного вам реверс-инжиниринга! Если у вас остались какие-то вопросы, задавайте их в комментариях - буду рад ответить. Большое спасибо за исследование особенностей Node.js и разработку модуля моим коллегам: Владимиру Кононовичу, Наталье Тляповой, Сергею Федонину. Hacker