В 2012 году я смог изменить звуковой сигнал запуска на своём Power Mac G3 (Blue and White), заодно познакомившись с языком программирования Forth. Мне пришлось провести глубокий реверс-инжиниринг скриптов обновления прошивки Apple, чтобы понять, что к чему.

Недавно один из моих читателей спросил, могу ли я повторить эту процедуру с его iMac. Этот конкретный iMac официально известен как "iMac Slot Loading", модель PowerMac2,1. Как можно догадаться из названия, он оснащён CD-приводом с щелевой загрузкой (у оригинального iMac лоток выезжал, как у ноутбука). Кроме того, в этом iMac вместо оригинального G3 установлен процессор PowerPC G4, впаянный в материнскую плату.

Первым делом я изучил содержимое файлов обновления, чтобы выяснить, смогу ли я изменить звук по тому же принципу, что и в моём Power Mac G3. Мне кажется, что будет забавно поделиться с другими людьми информацией о том, что можно сделать для изменения звука. Код утилиты, которую я создал для внедрения нового звука в файл обновления прошивки, вы найдёте в конце статьи.

Предварительно напомню, как происходила настройка звук запуска в моём Power Mac G3, потому что значительная часть этой работы актуальна и для iMac. Когда я сделал дамп моего G3, то импортировал его в Audacity как raw-файл и выбрал формат PCM: подписанный, 16-бит, с дискретизацией 44 100 Гц. Затем я воспроизвёл весь 1 МБ ПЗУ как целый длинный звук. При воспроизведении были слышны царапающие слух звуки, но нашлись два места, которые напоминали звуковые данные запуска. Пусть звучание их было очень нестабильным и слишком быстрым, но это явно был звук запуска. Можете послушать, но осторожно, он громкий.

Мне удалось настроить его правильную длину и высоту тона, снизив частоту дискретизации, но звук всё равно был лишком скрипучим. На изображении ниже (примерно с 2,75 секунды до 3,5 секунды) можно увидеть, как это выглядело в Audacity.

В то время я много копался в звуковых форматах для других исследований ROM в компьютерах Mac, поэтому некоторые из алгоритмов сжатия звука были мне знакомы. Я быстро наткнулся на пару ступенчатых таблиц IMA ADPCM в ПЗУ. Здравый смысл подсказывал, что Apple, вероятно, использовала свой собственный формат QuickTime IMA ADPCM, который группирует данные в 34-байтовые пакеты. Это знание позволило мне выяснить, где именно в ПЗУ начинаются и заканчиваются звуковые данные, и тогда я смог их декодировать. Вот так.

Поэтому, когда мне задали вопрос о том, возможно ли изменить звук на iMac, я взял сжатые звуковые данные, которые нашёл 10 лет назад в ПЗУ моего Power Mac, и поискал такой же фрагмент данных в файле обновления прошивки iMac. Разумеется, звук был точно таким же сжатым, вплоть до байта. В этом файле обновления был только один фрагмент звука запуска. Он находился здесь:

Однако просто внести аналогичный патч в файл обновления микропрограммы не удалось. Хотя файл обновления прошивки iMac представляет собой Forth-скрипт, как и у Power Mac G3, он устроен по-другому. Учитывая это, я нашёл несколько учебных пособий по Forth и начал более внимательно знакомиться с кодом. Попробую рассказать, как я анализировал файл обновления прошивки и что делал для изменения звука.

Код Forth начинается с определения набора структур. Эта структура интересна тем, что она относится к звуковому сигналу загрузки и соответствующему размеру:

Обратите внимание на то, что в коде есть структуры, связанные с некоторыми разделами. Это создаёт место для общего заголовка и 6 заголовков разделов: sbb, srec, sboot, ssys, stst и snv. Каждый раздел имеет контрольную сумму. Этот скрипт обновления очень полезен! Он сообщает мне значения многих данных в файле обновления.

Не вижу смысла детально изучать код Forth, который использует эти структуры и настраивает все — частично потому, что он довольно длинный, а частично потому, что я сам не до конца понимаю всё, что он делает. Важно то, что после выполнения скрипта (doit, за которым следует возврат каретки), сразу же следует куча необработанных данных, отличных от ASCII. Первый кусок этих данных оказывается содержимым структур >h и >s, описанных выше.

Используя определения структуры, можно декодировать все выделенные выше данные (имейте в виду, что на компьютерах Mac PowerPC используется порядок от старшего к младшему — big-endian):

  • Заголовки:

    • file-size = 0x000E11C0 = 922048 (это точный размер файла прошивки)

    • rom-size = 0x0010 = 16

    • header-size = 0x00A4 (таким образом, заголовок заканчивается 0x68DC)

    • build-version = 0x000419F1 = 4.1.9f1

    • build-date = 0x20010914 = September 14, 2001

    • model = 0xFFFF

    • fill-byte = 0xFF

    • num-sections = 6

  • sbb:

    • type = 0x00

    • flags = 0x81

    • reserved = 0x0000

    • position = 0x000068DC (сразу после заголовка)

    • offset = 0x00000000

    • size = 0x00003F00

    • actual = 0x00003A00

    • checksum = 0x43B8671B

  • srec:

    • type = 0x01

    • flags = 0x81

    • reserved = 0x0000

    • position = 0x0000A2DC (сразу после получения данных sbb)

    • offset = 0x00008000

    • size = 0x00078000

    • actual = 0x00063DA0

    • checksum = 0xF05F6B03

  • sboot:

    • type = 0x02

    • flags = 0x81

    • reserved = 0x0000

    • position = 0x0006E07C (сразу после получения данных srec)

    • offset = 0x00080000

    • size = 0x00080000

    • actual = 0x00072280

    • checksum = 0xD85D3F5A

Я остановился на этом, но вы можете расшифровать другие разделы, продолжив работу с данными.

После более детального изучения содержимого этих структур мне стало ясно, что position — это начальное местоположение данных для этого раздела в файле обновления прошивки, offset — это, вероятно, место хранения данных во флэш-чипе, size — это зарезервированный размер раздела в flash-чипе, а actual — это фактический объем данных, включенных для данного раздела в обновление прошивки.

Я определил, что данные для звука загрузки расположены в файле между 0xD1E2C и 0xE02DF. Раздел sboot выполняется с 0x6E07C по 0xE02FC.

Вспоминая структуру >dir, содержащую поле BOOT-BEEP, я предположил, что, возможно, разделsboot будет начинаться с этой структуры. Он будет содержать 20 32-разрядных слов общим объёмом 0x50 байт. Данные выделены на рисунке ниже:

Интерпретируя это, мы получаем:

  • inst0 = 0x48000080

  • inst1 = 0x000419F1

  • filler0 = 0x20010914

  • filler1 = 0x00100000

  • HWINIT = 0x00000000

  • HWINIT-size = 0x00006BF8

  • NUB = 0x00000000

  • NUB-size = 0x00000000

  • OF = 0x00006C01

  • OF-size = 0x0005D195

  • unused{0,1,2,3} = 0x00000000

  • unused{0,1,2,3}-size = 0x00000000 (в unused3-size отсутствует -size в определении структуры, вероятно, опечатка)

  • BOOT-BEEP = 0x00063DA0

  • BOOT-BEEP-size = 0x0000E4C4

Эти данные выглядят корректными. 0xE4C4 — разумная длина для звука. На изображении, где я показал данные звука, видно, что длина извлечённых данных была 0xE4B4, что на 16 байт меньше. Смещение 0x63DA0 для звука также разумно. Добавив его к позиции sboot 0x6E07C, мы получаем 0xD1E1C, что на 16 байт раньше в файле, чем найденные мной звуковые данные. Таким образом, похоже, что звук также имеет 16-байтовый заголовок:

Я не уверен, что именно является содержимым этого заголовка. Первое 32-битное значение 0x00000010 может представлять длину заголовка. Можно предположить, что 0x0000002C = 44 указывает на то, что частота дискретизации конечного звука составляет 44,1 кГц. Однако я могу ошибаться. Но я уверен в конечном 32-битном значении: 0x0001AE80 — это количество сэмплов. Длина звуковых данных составляет 0xE4B4 байта, а длина каждого блока данных IMA — 34 байта. Это означает, что существует 0xE4B4 / 34 = 1 722 блока. Каждый блок представляет 64 сэмпла, поэтому в звуке 64 * 1,722 = 110,208 = 0x1AE80 сэмплов.

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

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

  • Начните со звука, длина и частота дискретизации которого в точности совпадают с исходным звуком: 110 208 сэмплов при частоте 44,1 кГц = чуть менее 2,5 секунд..

  • Сожмите звук, используя формат IMA ADPCM от Apple.

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

Единственное, чего не хватает в этом списке, так это контрольных сумм. Напомню, что заголовок каждого раздела содержит контрольную сумму в качестве конечного 32-разрядного значения. Если посмотреть на код Forth в начале файла обновления, становится ясно, что это контрольная сумма Adler-32.

Похоже, он проверяет, установлен ли флаг контрольной суммы для флагов раздела, и если да, то вычисляет контрольную сумму Adler-32 всего размера раздела минус 4 байта. Это имеет смысл, потому что последние 4 байта были бы зарезервированы для фактического хранения контрольной суммы во флэш-чипе.

Я немного поигрался с этим, чтобы попытаться пересчитать существующую контрольную сумму оригинального обновления прошивки. Мои первые несколько попыток просто пересчитать контрольную сумму по "фактической" длине секции с треском провалились. Мне следовало бы внимательнее присмотреться к коду, потому что он ясно показывал, что важна длина "size".

Общая структура заголовка даже содержит элемент fill-byte (0xFF), поэтому я знал, что мне следует заполнить раздел этим значением. Чтобы вычислить правильную контрольную сумму, я начал с байтов для этого раздела в файле (“фактические” байты), затем добавлял 0xFF до тех пор, пока длина всего раздела не стала точно “size – 4”. В итоге расчёт контрольной суммы полностью совпал с оригиналом.

В ходе изучения файла обновления прошивки удалось найти ещё одну ссылку на слово “adler32” в скрипте. Скрипт также проверяет контрольную сумму самого себя, чтобы убедиться, что весь файл обновления прошивки находится в целости и сохранности.

Стремясь понять этот Forth-код, но не тратя слишком много времени на изучение синтаксиса, я могу сказать, что он вычисляет контрольную сумму всего файла обновления прошивки за вычетом последних четырёх байт (которые содержат ожидаемую контрольную сумму) и сравнивает вычисленную контрольную сумму с ожидаемой контрольной суммой.

Попытка воспроизвести существующую контрольную сумму удалась с первого раза. Ура! Теперь я знал правильный способ вычисления обеих контрольных сумм. Единственное, чего я опасался, так это того, что где-то может быть ещё одна контрольная сумма, связанная со звуком. Но мне казалось это маловероятным, потому что мой патч для Power Mac G3 со стартовым сигналом также включал обновление двух контрольных сумм, похожих на те, что я нашёл здесь. И больше ничего не было.

В итоге я собрал всё в программу на C++, которая обеспечивает правильную длину заменяемых данных, сжимает их с помощью IMA ADPCM, вставляет их в файл обновления прошивки в нужное место и пересчитывает контрольную сумму. По сути, это просто модифицированная версия аналогичной утилиты для обновления прошивки, которую я сделал для изменения звука запуска моего G3.

Запустив новую утилиту со звуком, который просил владелец компьютера, и оригинальным файлом обновления прошивки в качестве входных данных, я получил исправленный файл обновления прошивки, который можно было потестить. Я сделал это в Windows. Чтобы вернуть изменённый файл на Mac без потери разветвления, я использовал свой Linux-сервер, на котором были запущены netatalk и Samba. Я могу получить доступ к нему через Samba на Windows для изменения data fork, и он сохранит эту информацию при доступе через netatalk на Mac. Data fork хранится в netatalk в специальной папке .AppleDouble.

Можно было сразу пропросить владельца iMac вручную запустить обновление прошивки через команду в Open Firmware, но я не чувствовал себя достаточно комфортно в OF, чтобы так поступать. Так что следующим этапом стала попытка выяснить, как модифицировать программу обновления прошивки Apple, чтобы внести исправления в прошивку, когда она уже обновлена.

Нечто подобное я уже делал на своём G3 в 2012 году. Программа обновления прошивки моего G3 имела разрешённый список версий прошивки, которые она могла обновлять. Это означало, что исправление было очень простым: я просто изменил одну из записей в списке разрешённых версий, чтобы она соответствовала самой новой версии прошивки.

В обновлении iMac я просмотрел всё: такого списка не было. И это означало одно: пришло время запустить Ghidra!

Ассемблер Intel и ARM для меня не страшны, а вот PowerPC всегда пугал. К счастью, в Ghidra есть декомпилятор, так что я могу посмотреть на что-то более привычное. Вот верхняя часть main():

Ghidra смогла автоматически определить имена функций, что значительно облегчило мою работу. Большая часть кода на картинке выше выполняет различные проверки, чтобы убедиться, что обновление действительно разрешено к установке. Например, IsBlueBox() проверяет, что обновление не запускается внутри среды Classic в Mac OS X. Вот как выглядит ALRT 138 в ResEdit:

Я решил, что CompareWithCurrentVersion() будет хорошей функцией для исправления. Если она возвращает 5, то выводит сообщение ALRT resource 131, что соответствует сообщению, описанному ранее:

Посмотрите на CompareWithCurrentVersion(). Я вручную дал имя глобальной переменной firmwareVersion, чтобы было понятнее, что происходит:

Эта функция сравнивает переданный параметр (0x419f1 в предыдущей декомпиляции main()) с глобальной переменной и возвращает одно из трёх значений:

  • 6, если версия прошивки совпадает,

  • 7, если текущая прошивка достаточно старая для обновления,

  • 5, если прошивка новая и её не обновить.

Есть также особый случай, который я не понимаю, где он изменяет обе версии, отыскивая "d" в месте, где находится "f" в 0x419f1, и превращая его в "9" перед сравнением версий. Но это не имеет значения.

Изначально я решил изменить функцию, чтобы она возвращала 7 вместо 5, превратив "39 80 00 05" в "39 80 00 07", но владелец устройства сообщил, что это не работает. Я понял свою ошибку на следующий день — нужно было модифицировать случай, когда он возвращает 6, а не 5. Путаница возникла из-за разборки main(), где возвращаемое значение 5 приводило к отображению ALRT 131. Я знал, что ALRT 131 отображается, поэтому предположил, что это и есть необходимый патч. Оказалось, что функция FirmwareIsUpdated(), которая вызывается, когда CompareWithCurrentVersion() возвращает значение 6, также способна отображать ALRT 131. Мне следовало уделить больше внимания тому, что на самом деле делает CompareWithCurrentVersion().

Эта функция проверяет, первая ли это загрузка после попытки прошивки, и если да, то показывает ALRT 141, что говорит об успешной установке обновления.

Подытожу. В патче, который я выбрал для обновления прошивки, нужно изменить "39 80 00 06" в CompareWithCurrentVersion() на "39 80 00 07". Это позволяет обновить прошивку, даже если текущая версия прошивки совпадает с версией обновления. Это означает, что при следующей загрузке появится ошибочное сообщение о том, что прошивка обновилась неправильно, но это мелочи.

Если вы планируете сделать это обновление самостоятельно, имейте в виду, что в программе обновления прошивки есть две последовательности "39 80 00 06". Вторая — та, которая требует исправления.

Я уверен, что некоторые из вас хотели бы увидеть конечный результат. Я собрал файлы в образ диска Disk Copy, закодировал его как MacBinary. Вот видео, демонстрирующее установку обновления прошивки и последующую загрузку.

Забавный факт: звук запуска был создан на базе звучания 12-струнной гитары Power Mac 6100/7100/8100, созданной Стэнли Джорданом.

Я немного волновался по поводу совместимости с распаянным на плате этого iMac G4, но всё работает отлично. Возможно, вы не знали, но прошивки G3 Blue и White были специально изменены компанией Apple, чтобы устранить совместимость с G4. Поэтому для восстановления поддержки G4 требовалась кастомная прошивка. Чтобы перестраховаться, я попросил сначала провести тест на другом iMac, поскольку не хотел портить редкий G4 iMac в случае, если я допущу ошибку в процессе установки обновления. Но всё прошло штатно.

Патчер доступен на GitHub, если кому-то интересно поковыряться в этом. Там же лежит программа для Power Mac G3. Перед экспериментами убедитесь, что у вас будет возможность откатиться обратно.

Вот и все! Оглядываясь назад, я понимаю, что задачка была не такой уж и сложной. Код Forth, по сути, подавал мне решение на блюдечке с голубой каёмочкой. В секции sboot достаточно свободного места, чтобы попробовать вставить звуковой сигнал длиной ~4,5 секунд, но это усложнило бы патч, потому что пришлось бы менять позиции в структурах разделов после sboot. Я уступаю право на попытку сделать это кому-нибудь другому.


Что ещё интересного есть в блоге Cloud4Y

→ Информационная безопасность и глупость: необычные примеры

→ NAS за шапку сухарей

→ Взлом Hyundai Tucson, часть 1часть 2

→ Столетний язык программирования — какой он

→ 50 самых интересных клавиатур из частной коллекции