Модернизация GHIDRA. Загрузчик для ромов Sega Mega Drive

  • Tutorial


Приветствую вас, товарищи. Не слышал о пока-ещё-не-опенсорсной GHIDRA, наверное, только глухой/слепой/немой/без_интернета реверс-инженер. Её возможности из коробки поражают: декомпиляторы для всех поддерживаемых процессоров, простое добавление новых архитектур (с сразу же активной декомпиляцией благодаря грамотному преобразованию в IR), куча скриптов упрощающих жизнь, возможность Undo/Redo… И это только очень малая часть всех предоставляемых возможностей. Сказать что я был впечатлён — это практически ничего не сказать.


Так вот, в этой статье я хотел бы рассказать вам, как я написал свой первый модуль для GHIDRA — загрузчик ромов игр для Sega Mega Drive / Genesis. Чтобы написать его мне понадобилась… всего пара-тройка часов! Поехали.


А что же IDA?

На понимание процесса написания загрузчиков для IDA я потратил когда-то несколько дней. Тогда это была версия 6.5, кажется, а в те времена с документацией по SDK было очень много проблем.


Подготавливаем среду разработки


Разработчики GHIDRA продумали практически всё (Ильфак, где ты был раньше?). И, как раз для упрощения реализации нового функционала, ими был разработан плагин для EclipseGhidraDev, который фактически "помогает" писать код. Плагин интегрируется в среду разработки, и позволяет в несколько кликов создавать шаблоны проектов для скриптов, загрузчиков, процессорных модулей и расширений для них, а также — модули экспорта (как я понял, это какой-либо экспорт данных из проекта).


Для того, чтобы установить плагин, качаем Eclipse для Java, жмём Help -> Install New Software..., далее жмём кнопку Add, и открываем диалог выбора архива с плагином кнопкой Archive.... Архив с GhidraDev находится в каталоге $(GHIDRA)/Extensions/Eclipse/GhidraDev. Выбираем его, нажимаем кнопку Add.



В появившемся списке ставим галку на Ghidra, жмём Next >, соглашаемся с соглашениями, жмём Install Anyway (т.к. у плагина нет подписи), и перезапускаем Eclipse.



Итого, в менюшке IDE появится новый пункт GhidraDev для удобного создания и дистрибуции ваших проектов (конечно, можно создавать и через обычный мастер новых проектов Eclipse). Кроме этого, у нас появляется возможность отлаживать разрабатываемый плагин или скрипт.



А где же отладчик приложений?

Что очень бесит в ситуации с GHIDRA, так это долбаные скопипасченые хайповые статьи, содержащие практически один и тот же материал, который, к тому же, не соответствует действительности. Пример? Да, пожалуйста:


The current version of the tool is 9.0. and the tool has options to include additional functionality such as Cryptanalysis, interaction with OllyDbg, the Ghidra Debugger.

И где это всё? Нету!


Второй момент: опенсорсность. По факту, она почти есть, но её практически нет. В поставке GHIDRA имеются исходники компонентов, которые были написаны на Java, но, если посмотреть Gradle-скрипты, можно увидеть, что там есть зависимости от кучи внешних проектов из пока ещё секретных лабораторий репозиториев NSA.
На момент написания статьи, исходников декомпилятора и SLEIGH (это утилита для компиляции описаний процессорных модулей и преобразований в IR) нету.


Ну да ладно, я отвлёкся что-то.


Итак, давайте всё таки создадим новый проект в Eclipse.


Создаём проект загрузчика


Жмём GhidraDev -> New -> Ghidra Module Project...


Указываем имя проекта (учитываем, что к именам файлов будут доклеиваться слова типа Loader, и, чтобы не получить что-то типа sega_loaderLoader.java, называем соответствующим образом).



Жмём Next >. Здесь выставляем галки напротив категорий, которые нам необходимы. В моём случае это только Loader. Жмём Next >.



Здесь указываем путь к каталогу с Гидрой. Жмём Next >.



GHIDRA позволяет писать скрипты на питоне (через Jython). Я буду писать на Java, поэтому галку не ставлю. Жму Finish.



Пишем код


Дерево пустого проекта выглядит внушительно:



Все файлы с java-кодом лежат в ветке /src/main/java:



getName()


Для начала, давайте выберем имя для загрузчика. Его возвращает метод getName():


@Override
public String getName() {
    return "Sega Mega Drive / Genesis Loader";
}

findSupportedLoadSpecs()


Метод findSupportedLoadSpecs() решает (на основе данных, которые содержатся в бинарном файле), какой процессорный модуль должен быть использован для дизассемблирования (так же как и в IDA). В терминологии GHIDRA это называется Compiler Language. В него входят: процессор, endianness, битность и компилятор (если известен).


Данный метод возвращает список поддерживаемых архитектур и языков. Если же данные не того формата, мы просто вернём пустой список.


Итак, в случае с Sega Mega Drive, по смещению 0x100 заголовка чаще всего присутствует слово "SEGA" (это не обязательное условие, но выполняется в 99% случаев). Нужно проверить, имеется ли эта строка в импортируемом файле. Для этого, на вход findSupportedLoadSpecs() подаётся ByteProvider provider, с помощью которого мы и будем работать с файлом.


Создаём объект BinaryReader, для удобства чтения данных из файла:


BinaryReader reader = new BinaryReader(provider, false);

Аргумент false в данном случае указывает на использование Big Endian при чтении. Теперь давайте прочитаем строку. Для этого воспользуемся методом readAsciiString(offset, size) у объекта reader:


reader.readAsciiString(0x100, 4).equals(new String("SEGA"))

Если equals() вернёт true, значит мы имеем дело с сеговским ромом, и в список List<LoadSpec> loadSpecs = new ArrayList<>(); можно будет добавить мотороловский m68k. Для этого создаём новый объект типа LoadSpec, конструктор которого принимает на вход объект загрузчика (в нашем случае это this), ImageBase, в который будет грузиться ROM, объект типа LanguageCompilerSpecPair и флаг — предпочтительный ли этот LoadSpec среди остальных в списке (да, в списке может быть не один LoadSpec).


Формат конструктора у LanguageCompilerSpecPair следующий:


  1. Первый аргумент — languageID — строка вида "ProcessorName:Endianness:Bits:ExactCpu". В моём случае это должна быть строка "68000:BE:32:MC68020" (к сожалению, ровно MC68000 в поставке нет, но, это не такая уж и проблема). ExactCpu может быть и default
  2. Второй аргумент — compilerSpecID — найти то, что здесь необходимо указывать, можно в каталоге с процессорными описаниями Гидры ($(GHIDRA)/Ghidra/Processors/68000/data/languages) в файле 68000.opinion. Видим, что здесь указаны только default. Собственно, его и указываем

В итоге, имеем следующий код (как видим, пока ничего сложного):


@Override
public Collection<LoadSpec> findSupportedLoadSpecs(ByteProvider provider) throws IOException {
    List<LoadSpec> loadSpecs = new ArrayList<>();

    BinaryReader reader = new BinaryReader(provider, false);

    if (reader.readAsciiString(0x100, 4).equals(new String("SEGA"))) {
        loadSpecs.add(new LoadSpec(this, 0, new LanguageCompilerSpecPair("68000:BE:32:MC68020", "default"), true));
    }

    return loadSpecs;
}

Различие между IDA и GHIDRA в плане модулей

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


В IDA же это отдельный проект под каждый тип дополнения.


Насколько это удобнее? На мой взгляд, у GHIDRA — в разы!


load()


В этом методе мы будем создавать сегменты, обрабатывать входные данные, создавать код и заранее известные метки. Для этого, нам в помощь на вход подаются следующие объекты:


  1. ByteProvider provider: его мы уже знаем. Работа с бинарными данными файла
  2. LoadSpec loadSpec: спецификация архитектуры, которая была выбрана на этапе импорта файла методом findSupportedLoadSpecs. Нужно, если мы, к примеру, умеем работать с несколькими форматами данных в одном модуле. Удобно
  3. List<Option> options: список опций (включая кастомные). С ними я пока не научился работать
  4. Program program: основной объект, который предоставляет доступ ко всему необходимому функционалу: листинг, адресное пространство, сегменты, метки, создание массивов и прочее
  5. MemoryConflictHandler handler и TaskMonitor monitor: напрямую с ними нам редко придётся работать (обычно, достаточно передавать эти объекты в уже готовые методы)
  6. MessageLog log: собственно, логгер

Итак, для начала создадим некоторые объекты, которые упростят нам работу с сущностями GHIDRA и имеющимися данными. Конечно, нам обязательно понадобится BinaryReader:


BinaryReader reader = new BinaryReader(provider, false);

Далее. Нам очень пригодится и упростит практически всё объект класса FlatProgramAPI (далее вы увидите, что с его помощью можно делать):


FlatProgramAPI fpa = new FlatProgramAPI(program, monitor);

Заголовок рома


Для начала определимся, что из себя представляет заголовок обычного сеговского рома. В первых 0x100 байтах идёт таблица из 64-х DWORD-указателей на вектора, например: Reset, Trap, DivideByZero, VBLANK и прочие.


Далее идёт структура с именем рома, регионами, адресами начала и конца блоков ROM и RAM, чексумма (поле проверяется по желанию разработчиков, а не приставкой) и другая информация.


Давайте создадим java-классы для работы с этими структурами, а также для реализации типов данных, которые будут добавлены в список структур.


VectorsTable


Создаём новый класс VectorsTable, и, внимание, указываем, что он реализует интерфейс StructConverter. В этом классе мы будем хранить адреса векторов (for future use) и их имена.



Объявляем список имён векторов и их количество:


private static final int VECTORS_SIZE = 0x100;
private static final int VECTORS_COUNT = VECTORS_SIZE / 4;

private static final String[] VECTOR_NAMES = {
    "SSP", "Reset", "BusErr", "AdrErr", "InvOpCode", "DivBy0", "Check", "TrapV", "GPF", "Trace",
    "Reserv0", "Reserv1", "Reserv2", "Reserv3", "Reserv4", "BadInt", "Reserv10", "Reserv11",
    "Reserv12", "Reserv13", "Reserv14", "Reserv15", "Reserv16", "Reserv17", "BadIRQ", "IRQ1",
    "EXT", "IRQ3", "HBLANK", "IRQ5", "VBLANK", "IRQ7", "Trap0", "Trap1", "Trap2", "Trap3", "Trap4",
    "Trap5", "Trap6", "Trap7", "Trap8", "Trap9", "Trap10", "Trap11", "Trap12", "Trap13","Trap14",
    "Trap15", "Reserv30", "Reserv31", "Reserv32", "Reserv33", "Reserv34", "Reserv35", "Reserv36",
    "Reserv37", "Reserv38", "Reserv39", "Reserv3A", "Reserv3B", "Reserv3C", "Reserv3D", "Reserv3E",
    "Reserv3F"
};

Создаём отдельный класс для хранения адреса и имени вектора:


package sega;

import ghidra.program.model.address.Address;

public class VectorFunc {
    private Address address;
    private String name;

    public VectorFunc(Address address, String name) {
        this.address = address;
        this.name = name;
    }

    public Address getAddress() {
        return address;
    }

    public String getName() {
        return name;
    }
}

Список векторов будем хранить в массиве vectors:


private VectorFunc[] vectors;

Констуктор для VectorsTable у нас будет принимать:


  1. FlatProgramAPI fpa для преобразования long адресов в тип данных Address Гидры (по сути, этот тип данных дополняет простое числовое значение адреса привязкой его к ещё одной фишке — адресному пространству)
  2. BinaryReader reader — чтение двордов

У объекта fpa есть метод toAddr(), а у reader есть setPointerIndex() и readNextUnsignedInt(). В принципе, больше ничего не требуется. Получаем код:


public VectorsTable(FlatProgramAPI fpa, BinaryReader reader) throws IOException {
    if (reader.length() < VECTORS_COUNT) {
        return;
    }

    reader.setPointerIndex(0);
    vectors = new VectorFunc[VECTORS_COUNT];

    for (int i = 0; i < VECTORS_COUNT; ++i) {
        vectors[i] = new VectorFunc(fpa.toAddr(reader.readNextUnsignedInt()), VECTOR_NAMES[i]);
    }
}

Метод toDataType(), который нам требуется переопределить для реализации структуры, должен вернуть объект Structure, в котором должны быть объявлены имена полей структуры, их размеры, и комментарии к каждому полю (можно использовать null):


@Override
public DataType toDataType() {
    Structure s = new StructureDataType("VectorsTable", 0);

    for (int i = 0; i < VECTORS_COUNT; ++i) {
        s.add(POINTER, 4, VECTOR_NAMES[i], null);
    }

    return s;
}

Ну, и, давайте реализуем методы для получения каждого из векторов, либо всего списка целиком (куча шаблонного кода):


