• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Меняем промежуточное представление кода на лету в Ghidra

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Когда мы разрабатывали модуль ghidra nodejs для инструмента Ghidra, мы поняли, что не всегда получается корректно реализовать опкод V8 (движка JavaScript, используемого Node.js) на языке описания ассемблерных инструкций SLEIGH. В таких средах исполнения, как V8, JVM и прочие, один опкод может выполнять достаточно сложные действия. Для решения этой проблемы в Ghidra предусмотрен механизм динамической инъекции конструкций P-code — языка промежуточного представления Ghidra. Используя этот механизм, нам удалось превратить вывод декомпилятора из такого:

777d6ce524dc271f1fbf6af1ac8827ff.png

В такой:

558db21e09f253b9871aeac0cb618f15.png

Рассмотрим пример с опкодом CallRuntime. Он вызывает одну функцию из списка т. н. Runtime-функций V8 по индексу (kRuntimeId). Также данная инструкция имеет переменное число аргументов (range — номер начального регистра-аргумента, rangedst — число аргументов). Описание инструкции на языке SLEIGH, который Ghidra использует для определения ассемблерных инструкций, выглядит так:

fd6e26502129abe7ea87b4085be27683.png

Итого для, казалось бы, не очень сложной операции необходимо проделать целую кучу работы.

  1. Поиск нужного названия функции в массиве Runtime-функций по индексу kRuntimeId.
  2. Поскольку аргументы передаются через регистры, необходимо сохранить их предыдущее состояние.
  3. Передача в функцию переменного количества аргументов.
  4. Вызов функции и сохранение результата вызова в аккумулятор.
  5. Восстановление предыдущего состояния регистров.
Если вы знаете, как сделать такое на SLEIGH, пожалуйста, напишите комментарий. А мы решили, что все это (а особенно работу с переменным количеством аргументов-регистров) не очень удобно (если возможно) реализовывать на языке описания процессорных инструкций, и применили механизм динамических инъекций p-code, который как раз для таких случаев реализовали разработчики Ghidra. Что это за механизм?

Можно создать в файле описания ассемблерных инструкций (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);
}
Таким образом, любой опкод, начинающийся с байта 0x53, будет расшифрован как CallRuntime При попытке его декомпиляции будет вызываться обработчик операции CallRuntimeCallOtherс аргументами 2 и 0. Эти аргументы описывают тип инструкции (CallRuntime) и позволят нам написать один обработчик для нескольких похожих инструкций (CallWithSpread, CallUndefinedReceiverи т. п.).

Подготовительная работа​

Добавим класс, через который будет проходить инъекция кода: 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) {
    }
}
V8_PcodeInjectLibraryбудет использоваться не пользовательским кодом, а движком Ghidra, поэтому нам необходимо задать значение параметра pcodeInjectLibraryClassв файле pspec, чтобы движок Ghidra знал, какой класс задействовать для инъекции p-code.
Код:
<?xml version="1.0" encoding="UTF-8"?>
<processor_spec>
  <programcounter register="pc"/>
  <properties>
      <property key="pcodeInjectLibraryClass" value="v8_bytecode.V8_PcodeInjectLibrary"/>
  </properties>
</processor_spec>
Также нам понадобится добавить нашу инструкцию CallRuntimeCallOtherв файл cspec. Ghidra будет вызывать V8_PcodeInjectLibraryтолько для инструкций, определенных таким образом в cspec-файле.
Код:
    <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");
    }
}
Благодаря внесенным нами изменениям Ghidra будет вызывать метод 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);
Далее из аргумента context (контекст инъекции кода) мы можем получить адрес инструкции, который нам пригодится в дальнейшем.
Код:
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();
Реализуем обработку инструкции и генерации Pcode.
Код:
// проверка типа инструкции
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();
Теперь рассмотрим логику работы класса 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 То есть в итоге получается такая конструкция:
Код:
varnodeName = pcodeop(args[0], args[1], …);
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. Пользуйтесь, и удачного вам реверс-инжиниринга!

Автор: @sl4v Slava Moskvin
Positive Technologies
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх