![Реверс-инжиниринг ToumaPet: китайский клон тамагочи с цветным экраном Реверс-инжиниринг ToumaPet: китайский клон тамагочи с цветным экраном](https://habrastorage.org/getpro/habr/upload_files/9be/076/bb3/9be076bb33c56b38d35b068a00819d64.jpg)
Когда становишься слишком взрослым, то просто играть в игры становится не интересно. Зато разбираться в их устройстве увлекательно, я нашел себе новый объект для развлечения. На самом деле я с детства разбирал/ломал игрушки, чтобы посмотреть как внутри устроено. Так что, это не лечится и с возрастом никак не связано.
Dating Machine DM-800 идёт за тобой
Это не совсем так, модели начинаются с OK и до 800 еще далеко. Тем не менее на пластике с обратной стороны, если присмотреться, отпечатано “AQTC® dating machine”.
Для любителей посмеяться над Китайскими инструкциями.
В инструкции есть перлы вроде:
Date With Cute Pets!Communicate with other pets!
Funny Wireless Interactive Digital Dating Game Machine!
За этим заголовком идёт описание, плавно перетекающее в перечисление ключевых слов (как в HTML-заголовках):
You can choose your favorite pet,have role-play games,
Touma chat with friends,It contains baby pet birth form egg,
feeding,playing around,educating,dating,getting married,
having children;Touma chat,alarm clock,games,pocket
watches,communication,keyboard, gifts,appointments,
leisure,decompression,companionship,outdoor,bathing,
school,toilet,hospital,work,shop,travel,Emoticons,red
envelopes,traders,decoration stores,playgrounds,cinamas,
asd wireless interaction communications.
(Авторская орфография и пунктуация сохранена. Да, “birth form egg”, так и было.)
Не зовите меня смотреть видео для взрослых, если в нём не будет хотя бы половины перечисленного.
Вскрытие показало
В продаже есть модель с аккумулятором и разъёмом Type-C (OK-550), и модель использующая AAA батарейки (OK-560). Купил с Type-C, потому как удобнее заряжать, чем менять батарейки и можно попробовать подключить к компьютеру. Но компьютер ничего по USB не видит, модный порт оказался только для зарядки.
![Вскрытие OK-550. Вскрытие OK-550.](https://habrastorage.org/getpro/habr/upload_files/618/ba0/6a2/618ba06a25725718527f5dad7c3998ca.jpg)
Развинтил корпус, процессор и чип с прошивкой скрыты приклеенным на двухсторонний скотч аккумулятором на 300mAh, отклеился легко. Процессор залит чёрной каплей. Чип флэш памяти Puya P25D32SH, на 4 мегабайта. Прикрепил программатор к флэш чипу, и тут незадача – устройство включается при подключении программатора, и не даёт ему работать, прошивку считать нельзя. Быстро выяснилось, что кнопка на плате – это скрытая кнопка сброса (reset), и пока её держишь нажатой, то устройство находится в выключенном состоянии. Удерживал кнопку пальцем пока считывал, читается несколько минут, под конец устал держать.
Также на плате есть чип XN297LBW, это чип беспроводной связи 2.4ГГц. Через него устройства могут общаться друг с другом.
Когда открыл прошивку, то показалось прочитана неверно, данные выглядят странно. После исследования в хекс редакторе, заметил что байт 0xe4 часто встречается, иногда продолжительно. Также значение 0x1b на втором месте. Очевидно это однобайтовый XOR шифр. 0x1b это инверсия 0xe4, что будет соответствовать байту 0xff.
После расшифровки в начале файла стало видно строку “tony”, что похоже на magic для проверки заголовка. Начало прошивки, похожее на код, продолжается до 0x30000, данные после похожи на ресурсы.
Всё новое – это хорошо забытое старое
Какой у этого процессора набор инструкций? Это точно не RISC, который видно по выравниванию инструкций. Наоборот, заметил что инструкции переменного размера в байтах. Проверил популярный 8051, но это не выглядит как его код.
Нашел в начале прошивки таблицу, что сначала показалась набором 8-бит спрайтов:
не то Among Us, не то избушка на курьих ножках
00 00 00 00-00 00 00 FE-00 00 00 00-00 00 00 00 ?
00 00 00 00-00 00 00 00-00 00 18 18-00 00 00 00 ??
00 00 00 00-02 06 0C 18-30 60 C0 80-00 00 00 00 ????0`??
00 00 38 6C-C6 C6 D6 D6-C6 C6 6C 38-00 00 00 00 8l??????l8
00 00 18 38-78 18 18 18-18 18 18 7E-00 00 00 00 ?8x??????~
00 00 7C C6-06 0C 18 30-60 C0 C6 FE-00 00 00 00 |????0`???
00 00 7C C6-06 06 3C 06-06 06 C6 7C-00 00 00 00 |??????|
00 00 0C 1C-3C 6C CC FE-0C 0C 0C 1E-00 00 00 00 ??
Но на самом деле это шрифт 8x16, приведенные выше данные рисуют символы -./0123456789:;<
.
Например нолик:
..###... 38
.##.##.. 6C
##...##. C6
##...##. C6
##.#.##. D6
##.#.##. D6
##...##. C6
##...##. C6
.##.##.. 6C
..###... 38
В начале прошивки часто повторяются байты 0xa9 и 0x85, есть паттерн a9 ?? 85 ??
.
80 8D C0 01-A9 7E 85 80-A9 29 85 81-A9 00 85 82
A9 01 85 85-A2 14 20 00-60 A5 BE 30-04 A9 60 85
BE A9 68 85-80 A9 01 85-81 A9 00 85-82 A9 3D 85
83 A9 00 85-84 4C 52 60-8F 93 29 5F-93 04 A5 B3
F0 3B A9 98-85 80 A9 22-85 81 A9 00-85 82 A9 06
Как если бы 0xa9 означало загрузку константы в аккумулятор, а 0x85 сохранение в регистре. И еще байт 0x60, что идёт последним перед началом данных (например начала шрифта), так что 0x60 мне показалось командой возврата.
Решил поискать по гитхабу комбинации байт. На гитхабе бывают распечатки ROM-ов и архитектура подписана. Наугад поискал 4C 52 60
и нашел это.
В Readme репозитория написано: "This is a copy of recovered source code for Atari 400/800 PAC-MAN(tm)." Загуглил Atari 400, он использует MOS 6502. Который также в NES (Dendy) использовался, и куче древних персональных компьютеров. Нахожу документацию по командам 6502, и оказывается, что 0xa9 это LDA, а 0x85 это STA на нулевую страницу памяти. Значит набор инструкций определён правильно.
Не подумал проверять прошивку устройства с портом Type-C на набор инструкций 1975 года, которому уже почти 50 лет! И кому приспичило в 21-м веке писать игры на ассемблере для 8-бит чипа? Видимо в Китае/Тайване/Гонконге осталось много старых кадров, что когда-то писали бесчисленные примитивные игры для NES сборников "100500 в одном".
Но чистым 6502 некоторые команды расшифровывались как NOP, да еще и с операндом, что не выглядело правильно. Попробовал также 65816 и 65C02, но они при дизассемблировании начала прошивки выглядят неверными. Через какое-то время до меня дошло, что самое начало прошивки это данные, а для кода далее подходит 65C02.
Исследование прошивки
В прошивке частые вызовы и прыжки на адреса 0x6000, 0x6003, 0x6052 и 0x60de. Перед которыми заполняются значения в памяти, на адреса начинающиеся с 0x80 или 0x100, что похоже на аргументы для вызова. Но не нашел где в прошивке находится адрес 0x6000.
Если адресное пространство 65C02 ограничено 16-битами (64КБ), то как тогда организовано выполнение кода, на который в прошивке выделено в три раза больше этого объёма? Начал искать зацепки, по каким адресам вызывается код. Нашел функцию, после кода которой находятся данные в виде строк. В коде функции есть чтения массивов и если учитывать адрес этих данных в коде, то начало функции должно находиться на 0x300. Стал ясен алгоритм работы с большим объёмом кода. Код разделён на небольшие части, которые читаются с флэш памяти, записываются по адресу 0x300 и там исполняется.
Вызов 0x60de выполняет код по адресу записанному в 0x80,0x81,0x82, а в 0x83,0x84 лежит размер этого кода в 16-бит единицах (size / 2). Порядок байтов везде little-endian. После вызова 0x60de, по адресу 0x300 восстанавливается старый код. Вызов 0x6052 это то же самое что 0x60de, но это так называемый "tail-call", то есть после выполнения требуемого кода будет возврат.
0x6003 читает шесть байт прошивки по адресу из 0x80,0x81,0x82 и складывает в памяти по адресам 0x8d..0x92.
0x6000 вроде syscall-а, в регистре X записан номер вызова. Эти адреса, код которых я так и не нашел, я назвал биосом чипа. Это ROM процессора, которого в прошивке не содержится.
Ресурсы игры
В самом конце прошивки байты 0xff, не прошитая флэш память. Но если найти самый конец данных, то там таблица с 24-бит значениями, что начинается на 0x30000 и каждое следующее значение увеличивается. Это явно список адресов ресурсов.
Скопировал начало одного из ресурсов и начал изучать.
пример
19 00 19 80-0E 00 00 FF-0B F8 F8 F8-00 FF 0C FF
0E 00 0E 00-00 FF 0A 00-F8 05 00 FF-0B FF 0E 00
0E 00 00 FF-0A 00 F8 05-00 FF 0B FF-0E 00 1A 00
00 FF 04 F8-F8 F8 FF FF-FF 00 F8 05-FF FF FF F8
F8 F8 00 FF-05 FF 1A 00-1A 00 FF FF-FF 00 F8 05
FF FF FF F8-F8 F8 FF FF-FF 00 F8 05-00 FF 04 FF
1A 00 14 00-FF FF FF 00-F8 05 00 FF-09 00 F8 05
00 FF 04 FF-14 00 14 00-FF FF FF 00-F8 05 00 FF
09 00 F8 05-00 FF 04 FF-14 00 14 00-00 FF 04 F8
F8 F8 00 FF-0B F8 F8 F8-00 FF 05 FF-14 00 08 00
00 FF 1A FF-08 00 08 00-00 FF 1A FF-08 00 10 00
FF F4 F4 F4-00 FF 11 F8-F8 F8 FF FF-10 00 0E 00
00 F4 05 00-FF 0F 00 F8-05 FF 0E 00-0E 00 00 F4
05 00 FF 0F-00 F8 05 FF-0E 00 0E 00-00 F4 05 00
FF 0F 00 F8-05 FF 0E 00-10 00 FF F4-F4 F4 00 FF
11 F8 F8 F8-FF FF 10 00-08 00 00 FF-1A FF 08 00
08 00 00 FF-1A FF 08 00-14 00 00 FF-04 F0 F0 F0
00 FF 0B FC-FC FC 00 FF-05 FF 14 00-14 00 FF FF
FF 00 F0 05-00 FF 09 00-FC 05 00 FF-04 FF 14 00
14 00 FF FF-FF 00 F0 05-00 FF 09 00-FC 05 00 FF
04 FF 14 00-1A 00 FF FF-FF 00 F0 05-FF FF FF F0
F0 F0 FF FF-FF 00 FC 05-00 FF 04 FF-1A 00 1A 00
В начале байты 19 00 19
, может это ширина и высота изображения? (25x25)
Стал искать закономерности в этих данных. Заметил что этот ресурс состоит из кусков, в начале каждого из которых указан его размер. Причём размер всегда дублируется в конце. И таких кусков до начала следующего ресурса получается ровно 25. Похоже на количество строк в изображении.
пример
0E 00 00 FF 0B F8 F8 F8 00 FF 0C FF 0E 00
0E 00 00 FF 0A 00 F8 05 00 FF 0B FF 0E 00
0E 00 00 FF 0A 00 F8 05 00 FF 0B FF 0E 00
1A 00 00 FF 04 F8 F8 F8 FF FF FF 00 F8 05 FF FF FF FC FC FC 00 FF 05 FF 1A 00
1A 00 FF FF FF 00 F8 05 FF FF FF F8 F8 F8 FF FF FF 00 FC 05 00 FF 04 FF 1A 00
14 00 FF FF FF 00 F8 05 00 FF 09 00 FC 05 00 FF 04 FF 14 00
14 00 FF FF FF 00 F8 05 00 FF 09 00 FC 05 00 FF 04 FF 14 00
14 00 00 FF 04 F8 F8 F8 00 FF 0B FC FC FC 00 FF 05 FF 14 00
08 00 00 FF 1A FF 08 00
08 00 00 FF 1A FF 08 00
10 00 FF F8 F8 F8 00 FF 11 F0 F0 F0 FF FF 10 00
0E 00 00 F8 05 00 FF 0F 00 F0 05 FF 0E 00
0E 00 00 F8 05 00 FF 0F 00 F0 05 FF 0E 00
0E 00 00 F8 05 00 FF 0F 00 F0 05 FF 0E 00
10 00 FF F8 F8 F8 00 FF 11 F0 F0 F0 FF FF 10 00
08 00 00 FF 1A FF 08 00
08 00 00 FF 1A FF 08 00
14 00 00 FF 04 F8 F8 F8 00 FF 0B F0 F0 F0 00 FF 05 FF 14 00
14 00 FF FF FF 00 F8 05 00 FF 09 00 F0 05 00 FF 04 FF 14 00
14 00 FF FF FF 00 F8 05 00 FF 09 00 F0 05 00 FF 04 FF 14 00
1A 00 FF FF FF 00 F8 05 FF FF FF F4 F4 F4 FF FF FF 00 F0 05 00 FF 04 FF 1A 00
1A 00 00 FF 04 F8 F8 F8 FF FF FF 00 F4 05 FF FF FF F0 F0 F0 00 FF 05 FF 1A 00
0E 00 00 FF 0A 00 F4 05 00 FF 0B FF 0E 00
0E 00 00 FF 0A 00 F4 05 00 FF 0B FF 0E 00
0E 00 00 FF 0B F4 F4 F4 00 FF 0C FF 0E 00
Теперь посмотрим на самую короткую строку изображения - 00 FF 1A FF
. 0x1a это 26 что близко к предполагаемой ширине. Это явно RLE (Run Length Encoding) кодировка.
Теперь посмотрим на пару строк чуть больше:
Тут видим 0x0a, 0x05, 0x0b: 00 FF 0A 00 F8 05 00 FF 0B FF
Складываем 0x0a + 0x05 + 0x0b и получаем 0x1a (26), знакомое число. Значит тут закодировано три повтора байтов.
Похоже, что повтор значений начинается с кода 0x00, следом идёт значение для повтора, далее идёт количество повторов этого байта. В конце остаётся лишний 0xff, возможно это для выравнивания, так как все блоки имеют чётные размеры.
00 FF 0A
00 F8 05
00 FF 0B
FF
Теперь еще один блок: 00 FF 0B F8 F8 F8 00 FF 0C FF
Тут видим 00 FF 0B
: повтор 0x0b раз цвета 0xff, и 00 FF 0C
: повтор 0x0c раз цвета 0xff, 0x0b + 0x0c это 0x17, до 0x1a не хватает 3 байта, и вот они F8 F8 F8
. Теперь алгоритм понятен.
Написал программу, что делает декодирование. Так как палитра неизвестна - преобразовал индексы в градации серого. Это действительно похоже на изображения из игры. Проверил что в закодированных блоках всегда записано как минимум на один байт больше, чем указано в заголовке. Заголовок ww 00 hh 80
, второй и третий байт не изменяются, из назначение неизвестно. Исследовал значение дополнительного байта, оно не совсем случайное, но похоже на мусор, который к изображению явно не относится.
Где брать палитру? По фото LCD экрана сложно найти значения палитры, потому что они искажены экраном и камерой что снимает экран. Стал собирать картинки из разных магазинов, где картинки явно от производителя, и получены, видимо, на эмуляторе. И сопоставлять индексы на декодированных ресурсах без палитры, и на картинках от магазинов, где используются те же спрайты. После выписывания двух десятков индексов и соответствующих им цветов, до меня дошло - это RGB упакованные в 8 бит, в формате 3+3+2. Проверяю эту догадку, цвета получились правильные, но оттенки слишком блеклые. Поигрался с гаммой, со значением 2.0 цвета похожи на используемые в игре.
Не спешите оставлять комментарии "скорее всего там гамма 2.2", оказалось это не соответствует гамма-кривой, просто похоже. Реальное преобразование имеет разные кривые для каждого цвета, самые близкие к ним значения гаммы: gR = 1.78, gG = 2.07, gB = 1.42.
![Так выглядит на фото. Так выглядит на фото.](https://habrastorage.org/getpro/habr/upload_files/8cf/648/1d9/8cf6481d9224749858fd050015ca76e7.jpg)
![1. Индекс в серый, 2. RGB332 без гаммы, 3. гамма 2.0, 4. настоящий цвет. 1. Индекс в серый, 2. RGB332 без гаммы, 3. гамма 2.0, 4. настоящий цвет.](https://habrastorage.org/getpro/habr/upload_files/ed9/e94/cfa/ed9e94cfa1dcfc939ad2b486ddaa4023.png)
Тони, верни сотку!
Кроме таинственной строки "tony" в заголовке, я нашел код что сохраняет/загружает состояние игры в конец флэш памяти. Там сохранение начинается с "Tony", уже с заглавной. Не знаю, кто такой Тони и что он натворил, может не вернул кому-нибудь сотню юаней.
Написание эмулятора
Так как набор инструкций определён и часть вызовов биоса понятна, я начал писать эмулятор (тут), запускать на нём отдельные части прошивки, что рисуют разные меню. Постепенно добавлял эмуляцию портов ввода/вывода и функций биоса, как мне становилась понятна их работа. Пока не дошел до главной функции, что вызывается в цикле для отрисовки следующего кадра и обработки логики игры.
Для понимания работы таймера - записал на видео анимацию при перезапуске игры и вычислил частоту по кадрам, 30 кадров в секунду. Стал запускать мини-игры по одной и добавлять новые функции биоса и исправлять баги.
Интересная вещь выяснилась с мини-играми, на эмуляторе они работают слишком быстро. После копания в коде, я пришел к выводу, что никакого динамического изменения частоты кадров нет. Просто логика игр большей частью не полагается на таймеры, и один момент времени соответствует одному кадру. Хотя игры не сложные, процессор настолько слаб, что не может отрисовать всё за 1/30 секунды, и большинство игр пропускает 2 кадра, получается 10 кадров в секунду. Добавил в эмулятор эвристику, что пропускает кадры в зависимости от количества прочитанных/записанных пикселей.
Не стал заморачиваться с подсчётом таймингов процессора, даже если они соответствуют оригинальному 65C02, есть много других неизвестных задержек, например тайминги флэш памяти.
После того, как эмулятор стал достаточно стабильным, купил другую модель ToumaPet. Называется OK-560, отличия от OK-550:
-
Работает на трёх AAA батарейках, что точно минус. Хоть и продают вместе с батарейками.
-
Экран 128x160, что больше, но туда вынесено лишь одно меню. В играх никак не задействовано. Так что используется бесполезно.
-
Флэш память тоже Puya, но уже P25Q64SH на 8МБ.
-
Размер прошивки 4972292 байт, у модели OK-550 было 3129720. Я проверил, что прибавилось в ресурсах, там стало больше кадров анимации питомцев, никакого нового контента не нашел.
У обоих экземпляров, что мне попались, LCD экран с хорошими углами обзора, не инвертируют изображение в одном из направлений, похоже на IPS. Но прошивки поддерживают несколько моделей экрана, наверное бывают и с дешевыми LCD.
Формат звука
Определил какие функции отвечают за проигрывания звуков, им подаётся список с номерами ресурсов. При проигрывании анимации после перезапуска, проигрывается ресурс номер 9, один из самых больших звуковых файлов. Начинается с 0x81, возможно это заголовок, далее нули, в середине какие-то данные, под конец тоже нули. Анимация закрывается не по таймеру, а проверяет определённый бит в памяти, если он не установлен, то сразу же пропускает анимацию и переходит к меню установки времени. Значит этот бит установлен пока проигрывается звук, игра ждёт пока звук кончится. Так как звуки у меня не поддерживаются, то я установил бит в единицу. Анимация не пропускается, но и не заканчивается автоматически, надо нажать на кнопку для продолжения. Записал всю анимацию с реального устройства на видео и сопоставил, получается одна секунда на ~3500 звука. В каком формате может храниться звук для такого устройства? Я подумал про ADPCM, но их много разных. Проверил самые простые Dialogic ADPCM и IMA ADPCM, и получил очень искаженный звук, но это действительно мелодия после перезапуска. Поменял порядок сэмплов, сначала младший, искажений стало меньше. Заменил ADPCM простым фильтром:
sample = code & 8 ? -1 - (code & 7) : code
Стало еще меньше помех, но звук всё равно не оригинал. На этом остановился, потому что идей больше не было.
Синий экран
В прошивке нашлось секретное тестовое меню (показано на фото в начале статьи). Открывается если при сбросе зажаты левая и правая кнопки (не боковые).
Дамп биоса
Нашел где в прошивке находятся функции для сохранения данных в флэш память. Они рассчитаны только на чипы Puya. На командах 65C02 написал цикл использующий данные функции для сохранения всей области памяти в конец флэша, куда игра делает сохранения. Так как мне записывать меньше 256 байт патча, то меня заинтересовало, почему все GUI программы для программатора не имеют возможности перезаписать лишь часть прошивки, только всю целиком.
Эта утилита для командной строки умеет записывать часть, но не умеет очищать страницы, что нужно сделать до перезаписи. Запись флэша может изменить биты с единицами на нули, но не может изменить нули на единицы. Очистка блока памяти замещает весь блок байтами 0xff (все биты равны единицам).
Причина вскрылась после чтения документации на разные чипы. Команды чтения стандартизованы. Для команды записи нужно знать размер страницы, обычно это 256 байт, но бывают и другие размеры. Но команды очистки очень разные, из которых более менее стандартизовано лишь полная очистка чипа, что имеет код 0x60 или 0xc7 (флэш Puya поддерживает оба кода). С командами частичной очистки памяти много хуже, их коды не стандартизованы и зависят от производителя, если не от конкретных чипов. Один чип может поддерживать несколько команд очистки с разными размерами блока, другой поддерживает только полную очистку и очистку секторов по 64К, но не имеет очистки страниц по 256 байт.
Написал свой инструмент для CH341 (тут), в котором можно указать коды команд и размеры из документации. С помощью чего записал свой код в обработчик тестового меню. Перед этим проверил патч на своём эмуляторе. На устройстве он тоже отработал правильно, так я смог считать дамп, внутри которого оказался биос (ROM процессора).
![Дамп делал с модели OK-560, на этот раз позаботился об удобстве и зажал кнопку сброса прищепкой. Дамп делал с модели OK-560, на этот раз позаботился об удобстве и зажал кнопку сброса прищепкой.](https://habrastorage.org/getpro/habr/upload_files/13a/8f6/d8a/13a8f6d8a413eb8bcb201cf809cdf7c5.jpg)
Карта памяти (не SD)
По дампу памяти стало понятнее, сколько памяти на устройстве и как она используется.
Первые 128 байт это порты ввода/вывода. Диапазон 0x80..0x7ff
это RAM, которой 2КБ. Адреса 0x800..0x3fff
это зеркала на RAM, но первые 128 байт уже не заняты портами, их можно использовать. Биос занимает 8КБ по адресам 0x6000..0x7fff
, и также отзеркален на адреса 0x4000..0x5fff
. После 0x8000 в дампе одни нули. Из исследования прошивки я нашел что байты 0x8000 и 0xc000 это порты для коммуникации с LCD экраном (0x8000 для команд, 0xc000 для данных).
Настоящая палитра и формат звука
Нашел в биосе настоящую палитру, что не соответствует гамма кривым, но зеленый цвет оказался близко к моему предположению.
Также нашел декодирование звука по таблицам. Алгоритм оказался такой:
таблицы и код на Си
static const uint8_t adpcm_value[256] = {
0x00, 0x01, 0x02, 0x04, 0x05, 0x08, 0x0a, 0x0d,
0x00, 0x02, 0x05, 0x06, 0x0a, 0x0f, 0x16, 0x1a,
0x00, 0x03, 0x07, 0x09, 0x0c, 0x10, 0x12, 0x1c,
0x01, 0x04, 0x08, 0x0b, 0x0f, 0x14, 0x1b, 0x27,
0x02, 0x05, 0x0a, 0x0f, 0x14, 0x1a, 0x23, 0x32,
0x02, 0x07, 0x0c, 0x12, 0x19, 0x20, 0x29, 0x3b,
0x02, 0x09, 0x10, 0x17, 0x1e, 0x26, 0x30, 0x44,
0x02, 0x0a, 0x12, 0x1a, 0x22, 0x2c, 0x39, 0x51,
0x02, 0x0b, 0x15, 0x1e, 0x27, 0x32, 0x40, 0x5b,
0x03, 0x0d, 0x16, 0x20, 0x2a, 0x36, 0x45, 0x61,
0x04, 0x0f, 0x18, 0x22, 0x2d, 0x39, 0x4a, 0x6f,
0x04, 0x10, 0x1c, 0x26, 0x32, 0x40, 0x54, 0x7e,
0x04, 0x11, 0x1e, 0x2a, 0x38, 0x47, 0x61, 0xa0,
0x04, 0x12, 0x20, 0x2d, 0x3c, 0x4f, 0x6c, 0xa3,
0x05, 0x15, 0x23, 0x31, 0x41, 0x54, 0x70, 0xa6,
0x06, 0x17, 0x26, 0x35, 0x45, 0x58, 0x74, 0xaf,
0x06, 0x18, 0x29, 0x3a, 0x4a, 0x5e, 0x78, 0xb0,
0x08, 0x1a, 0x2b, 0x3d, 0x4f, 0x63, 0x7e, 0xb1,
0x09, 0x1d, 0x30, 0x43, 0x57, 0x6d, 0x89, 0xb4,
0x0a, 0x1e, 0x33, 0x47, 0x5c, 0x73, 0x8e, 0xb5,
0x0b, 0x21, 0x37, 0x4c, 0x63, 0x7b, 0x97, 0xc0,
0x0c, 0x24, 0x3c, 0x52, 0x68, 0x83, 0x9f, 0xce,
0x0d, 0x25, 0x41, 0x59, 0x72, 0x8d, 0xa9, 0xd1,
0x0f, 0x2a, 0x48, 0x60, 0x7c, 0x97, 0xb2, 0xd3,
0x11, 0x2f, 0x4e, 0x6c, 0x89, 0xa3, 0xc2, 0xe4,
0x12, 0x34, 0x56, 0x74, 0x8f, 0xaf, 0xd0, 0xec,
0x13, 0x3a, 0x5b, 0x7c, 0x97, 0xb1, 0xd3, 0xef,
0x18, 0x3c, 0x63, 0x83, 0xa4, 0xc1, 0xdd, 0xf0,
0x1a, 0x42, 0x69, 0x93, 0xb0, 0xd3, 0xec, 0xf5,
0x1b, 0x46, 0x6a, 0x96, 0xb9, 0xde, 0xed, 0xf9,
0x1d, 0x48, 0x6b, 0xa1, 0xba, 0xe3, 0xfe, 0xfe,
0x1e, 0x4d, 0x79, 0xb3, 0xf5, 0xfe, 0xfe, 0xfe };
static uint8_t adpcm_next[256] {
0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x20,
0x00, 0x00, 0x00, 0x08, 0x10, 0x10, 0x10, 0x28,
0x08, 0x08, 0x08, 0x10, 0x18, 0x18, 0x18, 0x30,
0x10, 0x10, 0x10, 0x18, 0x20, 0x20, 0x20, 0x38,
0x18, 0x18, 0x18, 0x20, 0x28, 0x28, 0x28, 0x40,
0x20, 0x20, 0x20, 0x28, 0x30, 0x30, 0x30, 0x48,
0x28, 0x28, 0x28, 0x30, 0x38, 0x38, 0x38, 0x50,
0x30, 0x30, 0x30, 0x38, 0x40, 0x40, 0x40, 0x58,
0x38, 0x38, 0x38, 0x40, 0x48, 0x48, 0x48, 0x60,
0x40, 0x40, 0x40, 0x48, 0x50, 0x50, 0x50, 0x68,
0x48, 0x48, 0x48, 0x50, 0x58, 0x58, 0x58, 0x70,
0x50, 0x50, 0x50, 0x58, 0x60, 0x60, 0x60, 0x80,
0x58, 0x58, 0x58, 0x60, 0x68, 0x68, 0x68, 0x90,
0x60, 0x60, 0x60, 0x68, 0x70, 0x70, 0x70, 0x98,
0x68, 0x68, 0x68, 0x70, 0x78, 0x78, 0x78, 0xa0,
0x70, 0x70, 0x70, 0x78, 0x80, 0x80, 0x80, 0xa8,
0x78, 0x78, 0x78, 0x80, 0x88, 0x88, 0x88, 0xb0,
0x80, 0x80, 0x80, 0x88, 0x90, 0x90, 0x90, 0xb8,
0x88, 0x88, 0x88, 0x90, 0x98, 0x98, 0x98, 0xc0,
0x90, 0x90, 0x90, 0x98, 0xa0, 0xa0, 0xa0, 0xc8,
0x90, 0x98, 0x98, 0xa0, 0xa8, 0xa8, 0xa8, 0xd0,
0x98, 0xa0, 0xa0, 0xa8, 0xb0, 0xb0, 0xb0, 0xd8,
0xa0, 0xa8, 0xa8, 0xb0, 0xb8, 0xb8, 0xb8, 0xe0,
0xa8, 0xb0, 0xb0, 0xb8, 0xc0, 0xc0, 0xc0, 0xe8,
0xb0, 0xb8, 0xb8, 0xc0, 0xc8, 0xc8, 0xc8, 0xf0,
0xb8, 0xc0, 0xc0, 0xc8, 0xd0, 0xd0, 0xd0, 0xf8,
0xc0, 0xc0, 0xc8, 0xd0, 0xd8, 0xd8, 0xd8, 0xf8,
0xc8, 0xc8, 0xd0, 0xd0, 0xe0, 0xe0, 0xe0, 0xf8,
0xd0, 0xd0, 0xd0, 0xd8, 0xe8, 0xe8, 0xe8, 0xf8,
0xd8, 0xd8, 0xd8, 0xd8, 0xf0, 0xf0, 0xf0, 0xf8,
0xd8, 0xd8, 0xe0, 0xe0, 0xf8, 0xf8, 0xf8, 0xf8,
0xe0, 0xe0, 0xe8, 0xe8, 0xf8, 0xf8, 0xf8, 0xf8 };
typedef struct { uint8_t idx; } adpcm_status_t;
void adpcm_init(adpcm_status_t *adpcm) { adpcm->idx = 0; }
int adpcm_decode(adpcm_status_t *adpcm, unsigned x) {
unsigned a = (x & 7) | adpcm->idx;
adpcm->idx = adpcm_next[a];
a = adpcm_value[a];
return (x & 8 ? -a : a);
}
Не нашел известных ADPCM кодеков, что работали бы по подобному алгоритму. Если вы знаете, что это за кодек - напишите мне.
После декодирования звук стал чистым, но вычисленная по записям звука с устройства частота составляет 6866, что очень странно. К тому же, я понял только как декодируются сэмплы, возможно там еще есть какая-то аппаратная магия, без знания что делают определённые порты процессора сложно понять весь алгоритм.
Также по биосу понял назначение пары странных функций, эмуляцию которых я просто пропускал. Ими оказались проигрывание и остановка музыки. Там есть специальный формат ресурсов для музыки, вроде MIDI или трекерной музыки, читается как скрипт, не смог понять как он работает.
Беспроводная связь
Биос этим не занимается, весь код в прошивке игры. Не разбирался как работает, но нашел через какие порты игра управляет беспроводным чипом.
Надёжное 48-бит шифрование с проверкой контрольных сумм
Наверное, именно так разработчики прошивки представляют это своему начальству/заказчику.
Биос находит 8-битный ключ для декодирования прошивки так:
Пусть "p" это указатель на начало прошивки.
key = ((((p[9] - p[10]) | p[11]) ^ p[12]) + p[13]) & p[14]
И получается 0xe4, эти значения в OK-550 и OK-560 одинаковые.
Далее контрольные проверки:
p[15] = p[9] ^ key
p[16] = p[10] ^ key
p[17] = p[11] ^ key
p[18] = p[12] ^ key
p[31] = p[13] ^ key
p[32] = p[14] ^ key
sum = p[9] + p[10] + p[11] + p[12] + p[13] + p[14]
p[33] = (sum & 0xff) ^ key
p[34] = (sum >> 8) ^ key
Затем проверяется, что по адресу 0x23 после декодирования должно получиться: "tony"
Как можно заметить, если расшифровать прошивку и просто обнулить байты 9..18 и 31..34, то проверку должно пройти.
В эмуляторе я сделал вычисление ключа по "tony", вместо их крутейшей защиты ключа в 48-и битах.
P.S.: Исследование проходило не совсем по порядку в статье, в статье разделено на темы, а в реальности некоторые части накладывались друг на друга и шли параллельно.
P.P.S.: Прошивку не распространяю, я не участвую в пиратстве.