Остальные методы
    public VectorFunc[] getVectors() {
        return vectors;
    }

    public VectorFunc getSSP() {
        if (vectors.length < 1) {
            return null;
        }
        return vectors[0];
    }

    public VectorFunc getReset() {
        if (vectors.length < 2) {
            return null;
        }
        return vectors[1];
    }

    public VectorFunc getBusErr() {
        if (vectors.length < 3) {
            return null;
        }
        return vectors[2];
    }

    public VectorFunc getAdrErr() {
        if (vectors.length < 4) {
            return null;
        }
        return vectors[3];
    }

    public VectorFunc getInvOpCode() {
        if (vectors.length < 5) {
            return null;
        }
        return vectors[4];
    }

    public VectorFunc getDivBy0() {
        if (vectors.length < 6) {
            return null;
        }
        return vectors[5];
    }

    public VectorFunc getCheck() {
        if (vectors.length < 7) {
            return null;
        }
        return vectors[6];
    }

    public VectorFunc getTrapV() {
        if (vectors.length < 8) {
            return null;
        }
        return vectors[7];
    }

    public VectorFunc getGPF() {
        if (vectors.length < 9) {
            return null;
        }
        return vectors[8];
    }

    public VectorFunc getTrace() {
        if (vectors.length < 10) {
            return null;
        }
        return vectors[9];
    }

    public VectorFunc getReserv0() {
        if (vectors.length < 11) {
            return null;
        }
        return vectors[10];
    }

    public VectorFunc getReserv1() {
        if (vectors.length < 12) {
            return null;
        }
        return vectors[11];
    }

    public VectorFunc getReserv2() {
        if (vectors.length < 13) {
            return null;
        }
        return vectors[12];
    }

    public VectorFunc getReserv3() {
        if (vectors.length < 14) {
            return null;
        }
        return vectors[13];
    }

    public VectorFunc getReserv4() {
        if (vectors.length < 15) {
            return null;
        }
        return vectors[14];
    }

    public VectorFunc getBadInt() {
        if (vectors.length < 16) {
            return null;
        }
        return vectors[15];
    }

    public VectorFunc getReserv10() {
        if (vectors.length < 17) {
            return null;
        }
        return vectors[16];
    }

    public VectorFunc getReserv11() {
        if (vectors.length < 18) {
            return null;
        }
        return vectors[17];
    }

    public VectorFunc getReserv12() {
        if (vectors.length < 19) {
            return null;
        }
        return vectors[18];
    }

    public VectorFunc getReserv13() {
        if (vectors.length < 20) {
            return null;
        }
        return vectors[19];
    }

    public VectorFunc getReserv14() {
        if (vectors.length < 21) {
            return null;
        }
        return vectors[20];
    }

    public VectorFunc getReserv15() {
        if (vectors.length < 22) {
            return null;
        }
        return vectors[21];
    }

    public VectorFunc getReserv16() {
        if (vectors.length < 23) {
            return null;
        }
        return vectors[22];
    }

    public VectorFunc getReserv17() {
        if (vectors.length < 24) {
            return null;
        }
        return vectors[23];
    }

    public VectorFunc getBadIRQ() {
        if (vectors.length < 25) {
            return null;
        }
        return vectors[24];
    }

    public VectorFunc getIRQ1() {
        if (vectors.length < 26) {
            return null;
        }
        return vectors[25];
    }

    public VectorFunc getEXT() {
        if (vectors.length < 27) {
            return null;
        }
        return vectors[26];
    }

    public VectorFunc getIRQ3() {
        if (vectors.length < 28) {
            return null;
        }
        return vectors[27];
    }

    public VectorFunc getHBLANK() {
        if (vectors.length < 29) {
            return null;
        }
        return vectors[28];
    }

    public VectorFunc getIRQ5() {
        if (vectors.length < 30) {
            return null;
        }
        return vectors[29];
    }

    public VectorFunc getVBLANK() {
        if (vectors.length < 31) {
            return null;
        }
        return vectors[30];
    }

    public VectorFunc getIRQ7() {
        if (vectors.length < 32) {
            return null;
        }
        return vectors[31];
    }

    public VectorFunc getTrap0() {
        if (vectors.length < 33) {
            return null;
        }
        return vectors[32];
    }

    public VectorFunc getTrap1() {
        if (vectors.length < 34) {
            return null;
        }
        return vectors[33];
    }

    public VectorFunc getTrap2() {
        if (vectors.length < 35) {
            return null;
        }
        return vectors[34];
    }

    public VectorFunc getTrap3() {
        if (vectors.length < 36) {
            return null;
        }
        return vectors[35];
    }

    public VectorFunc getTrap4() {
        if (vectors.length < 37) {
            return null;
        }
        return vectors[36];
    }

    public VectorFunc getTrap5() {
        if (vectors.length < 38) {
            return null;
        }
        return vectors[37];
    }

    public VectorFunc getTrap6() {
        if (vectors.length < 39) {
            return null;
        }
        return vectors[38];
    }

    public VectorFunc getTrap7() {
        if (vectors.length < 40) {
            return null;
        }
        return vectors[39];
    }

    public VectorFunc getTrap8() {
        if (vectors.length < 41) {
            return null;
        }
        return vectors[40];
    }

    public VectorFunc getTrap9() {
        if (vectors.length < 42) {
            return null;
        }
        return vectors[41];
    }

    public VectorFunc getTrap10() {
        if (vectors.length < 43) {
            return null;
        }
        return vectors[42];
    }

    public VectorFunc getTrap11() {
        if (vectors.length < 44) {
            return null;
        }
        return vectors[43];
    }

    public VectorFunc getTrap12() {
        if (vectors.length < 45) {
            return null;
        }
        return vectors[44];
    }

    public VectorFunc getTrap13() {
        if (vectors.length < 46) {
            return null;
        }
        return vectors[45];
    }

    public VectorFunc getTrap14() {
        if (vectors.length < 47) {
            return null;
        }
        return vectors[46];
    }

    public VectorFunc getTrap15() {
        if (vectors.length < 48) {
            return null;
        }
        return vectors[47];
    }

    public VectorFunc getReserv30() {
        if (vectors.length < 49) {
            return null;
        }
        return vectors[48];
    }

    public VectorFunc getReserv31() {
        if (vectors.length < 50) {
            return null;
        }
        return vectors[49];
    }

    public VectorFunc getReserv32() {
        if (vectors.length < 51) {
            return null;
        }
        return vectors[50];
    }

    public VectorFunc getReserv33() {
        if (vectors.length < 52) {
            return null;
        }
        return vectors[51];
    }

    public VectorFunc getReserv34() {
        if (vectors.length < 53) {
            return null;
        }
        return vectors[52];
    }

    public VectorFunc getReserv35() {
        if (vectors.length < 54) {
            return null;
        }
        return vectors[53];
    }

    public VectorFunc getReserv36() {
        if (vectors.length < 55) {
            return null;
        }
        return vectors[54];
    }

    public VectorFunc getReserv37() {
        if (vectors.length < 56) {
            return null;
        }
        return vectors[55];
    }

    public VectorFunc getReserv38() {
        if (vectors.length < 57) {
            return null;
        }
        return vectors[56];
    }

    public VectorFunc getReserv39() {
        if (vectors.length < 58) {
            return null;
        }
        return vectors[57];
    }

    public VectorFunc getReserv3A() {
        if (vectors.length < 59) {
            return null;
        }
        return vectors[58];
    }

    public VectorFunc getReserv3B() {
        if (vectors.length < 60) {
            return null;
        }
        return vectors[59];
    }

    public VectorFunc getReserv3C() {
        if (vectors.length < 61) {
            return null;
        }
        return vectors[60];
    }

    public VectorFunc getReserv3D() {
        if (vectors.length < 62) {
            return null;
        }
        return vectors[61];
    }

    public VectorFunc getReserv3E() {
        if (vectors.length < 63) {
            return null;
        }
        return vectors[62];
    }

    public VectorFunc getReserv3F() {
        if (vectors.length < 64) {
            return null;
        }
        return vectors[63];
    }

GameHeader


Поступим аналогичным образом, и создадим класс GameHeader, реализующий интерфейс StructConverter.


Структура заголовка игрового рома
Start Offset End Offset Description
$100 $10F Console name (usually 'SEGA MEGA DRIVE ' or 'SEGA GENESIS ')
$110 $11F Release date (usually '©XXXX YYYY.MMM' where XXXX is the company code, YYYY is the year and MMM — month)
$120 $14F Domestic name
$150 $17F International name
$180 $18D Version ('XX YYYYYYYYYYYY' where XX is the game type and YY the game code)
$18E $18F Checksum
$190 $19F I/O support
$1A0 $1A3 ROM start
$1A4 $1A7 ROM end
$1A8 $1AB RAM start (usually $00FF0000)
$1AC $1AF RAM end (usually $00FFFFFF)
$1B0 $1B2 'RA' and $F8 enables SRAM
$1B3 ---- unused ($20)
$1B4 $1B7 SRAM start (default $00200000)
$1B8 $1BB SRAM end (default $0020FFFF)
$1BC $1FF Notes (unused)

Заводим поля, проверяем на достаточную длину входных данных, пользуемся двумя новыми для нас методами readNextByteArray(), readNextUnsignedShort() объекта reader для чтения данных, и создаём структуру. Итоговый код получается следующим:


GameHeader
package sega;

import java.io.IOException;

import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.StructConverter;
import ghidra.program.flatapi.FlatProgramAPI;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.Structure;
import ghidra.program.model.data.StructureDataType;

public class GameHeader implements StructConverter {
    private byte[] consoleName = null;
    private byte[] releaseDate = null;
    private byte[] domesticName = null;
    private byte[] internationalName = null;
    private byte[] version = null;
    private short checksum = 0;
    private byte[] ioSupport = null;
    private Address romStart = null, romEnd = null;
    private Address ramStart = null, ramEnd = null;
    private byte[] sramCode = null;
    private byte unused = 0;
    private Address sramStart = null, sramEnd = null;
    private byte[] notes = null;

    FlatProgramAPI fpa;

    public GameHeader(FlatProgramAPI fpa, BinaryReader reader) throws IOException {
        this.fpa = fpa;

        if (reader.length() < 0x200) {
            return;
        }

        reader.setPointerIndex(0x100);

        consoleName = reader.readNextByteArray(0x10);
        releaseDate = reader.readNextByteArray(0x10);
        domesticName = reader.readNextByteArray(0x30);
        internationalName = reader.readNextByteArray(0x30);
        version = reader.readNextByteArray(0x0E);
        checksum = (short) reader.readNextUnsignedShort();
        ioSupport = reader.readNextByteArray(0x10);
        romStart = fpa.toAddr(reader.readNextUnsignedInt());
        romEnd = fpa.toAddr(reader.readNextUnsignedInt());
        ramStart = fpa.toAddr(reader.readNextUnsignedInt());
        ramEnd = fpa.toAddr(reader.readNextUnsignedInt());
        sramCode = reader.readNextByteArray(0x03);
        unused = reader.readNextByte();
        sramStart = fpa.toAddr(reader.readNextUnsignedInt());
        sramEnd = fpa.toAddr(reader.readNextUnsignedInt());
        notes = reader.readNextByteArray(0x44);
    }

    @Override
    public DataType toDataType() {
        Structure s = new StructureDataType("GameHeader", 0);

        s.add(STRING, 0x10, "ConsoleName", null);
        s.add(STRING, 0x10, "ReleaseDate", null);
        s.add(STRING, 0x30, "DomesticName", null);
        s.add(STRING, 0x30, "InternationalName", null);
        s.add(STRING, 0x0E, "Version", null);
        s.add(WORD, 0x02, "Checksum", null);
        s.add(STRING, 0x10, "IoSupport", null);
        s.add(POINTER, 0x04, "RomStart", null);
        s.add(POINTER, 0x04, "RomEnd", null);
        s.add(POINTER, 0x04, "RamStart", null);
        s.add(POINTER, 0x04, "RamEnd", null);
        s.add(STRING, 0x03, "SramCode", null);
        s.add(BYTE, 0x01, "Unused", null);
        s.add(POINTER, 0x04, "SramStart", null);
        s.add(POINTER, 0x04, "SramEnd", null);
        s.add(STRING, 0x44, "Notes", null);

        return s;
    }

    public byte[] getConsoleName() {
        return consoleName;
    }

    public byte[] getReleaseDate() {
        return releaseDate;
    }

    public byte[] getDomesticName() {
        return domesticName;
    }

    public byte[] getInternationalName() {
        return internationalName;
    }

    public byte[] getVersion() {
        return version;
    }

    public short getChecksum() {
        return checksum;
    }

    public byte[] getIoSupport() {
        return ioSupport;
    }

    public Address getRomStart() {
        return romStart;
    }

    public Address getRomEnd() {
        return romEnd;
    }

    public Address getRamStart() {
        return ramStart;
    }

    public Address getRamEnd() {
        return ramEnd;
    }

    public byte[] getSramCode() {
        return sramCode;
    }

    public byte getUnused() {
        return unused;
    }

    public Address getSramStart() {
        return sramStart;
    }

    public Address getSramEnd() {
        return sramEnd;
    }

    public boolean hasSRAM() {
        if (sramCode == null) {
            return false;
        }

        return sramCode[0] == 'R' && sramCode[1] == 'A' && sramCode[2] == 0xF8;
    }

    public byte[] getNotes() {
        return notes;
    }
}

Создаём объекты для заголовка:


vectors = new VectorsTable(fpa, reader);
header = new GameHeader(fpa, reader);

Сегменты


У Сеги есть вполне известная карта регионов памяти, которую я, пожалуй, тут приводить не буду в виде таблицы, а приведу лишь код, который используется для создания сегментов.


Итак, у объекта класса FlatProgramAPI есть метод createMemoryBlock(), с помощью которого удобно создавать регионы памяти. На вход он принимает следующие аргументы:


  1. name: имя региона
  2. address: адрес начала региона
  3. stream: объект типа InputStream, который будет являться основой для данных в регионе памяти. Если указать null, то будет создан неинициализированный регион (например, для 68K RAM или Z80 RAM нам именно такой и будет нужен
  4. size: размер создаваемого региона
  5. isOverlay: принимает true или false, и указывает, что регион памяти оверлейный. Где это нужно кроме исполняемых файлов я не знаю

На выходе createMemoryBlock() возвращает объект типа MemoryBlock, которому дополнительно можно установить флаги прав доступа (Read, Write, Execute).


В итоге, получится функция следующего вида:


private void createSegment(FlatProgramAPI fpa, InputStream stream, String name, Address address, long size, boolean read, boolean write, boolean execute) {
    MemoryBlock block = null;
    try {
        block = fpa.createMemoryBlock(name, address, stream, size, false);
        block.setRead(read);
        block.setWrite(read);
        block.setExecute(execute);
    } catch (Exception e) {
        Msg.error(this, String.format("Error creating %s segment", name));
    }
}

Здесь мы дополнительно вызвали статический метод error класса Msg, для вывода сообщения об ошибке.


Сегмент, содержащий игровой ром может иметь максимальный размер 0x3FFFFF (всё остальное уже будет принадлежать другим регионам). Создадим его:


InputStream romStream = provider.getInputStream(0);
createSegment(fpa, romStream, "ROM", fpa.toAddr(0x000000), Math.min(romStream.available(), 0x3FFFFF), true, false, true);

Здесь мы создали InputStream на основе входного файла, начиная со смещения 0.


Некоторые сегменты я бы не хотел создавать, не спросив у пользователя (это SegaCD и Sega32X сегменты). Для этого можно воспользоваться статическими методами класса OptionDialog. Например, showYesNoDialogWithNoAsDefaultButton() покажет диалоговое окно с кнопками YES и NO с активированной по-умолчанию кнопкой NO.


Создаём указанные выше сегменты:


if (OptionDialog.YES_OPTION == OptionDialog.showYesNoDialogWithNoAsDefaultButton(null, "Question", "Create Sega CD segment?")) {
    if (romStream.available() > 0x3FFFFF) {
        InputStream epaStream = provider.getInputStream(0x400000);

        createSegment(fpa, epaStream, "EPA", fpa.toAddr(0x400000), 0x400000, true, true, false);
    } else {
        createSegment(fpa, null, "EPA", fpa.toAddr(0x400000), 0x400000, true, true, false);
    }
}

if (OptionDialog.YES_OPTION == OptionDialog.showYesNoDialogWithNoAsDefaultButton(null, "Question", "Create Sega 32X segment?")) {
    createSegment(fpa, null, "32X", fpa.toAddr(0x800000), 0x200000, true, true, false);
}

Теперь можно создать все остальные сегменты:


createSegment(fpa, null, "Z80", fpa.toAddr(0xA00000), 0x10000, true, true, false);
createSegment(fpa, null, "SYS1", fpa.toAddr(0xA10000), 16 * 2, true, true, false);
createSegment(fpa, null, "SYS2", fpa.toAddr(0xA11000), 2, true, true, false);
createSegment(fpa, null, "Z802", fpa.toAddr(0xA11100), 2, true, true, false);
createSegment(fpa, null, "Z803", fpa.toAddr(0xA11200), 2, true, true, false);
createSegment(fpa, null, "FDC", fpa.toAddr(0xA12000), 0x100, true, true, false);
createSegment(fpa, null, "TIME", fpa.toAddr(0xA13000), 0x100, true, true, false);
createSegment(fpa, null, "TMSS", fpa.toAddr(0xA14000), 4, true, true, false);
createSegment(fpa, null, "VDP", fpa.toAddr(0xC00000), 2 * 9, true, true, false);
createSegment(fpa, null, "RAM", fpa.toAddr(0xFF0000), 0x10000, true, true, true);

if (header.hasSRAM()) {
    Address sramStart = header.getSramStart();
    Address sramEnd = header.getSramEnd();

    if (sramStart.getOffset() >= 0x200000 && sramEnd.getOffset() <= 0x20FFFF && sramStart.getOffset() < sramEnd.getOffset()) {
        createSegment(fpa, null, "SRAM", sramStart, sramEnd.getOffset() - sramStart.getOffset() + 1, true, true, false);
    }
}

Массивы, метки и конкретные адреса


Для создания массивов имеется специальный класс CreateArrayCmd. Создаём объект класса, указывая в конструкторе следующие поля:


  1. address: адрес, по которому будет создан массив
  2. numElements: количество элементов массива
  3. dataType: тип данных у элементов в массиве
  4. elementSize: размер одного элемента

Далее достаточно вызвать у объекта класса метод applyTo(program), чтобы создать массив.


Для некоторых адресов мне требуется создать не массив, а конкретный тип данных, например BYTE, WORD, DWORD или структура. Для этого, у объекта класса FlatProgramAPI есть методы createByte(), createWord(), createDword() и т.д.


Так же, кроме указания типа данных, необходимо дать имя каждому конкретному адресу (например, это могут быть порты VDP). Для этого, используется следующая хитрая конструкция:


  1. У объекта типа Program вызываем метод getSymbolTable(), который предоставляет нам доступ к таблице символов, меток и т.д.
  2. У таблицы символов дёргаем метод createLabel(), который принимает на вход адрес, имя и тип символа. С типом символов не очень понятно, но, в имеющихся примерах используется SourceType.IMPORTED и я поступил так же

В итоге получаем парочку шаблонных методов для создания именованных массивов, либо одиночных данных:


Создание массивов и одиночных элементов
private void createNamedByteArray(FlatProgramAPI fpa, Program program, Address address, String name, int numElements) {
    if (numElements > 1) {
        CreateArrayCmd arrayCmd = new CreateArrayCmd(address, numElements, ByteDataType.dataType, ByteDataType.dataType.getLength());
        arrayCmd.applyTo(program);
    } else {
        try {
            fpa.createByte(address);
        } catch (Exception e) {
            Msg.error(this, "Cannot create byte. " + e.getMessage());
        }
    }

    try {
        program.getSymbolTable().createLabel(address, name, SourceType.IMPORTED);
    } catch (InvalidInputException e) {
        Msg.error(this, String.format("%s : Error creating array %s", getName(), name));
    }
}

private void createNamedWordArray(FlatProgramAPI fpa, Program program, Address address, String name, int numElements) {
    if (numElements > 1) {
        CreateArrayCmd arrayCmd = new CreateArrayCmd(address, numElements, WordDataType.dataType, WordDataType.dataType.getLength());
        arrayCmd.applyTo(program);
    } else {
        try {
            fpa.createWord(address);
        } catch (Exception e) {
            Msg.error(this, "Cannot create word. " + e.getMessage());
        }
    }

    try {
        program.getSymbolTable().createLabel(address, name, SourceType.IMPORTED);
    } catch (InvalidInputException e) {
        Msg.error(this, String.format("%s : Error creating array %s", getName(), name));
    }
}

private void createNamedDwordArray(FlatProgramAPI fpa, Program program, Address address, String name, int numElements) {
    if (numElements > 1) {
        CreateArrayCmd arrayCmd = new CreateArrayCmd(address, numElements, DWordDataType.dataType, DWordDataType.dataType.getLength());
        arrayCmd.applyTo(program);
    } else {
        try {
            fpa.createDWord(address);
        } catch (Exception e) {
            Msg.error(this, "Cannot create dword. " + e.getMessage());
        }
    }

    try {
        program.getSymbolTable().createLabel(address, name, SourceType.IMPORTED);
    } catch (InvalidInputException e) {
        Msg.error(this, String.format("%s : Error creating array %s", getName(), name));
    }
}

Сами элементы
createNamedDwordArray(fpa, program, fpa.toAddr(0xA04000), "Z80_YM2612", 1);

createNamedWordArray(fpa, program, fpa.toAddr(0xA10000), "IO_PCBVER", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10002), "IO_CT1_DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10004), "IO_CT2_DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10006), "IO_EXT_DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10008), "IO_CT1_CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1000A), "IO_CT2_CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1000C), "IO_EXT_CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1000E), "IO_CT1_RX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10010), "IO_CT1_TX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10012), "IO_CT1_SMODE", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10014), "IO_CT2_RX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10016), "IO_CT2_TX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10018), "IO_CT2_SMODE", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1001A), "IO_EXT_RX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1001C), "IO_EXT_TX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1001E), "IO_EXT_SMODE", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA11000), "IO_RAMMODE", 1);

createNamedWordArray(fpa, program, fpa.toAddr(0xA11100), "IO_Z80BUS", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA11200), "IO_Z80RES", 1);

createNamedByteArray(fpa, program, fpa.toAddr(0xA12000), "IO_FDC", 0x100);

createNamedByteArray(fpa, program, fpa.toAddr(0xA13000), "IO_TIME", 0x100);

createNamedDwordArray(fpa, program, fpa.toAddr(0xA14000), "IO_TMSS", 1);

createNamedWordArray(fpa, program, fpa.toAddr(0xC00000), "VDP_DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00002), "VDP__DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00004), "VDP_CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00006), "VDP__CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00008), "VDP_CNTR", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC0000A), "VDP__CNTR", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC0000C), "VDP___CNTR", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC0000E), "VDP____CNTR", 1);
createNamedByteArray(fpa, program, fpa.toAddr(0xC00011), "VDP_PSG", 1);

Применение структур заголовка


Для применения структур на конкретные адреса я воспользуюсь статическим методом createData() класса DataUtilities. Данный метод принимает на вход следующие аргументы:


  1. program: объект класса Program
  2. address: адрес, на который будет применена структура
  3. dataType: тип структуры
  4. dataLength: размер структуры. Можно указать -1 для автоматического подсчёта
  5. stackPointers: если true, происходит какая-то магия с подсчётом глубины указателей. Ставлю false
  6. clearDataMode: если вдруг на месте создания структуры уже есть объявленные данные, выбираем метод их андефайна (простите, не смог придумать русское слово)

Остался ещё один момент: т.к. мы применяем структуру с векторами (читай, адресами функций), то было бы логично по этим адресам объявить функции. Для этого у объекта типа FlatProgramAPI можно вызвать метод createFunction(), принимающий на вход адрес и имя функции.


Теперь у нас всё есть для создания структур заголовка и обозначения данных по адресам векторов как функций:


private void markVectorsTable(Program program, FlatProgramAPI fpa) {
    try {
        DataUtilities.createData(program, fpa.toAddr(0), vectors.toDataType(), -1, false, ClearDataMode.CLEAR_ALL_UNDEFINED_CONFLICT_DATA);

        for (VectorFunc func : vectors.getVectors()) {
            fpa.createFunction(func.getAddress(), func.getName());
        }
    } catch (CodeUnitInsertionException e) {
        Msg.error(this, "Vectors mark conflict at 0x000000");
    }
}

private void markHeader(Program program, FlatProgramAPI fpa) {
    try {
        DataUtilities.createData(program, fpa.toAddr(0x100), header.toDataType(), -1, false, ClearDataMode.CLEAR_ALL_UNDEFINED_CONFLICT_DATA);
    } catch (CodeUnitInsertionException e) {
        Msg.error(this, "Vectors mark conflict at 0x000100");
    }
}

Завершаем метод load()


Для красивого уведомления пользователя о ходе работы метода load() можно воспользоваться методом setMessage() объекта типа TaskMonitor, который у нас уже есть.


monitor.setMessage(String.format("%s : Start loading", getName()));

Собираем воедино получившийся набор функций, и получаем такой вот код:


@Override
protected void load(ByteProvider provider, LoadSpec loadSpec, List<Option> options,
        Program program, MemoryConflictHandler handler, TaskMonitor monitor, MessageLog log)
        throws CancelledException, IOException {

    monitor.setMessage(String.format("%s : Start loading", getName()));

    BinaryReader reader = new BinaryReader(provider, false);
    FlatProgramAPI fpa = new FlatProgramAPI(program, monitor);

    vectors = new VectorsTable(fpa, reader);
    header = new GameHeader(fpa, reader);

    createSegments(fpa, provider, program, monitor);
    markVectorsTable(program, fpa);
    markHeader(program, fpa);

    monitor.setMessage(String.format("%s : Loading done", getName()));
}

getDefaultOptions и validateOptions


В данной статье я их не рассматриваю, потому как пока не пригодились


Отлаживаем результаты наших трудов


Для отладки достаточно поставить бряки и нажать Run -> Debug As -> 1 Ghidra. Тут всё просто.



Экспорт дистрибутива и установка в GHIDRA


Прежде чем экспортировать наш дистрибутив, давайте добавим какое-то описание для нашего проекта. Для этого в корне проекта в Eclipse находим файл extension.properties, и редактируем поля:


description=Loader for Sega Mega Drive / Genesis ROMs
author=Dr. MefistO
createdOn=20.03.2019

Для создания дистрибутива вашего плагина жмём GhidraDev -> Export -> Ghidra Module Extension... и следуем подсказкам мастера создания дистрибутива:





После всех манипуляций в папке dist вашего проекта получим zip-архив (что-то типа ghidra_9.0_PUBLIC_20190320_Sega.zip) с готовым к употреблению плагином для GHIDRA.


Давайте теперь установим наш плагин. Запускаем Гидру, жмём File -> Install Extensions..., жмём значок с зелёным плюсом, и выбираем созданный ранее архив. Вуаля...




Исходники и прочее


Все исходники вы сможете найти в github-репозитории, включая готовый релиз.


А вывод можно сделать такой: гонка между IDA и GHIDRA потихоньку начинает быть проигранной одной из сторон. Как мне кажется.

Поделиться публикацией

Комментарии 15

    –1
    Спасибо за статью.
    Я так понимаю офф сайт лежит, с этой Chidra.
      +1

      Вроде все работает, только что проверил.

        0
        Он ghidra-sre.org или что то поменялось?
          +2

          Он самый, ничего не поменялось. Думаю мог поменяться доступ к сайту из некоторых стран. Ради интереса попробуйте через прокси или vpn.

            0
            Через ТОР зашел без проблем, напрямую 403 выдает.
              +1
              Если не можете скачать, возьмите здесь 4pda.ru/forum/index.php?showtopic=944552
              Нашлись добрые люди, которые о нас позаботились :)))
                0
                Через TOR слилось без проблем.
                0

                Значит моя теория была верна)

          0

          Сайт Гидры "лежит" только для русских IP.

          +2
          Стоит уточнить, что Jython не поддерживает Python 3, и медленно умирает. В то же время, сегодня была выпущена новая версия Cutter — 1.8.0, с поддержкой плагинов на C++ и Python, улучшенной поддержкой типов, поддержкой миниграфа и улучшениями интерфейса.
            +2

            Я знал, что будет комментарий про Радар)
            Увы, но, с выходом Гидры r2 обречён ра медленное вымирание, как мне кажется.

            +1
            Большое спасибо, очень познавательный материал. Только что скачал по Вашей наводке. Так что как видите, глухой/слепой/немой/без_интернета тут тоже нашелся :)))) Несколько удивляет, что такой проект написан на java. Наверно при серьёзной работе отжирает туеву хучу памяти. Впрочем буду смотреть.
              0

              Самое активное потребление памяти начинается во время анализа файла. Многопоточность есть, объём памяти для JVM задаётся строчкой в ланчере Гидры.

              –1
              Не страшновато ли запускать-то? Всё-таки странен что сам выпуск (импортозамещение российскому продукту? Хвастовство? Налогоплательщики должны видеть, на что ушли их деньги?), что отложенный выпуск исходников.
              If you are interested in projects like this and would like to develop this, and other cybersecurity tools, for NSA to help protect our nation and its allies, consider applying for a career with us.
              А, так может это такой способ привлечь спецов?
                0
                Как ни странно, нет, не страшновато. Это не настолько популярная программа (как и люди, ей пользующиеся), чтобы ставить закладки или типа того. Да и без исходников там пока только декомпилер и SLEIGH — а их можно отреверсить.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое