Когда мы разрабатывали модуль ghidra nodejs для инструмента Ghidra, мы поняли, что не всегда получается корректно реализовать опкод V8 (движка JavaScript, используемого Node.js) на языке описания ассемблерных инструкций SLEIGH. В таких средах исполнения, как V8, JVM и прочие, один опкод может выполнять достаточно сложные действия. Для решения этой проблемы в Ghidra предусмотрен механизм динамической инъекции конструкций P-code — языка промежуточного представления Ghidra. Используя этот механизм, нам удалось превратить вывод декомпилятора из такого:
В такой:
Рассмотрим пример с опкодом CallRuntime. Он вызывает одну функцию из списка т. н. Runtime-функций V8 по индексу (kRuntimeId). Также данная инструкция имеет переменное число аргументов (range — номер начального регистра-аргумента, rangedst — число аргументов). Описание инструкции на языке SLEIGH, который Ghidra использует для определения ассемблерных инструкций, выглядит так:
Итого для, казалось бы, не очень сложной операции необходимо проделать целую кучу работы.
Можно создать в файле описания ассемблерных инструкций (slaspec) специальную пользовательскую операцию, например CallRuntimeCallOther. Далее, изменив конфигурацию вашего модуля (подробнее об этом — ниже), вы можете сделать так, чтобы при нахождении в коде данной инструкции Ghidra передавала бы обработку в Java динамически, и уже на языке Java написать обработчик, который будет динамически формировать p-code для инструкции, пользуясь всей гибкостью Java.
Рассмотрим подробно, как это сделать.
Определим служебную операцию:
И опишем саму инструкцию:
Таким образом, любой опкод, начинающийся с байта 0x53, будет расшифрован как CallRuntime При попытке его декомпиляции будет вызываться обработчик операции CallRuntimeCallOtherс аргументами 2 и 0. Эти аргументы описывают тип инструкции (CallRuntime) и позволят нам написать один обработчик для нескольких похожих инструкций (CallWithSpread, CallUndefinedReceiverи т. п.).
Начнем написание класса V8_PcodeInjectLibraryс такого шаблона:
V8_PcodeInjectLibraryбудет использоваться не пользовательским кодом, а движком Ghidra, поэтому нам необходимо задать значение параметра pcodeInjectLibraryClassв файле pspec, чтобы движок Ghidra знал, какой класс задействовать для инъекции p-code.
Также нам понадобится добавить нашу инструкцию CallRuntimeCallOtherв файл cspec. Ghidra будет вызывать V8_PcodeInjectLibraryтолько для инструкций, определенных таким образом в cspec-файле.
После всех этих нехитрых процедур (которые, к слову, на момент создания нашего модуля почти не были описаны в документации) можно перейти к написанию кода.
Создадим HashSet, в котором будем хранить реализованные нами инструкции. Также мы создадим и проинициализируем член нашего класса — переменную language. Данный код сохраняет операцию
Благодаря внесенным нами изменениям Ghidra будет вызывать метод
Как нетрудно догадаться, нам необходимо разработать нашу реализацию метода
Далее из аргумента context (контекст инъекции кода) мы можем получить адрес инструкции, который нам пригодится в дальнейшем.
С помощью данного адреса мы получим объект текущей инструкции:
Также с помощью аргумента
Реализуем обработку инструкции и генерации 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.
Вернемся к методу. Данный метод генерирует
emitPushCat1Value(String valueName) и emitPopCat1Value (String valueName)
Генерирует p-code для аналогов ассемблерных операций push и pop соответственно с Varnode valueName.
emitAssignVarnodeFromVarnode (String varnodeOutName, String varnodeInName, int size)
Генерирует p-code для операции присвоения значения
emitVarnodeCall (String target, int size)
Генерирует P-Code для вызова функции target.
Автор: @sl4v Slava Moskvin
Positive Technologies
В такой:
Рассмотрим пример с опкодом CallRuntime. Он вызывает одну функцию из списка т. н. Runtime-функций V8 по индексу (kRuntimeId). Также данная инструкция имеет переменное число аргументов (range — номер начального регистра-аргумента, rangedst — число аргументов). Описание инструкции на языке SLEIGH, который Ghidra использует для определения ассемблерных инструкций, выглядит так:
Итого для, казалось бы, не очень сложной операции необходимо проделать целую кучу работы.
- Поиск нужного названия функции в массиве Runtime-функций по индексу kRuntimeId.
- Поскольку аргументы передаются через регистры, необходимо сохранить их предыдущее состояние.
- Передача в функцию переменного количества аргументов.
- Вызов функции и сохранение результата вызова в аккумулятор.
- Восстановление предыдущего состояния регистров.
Можно создать в файле описания ассемблерных инструкций (slaspec) специальную пользовательскую операцию, например CallRuntimeCallOther. Далее, изменив конфигурацию вашего модуля (подробнее об этом — ниже), вы можете сделать так, чтобы при нахождении в коде данной инструкции Ghidra передавала бы обработку в Java динамически, и уже на языке Java написать обработчик, который будет динамически формировать p-code для инструкции, пользуясь всей гибкостью Java.
Рассмотрим подробно, как это сделать.
Создание служебной операции SLEIGH
Опишем опкод CallRuntime следующим образом. Подробнее об описании процессорных инструкций на языке SLEIGH все можете узнать из статьи Создаем процессорный модуль под Ghidra на примере байткода v8.Определим служебную операцию:
Код:
define pcodeop CallRuntimeCallOther;
Код:
:CallRuntime [kRuntimeId], range^rangedst is op = 0x53; kRuntimeId; range; rangedst {
CallRuntimeCallOther(2, 0);
}
Подготовительная работа
Добавим класс, через который будет проходить инъекция кода: V8_PcodeInjectLibrary. Этот класс мы унаследуем от ghidra.program.model.lang.PcodeInjectLibrary который реализует большую часть необходимых для инъекции p-code методов.Начнем написание класса V8_PcodeInjectLibraryс такого шаблона:
Код:
package v8_bytecode;
import …
public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {
public V8_PcodeInjectLibrary(SleighLanguage l) {
}
}
Код:
<?xml version="1.0" encoding="UTF-8"?>
<processor_spec>
<programcounter register="pc"/>
<properties>
<property key="pcodeInjectLibraryClass" value="v8_bytecode.V8_PcodeInjectLibrary"/>
</properties>
</processor_spec>
Код:
<callotherfixup targetop="CallRuntimeCallOther">
<pcode dynamic="true">
<input name=«outsize"/>
</pcode>
</callotherfixup>
Создадим HashSet, в котором будем хранить реализованные нами инструкции. Также мы создадим и проинициализируем член нашего класса — переменную language. Данный код сохраняет операцию
CallRuntimeCallOther в наборе поддерживаемых операций, а также выполняет ряд служебных действий, в которые мы не будем подробно вдаваться.
Код:
public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {
private Set<String> implementedOps;
private SleighLanguage language;
public V8_PcodeInjectLibrary(SleighLanguage l) {
super(l);
language = l;
String translateSpec = language.buildTranslatorTag(language.getAddressFactory(),
getUniqueBase(), language.getSymbolTable());
PcodeParser parser = null;
try {
parser = new PcodeParser(translateSpec);
}
catch (JDOMException e1) {
e1.printStackTrace();
}
implementedOps = new HashSet<>();
implementedOps.add("CallRuntimeCallOther");
}
}
getPayload нашего класса V8_PcodeInjectLibrary каждый раз при попытке декомпиляции инструкции CallRuntimeCallOther. Создадим данный метод, который при наличии инструкции в списке реализованных операций будет создавать объект класса V8_InjectCallVariadic(этот класс мы реализуем чуть позже) и возвращать его.
Код:
@Override
/**
* This method is called by DecompileCallback.getPcodeInject.
*/
public InjectPayload getPayload(int type, String name, Program program, String context) {
if (type == InjectPayload.CALLMECHANISM_TYPE) {
return null;
}
if (!implementedOps.contains(name)) {
return super.getPayload(type, name, program, context);
}
V8_InjectPayload payload = null;
switch (name) {
case ("CallRuntimeCallOther"):
payload = new V8_InjectCallVariadic("", language, 0);
break;
default:
return super.getPayload(type, name, program, context);
}
return payload;
}
Генерация p-code
Основная работа по динамическому созданию p-code будет происходить в классе V8_InjectCallVariadic. Давайте его создадим и опишем типы операций.
Код:
package v8_bytecode;
import …
public class V8_InjectCallVariadic extends V8_InjectPayload {
public V8_InjectCallVariadic(String sourceName, SleighLanguage language, long uniqBase) {
super(sourceName, language, uniqBase);
}
// Типы операций. В данном примере мы рассматриваем RUNTIMETYPE
int INTRINSICTYPE = 1;
int RUNTIMETYPE = 2;
int PROPERTYTYPE = 3;
@Override
public PcodeOp[] getPcode(Program program, InjectContext context) {
}
@Override
public String getName() {
return "InjectCallVariadic";
}
}
getPcode Для начала создадим объект pCode класса V8_PcodeOpEmitter Этот класс будет помогать нам создавать инструкции pCode (позже мы ознакомимся с ним подробнее).
Код:
V8_PcodeOpEmitter pCode = new V8_PcodeOpEmitter(language, context.baseAddr, uniqueBase);
Код:
Address opAddr = context.baseAddr;
Код:
Instruction instruction = program.getListing().getInstructionAt(opAddr);
context мы получим значения аргументов, которые ранее описывали на языке SLEIGH.
Код:
Integer funcType = (int) context.inputlist.get(0).getOffset();
Integer receiver = (int) context.inputlist.get(1).getOffset();
Код:
// проверка типа инструкции
if (funcType != PROPERTYTYPE) {
// получаем kRuntimeId — индекс вызываемой функции
Integer index = (int) instruction.getScalar(0).getValue();
// сгенерируем Pcode для вызова инструкции cpool с помощью объекта pCode класса V8_PcodeOpEmitter. Подробнее остановимся на нем ниже.
pCode.emitAssignVarnodeFromPcodeOpCall("call_target", 4, "cpool", "0", "0x" + opAddr.toString(), index.toString(),
funcType.toString());
}
…
// получаем аргумент «диапазон регистров»
Object[] tOpObjects = instruction.getOpObjects(2);
// get caller args count to save only necessary ones
Object[] opObjects;
Register recvOp = null;
if (receiver == 1) {
…
}
else {
opObjects = new Object[tOpObjects.length];
System.arraycopy(tOpObjects, 0, opObjects, 0, tOpObjects.length);
}
// получаем количество аргументов вызываемой функции
try {
callerParamsCount = program.getListing().getFunctionContaining(opAddr).getParameterCount();
}
catch(Exception e) {
callerParamsCount = 0;
}
// сохраняем старые значения регистров вида aN на стеке. Это необходимо для того, чтобы Ghidra лучше распознавала количество аргументов вызываемой функции
Integer callerArgIndex = 0;
for (; callerArgIndex < callerParamsCount; callerArgIndex++) {
pCode.emitPushCat1Value("a" + callerArgIndex);
}
// сохраняем аргументы вызываемой функции в регистры вида aN
Integer argIndex = opObjects.length;
for (Object o: opObjects) {
argIndex--;
Register currentOp = (Register)o;
pCode.emitAssignVarnodeFromVarnode("a" + argIndex, currentOp.toString(), 4);
}
// вызов функции
pCode.emitVarnodeCall("call_target", 4);
// восстанавливаем старые значения регистров со стека
while (callerArgIndex > 0) {
callerArgIndex--;
pCode.emitPopCat1Value("a" + callerArgIndex);
}
// возвращаем массив P-Code операций
return pCode.getPcodeOps();
emitAssignVarnodeFromPcodeOpCall(String varnodeName, int size, String pcodeop, String... args)
Для понимания работы данного метода сначала рассмотрим понятие Varnode —один из основных элементов p-code, по сути представляющий собой любую переменную, задействованную в p-code. Регистры, локальные переменные — всё это Varnode.
Вернемся к методу. Данный метод генерирует
p-code для вызова функции pcodeopс аргументами args и сохраняет результат работы функции в varnodeName То есть в итоге получается такая конструкция:
Код:
varnodeName = pcodeop(args[0], args[1], …);
Генерирует p-code для аналогов ассемблерных операций push и pop соответственно с Varnode valueName.
emitAssignVarnodeFromVarnode (String varnodeOutName, String varnodeInName, int size)
Генерирует p-code для операции присвоения значения
varnodeOutName = varnodeInNameemitVarnodeCall (String target, int size)
Генерирует P-Code для вызова функции target.
Заключение
Благодаря вышеизложенному механизму у нас получилось значительно улучшить вывод декомплилятора Ghidra. В итоге динамическая генерация p-code стала еще одним кирпичиком в нашем большом инструменте — модуле для анализа скомпилированного bytenode скриптов Node.JS. Исходный код модуля доступен в нашем репозитории на github.com. Пользуйтесь, и удачного вам реверс-инжиниринга!Автор: @sl4v Slava Moskvin
Positive Technologies