Skip to content

Instantly share code, notes, and snippets.

@AlekseyCherepanov
Last active June 3, 2019 13:25
Show Gist options
  • Save AlekseyCherepanov/f7125b8b0e1b68900f008f0a7ce3fd2e to your computer and use it in GitHub Desktop.
Save AlekseyCherepanov/f7125b8b0e1b68900f008f0a7ce3fd2e to your computer and use it in GitHub Desktop.
Write-up for Best Reverser contest at PHDays 9 (2019)

Райтап конкурса Best Reverser на конференции PHDays 9 (2019)

Работа над задачей позволила познакомиться с архитектурой m68k и попробовать сделать простые инструменты для работы с трассировкой инструкций.

Райтап описывает успешную часть решения, опуская тупиковые и не использованные ветки развития решения.

TL;DR, очень коротко:

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

Задача

С веб-страницы конкурса:

Best Reverser

Конкурс для реверс-инженеров, в котором любой может продемонстрировать навыки анализа исполняемых файлов. Проводится с 1 по 14 мая.

Условия участия

К конкурсу допускается любой интернет-пользователь.

Технические детали

Участникам для анализа представляется ROM игры для Sega Mega Drive (best_reverser_phd9_rom_v4.bin).

Задача: подобрать такой ключ, который в паре с email-адресом участника будет признан валидным.

Для проверки правильности решения можно использовать эмуляторы Sega Mega Drive, доступные по следующим ссылкам:

https://www.carpeludum.com/download/Fusion364.zip (Windows)
https://www.carpeludum.com/download/Fusion363x.tar.gz (Linux)
https://www.carpeludum.com/download/Fusion363i.zip (macOS)

Ответы и комментарии к решению необходимо присылать на адрес best2019re@ptsecurity.com.

Призы

Инвайты на PHDays и памятные призы от организаторов.

Финалисты Best Reverser

Место Время отправки решения Имя/никнейм
1 02.05.2019 14:55 sysenter
2 02.05.2019 19:49 Vesemir
3 02.05.2019 20:55 a1exdandy
4 03.05.2019 16:47 Aleksey Cherepanov
5 05.05.2019 21:33 Marat Gayanov
6 06.05.2019 21:26 AV1ct0r
7 08.05.2019 19:21 mostobriv
8 10.05.2019 01:34 Danila Parnishchev
9 10.05.2019 02:15 Ivan Uschapovskiy
10 10.05.2019 14:44 Dmitriy Sklyar
11 10.05.2019 19:13 vient
12 13.05.2019 03:08 Sergey Karbovsky

Райтап:

Ключевые моменты решения

  • Запуск ROM'а в эмуляторе дал представление о приложении: это классическое crackme, только вводить текст неудобно из-за наэкранной клавиатуры (всё-таки для ввода используется gamepad).

  • Сообщения из интерфейса были сопоставлены с результатами strings. Для сообщений о результате проверки были найдены адреса - простое смещение от начала файла.

  • Для дизассемблирования использовался radare2 последней версии. (Хотя можно использовать objdump из пакета binutils-multiarch.)

  • По адресам строк сообщений о результатах проверки были найдены концы проверки в коде.

  • Поиск инструментов в интернете привёл к эмулятору Gens KMod, который предоставляет gdb server.

    • Получилось выяснить адреса буферов с почтой и ключом: 0xff01aa и 0xff01c7.

    • Однако подход оказался слишком медленным для трассировки через gdb. Так что было решено добавить трассировку в эмулятор.

  • Для модификаций был выбран эмулятор Mednafen. Он есть в основном репозитории Debian и, соответственно, его можно легко собрать из исходников при помощи команды apt-get -b source.

  • В Mednafen было создано сохранённое состояние с введённой почтой и ключом-пустышкой A01BC23DE45FG67H, который легко ввести, но который при этом не имеет повторов в памяти, что позволяет его легко заменить на другой ключ в файле состояния.

  • Эмулятор был настроен так, чтобы стартовать сразу с заданного состояния.

  • Для трассировки был исправлен исходный код Mednafen:

    • был добавлен выход по достижению одной из концовок проверки ключа,
    • было добавлено зажатие кнопки Enter, чтобы при старте эмулятора сразу вызывалась проверка ключа,
    • был добавлен вывод адреса текущей инструкции, её опкода и всех регистров в файл.
  • По полученным записям выполнения проверки получилось найти первую проверку по адресу 0x1f12 со сравнением, которое забраковывало ключ, если младшие два байта регистра d2 не равны значению 0xcb4c (лежащему в памяти). При этом значение регистра зависело от ключа.

  • Чтобы пройти первую проверку, использовался перебор: эмулятор запускался со случайными ключами, результаты записывались.

  • Через несколько часов был найден подходящий ключ: D2S5YSX951P0OAD3. Тут не обошлось без везения: за 117 тысяч запусков получилось найти только 54 тысячи из 65 тысяч возможных значений в регистре в момент проверки.

  • Найденный ключ проходил проверку со значением 0xcb4c, но оказалось, что этого недостаточно для решения задачи. И дальше всё становилось только интереснее, потому что трассировка показывала, что часть код находится в изменяемой памяти.

  • Вторая проверка была устроена несколько иначе и была расположена как раз в изменяемой памяти по адресу 0xff0daa. Она сравнивала двухбайтовое значение, нетривиально зависящее от ключа, со значением, зависящим от почты. При этом изменение почты не могло создать подходящее значение.

  • Было замечено, что код в изменяемой памяти вызывает функцию по адресу 0x1526. Ей через стек передаётся указатель, который в одном вызове совпадает с адресом ключа. А в 3 других вызовах этот аргумент является отступами от начала буфера ключа, идущими по 4 байта. При этом два вызова происходят до первой проверки, а ещё два - после.

  • Это привело к предположению, что ключ не используется целиком для первой проверки и его части можно менять независимо. Были попробованы ключи с другими последними 4 байтами.

    • Изменения последних 4 байт ключа не повлияли на первую проверку.

    • Оказалось, что последние 2 байта ключа - hex младшего байта значения, участвующего во второй проверке.

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

  • Новый ключ прошёл обе проверки и оказался решением задачи.

