[Перевод] Первое знакомство с отладчиком Ghidra и взлом игры Spiderman
В середине декабря в твиттер-аккаунте NSA было объявлено о релизе новой ветки Ghidra с долгожданной поддержкой отладки. Теперь с помощью GDB-заглушки и прочих механизмов можно будет выполнять ее пошагово внутри самой Ghidra. Желая отпраздновать это событие, которое совпало с моим домашним карантином, я подготовил небольшой обзор сборки этой версии, включая пример использования ее отладчика для интересной цели.
В этой статье мы:
• научимся собирать последнюю (да и любую) версию Ghidra при помощи Docker Container;
• настроим плагины Ghidra Eclipse;
• выполним сборку программного загрузчика для Ghidra;
• прогоним через отладчик программу, использовав GDB-заглушку;
• с помощью той же отладки разберемся, как обрабатываются пароли для игры на Game Boy Advance.
Меня очень вдохновила прекрасная работа, которую в этом направлении проделывают stackmashing и LiveOverflow. Советую заглянуть на их канал. В нашем же случае в качестве подопытной программы выступит игра Spiderman: Mysterio«s Menace. В свое время я играл в нее очень много, к тому же всегда приятно снова взглянуть на свои детские увлечения с позиции опыта. Конечная цель — показать, как правильно загружать этот образ ROM через настраиваемый загрузчик и подключать GDB-заглушку эмулятора при помощи отладчика Ghidra.
К сведению: при начале очередного проекта по реверс-инжинирингу важно правильно определить задачи. Например, если мы говорим, что хотим просто разобрать игру, то здесь допустимо огромное число вариантов. Можно, к примеру, разобрать механику обнаружения столкновений, принцип работы ИИ или способ создания карт уровней. В этой же статье конечной целью мы обозначим изучение механизма паролей.
Проект реализуется под Ubuntu 20.04 со всеми последними обновлениями.
Сборка Ghidra
Начнем с основного. Ветка отладчика еще не была включена в официальный релиз, так что мы его будем собирать сами. К нашему везению, уважаемый dukebarman уже создал для этой задачи docker-контейнер, и нам осталось только изменить скрипт build_ghidra.sh для переключения на ветку отладчика:
git clone https://github.com/NationalSecurityAgency/ghidra -b debugger
Мы также настроим для этой версии Ghidra расширения разработки Eclipse, что пригодится нам позже при сборке загрузчика и написании сценариев анализа. Для этого нужно добавить в скрипт build_ghidra.sh следующее:
gradle prepDev
gradle eclipse -PeclipsePDE
Далее следуйте инструкциям в README:
cd ghidra-builder
sudo docker-tpl/build
cd workdir
sudo ../docker-tpl/run ./build_ghidra.sh
Это займет какое-то время, так что можете отвлечься на кофе, а к возвращению вас уже будет ждать свежесобранная Гидра. Готовая сборка находится в workdir/out:
wrongbaud@wubuntu:~/blog/gba-re-gbd/ghidra-builder/workdir$ ls out/
ghidra_9.3_DEV_20201218_linux64.zip
Распакуйте файл и можете запускать Ghidra через скрипт ./ghidraRun. Я распакую содержимое в каталог ghidra-builder/workdir, так как для сборки этой версии мы будем использовать docker-контейнер. Если вы следуете за процессом, то сейчас ваша рабочая директория должна выглядеть так:
wrongbaud@wubuntu:~/blog/gba-re-gbd/ghidra-builder/workdir$ ls
build_ghidra.sh ghidra ghidra_9.3_DEV out set_exec_flag.sh
Сборка плагинов Eclipse
Закончив с Ghidra, можно переходить к сборке плагинов GhidraDev для Eclipse. Эти проекты находятся в каталоге ghidra-builder/workdir/ghidra/GhidraBuild/EclipsePlugins/GhidraDev.
1. Установите Eclipse
- Выберите Java IDE
2. Установите CDT, PyDev, и Plugin Development Environment.
- Это можно сделать из маркетплейса Eclipse.
3. Импортируйте проекты GhidraDevFeature и GhidraDevPlugin.
- Они находятся в каталоге ghidra-builder/workdir/ghidra/GhidraBuild/EclipsePlugins/GhidraDev/
- File → Import → General → Existing Projects into Workspace
- Добавьте ghidra-builder/workdir/ghidra/GhidraBuild/EclipsePlugins/GhidraDev
- Выберите «Search for nested projects»
- Импортируйте проекты.
К сведению: после импорта вы можете заметить некоторые ошибки сборки. Не обращайте внимания, так как вы просто экспортируете плагин.
4. Теперь перейдем к экспорту:
- File → Export
- Plug-in Development → Deployable Features
- ghidradev.ghidradev
- Выберите местоположение архива для экспорта плагина.
- Жмите Finish.
Теперь у нас есть плагин для настраиваемой версии Ghidra, который можно скачать через Help→Install New Software.
При этом мы собрали Ghidra из ветки debugger, а также настроили расширения разработки Eclipse, получив возможность создавать плагины для нашей новой версии Ghidra.
К сведению: я хочу подчеркнуть, насколько полезно заглядывать в документацию Ghidra. В ней содержится все необходимое, начиная с мануалов по P-Code и заканчивая инструкциями по сборке и экспорту плагинов.
Создание загрузчика ROM
Для успешного анализа образа ROM нам понадобится определить все области памяти и периферийные устройства GBA. И снова, к нашей удаче, SiD3W4y уже написал для этого решение на GitHub.
Задача загрузчика Ghidra в настройке всех необходимый областей памяти, определении отладочной информации и символов, которые могут присутствовать в файле, а также выдача всей доступной информации о целевом файле. Упомянутый выше загрузчик описывает все основные периферийные устройства GBA и прекрасно подойдет для нашей задачи, так что начнем с его копирования в тот же каталог ghidra-builder/workdir, поскольку для сборки будем использовать тот же контейнер docker, с помощью которого собирали Ghidra.
cd ghidra-builder/workdir
git clone https://github.com/SiD3W4y/GhidraGBA
sudo ../docker-tpl/run /bin/bash
dockerbot@797eb43ce05f:/files/GhidraGBA$ export GHIDRA_INSTALL_DIR=/files/ghidra_9.3_DEV/
dockerbot@797eb43ce05f:/files/GhidraGBA$ gradle
dockerbot@797eb43ce05f:/files/GhidraGBA$ cp dist/ghidra_9.3_DEV_20201218_GhidraGBA.zip ../ghidra_9.3_DEV/Extensions/Ghidra/
dockerbot@797eb43ce05f:/files/GhidraGBA$ exit
exit
Здесь мы:
1. Запускаем docker-контейнер.
2. Собираем расширение GhidraGBA, указывая путь к месту установки.
3. Копируем каталог расширений Ghidra, чтобы он показывался под меню Install Extensions.
4. Выходим из контейнера docker.
Запустите Ghidra командой ghidraRun и перейдите в File→ Install Extensions. Выерите загрузчик GhidraGBA и кликните OK. Для применения изменений потребуется перезапустить Ghidra. Теперь при загрузке GBA ROM должно отображаться следующее:
После выполнения автоматического анализа Ghidra неплохо поняла этот образ ROM. В нем определено много функций и выглядит все достаточно хорошо. Следующим шагом будет найти способ сузить нашу область интереса в этом образе. Говоря условно, нам нужно найти иголку в стоге сена. Начнем с выяснения принципа работы системы паролей, для чего просто попробуем ввести несколько их вариантов.
Анализ ROM
При вводе пароля мы наблюдаем такой экран:
Заметьте, что используются только согласные буквы и цифры от 0 до 9. Сам же пароль состоит из 5 символов. Для реверсинга это будет неплохой отправной точкой. С помощью данной информации можно сузить область интересующих нас функций. Например, давайте просмотрим строки ROM в поиске этих значений. Если открыть окно строк, Window → Defined Strings, и сделать выборку по пяти первым доступным символам, то мы увидим следующее:
Кое-какой результат имеется — мы обнаружили две точки использования этой строки. Одна расположена в 0×804c11fc, а вторая в 0×84b86f0. При проверке первой строки мы видим, что она передается функции в подпрограмме по адресу 0×8003358:
undefined4 passwd_1(int param_1,int param_2)
{
int iVar1;
uint uVar2;
uint uVar3;
undefined4 in_lr;
undefined auStack52 [36];
undefined4 uStack4;
uStack4 = in_lr;
FUN_080231f4(auStack52,"BCDFGHJKLMNPQRSTVWXYZ0123456789-",0x21);
*(uint *)(param_1 + 0x8c) = 0;
FUN_080025f8(param_1);
FUN_08002674(param_1);
FUN_08002714(param_1);
FUN_0800282c(param_1);
iVar1 = 0;
uVar3 = *(uint *)(param_1 + 0x8c);
uVar2 = 0;
do {
*(undefined *)(param_2 + iVar1) = auStack52[uVar3 >> (uVar2 & 0xff) & 0x1f];
uVar2 = uVar2 + 5;
iVar1 = iVar1 + 1;
} while (iVar1 < 5);
return uStack4;
}
Обратите внимание на цикл, продолжающий выполнение при переменной < 5. Это говорит о том, что данная функция может оказаться полезной, поскольку пароль как раз содержит именно 5 символов. Давайте отметим ее как passwd_1 и перейдем к остальным местам использования нашей строки символов. Далее она встречается в функции по адресу 0×8002CEC. Вот декомпилированный вариант:
undefined8 passwd_2(void)
{
int iVar1;
int iVar2;
uint uVar3;
undefined4 in_lr;
undefined local_98 [5];
undefined local_93;
undefined auStack144 [36];
undefined auStack108 [8];
undefined auStack100 [72];
undefined4 uStack4;
uStack4 = in_lr;
FUN_08000b0c(0,1,0,0);
DAT_03001fd0._0_2_ = 0x1444;
DISPCNT = 0x1444;
FUN_0801e330(&DAT_0838277c);
iVar1 = DAT_03001fe0;
FUN_080231f4(auStack144,"BCDFGHJKLMNPQRSTVWXYZ0123456789-",0x21);
*(uint *)(iVar1 + 0x8c) = 0;
FUN_080025f8(iVar1);
FUN_08002674(iVar1);
FUN_08002714(iVar1);
FUN_0800282c(iVar1);
iVar2 = 0;
uVar3 = 0;
do {
local_98[iVar2] = auStack144[*(uint *)(iVar1 + 0x8c) >> (uVar3 & 0xff) & 0x1f];
uVar3 = uVar3 + 5;
iVar2 = iVar2 + 1;
} while (iVar2 < 5);
local_93 = 0;
FUN_0801d1bc(auStack108,local_98);
FUN_0801d92c(DAT_03001ff0,0x10,0);
FUN_08000b0c(1,1,0,0);
*(undefined4 *)(DAT_03002028 + 0xc) = 0x200;
FUN_08000f1c();
iVar1 = FUN_0801d26c(auStack108);
*(undefined4 *)(DAT_03002028 + 0xc) = 0;
FUN_08000f1c();
FUN_0801dcac(DAT_03001ff0,0);
FUN_08000b0c(0,1,0,0);
FUN_08004408(auStack100,2);
return CONCAT44(uStack4,(uint)(iVar1 == 0));
}
И снова мы видим передачу этой строки в функцию, а также очередной цикл, выполняющий 5 итераций — отметим его как passwd_2 и перейдем далее. Следующая строка встречается по адресу 0×84b86f0 и также используется в двух подпрограммах. Вот первая, расположенная в FUN_0801c37c:
undefined4 render_pw_screen(int param_1)
{
int iVar1;
int iVar2;
uint uVar3;
undefined4 uVar4;
uint uVar5;
undefined4 in_lr;
char local_1c [8];
undefined4 uStack4;
uStack4 = in_lr;
iVar2 = FUN_0801b834(DAT_03001ffc,"@ - Accept & - Backspace");
iVar1 = DAT_03001ffc;
*(uint *)(DAT_03001ffc + 0x90) = 0xf0U - iVar2 >> 1;
*(undefined4 *)(iVar1 + 0x94) = 0x96;
FUN_0801b764(iVar1,"@ - Accept & - Backspace");
uVar3 = *(uint *)(param_1 + 0x51c);
if (uVar3 != 0) {
uVar5 = 0;
if (uVar3 != 0) {
do {
local_1c[uVar5] = "BCDFGHJKLMNPQRSTVWXYZ0123456789-"[*(byte *)(param_1 + 0x520 + uVar5)];
uVar5 = uVar5 + 1;
} while (uVar5 < uVar3);
}
local_1c[*(int *)(param_1 + 0x51c)] = '\0';
iVar2 = FUN_0801b834(DAT_03002000,local_1c);
iVar1 = DAT_03002000;
*(uint *)(DAT_03002000 + 0x90) = 0xf0U - iVar2 >> 1;
*(undefined4 *)(iVar1 + 0x94) = 0x3f;
iVar2 = FUN_0800118c(DAT_03001fdc,5);
*(byte *)(iVar1 + 5) = *(byte *)(iVar1 + 5) & 0xf | (byte)(iVar2 << 4);
FUN_0801b764(DAT_03002000,local_1c);
}
if (*(int *)(param_1 + 0x51c) != 5) {
uVar4 = FUN_0801a6d4(*(undefined4 *)(param_1 + 0x18));
*(undefined4 *)(param_1 + 4) = uVar4;
}
return uStack4;
}
В этой функции мы видим, что FUN_0801b764 вызывается со строкой @ — Accept & — Backspace. Несколько далее та же функция вызывается с переменной, содержащей интересующую нас строку. При дальнейшем рассмотренииFUN_0801b764 мы узнаем, что она копирует данные из второй переменной (строки ASCII) в адрес памяти первого аргумента. Здесь уже нельзя сказать уверенно, но меня кажется, что конкретно эта подпрограмма служит для отрисовки текста на экране, поэтому пока что я ее пропущу и перейду к следующему месту использования строки символов, которое привожу ниже:
undefined8 FUN_0801c454(int param_1)
{
int iVar1;
int iVar2;
undefined4 in_lr;
char local_14 [8];
undefined4 uStack4;
iVar2 = 1;
uStack4 = in_lr;
FUN_080231f4(local_14,"CRDT5",6);
iVar1 = 0;
do {
if (local_14[iVar1] != "BCDFGHJKLMNPQRSTVWXYZ0123456789-"[*(byte *)(param_1 + 0x520 + iVar1)]) {
iVar2 = 0;
}
iVar1 = iVar1 + 1;
} while ((iVar1 < 5) && (iVar2 != 0));
return CONCAT44(uStack4,iVar2);
}
Что у нас здесь? Во-первых, здесь мы видим FUN_080231f4, по сути являющуюся операцией memcpy:
undefined4 * memcpy_1(undefined4 *dest,undefined4 *src,uint count)
{
undefined4 uVar1;
undefined4 *puVar2;
undefined4 *puVar3;
puVar2 = dest;
if ((0xf < count) && ((((uint)src | (uint)dest) & 3) == 0)) {
do {
*puVar2 = *src;
puVar2[1] = src[1];
puVar3 = src + 3;
puVar2[2] = src[2];
src = src + 4;
puVar2[3] = *puVar3;
puVar2 = puVar2 + 4;
count = count - 0x10;
} while (0xf < count);
while (3 < count) {
uVar1 = *src;
src = src + 1;
*puVar2 = uVar1;
puVar2 = puVar2 + 1;
count = count - 4;
}
}
while (count = count - 1, count != 0xffffffff) {
*(undefined *)puVar2 = *(undefined *)src;
src = (undefined4 *)((int)src + 1);
puVar2 = (undefined4 *)((int)puVar2 + 1);
}
return dest;
}
Ее задача — копирование строки CRDT5 в указатель ячейки памяти в local_14. Далее мы видим, что в цикле while это значение используется в сравнении:
if (local_14[iVar1] != "BCDFGHJKLMNPQRSTVWXYZ0123456789-"[*(byte *)(param_1 + 0x520 + iVar1)])
Что же происходит здесь? В каждой итерации символ из local_14 сравнивается со значением из нашей строки доступных символов BCDFGHJKLMNPQRSTVWXYZ0123456789-. Такое поведение вполне соответствует предполагаемым действиям функции проверки пароля. Но мы знаем, что iVar1 при каждой итерации увеличивается на 1. Значит ли это, что пароли должны состоять из смежных символов в BCDFGHJKLMNPQRSTVWXYZ0123456789-? Это бы было очень глупо, к тому же строка CRDT5 никогда бы не прошла такую проверку. Если еще раз взглянуть на условие сравнения, то можно заметить, что в нем присутствует переменная param_1, которая тоже используется в качестве индекса, к которому прибавляются iVar1 и 0×520 — затем эти значения используются как INDEX в доступных для набора символах.
О чем это говорит? Переменная param_1 скорее всего указывает на массив смещений, представляющих введенные на экране пароля символы. Например, если мы введем GHDRR, то массив будет содержать [0×4,0×5,0×2,0xd,0xd].
Но давайте не будем забегать вперед и для начала попробуем пароль CRDT5:
Интересно! Мы попали в сцену с титрами!
Выглядит просто, не так ли? Но было бы неплохо выяснить, где именно в памяти хранится наш пароль. Если узнать, куда указывает param_1, то можно вычислить местоположение пароля в RAM и поискать перекрестные ссылки. Ну, а раз у нас теперь есть нужная функция, давайте задействуем отладчик!
Отладка ROM
Те, кто повторяет процесс, должны были заметить появление нового инструмента в менеджере проектов:
Обратите внимание на иконку жука — с ее помощью открывается отладчик. Кликнув по ней, вы увидите следующее окно:
В отличие от обычного представления анализатора здесь находится много дополнительных вкладок и окон. В верхнем левом углу расположено окошко Debugger Targets (цели отладчика), которое мы используем для установки соединения с отладчиком или запуска новой сессии отладки.
Под ним располагается окно «Objects», показывающее находящиеся в режиме отладки «Objects». Отсюда можно делать паузу, выполнять шаги и т.д.
В самом низу находится представление трех вкладок: Regions (области памяти), Stack (стек) и Console (консоль).
Справа мы видим окно для показа двух других вкладок: Threads (потоки) и Time (время). Для нашей задачи отладки однопоточной ARM-системы эти окна не пригодятся.
И наконец, оставшаяся справа часть экрана выделена под еще несколько вкладок, которые обычно представлены в разделе анализатора Ghidra. Здесь у нас вкладка Breakpoints, отображающая заданные точки останова:
Вторая вкладка Registers будет обновляться значениями регистра при достижении точек останова:
Последняя же вкладка — это представление Modules, где при необходимости отображаются загруженные модули. Мы же в случае нашего простого приложения ничего в ней не увидим:
Подключение к эмулятору
Для этого проекта я использую эмулятор mGBA, главным образом потому, что он может представлять удаленную GDB-заглушку. Подключаться к нему мы будем с помощью gdb-multiarch. Чтобы выполнить это из представления отладчика нужно в окошке Debugger Targets кликнуть по зеленой вилке (Connect), что вызовет следующее окно:
Здесь есть много опций для удаленной отладки. В целях данной статьи я использую IN-VM GNU gdb local debugger.
Я добавил gdb-multiarch в путь команды запуска gdb. После нажатия Connect появится стандартное диалоговое окно:
Теперь нужно запустить сервер. Загрузите образ ROM в mGBA и выберите Tools → Start GDB Server, всплывет такое окно:
Кликните Start и возвращайтесь в окно отладчика Ghidra. В диалоговом окне gdb выполните следующие команды:
set architecture arm
set arm fallback-mode thumb
set arm force-mode thumb
target remote localhost:2345
break *0x801c470
c
Здесь мы устанавливаем gdb архитектуру, подключаемся к удаленному серверу и в завершении определяем точку останова у функции, которая, как мы считаем, проверяет, нужно ли показывать сцену с титрами. Говоря конкретнее, устанавливаем ее у сегмента, сравнивающего переданный нами символ с извлеченным из строки доступных символов. Рассматривать мы будем этот фрагмент ассемблера:
LAB_0801c470 XREF[1]: 0801c48c(j)
0801c470 69 46 mov r1,sp
0801c472 88 18 add r0,r1,r2
0801c474 a1 18 add r1,r4,r2 ; Обновление указателя на введенный пароль текущим индексом
0801c476 09 78 ldrb r1,[r1,#0x0]; r1 содержит значение индекса переданного символа пароля. Например, "B" == 0, "C"==1, и т.д.
0801c478 c9 18 add r1,r1,r3; r3 содержит указатель на строку доступных символов. Мы добавляем к этому указателю индекс текущего символа пароля.
0801c47a 00 78 ldrb r0=>local_14,[r0,#0x0] ; Загрузка r0 из стека со значением строки "CRDT5" по индексу, указанному r2
0801c47c 09 78 ldrb r1,[r1,#0x0]=>s_BCDFGHJKLMNPQRSTVWXYZ012345678 = "BCDFGHJKLMNPQRSTVWXYZ01234567 ; Загрузка представления символа на основе введенного для пароля значения
0801c47e 88 42 cmp r0,r1 ; Сравнение!
0801c480 00 d0 beq LAB_0801c484
0801c482 00 25 mov r5,#0x0
LAB_0801c484 XREF[1]: 0801c480(j)
0801c484 01 32 add r2,#0x1; Увеличение счетчика индекса
0801c486 04 2a cmp r2,#0x4
0801c488 01 dc bgt LAB_0801c48e
0801c48a 00 2d cmp r5,#0x0
0801c48c f0 d1 bne LAB_0801c470
Введя все вышеприведенные команды, посмотрим, сработает ли точка останова…
Превосходно! Мы не только достигли точки останова, но и зафиксировали все регистры. Теперь проверим, врены ли были все наши предположения в отношении проверки пароля. Прошагаем через несколько инструкций до позиции 0801c474. Здесь мы предполагаем, что r1 будет указывать на массив индексов, представляющих введенные нами символы. Для выяснения этого заглянем в память:
К сведению: если вы делаете отладку удаленно при помощи gdb-multiarch, и при этом некоторые точки останова не срабатывают, попробуйте использовать команду stepi вместо c. Такую проблему я встречал в mGBA ранее, и она не связана с сервером GDB.
(gdb)x/10x $r1
0x2005998: 0x01 0x0d 0x02 0x0f 0x1a 0x00 0x00 0x00
0x20059a0: 0x00 0x4f
Вот оно! Что и следовало ожидать — вместо сохранения фактических символов ascii, вводимых в качестве пароля, сохраняются значения их индексов в таблице доступных символов:
Просто ради проверки, давайте посмотрим, что произойдет, если ввести в качестве пароля CGHDR и установить те же точки останова:
Breakpoint 3, 0x0801c476
Can't determine the current process's PID: you must name one.
(gdb)x/10x $r1
0x2005998: 0x01 0x04 0x05 0x02 0x0d 0x00 0x00 0x00
0x20059a0: 0x00 0x60
Все так и есть! Теперь мы знаем, как сохраняются пароли, и как они выглядят в памяти, а также умеем делать отладку из Ghidra. Думаю, что для данной статьи на этом можно прерваться — в следующей же мы исследуем другие особенности пароля при помощи той же Ghidra и возможностей удаленной отладки GDB.
Заключение
Сегодня мы познакомились с инструментами, позволяющими собрать Ghidra, рассмотрели некоторые из заявленных возможностей отладчика, с помощью которых смогли произвести удаленную отладку игры на Game Boy Advance. Многое из проделанного вы можете выполнить и без Ghidra, используя только gdb-multiarch, но я хотел познакомиться с этими возможностями и попутно поделиться с вами опытом.
Как всегда, по любым возникшим вопросам обращайтесь ко мне в Twitter. Если же вам интересно побольше узнать о Ghidra или взломе аппаратных средств в общем, можете ознакомиться с подготовленными мной обучающими материалами (англ.).
Дополнительная информация / Примечания
Основные выводы здесь: gvba не работает ни с какими современными GDB. По какой-то причине gdb-multiarch пропускает точки останова, а gdb из devkitarm не отвечает должным образом ghidra для предоставления регистров.