
Родители малышей постоянно фотографируют их проделки. Дети копируют наше поведение как обезьяны, поэтому наш младший тоже вскоре захотел щёлкать затвором. Обычно мы делаем снимки на телефон, но давать его будет не очень хорошей идеей: дети иногда роняют вещи. Что ещё хуже, иногда в телефоне включаются какие-то странные будильники и меняются настройки. Чтобы такого не происходило, я решил специально для младшего ребёнка купить камеру.
Очевидно, дети в таком возрасте не могут пользоваться всеми функциями зеркальных или даже компактных камер. К счастью, в мире много детей с интересом к фотографии, поэтому есть и рынок для дешёвых (в буквальном смысле) камер: в них не так много функций и наворотов, крепкий пластмассовый корпус, а низкая цена не позволяет расстроиться из-за поломки. Я поискал такую камеру на Taobao и остановился на той, которая показалась мне достаточно простой. Я не надеялся, что фотографии будут качественными, но хотя бы разрешение должно оказаться приемлемым.
Для включения камеры достаточно было нажать на кнопку. Однако дальше всё становилось сложнее: как и во многих китайских устройствах, производитель стремился запихнуть в камеру кучу функций, поэтому после загрузки открывалось меню, где одной из опций был режим камеры. Если бы у камеры был сенсорный экран, как у наших телефонов, то это не вызвало бы проблемы, но перемещаться по меню нужно было при помощи курсорных клавиш, а это оказалось слишком сложно для трёхлетки, живущего в нашем современном мире.
Поэтому я задумался: наверняка ведь есть способ пропустить меню и сразу перейти в режим камеры? Должно быть, внутри камеры находится какой-нибудь процессор ARM, и я не думаю, что разработчики заморочились с защитой кода. Достаточно считать его через JTAG, загрузить в Ghidra, найти нужный бит, изменить его, снова записать код во флэш-память, и всё.
Разбираемся с ISA
Как я и ожидал, корпус камеры имел такой размер в основном для того, чтобы ребёнку было удобно её держать, а не потому, что внутренности занимали много места: бóльшая часть корпуса оказалась пустой. Я изначально не надеялся на многое, но модули основной и селфи-камеры оказались даже меньше, чем я думал, абсолютно крошечными. Кроме модулей камеры, внутри находилась крошечная печатная плата со светодиодом для флэш-карты и динамика; всё это соединялось с литий-ионным аккумулятором и основной платой, к которой подключён ЖК-экран.


Основная плата оказалась практически пустой. На ней находятся коннекторы для всех внешних компонентов, а также кнопки упра��ления камерой. Также на ней есть USB-порт и разъём для карты microSD. Набор активных компонентов довольно стандартен: интегральная схема зарядки литий-ионных аккумуляторов SOT23 и регулятор 3,3 В в корпусе SOT23-5. На плате есть 4 МиБ флэш-памяти в виде чипа Spansion S25FL132K SPI, а все остальные задачи выполняются основным чипом HX-Tech HX3302B.
Похоже, это самый неизвестный в Интернете чип. На момент написания статьи запрос «HX3302B» в кавычках возвращал только две страницы Aliexpress, на которых продавались камеры, где этот чип использовался в «основной схеме управления», ещё две страницы с простым перечислением интегральных схем и одно упоминание в Twitter от японца, который вскрыл похожую на мою камеру и нашёл этот чип. Кажется, даже название компании HX-Tech возвращает только ложноположительные результаты.
Значит, мы зашли в тупик? Неизвестный чип с неизвестной архитектурой, неизвестными способами отладки и интерфейсами... Мне даже не удалось найти отладочный последовательный порт: хотя тестовая точка помечена, как TX, она соединяется с линией, используемой светодиодом активности SD-карты, и в ней нет последовательных данных. Возможно, у разработчиков закончились GPIO, и этот контакт выводит сигнал UART, только если загрузить специальную отладочную сборку прошивки. Но постойте, у нас есть ещё флэш-чип на 4 МиБ; возможно, он прольёт свет на устройство?