Первое знакомство с выданным файлом

$ file best_reverser_phd9_rom_v4.bin
best_reverser_phd9_rom_v4.bin: Sega MegaDrive/Genesis raw ROM dump Name: "BEST REVERSER PH" (C)PT RE 2019   
$ sha256sum best_reverser_phd9_rom_v4.bin
381efbbe1a1be2c683858dbf0c541c414565bab6ff17438e0dd19c46cadacae2  best_reverser_phd9_rom_v4.bin

При запуске в эмуляторе появляется следующий текст:

Sega MD [KEYGEN ME]
A=INPUT B=ERASE C=CLEAR S=CHECK

0 1 2 3 4 5 6 7 8 9
A B C D E F G H I J
K L M N O P Q R S T
U V W X Y Z @ _ . -

Email:
  Key:

При нажатии кнопки Start ввод переходит во второе поле, потом вызывает проверку и переходит обратно в первое поле. В результате проверки показывается одно из трёх сообщений:

  • Wrong length! Try again... (смещение 0xfdfa), если в поле ключа меньше 16 символов,
  • Wrong key! Try again... (смещение 0xfe30), если ключ не подошёл,
  • YOU ARE THE BEST REVERSER! (смещение 0xfe15), если ключ подошёл.

Но третье сообщение не получается легко. Его можно найти в самом файле между двумя другими сообщениями.

Смещения сообщений относительно начала файла находились по-простому:

$ python -c 'import sys; print hex(sys.stdin.read().index("Wrong length! Try again..."))' < best_reverser_phd9_rom_v4.bin
0xfdfa

Дизассемблирование

Для дизассемблирования использовался radare2. При этом был дизассемблирован весь файл, включая служебный заголовок:

$ r2 -AA best_reverser_phd9_rom_v4.bin
WARNING: Plugin smd should implement load_buffer method instead of load_bytes.
Copyright: SEGA MEGA DRIVE (C)PT RE 2019   
DomesticName: BEST REVERSER PHD 9 (2019)                      
OverseasName: BEST REVERSER PHD 9 (2019)                      
ProductCode: GM 12345678-90
Checksum: 0x0000
Peripherials: JD              
SramCode:   
ModemCode:             
CountryCode: JUE             
[...]
[0x00000200]> 0
[0x00000000]> pD $s >bdis

Результат дизассемблирования был записан в файл bdis. Этот файл использовался, как импровизированная база данных для добавления ассемблера в результаты трассировки.

  • Для использования файла bdis вместе с результатом трассировки оказалось недостаточно простого дизассемблирования, потому что при работе происходит прыжок на адрес 0x23e8, который не был дизассеблирован, как отдельная инструкция.

Это несложно исправить, добавив ещё раз правильный кусок кода в файл bdis.

$ r2 -AA best_reverser_phd9_rom_v4.bin
[...]
[0x00000200]> 0x23e8
[0x000023e8]> pD 100
            0x000023e8      4feffff4       lea.l -0xc(a7), a7
            0x000023ec      48e73c3c       movem.l d2-d5/a2-a5, -(a7)
            0x000023f0      303900ff0d1a   move.w 0xff0d1a.l, d0
            0x000023f6      00400001       ori.w 0x1, d0
            0x000023fa      33c000ff0d1a   move.w d0, 0xff0d1a.l
            0x00002400      203900ff0c78   move.l 0xff0c78.l, d0
            0x00002406      5280           addq.l 0x1, d0
            0x00002408      23c000ff0c78   move.l d0, 0xff0c78.l
            0x0000240e      207900ff0c94   movea.l 0xff0c94.l, a0
            0x00002414      b0fc0000       cmpa.w 0x0, a0
        ,=< 0x00002418      6702           beq.b 0x241c
        |   0x0000241a      4e90           jsr (a0)
        |   ; CODE XREF from fcn.00001608 (+0xe10)
        `-> 0x0000241c      263900ff0cf0   move.l 0xff0cf0.l, d3
[...]
  • В bdis было вырезана левая часть строк, где проходят визуальные линии. Только < и > были оставлены.

  • Позже пришлось добавить ещё инструкции, которые не были дизассеблированы сразу:

  0x0000151e      203900ff017e   move.l 0xff017e.l, d0
  0x00001524      4e75           rts
  • Предполагалось вставлять ассемблер в результат трассировки по адресам, благо в ходе программы код не изменялся. Но после первой проверки код был в изменяемой части памяти, так что пришлось добавить соединение ассемблера и истории по опкодам.

Отдельно были дизассеблированы некоторые инструкции вот такой командой (неправильной для некоторых инструкций, типа условных переходов):

$ rasm2 -a m68k -d 12300800
move.b (a0, d0.l), d1

Тут надо обратить внимание на 2 особенности: во-первых, если дать слишком мало данных, то ошибки не будет, просто инструкция будет дизассемблирована неправильно, во-вторых, условные переходы обычно кодируются, используя относительное смещение, а в ассемблере показываются с абсолютным адресом. Так что условные переходы не получится корректно показывать, связывая статичную строку ассемблера с опкодом.

Для задания адреса инструкции для дизассемблера есть опция -o:

$ rasm2 -a m68k -d 66ea -o 0xff0d96
bne.b 0xff0d82

Так что в файл были добавлены ещё такие строки:

  0x12345678      1002           move.b d2, d0
  0x12345678      12300800       move.b (a0, d0.l), d1
  0x12345678      0c010020       cmpi.b 0x20, d1
  0x12345678      4e560000       link.w a6, 0x0
  0x12345678      48e73838       movem.l d2-d4/a2-a4, -(a7)
  0x12345678      206f0032       movea.l 0x32(a7), a0
  0x12345678      22680006       movea.l 0x6(a0), a1
  0x12345678      2668000e       movea.l 0xe(a0), a3
  0x12345678      d1fc00000012   adda.l 0x12, a0
  0x12345678      280c           move.l a4, d4
  0x12345678      5084           addq.l 0x8, d4
  0x12345678      49ec000c       lea.l 0xc(a4), a4
  0x12345678      b143           eor.w d0, d3
  0x12345678      2480           move.l d0, (a2)
  0x12345678      4cee1c1cffe8   movem.l -0x18(a6), d2-d4/a2-a4
  0x12345678      2468000a       movea.l 0xa(a0), a2

  0x00ff0d96      66ea           bne.b 0xff0d82

Инструкция по адресу 0xff0d96 - не единственный прыжок в коде в изменяемой области памяти, но именно этот прыжок сильно мешал пониманию кода.

  • Можно воспользоваться objdump для дизассемблерования. Он есть в основном репозитории операционной системы Debian. Однако для поддержки архитектуры m68k надо использовать пакет binutils-multiarch. Плюс формат файла не опознаётся, так что надо будет дизассемблировать файл целиком:
$ objdump -m m68k -b binary -D best_reverser_phd9_rom_v4.bin

Пример использования objdump для одной инструкции с указанием адреса инструкции:

$ python -c 'import sys; sys.stdout.write("66ea".decode("hex"))' > t.bin
$ objdump -m m68k -b binary -D t.bin --adjust-vma=0xff0d96
[...]
  ff0d96:	66ea           	bnes 0xff0d82

Чтение ассемблера

Вначале были найдены смещения строк с сообщениями о результате проверки ключ. По смещениям можно найти код вывода результатов, потому что они используются напрямую.

> 0x000019aa      4878000e       pea.l 0xe.w
  0x000019ae      48780008       pea.l 0x8.w
  0x000019b2      48790000fe30   pea.l 0xfe30.l  ; "Wrong key..."
  0x000019b8      4eb900006fc0   jsr fcn.00006fc0
> 0x00001d1c      4878000e       pea.l 0xe.w
  0x00001d20      48780008       pea.l 0x8.w
  0x00001d24      48790000fdfa   pea.l 0xfdfa.l  ; "Wrong length..."
  0x00001d2a      4eb900006fc0   jsr fcn.00006fc0
> 0x00001a6c      4878000e       pea.l 0xe.w
  0x00001a70      48780008       pea.l 0x8.w
  0x00001a74      48790000fe15   pea.l 0xfe15.l  ; "YOU ARE THE..."
  0x00001a7a      4eb900006fc0   jsr fcn.00006fc0
  0x00001a80      4fef000c       lea.l 0xc(a7), a7
< 0x00001a84      6000ff3c       bra.w 0x19c2
  • bra - безусловный переход, от BRanch Always (что написано тут).

  • pea кладёт константу в стек, от Push Effective Address (что написано тут).

По адресам кода с выводом можно найти некоторые проверки.

Вот проверка длины ключа:

> 0x000017de      41f900ff01c7   lea.l 0xff01c7.l, a0
  0x000017e4      7000           moveq 0x0, d0
  0x000017e6      303900ff01e4   move.w 0xff01e4.l, d0
  0x000017ec      0c3000200800   cmpi.b 0x20, (a0, d0.l)
< 0x000017f2      67000528       beq.w 0x1d1c  ; go to "Wrong length..."

Отсюда можно предположить, что буфер с введённым ключом располагается по адресу 0xff01c7. Туда точно стоит заглянуть.

Видно ещё две проверки, которые могут забраковать ключ.

  0x00001998      4a81           tst.l d1
< 0x0000199a      660e           bne.b 0x19aa  ; go to "Wrong key..."
  0x0000199c      2f0b           move.l a3, -(a7)
  0x0000199e      4ebafb64       jsr fcn.00001504(pc)
  0x000019a2      588f           addq.l 0x4, a7
  0x000019a4      4a80           tst.l d0
< 0x000019a6      660000c4       bne.w 0x1a6c  ; go to "YOU ARE ..."

Если условный прыжок по адресу 0x19a6 не переходит на победную концовку, то получается Wrong key..., потому что этот код идёт сразу за этим блоком.

gdb и Gens KMod

Поверхностное изучение доступных инструментов для разработки и отладки ROM'ов привело на страницу с разными инструментами и в частности к эмулятору Gens KMod, в котором есть встроенный gdb server. Этот эмулятор поддерживает только ОС Windows, но он без проблем работает под Wine.

(Если говорить об инструментах и материалах, надо заметить, что в ROM'е есть строка DR. MEFISTO (03/26/2019), а 20 марта 2019 была опубликована статья о добавлении загрузки ROM'ов в GHIDRA за авторством DrMefistO, у которого есть и другие статьи по теме Sega Mega Drive.)

gdb server надо включить в настройках Gens KMod. После этого можно использовать gdb-multiarch, чтобы отлаживать программу со всеми удобствами. При этом gdb-multiarch может работать прямо на Debian, пока Gens KMod работает в Wine.

Для корректной работы gdb надо указать архитектуру и порядок байтов в машинных словах (endianness).

$ gdb-multiarch
[...]

(gdb) set architecture m68k
The target architecture is assumed to be m68k

(gdb) show endian
The target endianness is set automatically (currently little endian)

(gdb) set endian big
The target is assumed to be big endian

(gdb) target remote localhost:6868
Remote debugging using localhost:6868
  • Изучение памяти позволило найти буферы для почты и ключа.

После введения 1111BBBBBBBBB в качестве почты:

(gdb) x/4096b 0xff0000
[...]
0xff01a8:  0x26  0x10  0x31  0x31  0x31  0x31  0x42  0x42
0xff01b0:  0x42  0x42  0x42  0x42  0x42  0x42  0x42  0x20
0xff01b8:  0x20  0x20  0x20  0x20  0x20  0x20  0x20  0x20
0xff01c0:  0x20  0x20  0x20  0x20  0x20  0x20  0x00  0x20
0xff01c8:  0x20  0x20  0x20  0x20  0x20  0x20  0x20  0x20
0xff01d0:  0x20  0x20  0x20  0x20  0x20  0x20  0x00  0x00
[...]

Для почты 1111BBBBBBBBBBDDDDDDDDDDDDDD и ключа DEEEEEEEEEEEEEEE:

(gdb) x/4096b 0xff01a8
0xff01a8:  0x26  0x10  0x31  0x31  0x31  0x31  0x42  0x42
0xff01b0:  0x42  0x42  0x42  0x42  0x42  0x42  0x42  0x42
0xff01b8:  0x44  0x44  0x44  0x44  0x44  0x44  0x44  0x44
0xff01c0:  0x44  0x44  0x44  0x44  0x44  0x44  0x00  0x44
0xff01c8:  0x45  0x45  0x45  0x45  0x45  0x45  0x45  0x45
0xff01d0:  0x45  0x45  0x45  0x45  0x45  0x45  0x45  0x00
0xff01d8:  0x00  0xff  0x25  0x3e  0x00  0x0e  0x00  0x00
[...]

Так что адреса почты и ключа - 0xff01aa и 0xff01c7. При этом изначально буферы заполнены пробелами, но для ключа там только 15 пробелов, хотя ввести можно 16 символов.

  • С gdb можно попробовать трассировать по инструкциям при помощи простого цикла с x/1i $pc и si.

Однако при этом трассировка работала медленно и застревала в цикле, который, вероятно, ждёт ввод:

(gdb) while 1
  x/1i $pc
  si
end
[...]
0x00000c92 in ?? ()
=> 0xc92:	movew 0xc00004,%d0
0x00000c98 in ?? ()
=> 0xc98:	btst #3,%d0
0x00000c9c in ?? ()
=> 0xc9c:	bnes 0xc92

Вместе с такой трассировкой не получалось нажать "Start" и запустить проверку. Выбраться из цикла точкой останова сразу за 0xc9c тоже не получилось.

Хотя, вероятно, можно было бы поставить точку останова на коде для проверки длины. Но это было упущено.

Модификация эмулятора Mednafen

Трассировка

Желание получить быструю трассировку инструкций привело к идее поправить эмулятор для записи хода выполнения в файл.

Выбор пал на Mednafen, который есть в основном репозитории операционной системы Debian и является свободным программным обеспечением.

Чтобы собрать Mednafen из исходников, можно использовать команду apt-get -b source mednafen. Все зависимости нужные для сборки можно поставить командой apt-get build-dep mednafen. Если её не использовать и не будет нужных пакетов, будет выдана ошибка с перечислением отсутствующих пакетов.

После сборки пакета mednafen остаётся каталог с исходниками. Внутри есть папка src/, в которой есть объектные файлы и готовый Makefile, так что после правок можно использовать make в каталоге src/. Там же находится исполняемый файл mednafen.

Код эмулятора процессора m68k находится в файле hw_cpu/c68k/c68kexec.c. Функция C68k_Exec вызывается на каждом шаге. В неё-то и был добавлен код для трассировки.

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

#include <stdio.h>
#include <stdlib.h>

В функции C68k_Exec сразу после строки Opcode = FETCH_WORD; был добавлен код, приведённый ниже.

Код для открытия файла для результатов трассировки (trace.txt):

    static FILE *trace = 0;
    if (!trace) {
        trace = fopen("trace.txt", "wb");
        if (!trace) {
            puts("fopen trace.txt failed");
            exit(1);
        }
    }

Код для выхода по достижению одной из веток с показом результата проверки ключа:

    if (PC == 0x000019b8) {
        puts("wrong_key branch");
        exit(2);
    }
    if (PC == 0x00001a7a) {
        puts("you_are branch");
        exit(3);
    }
    if (PC == 0x00001d2a) {
        puts("wrong_length branch");
        exit(4);
    }

Надо заметить, что exit() - "грязный" выход для многопоточного положения, использующего SDL/OpenGL. Иногда Mednafen падает на этом и получает сигнал, так что полагаться на код возврата не стоит.

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

    u32 mypc = PC;
    PC += 2; /* <-- original line */
    u32 op2 = FETCH_WORD;
    PC += 2;
    u32 op3 = FETCH_WORD;
    PC -= 2;

Собственно, код, выводящий данные в файл, пропуская некоторые адреса:

    if (0x00000c92 <= PC && PC <= 0x00000cb6) {
        /* skipped */
    } else if (0x0000702a <= PC && PC <= 0x00007034) {
        /* skipped */
    } else {
        fprintf(trace,
                "%04x %04x%04x%04x"
                " [A %x %x %x %x %x %x %x %x"
                " D %x %x %x %x %x %x %x %x]\n",
                mypc, Opcode, op2, op3,
                CPU->A[0], CPU->A[1], CPU->A[2], CPU->A[3],
                CPU->A[4], CPU->A[5], CPU->A[6], CPU->A[7],
                CPU->D[0], CPU->D[1], CPU->D[2], CPU->D[3],
                CPU->D[4], CPU->D[5], CPU->D[6], CPU->D[7]
            );
    }

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

Изначально выводились только PC и Opcode и для всех инструкций. При этом в процессе трассировки записывалось около 2.5 миллионов инструкций, из которых ~4/5 были в цикла 0x0c92-0x0cb6 и 0x702a-0x7034.

> 0x00000c92      303900c00004   move.w 0xc00004, d0
  0x00000c98      08000003       btst.b 0x3, d0
< 0x00000c9c      66f4           bne.b 0xc92
  ; CODE XREFS from fcn.0000067c (0xcb6, 0xce0)
> 0x00000c9e      303900c00004   move.w 0xc00004, d0
  0x00000ca4      08000003       btst.b 0x3, d0
< 0x00000ca8      6600ff4a       bne.w 0xbf4
> 0x0000702a      1018           move.b (a0)+, d0
  0x0000702c      4880           ext.w d0
  0x0000702e      d041           add.w d1, d0
  0x00007030      32c0           move.w d0, (a1)+
  0x00007032      b5c8           cmpa.l a0, a2
< 0x00007034      66f4           bne.b 0x702a

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

Автоматический запуск проверки ключа

Чтобы нажимать сразу Start, была добавлена строка keys[SDLK_RETURN] = 1; в функции UpdatePhysicalDeviceState в файле drivers/input.cpp.

 memcpy(keys, SDL_GetKeyState(0), MKK_COUNT);  /* <-- original line */
 keys[SDLK_RETURN] = 1;

Старт с заданного состояния

Старт с заданного состояния не потребовал модификаций исходников. Загрузка сохранённого состояния - стандартная возможность эмуляторов. Некоторые эмуляторы позволяют указывать файл с сохранением через командную строку. Mednafen использует autosave: есть особый файл с состоянием, с которым стартует эмулятор и в который автоматически сохраняется состояние при закрытии эмулятора. Для использования этой возможности надо указать autosave 1 в файле конфигурации Mednafen (~/.mednafen/mednafen-09x.cfg).

При этом состояние будет сохраняться в файл с расширением .mca:

~/.mednafen/mcs/best_reverser_phd9_rom_v4.99e440e408b93591d6fbedfb548d8334.mca

Файл с состоянием - бинарный файл, сжатый gzip'ом. Если его распаковать, можно увидеть строки из памяти, в частности введённые почту и ключ. Оказалось, что можно не вводить ключ вручную, заменяя его в файле состояния.

С заготовленным состоянием и автоматической проверкой при старте, количество строк в результате трассировки упало до 28900.

Анализ результатов трассировки

Формат результатов:

pc opcode [A a0 a1 a2 a3 a4 a5 a6 a7 D d0 d1 d2 d3 d4 d5 d6 d7]

где [A, D, ] и пробелы вставлены, как есть; на месте остальных подстрок находятся следующие значения:

  • pc - адрес инструкции в формате %04x (хотя может быть 5 цифр),
  • opcode - 6 байт в hex из памяти по адресу инструкции (скрипт для добавления ассемблера ниже обрезает до 2 байт),
  • a0-a7, d0-d7 - значения регистров на момент до выполнения инструкции, записанные в шестнадцатеричной системе счисления.

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

Команды для анализа

Результаты трассировки записывались в текстовый файл в простом формате, так что использовались соответствующие инструменты для работы с этим файлом.

  • Подсчёт статистики по файлу трассировки:
$ cut -d ' ' -f 1 trace.txt | sort | uniq -c

(Тут можно было бы добавить | less, но использовался shell-mode в emacs, так что свободная работа с историей вывода команд была без дополнительных усилий.)

  • Поиск инструкции по адресу:
$ grep '^1f12 ' trace.txt
1f12 b46e00226700 [A ff1fd4 ff0de4 9d ff0d46 56bc 11d0 ff1d48 ffffff4e D ffffff 9fd4 cb4c ff0d46 1c 9 fffedc e3e]

Подобным образом можно выбирать строки по содержимому регистра. И команды можно сочетать.

  • Взятие части, начиная с какого адреса:
$ sed -ne '/^1f12 /,$ p' trace.txt

Или вариант, требующий знания общего количества (зато легко получаемый из поиска):

$ grep -A 100000 '^1f12 ' trace.txt

Подобным образом можно брать и небольшое количество соседних строк.

  • Добавление ассемблера (это должен был быть маленький one-liner, но что-то пошло не так...):
grep -B 100000 '^1f12 ' trace.txt | python -c '

import sys;
h = {};
h2 = {};
[ (h.__setitem__(int(l[2 : 12], 16), l[33 : ]),
   h2.__setitem__(l[18 : 32].strip(), l[33 : ]))
  for l in open("bdis")
  if l[2 : 4] == "0x" ];
print "-*- truncate-lines: t -*-";
[ sys.stdout.write(
    l
    if l == "\n"
    else l.strip() + " " + h.get(
        int(l.split(" ", 1)[0], 16),
        h2.get(l.split(" ", 2)[1],
            h2.get(l.split(" ", 2)[1][ : 4],
                h2.get(l.split(" ", 2)[1][ : 8],
                    "???\n")))))
  for l in sys.stdin ]

' \
| perl -pe 's/(\[.*\])(.*)/$2  $1/' \
| perl -pe 's/^([a-f0-9]+ [a-f0-9]{4})[a-f0-9]{8}/$1/' \
| python -c '

import sys;
[ sys.stdout.write(
    l
    if "[" not in l
    else l.split("  [")[0].ljust(38) + "[" + l.split("[")[1])
  for l in sys.stdin ]

' > ttb2.txt

На выходе получается история выполнения с отформатированным ассемблером и значениями регистров:

1f0c 508f  addq.l 0x8, a7             [A ff1fd4 ff0de4 9d ff0d46 56bc 11d0 ff1d48 ffffff46 D ffffff 9fd4 9e ff0d46 1c 9 fffedc e3e]
1f0e 342e  move.w 0x24(a6), d2        [A ff1fd4 ff0de4 9d ff0d46 56bc 11d0 ff1d48 ffffff4e D ffffff 9fd4 9e ff0d46 1c 9 fffedc e3e]
1f12 b46e  cmp.w 0x22(a6), d2         [A ff1fd4 ff0de4 9d ff0d46 56bc 11d0 ff1d48 ffffff4e D ffffff 9fd4 cb4c ff0d46 1c 9 fffedc e3e]

Похоже, это немного не логично, что регистры справа, но при этом состояние в них приводится на момент до выполнения этой инструкции.

Связь по опкодам длиннее 6 байт не реализована. В выводе все опкоды обрезаны до 2 байт.

При этом комментарии из файла bdis, записанные на строках вместе с инструкциями, попадают в вывод.

  • Изучение результата трассировки подряд происходило в текстовом редакторе.

Проверка 1

Для ключа A01BC23DE45FG67H можно получить следующий результат.

Самый конец трассировки:

1998 4a81  tst.l d1                   [A ... D 0 5 ...]
199a 660e  bne.b 0x19aa  ; go to "Wrong key..."[A ... D ...]
19aa 4878  pea.l 0xe.w                [A ... ffffff4e D ...]
19ae 4878  pea.l 0x8.w                [A ... ffffff4a D ...]
19b2 4879  pea.l 0xfe30.l  ; "Wrong key..."[A ... ffffff46 D ...]

Регистр a7 является указателем на стек, так что инструкции pea меняют его.

Для перехода в ветку "Wrong key" проверяется регистр d1. Его значение - 5, а должно быть 0. Следя за изменением значения регистра, за 112 инструкций до этого можно найти такую проверку:

1f12 b46e  cmp.w 0x22(a6), d2         [A ... ff1d48 . D . 9fd4 7d24 ...]
1f16 6700  beq.w 0x20ea               [A ... ff1d48 . D . 9fd4 7d24 ...]
1f1a 7205  moveq 0x5, d1              [A ... ff1d48 . D . 9fd4 7d24 ...]
1f1c 6000  bra.w 0x190c               [A ... ff1d48 . D . 5 7d24 ...]

В ветке 0x20ea находится присваивание 0 в d1:

> 0x000020ea      7200           moveq 0x0, d1
< 0x000020ec      6000f81e       bra.w 0x190c

Так что можно было надеяться, что нужно свернуть именно туда.

Поиск 0x22(a6) приводит к следующей инструкции:

1de0 3d41  move.w d1, 0x22(a6)        [A . ff1d48 . D . cb4c ...]

Это запись значения в память. Но между этой записью и той проверкой случается ещё 25254 инструкций, так что нет гарантий, что память не была перезаписана другими инструкциями.

С другой стороны, проверка сравнивает только 2 байта, так что её должно быть легко преодолеть перебором.

Цикл для пробы случайных ключей:

$ while true; do
    perl -C0 -e 'print +(A..Z,0..9)[rand 36] for 0..15' > key \
    && perl -C0 -pe "s/A01BC23DE45FG67H/`cat key`/" < state0 > state1 \
    && gzip < state1 > ~/.mednafen/mcs/best_reverser_phd9_rom_v4.99e440e408b93591d6fbedfb548d8334.mca \
    && DISPLAY=:0 ./src/mednafen best_reverser_phd9_rom_v4.bin 2>/dev/null \
    | tail -n 1;
    target="`grep ^1f12 trace.txt | cut -d ' ' -f 15`";
    echo "`cat key` $target";
    mv key outkeys/"$target";
done

Тут есть некоторая избыточность, чтобы не потерять прогресс. Плюс потребовалось небольшое везение, потому что такой перебор идёт не подряд и не гарантирует нахождение всех значений за разумное время. Хотя один запуск Mednafen занимал ~0.26 секунды, что давало примерно 3 итерации цикла в секунду.

В итоге, был найден ключ D2S5YSX951P0OAD3. Он даёт значение 0xcb4c в момент проверки по адресу 0x1f12 и проверка проходит успешно, так что значение в памяти не было перезаписано (или было восстановлено после перезаписи).

Проверка 2

С новым ключом код проходит проверку, попадает в другую ветку, сталкивается с неправильной инструкцией и попадает в обработчик исключения по адресу 0x0408. (Тут упоминается, что такое исключение позволяет старым моделям процессора эмулировать инструкции новых моделей.)

1998 4a81  tst.l d1                   [A ... D 0 0 ...]
199a 660e  bne.b 0x19aa  ; go to "Wrong key..."[A ... D ...]
199c 2f0b  move.l a3, -(a7)           [A ... D ...]
199e 4eba  jsr fcn.00001504(pc)       [A ... D ...]
1504 23ef  move.l 0x4(a7), 0xff0d12.l [A ... D ...]
150c f000  invalid                    [A ... D ...]
0408 4eba  jsr fcn.00000344(pc)       [A ... D ...]

Из дизассемблера:

  ;-- Reserv0:
  ;-- Reserv1:
  0x00000408      4ebaff3a       jsr fcn.00000344(pc)
  0x0000040c      48e7c0c0       movem.l d0-d1/a0-a1, -(a7)
  0x00000410      207900ff0d12   movea.l 0xff0d12.l, a0
  0x00000416      4e90           jsr (a0)
  0x00000418      4cdf0303       movem.l (a7)+, d0-d1/a0-a1
  0x0000041c      4e73           rte

Однако на общий подход это не влияет. В этой задаче надо смотреть на конец трассировки:

19a4 4a80  tst.l d0                   [A ... D 0 ...]
19a6 6600  bne.w 0x1a6c  ; go to "YOU ARE ..."[A ... D ...]
19aa 4878  pea.l 0xe.w                [A ... D ...]
19ae 4878  pea.l 0x8.w                [A ... D ...]
19b2 4879  pea.l 0xfe30.l  ; "Wrong key..."[A ... D ...]

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

Пропустив 9 инструкций до проверки:

ff0daa b642  cmp.w d2, d3             [A ... D . . 671090d3 5617 ...]
ff0dac 57c0  seq.b d0                 [A ... D f8ad3 ...]
ff0dae 4880  ext.w d0                 [A ... D f8a00 ...]
ff0db0 48c0  ext.l d0                 [A ... D f0000 ...]
ff0db2 4480  neg.l d0                 [A ... D 0 ...]

По адресу 0xff0daa сравниваются значения по 2 байта из регистров d2 и d3. Старшие байты в регистрах игнорируются. Результат проверки сохраняется в регистр d0 следующими инструкциями. При этом значение в регистре d2 зависит от ключа (а в d3 от почты).

Изучая историю выполнения дальше, были замечены повторы кода, начинающегося с адреса 0x1526. По адресу 0x152e в регистре видно аргумент функции:

$ grep -e '^152e ' -e '^199a ' trace.txt | ...оформление...
152e 1210  move.b (a0), d1            [A ff01c7 ... D ...]
152e 1210  move.b (a0), d1            [A ff01cb ... D ...]
199a 660e  bne.b 0x19aa  ; go to "Wrong key..."[...]
152e 1210  move.b (a0), d1            [A ff01cf ... D ...]
152e 1210  move.b (a0), d1            [A ff01d3 ... D ...]

Тут видно, что в регистре a0 оказываются указатели в буфере ключа с шагом по 4 байта. Смещения +0 и +4 оказываются до первой проверки, а смещения +8 и +12 - после первой проверки.

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

Можно заметить, что d3 в значении ...90d3 совпадает с D3 на конце использованного ключа. Оказалось, что это не просто совпадение. Второй байт зависит от предпоследней пары символов в ключе немного сложнее, но получилось подобрать эти символы вручную.

Новый ключ с исправленным концом прошёл вторую проверку и программа вывела долгожданное сообщение: YOU ARE THE BEST REVERSER!

Пример успешной пары: почта 000 и ключ D2S5YSX951P05300.

ff0daa b642  cmp.w d2, d3             [A ... D . . 67105100 5100 ...]

Выводы

Задача позволяет познакомиться с ассемблером архитектуры m68k и с некоторыми особенностями Sega Mega Drive. При этом задачу можно решить, избегая глубокого вникания в код.

Уровень удовольствия от задачи: 10 непонятных функций из 10! Рекомендую!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment