Не так давно автор материала работал над сложной головоломкой: как пропатчить операционную систему сэмплового синтезатора 1996 года Kurzweil K2500. О путешествии автора в старый код музыкального инструмента рассказываем к старту курса по разработке на C++.
Как и многие цифровые музыкальные инструменты, этот синтезатор — просто компьютер с дополнительными микросхемами, здесь — на основе популярного в то время процессора Motorola 68000, который широко применялся в первых Macintosh и Sega Genesis. Я хочу пропатчить ОС этого зверя, чтобы делать много вещей, большинство из которых оставлю воображению читателя.
Поиск операционной системы
«Пропатчить ОС» — звучит прекрасно, но как вообще добраться до кода? К счастью, операционная система K2500 по-прежнему лежит на старом FTP производителя. В архиве по ссылке архиве находится файл .KOS. Видимо, это пользовательский формат.
Hex Fiend отображает его байты:
В глаза не бросается ничего. Кажется, вверху — удобочитаемый 4-байтовый заголовок: SYS0, за которым, возможно, идут другие байты заголовка, но утверждать это сложно. Хотя уже известно, что ОС работает на процессоре Motorola 68000: просто попробуем интерпретировать данные как бинарники и посмотрим, что сможем сделать.
Ghidra
Файл операционной системы — вероятно, сырой машинный код: буквально инструкции и данные, интерпретируемые самим процессором. Чтобы сделать файл хоть немного понятнее, потребуется дизассемблировать и, надеюсь, декомпилировать его в код на C.
Для этого воспользуемся Ghidra — программой для реверс-инжиниринга с открытым исходным кодом, созданной, поддерживаемой и выпущенной АНБ США. Да, тот самый. Правда. Импортируем файл .KOS в Ghidra и проанализируем с настройками по умолчанию.
Прокрутка файла показывает, что части данных разобраны Ghidra как допустимые инструкции Motorola 68000, но большая часть файла остаётся загадкой. Как ни странно, Ghidra правильно определила в файле несколько удобочитаемых строк.
Отлично! Но выглядит так, как будто код ссылается к строкам, смещённым на некоторую величину: они отображаются обрезанными.
Строки обрезаны из-за того, что загружен весь файл .KOS, проигнорирован его заголовок и, возможно, другие дополнительные байты. Довольно ощутимое препятствие.
Все перекрёстные ссылки функций неточные: мы реконструируем данные, отправляясь в неверном направлении после почти каждой попытки пройти по ссылке. Исправить это нужно в первую очередь.
Реверс-инжиниринг загрузчика
Для реверс-инжиниринга .KOS крайне полезно погрузиться в код, который создаёт или использует эти файлы. Кода создания файлов у нас нет, но есть доступ к тому коду, что использует эти файлы: можно скачать загрузчик самого синтезатора!
Откроем его в Ghidra и, чтобы облегчить себе жизнь, предположим, что первые 8 байт файла — часть заголовка. Откуда взялась эта цифра? Я попробовал 0, +4, +8, +12, +16 и +20 байт смещения, и +8 дизассемблировался наиболее правильно. Да, попытки заняли некоторое время.
Оглядываясь назад, отмечу, что всё это работает, потому что код файла загружается по адресу памяти 0x0. Если бы он загружался где-то ещё, до дизассемблирования пришлось бы выяснить этот адрес.
Как и прежде, сначала поищем что-нибудь понятное человеку. Поиск по строкам выдаёт пару строк с описанием ошибок, которые выглядят как ошибки, бросаемые нужным кодом:
Ghidra определила то, что здесь называется XREF, как перекрёстные ссылки. Они указывают, что эти строки вызываются из определённого места. Пройдём по ссылке:
Ага! Мы куда-то движемся. Это очень похоже на оператор switch, декомпилированный в дерево if. Выглядит как ряд кодов ошибок от 0x100 до 0x105, от 0x200, 0x201 и так далее. Каждый код связан со строкой описания ошибки, которая предположительно выводится на экран. Продолжаем тянуть за эту ниточку. Через «Find References» в Ghidra находим эту функцию:
Мы всё ближе! Здесь Ghidra сделала кое-что очень важное: декомпилированный код содержит имена некоторых переменных, автоматически определяемые по строкам, на которые эти переменные указывают.
Некоторые из этих переменных — собственно строки, так что можно предположить их значение и воспользоваться переименованием и повторным вводом Ghidra, чтобы намного легче читать функцию:
Это выглядит как двухэтапный процесс:
Новый файл ОС проверяется вызовом ActuallyCheckOrFlashTheOS?(0, ?).
Если проверка пройдена, то та же функция снова вызывается с аргументом 1.
Похоже, эта функция читает формат файла .KOS. Давайте углубимся в неё:
Здесь нет ни понятных сразу строк, ни имён функций, но можно взглянуть на структуру, чтобы понять, что эта функция делает. Даже без имён переменных структура этого кода очень похожа на открытие файла в C!
Похоже, есть вызов в стиле fopen, за которым следует fread, а после fread снова в цикле while. Для ясности добавим комментарии:
Судя по ним, у нас есть ответы на несколько вопросов:
Файл .KOS начинается с заголовка в 4 байта: SYS0.
После заголовка он делится на фрагменты фиксированного размера.
Каждый фрагмент начинается с одного 4-байтового целого числа.
Читается неизвестное количество байтов данных.
Каждый фрагмент заканчивается одним 4-байтовым целым числом, похожим на контрольную сумму.
И появился ещё один новый вопрос, на который нужен ответ: почему ссылки на определённые константы и функции имеют очень высокие адреса в памяти? То есть 0x021317ac, похоже, содержит то самое неизвестное количество байтов в каждом блоке .KOS, но данные в ПЗУ не достигают настолько высоких адресов!
Чтобы лучше понять, что находится по этим старшим адресам, обратимся к сервис-мануалу синтезатора (огромный привет Дэвиду Рыскальчику). Этот полезный, лакомый кусочек информации, скрытый глубоко в нераспознанном PDF-файле, в списке диагностических процедур:
Спасибо, сервис-мануал! Видимо, 0x021317ac ведёт прямо к середине «энергозависимой оперативной памяти», то есть памяти, используемой процессором во время его работы. Здорово, что у нас есть руководство по обслуживанию.
Без него мы могли бы сделать обоснованное предположение по часто встречающимся в коде префиксам адресов. Если бы это предположение не помогло, можно было попытаться найти электрическую схему устройства и проследить адресные линии от микросхем к процессору, резко усложнив этим задачу.
Теперь скажем Ghidra рассматривать эту память как ОЗУ в окне «Memory Map», и перейти к интересующему нас адресу 0x021317ac:
Здесь нет данных (Ghidra знает, что это оперативная память, которая случайным образом инициализируется при запуске компьютера), и похоже, что 0x021317ac читается из ((R)), но никогда не записывается в ((W)). Может, записи происходят дальше?
Ага! Ghidra показывает нам, что функция пишет прямо в начало оперативной памяти. Нам пришлось прокрутить 6060 байт, поэтому, возможно, чтобы найти первую запись, этот метод копирует много данных в ОЗУ. Кликнем, чтобы посмотреть, что там.
Одну секунду. Снова переименуем переменные:
Так намного лучше. Похоже, много данных копируется из ПЗУ в ОЗУ, а именно из 0x0001860a в 0x02130000. Насколько много? Ну, это 0x69016 32-битных слова, то есть 6720 байт. Этот фрагмент кода затем обнуляет следующие 0x1e116 32-битных слова или 1924 байта.
Теперь, когда понятно, что код, вероятно, инициализирован теми же данными, что и в этой части ПЗУ, можно сказать Ghidra напрямую сопоставить данную часть ПЗУ с данной частью ОЗУ.
Возвращаясь к прочитанной части оперативной памяти, вместо вопросительных знаков мы видим байты.
Значение по адресу 0x021317ac равно 0x20000, что соответствует 131 072 байтов, то есть 128 Кб! Замечательно!
Итак, мы выяснили, что каждый фрагмент файла формата .KOS имеет размер 128 КБ. И это всё, что нужно знать, чтобы построить декодер .KOS, удалить заголовки фрагментов и получить файл с корректными относительными смещениями.
Этот файл позволит Ghidra правильно дизассемблировать и декомпилировать файл, а ещё — копаться в коде операционной системы. Я уже делал это — вот упаковщик/распаковщик файлов .KOS на GitHub.
Исследуем ОС
Теперь у нас есть «чистый» дамп операционной системы. Давайте откроем этот файл в Ghidra, как мы уже пытались. Чтобы найти несколько интересных строк, воспользуемся поиском Ghidra.
Проведём быстрый тест, чтобы увидеть, можно ли изменить ОС. Ghidra позволяет изменять инструкции или данные в двоичном формате; давайте изменим одну из строк выше. Чтобы не перемещать код, длину строки нужно сохранить.
Переупаковав операционную систему, загрузим её на дискету, установим на реальное железо — и…
Помните поле «некой контрольной суммы», которое мы видели в .KOS? Оказывается, эта сумма действительно проверяется железом при установке новой ОС. К счастью, Ghidra может помочь нам и здесь: вернёмся к загрузчику и нажмём FUN_0x021302b2, которая, видимо, вычисляет какую-то контрольную сумму:
Снова угадав имена некоторых переменных, имеем:
Эта функция контрольной суммы довольно проста: для каждого байта x checksum равна значению x + checksum, сдвинутому на один бит влево, а затем сложенному побитовым ИЛИ со значением x + checksum, которое на 31 бит сдвинуто вправо.
Это точная контрольная сумма, которую я раньше вообще не видел, не видят её и мощные инструменты реверсирования контрольных сумм, такие как замечательный delsum. С этой контрольной суммой изменим наш скрипт дампа .KOS, чтобы он переупаковывал данные правильно. И, как только это будет сделано, снова попробуем прошить ОС:
Теперь на это железо можно установить новую операционную систему, по желанию изменяя или расширяя её. Эту часть, однако, я оставляю читателю.
Что мы узнали
Ух, это было немного сурово. Я никогда не использовал Ghidra до этого проекта, и теперь чувствую себя довольно комфортно, чтобы использовать его в будущем. Вот методы, которые, как оказалось, работают лучше всего:
В первую очередь ищите читаемые строки.
Не бойтесь выполнять «побочные квесты» (например, реинжиниринг загрузчика), чтобы сделать ваши основные усилия (исправление операционной системы) успешнее.
Используйте декомпилятор Ghidra. Он по-настоящему удивительный.
Ищите структуру в декомпилированном коде.
Переименовывайте функции, переменные и типы данных, как только хотя бы предположите, что они могут делать.
Просмотрите документацию и ресурсы по системе, если они есть.
Иногда приходится вручную перебирать десятки возможных примеров, чтобы найти искомое. Чем больше вы это делаете, тем легче будет.
И отчасти благодаря этому реверс-инжинирингу эмуляция K2000 в проекте MAME теперь загружается!
Особенно благодарен Дэвиду Рыскальчику за то, что на полпути сдвинул мою работу с мёртвой точки, и снова Дэвиду Рыскальчику с Замиром Манджи за рецензирование черновиков этого поста.
А мы поможем вам прокачать навыки или с самого начала освоить профессию, актуальную в любое время:
Выбрать другую востребованную профессию.
Краткий список курсов и профессий
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также