После считывания флэш-памяти и применения обычных инструментов оказалось, что нам вроде бы повезло:
Кажется, на ней хранится прошивка, она не зашифрована и не сжата.
В прошивке есть довольно много отладочных строк.
При изменении одной из строк и записи кода в память оказывается, что никакой криптографический хэш или даже контрольная сумма не проверяются. Если знать, что изменять, то камера тривиальным образом примет все изменения.
Но есть и плохие новости:
Никак нельзя понять, как включить отладочный UART.
Никаких подсказок о том, какую архитектуру имеет чип. Только то, что он относится к семейству HX330x, но поисковые движки по этому запросу не выдают полезных результатов.
Как ни странно, даже автоматизированные инструменты не могут определить, какая ISA используется в CPU внутри чипа. Cpu_rec просто ответил мне молчанием, в isadetect перечислил различные варианты с крайне низкой степенью достоверности.
Неудачей закончились даже ручные сравнения с известными мне ISA, не охватываемых этими инструментами (например, с OpenRISC и некоторыми архитектурами Sunplus).
Я не имею ничего против реверс-инжиниринга ISA с нуля, но для этого мне нужно чуть больше, чем сырой двоичный файл. Мне бы очень помогло знание о том, какие части двоичного файла являются командами, а также приблизительная информация о том, для чего какие области используются. Основной SoC всё равно нужно загружать эту информацию из флэш-памяти, так почему бы не подключить логический анализатор и не посмотреть, чем она занимается? Возможно, мы увидим, какие биты загружаются при запуске; это уже ограничит пространство, которое нам нужно исследовать.

Так как печатная плата содержит MOSI и MISO флэш-чипа, мне нужно было подключить всего три провода плюс заземление. Однако мне не удалось добиться качественного перехвата данных, плюс устройство время от времени отказывалось загружаться. Оказалось, что линия CLK чипа пульсировала с частотой примерно 70 МГц, а добавленная ёмкость щупа логического анализатора ухудшала сигнал и устройство не работало. Чтобы решить проблему, я подключил 74LVC00 в качестве простого буфера между линией CLK и щупом. Также стоит отметить, что я переставил флэш-память на шестиконтактный держатель, чтобы его проще было извлекать, считывать и перепрограммировать.
ПО моего логического анализатора без проблем декодировало сигнал SPI во флэш-команды.

