Первое знакомство с отладчиком 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, и сделать выборку по пяти первым доступным символам, то мы увидим следующее:

Кое-какой результат имеется – мы обнаружили две точки использования этой строки. Одна расположена в 0x804c11fc, а вторая в 0x84b86f0. При проверке первой строки мы видим, что она передается функции в подпрограмме по адресу 0x8003358:

	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 и перейдем к остальным местам использования нашей строки символов. Далее она встречается в функции по адресу 0x8002CEC. Вот декомпилированный вариант:

	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 и перейдем далее. Следующая строка встречается по адресу 0x84b86f0 и также используется в двух подпрограммах. Вот первая, расположенная в 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 и 0x520 – затем эти значения используются как INDEX в доступных для набора символах.

О чем это говорит? Переменная param_1 скорее всего указывает на массив смещений, представляющих введенные на экране пароля символы. Например, если мы введем GHDRR, то массив будет содержать [0x4,0x5,0x2,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 для предоставления регистров.

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/535564/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *