В середине декабря в твиттер-аккаунте 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 для переключения на ветку отладчика:
1 | git <span class="hljs-built_in">clone</span> https://github.com/NationalSecurityAgency/ghidra -b debugger |
Мы также настроим для этой версии Ghidra расширения разработки Eclipse, что пригодится нам позже при сборке загрузчика и написании сценариев анализа. Для этого нужно добавить в скрипт build_ghidra.sh следующее:
1 2 | gradle prepDev gradle eclipse -PeclipsePDE |
Далее следуйте инструкциям в README:
1 2 3 4 | <span class="hljs-built_in">cd</span> ghidra-builder sudo docker-tpl/build <span class="hljs-built_in">cd</span> workdir sudo ../docker-tpl/run ./build_ghidra.sh |
Это займет какое-то время, так что можете отвлечься на кофе, а к возвращению вас уже будет ждать свежесобранная Гидра. Готовая сборка находится в workdir/out:
1 2 | wrongbaud@wubuntu:~/blog/gba-re-gbd/ghidra-builder/workdir$ ls out/ ghidra_9.3_DEV_20201218_linux64.zip |
Распакуйте файл и можете запускать Ghidra через скрипт ./ghidraRun. Я распакую содержимое в каталог ghidra-builder/workdir, так как для сборки этой версии мы будем использовать docker-контейнер. Если вы следуете за процессом, то сейчас ваша рабочая директория должна выглядеть так:
1 2 3 4 | 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. Ломаем игру Spiderman.
Задача загрузчика Ghidra в настройке всех необходимый областей памяти, определении отладочной информации и символов, которые могут присутствовать в файле, а также выдача всей доступной информации о целевом файле. Упомянутый выше загрузчик описывает все основные периферийные устройства GBA и прекрасно подойдет для нашей задачи, так что начнем с его копирования в тот же каталог ghidra-builder/workdir, поскольку для сборки будем использовать тот же контейнер docker, с помощью которого собирали Ghidra.
1 2 3 4 5 6 7 8 | <span class="hljs-built_in">cd</span> ghidra-builder/workdir git <span class="hljs-built_in">clone</span> https://github.com/SiD3W4y/GhidraGBA sudo ../docker-tpl/run /bin/bash dockerbot@797eb43ce05f:/files/GhidraGBA$ <span class="hljs-built_in">export</span> 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$ <span class="hljs-built_in">exit</span> <span class="hljs-built_in">exit</span> |
Здесь мы:
1. Запускаем docker-контейнер.
2. Собираем расширение GhidraGBA, указывая путь к месту установки.
3. Копируем каталог расширений Ghidra, чтобы он показывался под меню Install Extensions.
4. Выходим из контейнера docker.
Запустите Ghidra командой ghidraRun и перейдите в File-> Install Extensions. Выерите загрузчик GhidraGBA и кликните OK. Для применения изменений потребуется перезапустить Ghidra. Теперь при загрузке GBA ROM должно отображаться следующее:
После выполнения автоматического анализа Ghidra неплохо поняла этот образ ROM. В нем определено много функций и выглядит все достаточно хорошо. Следующим шагом будет найти способ сузить нашу область интереса в этом образе. Говоря условно, нам нужно найти иголку в стоге сена. Начнем с выяснения принципа работы системы паролей, для чего просто попробуем ввести несколько их вариантов.
Ломаем игру Spiderman: анализ ROM
При вводе пароля мы наблюдаем такой экран:
Заметьте, что используются только согласные буквы и цифры от 0 до 9. Сам же пароль состоит из 5 символов. Для реверсинга это будет неплохой отправной точкой. С помощью данной информации можно сузить область интересующих нас функций. Например, давайте просмотрим строки ROM в поиске этих значений. Если открыть окно строк, Window -> Defined Strings, и сделать выборку по пяти первым доступным символам, то мы увидим следующее:
Кое-какой результат имеется – мы обнаружили две точки использования этой строки. Одна расположена в 0x804c11fc, а вторая в 0x84b86f0. При проверке первой строки мы видим, что она передается функции в подпрограмме по адресу 0x8003358:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <span class="hljs-function">undefined4 <span class="hljs-title">passwd_1</span><span class="hljs-params">(<span class="hljs-keyword">int</span> param_1,<span class="hljs-keyword">int</span> param_2)</span> </span>{ <span class="hljs-keyword">int</span> iVar1; uint uVar2; uint uVar3; undefined4 in_lr; undefined auStack52 [<span class="hljs-number">36</span>]; undefined4 uStack4; uStack4 = in_lr; FUN_080231f4(auStack52,<span class="hljs-string">"BCDFGHJKLMNPQRSTVWXYZ0123456789-"</span>,<span class="hljs-number">0x21</span>); *(uint *)(param_1 + <span class="hljs-number">0x8c</span>) = <span class="hljs-number">0</span>; FUN_080025f8(param_1); FUN_08002674(param_1); FUN_08002714(param_1); FUN_0800282c(param_1); iVar1 = <span class="hljs-number">0</span>; uVar3 = *(uint *)(param_1 + <span class="hljs-number">0x8c</span>); uVar2 = <span class="hljs-number">0</span>; <span class="hljs-keyword">do</span> { *(undefined *)(param_2 + iVar1) = auStack52[uVar3 >> (uVar2 & <span class="hljs-number">0xff</span>) & <span class="hljs-number">0x1f</span>]; uVar2 = uVar2 + <span class="hljs-number">5</span>; iVar1 = iVar1 + <span class="hljs-number">1</span>; } <span class="hljs-keyword">while</span> (iVar1 < <span class="hljs-number">5</span>); <span class="hljs-keyword">return</span> uStack4; } |
Обратите внимание на цикл, продолжающий выполнение при переменной < 5. Это говорит о том, что данная функция может оказаться полезной, поскольку пароль как раз содержит именно 5 символов. Давайте отметим ее как passwd_1 и перейдем к остальным местам использования нашей строки символов. Далее она встречается в функции по адресу 0x8002CEC. Вот декомпилированный вариант:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <span class="hljs-function">undefined8 <span class="hljs-title">passwd_2</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{ <span class="hljs-keyword">int</span> iVar1; <span class="hljs-keyword">int</span> iVar2; uint uVar3; undefined4 in_lr; undefined local_98 [<span class="hljs-number">5</span>]; undefined local_93; undefined auStack144 [<span class="hljs-number">36</span>]; undefined auStack108 [<span class="hljs-number">8</span>]; undefined auStack100 [<span class="hljs-number">72</span>]; undefined4 uStack4; uStack4 = in_lr; FUN_08000b0c(<span class="hljs-number">0</span>,<span class="hljs-number">1</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>); DAT_03001fd0._0_2_ = <span class="hljs-number">0x1444</span>; DISPCNT = <span class="hljs-number">0x1444</span>; FUN_0801e330(&DAT_0838277c); iVar1 = DAT_03001fe0; FUN_080231f4(auStack144,<span class="hljs-string">"BCDFGHJKLMNPQRSTVWXYZ0123456789-"</span>,<span class="hljs-number">0x21</span>); *(uint *)(iVar1 + <span class="hljs-number">0x8c</span>) = <span class="hljs-number">0</span>; FUN_080025f8(iVar1); FUN_08002674(iVar1); FUN_08002714(iVar1); FUN_0800282c(iVar1); iVar2 = <span class="hljs-number">0</span>; uVar3 = <span class="hljs-number">0</span>; <span class="hljs-keyword">do</span> { local_98[iVar2] = auStack144[*(uint *)(iVar1 + <span class="hljs-number">0x8c</span>) >> (uVar3 & <span class="hljs-number">0xff</span>) & <span class="hljs-number">0x1f</span>]; uVar3 = uVar3 + <span class="hljs-number">5</span>; iVar2 = iVar2 + <span class="hljs-number">1</span>; } <span class="hljs-keyword">while</span> (iVar2 < <span class="hljs-number">5</span>); local_93 = <span class="hljs-number">0</span>; FUN_0801d1bc(auStack108,local_98); FUN_0801d92c(DAT_03001ff0,<span class="hljs-number">0x10</span>,<span class="hljs-number">0</span>); FUN_08000b0c(<span class="hljs-number">1</span>,<span class="hljs-number">1</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>); *(undefined4 *)(DAT_03002028 + <span class="hljs-number">0xc</span>) = <span class="hljs-number">0x200</span>; FUN_08000f1c(); iVar1 = FUN_0801d26c(auStack108); *(undefined4 *)(DAT_03002028 + <span class="hljs-number">0xc</span>) = <span class="hljs-number">0</span>; FUN_08000f1c(); FUN_0801dcac(DAT_03001ff0,<span class="hljs-number">0</span>); FUN_08000b0c(<span class="hljs-number">0</span>,<span class="hljs-number">1</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>); FUN_08004408(auStack100,<span class="hljs-number">2</span>); <span class="hljs-keyword">return</span> CONCAT44(uStack4,(uint)(iVar1 == <span class="hljs-number">0</span>)); } |
И снова мы видим передачу этой строки в функцию, а также очередной цикл, выполняющий 5 итераций – отметим его как passwd_2 и перейдем далее. Следующая строка встречается по адресу 0x84b86f0 и также используется в двух подпрограммах. Вот первая, расположенная в FUN_0801c37c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <span class="hljs-function">undefined4 <span class="hljs-title">render_pw_screen</span><span class="hljs-params">(<span class="hljs-keyword">int</span> param_1)</span> </span>{ <span class="hljs-keyword">int</span> iVar1; <span class="hljs-keyword">int</span> iVar2; uint uVar3; undefined4 uVar4; uint uVar5; undefined4 in_lr; <span class="hljs-keyword">char</span> local_1c [<span class="hljs-number">8</span>]; undefined4 uStack4; uStack4 = in_lr; iVar2 = FUN_0801b834(DAT_03001ffc,<span class="hljs-string">"@ - Accept & - Backspace"</span>); iVar1 = DAT_03001ffc; *(uint *)(DAT_03001ffc + <span class="hljs-number">0x90</span>) = <span class="hljs-number">0xf0</span>U - iVar2 >> <span class="hljs-number">1</span>; *(undefined4 *)(iVar1 + <span class="hljs-number">0x94</span>) = <span class="hljs-number">0x96</span>; FUN_0801b764(iVar1,<span class="hljs-string">"@ - Accept & - Backspace"</span>); uVar3 = *(uint *)(param_1 + <span class="hljs-number">0x51c</span>); <span class="hljs-keyword">if</span> (uVar3 != <span class="hljs-number">0</span>) { uVar5 = <span class="hljs-number">0</span>; <span class="hljs-keyword">if</span> (uVar3 != <span class="hljs-number">0</span>) { <span class="hljs-keyword">do</span> { local_1c[uVar5] = <span class="hljs-string">"BCDFGHJKLMNPQRSTVWXYZ0123456789-"</span>[*(byte *)(param_1 + <span class="hljs-number">0x520</span> + uVar5)]; uVar5 = uVar5 + <span class="hljs-number">1</span>; } <span class="hljs-keyword">while</span> (uVar5 < uVar3); } local_1c[*(<span class="hljs-keyword">int</span> *)(param_1 + <span class="hljs-number">0x51c</span>)] = <span class="hljs-string">'\0'</span>; iVar2 = FUN_0801b834(DAT_03002000,local_1c); iVar1 = DAT_03002000; *(uint *)(DAT_03002000 + <span class="hljs-number">0x90</span>) = <span class="hljs-number">0xf0</span>U - iVar2 >> <span class="hljs-number">1</span>; *(undefined4 *)(iVar1 + <span class="hljs-number">0x94</span>) = <span class="hljs-number">0x3f</span>; iVar2 = FUN_0800118c(DAT_03001fdc,<span class="hljs-number">5</span>); *(byte *)(iVar1 + <span class="hljs-number">5</span>) = *(byte *)(iVar1 + <span class="hljs-number">5</span>) & <span class="hljs-number">0xf</span> | (byte)(iVar2 << <span class="hljs-number">4</span>); FUN_0801b764(DAT_03002000,local_1c); } <span class="hljs-keyword">if</span> (*(<span class="hljs-keyword">int</span> *)(param_1 + <span class="hljs-number">0x51c</span>) != <span class="hljs-number">5</span>) { uVar4 = FUN_0801a6d4(*(undefined4 *)(param_1 + <span class="hljs-number">0x18</span>)); *(undefined4 *)(param_1 + <span class="hljs-number">4</span>) = uVar4; } <span class="hljs-keyword">return</span> uStack4; } |
В этой функции мы видим, что FUN_0801b764 вызывается со строкой @ — Accept & — Backspace. Несколько далее та же функция вызывается с переменной, содержащей интересующую нас строку. При дальнейшем рассмотренииFUN_0801b764 мы узнаем, что она копирует данные из второй переменной (строки ASCII) в адрес памяти первого аргумента. Здесь уже нельзя сказать уверенно, но меня кажется, что конкретно эта подпрограмма служит для отрисовки текста на экране, поэтому пока что я ее пропущу и перейду к следующему месту использования строки символов, которое привожу ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="hljs-function">undefined8 <span class="hljs-title">FUN_0801c454</span><span class="hljs-params">(<span class="hljs-keyword">int</span> param_1)</span> </span>{ <span class="hljs-keyword">int</span> iVar1; <span class="hljs-keyword">int</span> iVar2; undefined4 in_lr; <span class="hljs-keyword">char</span> local_14 [<span class="hljs-number">8</span>]; undefined4 uStack4; iVar2 = <span class="hljs-number">1</span>; uStack4 = in_lr; FUN_080231f4(local_14,<span class="hljs-string">"CRDT5"</span>,<span class="hljs-number">6</span>); iVar1 = <span class="hljs-number">0</span>; <span class="hljs-keyword">do</span> { <span class="hljs-keyword">if</span> (local_14[iVar1] != <span class="hljs-string">"BCDFGHJKLMNPQRSTVWXYZ0123456789-"</span>[*(byte *)(param_1 + <span class="hljs-number">0x520</span> + iVar1)]) { iVar2 = <span class="hljs-number">0</span>; } iVar1 = iVar1 + <span class="hljs-number">1</span>; } <span class="hljs-keyword">while</span> ((iVar1 < <span class="hljs-number">5</span>) && (iVar2 != <span class="hljs-number">0</span>)); <span class="hljs-keyword">return</span> CONCAT44(uStack4,iVar2); } |
Ломаем игру Spiderman. Что у нас здесь? Во-первых, здесь мы видим FUN_080231f4, по сути являющуюся операцией memcpy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <span class="hljs-function">undefined4 * <span class="hljs-title">memcpy_1</span><span class="hljs-params">(undefined4 *dest,undefined4 *src,uint count)</span> </span>{ undefined4 uVar1; undefined4 *puVar2; undefined4 *puVar3; puVar2 = dest; <span class="hljs-keyword">if</span> ((<span class="hljs-number">0xf</span> < count) && ((((uint)src | (uint)dest) & <span class="hljs-number">3</span>) == <span class="hljs-number">0</span>)) { <span class="hljs-keyword">do</span> { *puVar2 = *src; puVar2[<span class="hljs-number">1</span>] = src[<span class="hljs-number">1</span>]; puVar3 = src + <span class="hljs-number">3</span>; puVar2[<span class="hljs-number">2</span>] = src[<span class="hljs-number">2</span>]; src = src + <span class="hljs-number">4</span>; puVar2[<span class="hljs-number">3</span>] = *puVar3; puVar2 = puVar2 + <span class="hljs-number">4</span>; count = count - <span class="hljs-number">0x10</span>; } <span class="hljs-keyword">while</span> (<span class="hljs-number">0xf</span> < count); <span class="hljs-keyword">while</span> (<span class="hljs-number">3</span> < count) { uVar1 = *src; src = src + <span class="hljs-number">1</span>; *puVar2 = uVar1; puVar2 = puVar2 + <span class="hljs-number">1</span>; count = count - <span class="hljs-number">4</span>; } } <span class="hljs-keyword">while</span> (count = count - <span class="hljs-number">1</span>, count != <span class="hljs-number">0xffffffff</span>) { *(undefined *)puVar2 = *(undefined *)src; src = (undefined4 *)((<span class="hljs-keyword">int</span>)src + <span class="hljs-number">1</span>); puVar2 = (undefined4 *)((<span class="hljs-keyword">int</span>)puVar2 + <span class="hljs-number">1</span>); } <span class="hljs-keyword">return</span> dest; } |
Ее задача – копирование строки CRDT5 в указатель ячейки памяти в local_14. Далее мы видим, что в цикле while это значение используется в сравнении:
1 2 | <span class="hljs-keyword">if</span> (local_14[iVar1] != <span class="hljs-string">"BCDFGHJKLMNPQRSTVWXYZ0123456789-"</span>[*(byte *)(param_1 + <span class="hljs-number">0x520</span> + iVar1)]) |
Что же происходит здесь? В каждой итерации символ из local_14 сравнивается со значением из нашей строки доступных символов BCDFGHJKLMNPQRSTVWXYZ0123456789-. Такое поведение вполне соответствует предполагаемым действиям функции проверки пароля. Но мы знаем, что iVar1 при каждой итерации увеличивается на 1. Значит ли это, что пароли должны состоять из смежных символов в BCDFGHJKLMNPQRSTVWXYZ0123456789-? Это бы было очень глупо, к тому же строка CRDT5 никогда бы не прошла такую проверку. Если еще раз взглянуть на условие сравнения, то можно заметить, что в нем присутствует переменная param_1, которая тоже используется в качестве индекса, к которому прибавляются iVar1 и 0x520 – затем эти значения используются как INDEX в доступных для набора символах.
О чем это говорит? Переменная param_1 скорее всего указывает на массив смещений, представляющих введенные на экране пароля символы. Например, если мы введем GHDRR, то массив будет содержать [0x4,0x5,0x2,0xd,0xd].
Ломаем игру Spiderman. Но давайте не будем забегать вперед и для начала попробуем пароль 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 выполните следующие команды:
1 2 3 4 5 6 7 | <span class="hljs-built_in">set</span> architecture arm <span class="hljs-built_in">set</span> arm fallback-mode thumb <span class="hljs-built_in">set</span> arm force-mode thumb target remote localhost:2345 <span class="hljs-built_in">break</span> *0x801c470 c |
Здесь мы устанавливаем gdb архитектуру, подключаемся к удаленному серверу и в завершении определяем точку останова у функции, которая, как мы считаем, проверяет, нужно ли показывать сцену с титрами. Говоря конкретнее, устанавливаем ее у сегмента, сравнивающего переданный нами символ с извлеченным из строки доступных символов. Рассматривать мы будем этот фрагмент ассемблера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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,<span class="hljs-comment">#0x0]; r1 содержит значение индекса переданного символа пароля. Например, "B" == 0, "C"==1, и т.д.</span> 0801c478 c9 18 add r1,r1,r3; r3 содержит указатель на строку доступных символов. Мы добавляем к этому указателю индекс текущего символа пароля. 0801c47a 00 78 ldrb r0=>local_14,[r0,<span class="hljs-comment">#0x0] ; Загрузка r0 из стека со значением строки "CRDT5" по индексу, указанному r2 </span> 0801c47c 09 78 ldrb r1,[r1,<span class="hljs-comment">#0x0]=>s_BCDFGHJKLMNPQRSTVWXYZ012345678 = "BCDFGHJKLMNPQRSTVWXYZ01234567 ; Загрузка представления символа на основе введенного для пароля значения</span> 0801c47e 88 42 cmp r0,r1 ; Сравнение! 0801c480 00 d0 beq LAB_0801c484 0801c482 00 25 mov r5,<span class="hljs-comment">#0x0</span> LAB_0801c484 XREF[1]: 0801c480(j) 0801c484 01 32 add r2,<span class="hljs-comment">#0x1; Увеличение счетчика индекса</span> 0801c486 04 2a cmp r2,<span class="hljs-comment">#0x4</span> 0801c488 01 dc bgt LAB_0801c48e 0801c48a 00 2d cmp r5,<span class="hljs-comment">#0x0</span> 0801c48c f0 d1 bne LAB_0801c470 |
Введя все вышеприведенные команды, посмотрим, сработает ли точка останова…
Превосходно! Мы не только достигли точки останова, но и зафиксировали все регистры. Теперь проверим, верны ли были все наши предположения в отношении проверки пароля. Прошагаем через несколько инструкций до позиции 0801c474. Здесь мы предполагаем, что r1 будет указывать на массив индексов, представляющих введенные нами символы. Для выяснения этого заглянем в память:
К сведению: если вы делаете отладку удаленно при помощи gdb-multiarch, и при этом некоторые точки останова не срабатывают, попробуйте использовать команду stepi вместо c. Такую проблему я встречал в mGBA ранее, и она не связана с сервером GDB.
1 2 3 4 | (gdb)x/10x <span class="hljs-variable">$r1</span> 0x2005998: 0x01 0x0d 0x02 0x0f 0x1a 0x00 0x00 0x00 0x20059a0: 0x00 0x4f |
Вот оно! Что и следовало ожидать – вместо сохранения фактических символов ascii, вводимых в качестве пароля, сохраняются значения их индексов в таблице доступных символов:
Просто ради проверки, давайте посмотрим, что произойдет, если ввести в качестве пароля CGHDR и установить те же точки останова:
1 2 3 4 5 | Breakpoint 3, 0x0801c476 Can<span class="hljs-string">'t determine the current process'</span>s PID: you must name one. (gdb)x/10x <span class="hljs-variable">$r1</span> 0x2005998: 0x01 0x04 0x05 0x02 0x0d 0x00 0x00 0x00 0x20059a0: 0x00 0x60 |
Все так и есть! Теперь мы знаем, как сохраняются пароли, и как они выглядят в памяти, а также умеем делать отладку из Ghidra. Думаю, что для данной статьи на этом можно прерваться – в следующей же мы исследуем другие особенности пароля при помощи той же Ghidra и возможностей удаленной отладки GDB.
Ломаем игру Spiderman. Заключение
Сегодня мы познакомились с инструментами, позволяющими собрать Ghidra, рассмотрели некоторые из заявленных возможностей отладчика, с помощью которых смогли произвести удаленную отладку игры на Game Boy Advance. Многое из проделанного вы можете выполнить и без Ghidra, используя только gdb-multiarch, но я хотел познакомиться с этими возможностями и попутно поделиться с вами опытом.
Как всегда, по любым возникшим вопросам обращайтесь ко мне в Twitter. Если же вам интересно побольше узнать о Ghidra или взломе аппаратных средств в общем, можете ознакомиться с подготовленными мной обучающими материалами (англ.).
Дополнительная информация / Примечания
Основные выводы здесь: gvba не работает ни с какими современными GDB. По какой-то причине gdb-multiarch пропускает точки останова, а gdb из devkitarm не отвечает должным образом ghidra для предоставления регистров. Ломаем игру Spiderman.