Опишу в упрощённом виде происходящее:
Чип считывает 512 байт из адреса 0.
Чип считывает 10752 байта из адреса 0x20.
Чип считывает 34816 байт из адреса 0x2c00.
Выполняется множество операций считывания из флэш-памяти по 4 байта, в основном из последовательных адресов, но иногда адреса «скачут».
Затем что-то меняется, и дальше происходят только считывания по 16 байт.
У меня есть опыт работы с чипами, загружающимся из внешней флэш-памяти, поэтому с большой долей уверенности могу предположить, что здесь происходит:
Первые 512 байт — это некий заголовок: он показывает, какие разделы нужно считать в ОЗУ.
Две следующие большие операции считывания выполняют именно это: копируют блок флэш-памяти в ОЗУ, чтобы выполнять его оттуда.
Считывания по 4 байта похожи на то, что CPU выполняет код напрямую из флэша с отображением в память, считывая по одной команде за раз, затем в основном просто переходя к следующей команде, но иногда выполняя циклы, переходы к подпрограмме и так далее. То, что считываются всегда 4 байта, намекает, что CPU 32-битный.
Так как выполнение кода непосредственно из флэш-памяти неэффективно, позже инициализируется кэш. Размер линии кэша равен 16 байт, поэтому мы редко видим считывания этого размера и только для адресов, доступ к которым выполняется впервые.
Считывания по 4 байта очень полезны, потому что они демонстрируют поток выполнения программы, а посмотрев, куда поток выполняет переход не последовательно, можно декодировать некоторые команды потока программы.
Одно это уже позволяет мне распознать подпрограммы и переходы в коде. Однако до включения кэша происходит всего примерно сотня считываний по 4 байта, и из-за включения кэша трассировка, по сути, становится невозможной. Но что, если мы сможем этому помешать? Включение кэша в исходниках, вероятно, выполняется подпрограммой enable_icache() или чем-то подобным, так почему бы не заменить последнюю команду вызова до включения кэша?
Выполнив шестнадцатеричное редактирование, операцию программирования флэша и трассировку логическим анализатором, я получил файл размером 300 МиБ, в котором подробно описаны команды, получаемые CPU для всего процесса запуска. Отлично, теперь это позволит нам декодировать ещё больше команд.
Сразу было очевидно, что в прошивке присутствуют отладочные printf. Так как сам UART не был привязан ни к какой строке, я не мог считывать передаваемые сообщения, но мог видеть, что они считывают в функции printf. Удобство функции printf заключается в том, что реализовать её можно не таким уж большим количеством способов, поэтому её структура примерно одинакова для всех CPU: считываем байт, сравниваем с символом «%», выполняем вывод, если они не совпадают, интерпретируем символ за ним, если он есть. Так как структура хорошо известна, а я могу провести корреляцию между потоком программы и шестнадцатеричными командами во флэш-памяти, это позволило мне выявить множество типов команд: загрузка и сохранение в память, копирование между регистрами, смещения, подробности сравнений регистров и так далее. Я был уверен, что быстро смогу задокументировать большую часть ISA. Однако я ошибался насчёт скорости этого процесса.
Для документирования декодированного мной набора команд я создавал определение процессора в Ghidra. Преимущество такого подхода в том, что чем больше команд декодируешь, тем лучше становится декодирование Ghidra, и в результате этого упрощается декодирование новых команд. Однако язык P-code, на котором пишутся эти определения процессоров, довольно мощен (если писать их правильно, то Ghidra сможет правильно выполнить свой трюк декодирования ассемблерного кода в псевдо-C), но в то же время нетривиален. В какой-то момент мне нужно было кое-что проверить, и я решил посмотреть это в скачанном ранее определении OpenRisc-1000 (OR1K)... но внезапно многое в этом определении показалось мне до странности знакомым.
Оказалось, что архитектура CPU OR1K очень хочет быть big-endian, поэтому именно таково 99% её реализаций. Однако при сильной необходимости можно добавить в ядро скрэмблер шины данных, чтобы сделать его little-endian. Похоже, именно так поступили люди, проектировавшие SoC этой камеры. Однако поскольку это решение используется довольно редко, ни один из инструментов не распознал двоичный файл как известную ISA. К счастью, было очень легко добавить вариант процессора OR1K с little-endian, после чего Ghidra сразу научилась отлично декодировать весь двоичный файл.
Модификация
После того, как решение сложной задачи оказалось тривиальным, я мог вернуться к основной задаче: запуску камеры сразу в режиме съёмки. Однако для этого мне требовалось больше данных: хотя основная часть кода находилась во флэш-памяти, часть размещалась в ОЗУ или, возможно, в ROM, и я не знал её содержимого. Переписать часть кода, чтобы сдампить её через отладочный UART, тоже было нельзя. Но я уже нашёл код, записывающий данные JPEG на диск при съёмке; в нём была одна подпрограмма, записывающая структуру с информацией EXIF... Благодаря модификациям я заставил её записывать произвольный блок памяти. Оставалось лишь снова записать изменённую прошивку во флэш-память, сделать снимок, после чего нужное мне содержимое памяти будет прикреплено к фотографии.
Благодаря этому я смог найти, куда прошивка камеры решает пойти при включении питания. Это было непросто, потому что прошивка использует какой-то странный планировщик и систему событий для хранения указателей на функции в таблицах во флэш-памяти: поскольку Ghidra не видит чёткой точки входа в эти функции, она не может автоматически декодировать их, поэтому чтобы система стала понятной, мне пришлось разбираться в формате и местоположении этих таблиц.
Подобный хак обычно требует кучи работы для создания минимального изменения, выполняющего нужную задачу. У прошивки есть множество разных режимов (главное меню, фотосъёмка, видео, просмотр фотографий, воспроизведение mp3, игры), а текущий режим хранится в глобальной переменной в памяти. При запуске эта переменная инициализируется значением режима главного меню. Если изменить её значение на режим фотосъёмки, то прошивка будет загружаться в этом режиме. Изменить один байт можно за считаные секунды, но чтобы понять, какой байт менять, требуется гораздо больше времени. В процессе анализа кода я нашёл и некоторые другие интересные аспекты.


Во-первых, кое-что «осталось на монтажном столе», например, выше показана, похоже, более ранняя версия меню. Всё, кроме двух первых двух пунктов, зачёркнуто; возможно, кто-то поменял своё решение и выразил это в Paint?

А это ещё одна игра из камеры — тетрис. Код есть в прошивке, просто он ни с чем не соединён; подключив его, становится видно, что игра работоспособна лишь частично. Это можно объяснить тем, что она предназначалась для другого устройства (похоже, она ожидает, что дисплей будет иметь разрешение 220x176, а у моей камеры он размером 320x240), но не знаю, только ли в этом дело. Возможно, работа над игрой началась, но была отменена, потому что для законного добавления тетриса нужна лицензия.
Как вполне можно было ожидать в подобном случае, маркетинг устройства сильно преувеличивает его технические возможности. Даже на самой скромной странице продаж говорится, что в камере установлен двухмегапиксельный сенсор. На самом деле, хоть я и не могу точно сказать, какой модуль камеры использовался, ни один из поддерживаемых прошивкой модулей не имеет разрешения больше 720P, то есть меньше одного мегапикселя, а большинство поддерживаемых модулей имеет разрешение всего 640x480, или 0,3 мегапикселя.
Наконец, я решил изучить используемый формат обновлений прошивки: на текущем этапе единственный известный мне способ изменения прошивки заключался в выпаивании чипа флэш-памяти и его перепрограммировании. Так как камера моего ребёнка всё ещё оставалась целой, было бы удобно обновлять её без вскрытия.
Оказалось, формат обновлений прошивки довольно тривиален. Логика ищет на SD-карте файл hx330x_sdk.bin, и если он существует, то она проверяет несколько полей заголовка на валидность. Затем она берёт одно поле, содержащее временную метку, и если метка в файле новее, чем во флэш-памяти, то она просто копирует файл обновления во флэш-память. Это значит, что можно просто создать дамп флэш-памяти, обрезать его так, чтобы он содержал только прошивку, внести нужные мне изменения, а затем сохранить всё в файл hx330x_sdk.bin. Чтобы камера приняла обновление, нужно ввести в поле временной метки значение больше, чем у прошивки, записать файл на SD-карту, и камера обновится с неё.
Заключение
Позвольте показать апгрейд:
Возникает вопрос: стоило ли оно того? С точки зрения траты времени, вероятно, нет. Если бы я купил дешёвую ESP-Cam и распечатал для неё корпус, то, наверно потратил бы меньше времени и столько же денег, а ребёнок снимал бы на камеру более высокого разрешения и с более удобной прошивкой. Однако реверс-инжиниринг доставляет удовольствие: иногда эта головоломка так затягивает, что ты переходишь в состояние потока. Приятно и осознавать, что в этой статье, вероятно, содержится больше информации о чипсете, чем в любом другом месте Интернета. В конце концов, это моё хобби: радость от качественно выполненной работы и осознание того, что ты добавил что-то в копилку общемировых знаний, тоже чего-то да стоят.
А что мой ребёнок? Вроде ему нравится снимать на камеру, и я уверен, что скоро в нашей семье появится профессиональный фотограф... или же интерес к съёмке угаснет через пару дней, как это обычно бывает у малышей. Закончу статью я одними из лучших его работ:





