Вы когда-нибудь задумывались, сколько существует умной электроники? Куда ни глянь, можно встретить устройство с микроконтроллером с собственной прошивкой. Камера, микроволновка, фонарик … Да, у некоторых кабелей USB Type-C тоже есть прошивка! И все это теоретически можно перепрограммировать, переделать, модифицировать. Но как это сделать без документации и исходников? Обратный инжиниринг, конечно! Давайте подробнее рассмотрим этот же процесс инверсии, от самой идеи до конечного результата, используя несколько небольших, но интересных примеров и осуществим тонкую настройку SATA контроллера!
Идея
Меня давно привлекала шина PCI Express. Судите сами — высокие скорости, прямой доступ к памяти компьютера, множество устройств и производителей, масса стандартов и реализаций. Что делать, если вы возьмете какое-нибудь устройство PCI-E и переделаете прошивку так, чтобы в процессе работы, параллельно, вы также могли читать / записывать оперативную память компьютера по желанию?
Вы скажете — но есть же готовые решения, например, PCI Leech и Thunderclap. Но PCI Leech не умеет работать как «обычное» устройство, а Thunderclap, хоть и имитирует сетевую карту, но базируется на громоздкой и дорогущей FPGA девборде. По моей же задумке, устройство должно корректно определяться драйверами в любой системе и при этом недорого стоить.
Такое устройство можно использовать, к примеру, в качестве аппаратного отладчика x86 — имея возможность чтения и записи ОЗУ, разрабатывать драйвера для BIOS «на коленке» гораздо проще.
Выбираем подопытного
К различным PCI-E устройствам я начал присматриваться ещё несколько лет назад, но большинство из встреченных мной девайсов либо были слишком просты и имели намертво зафиксированную функциональность, и очень небольшие возможности со стороны прошивки:
USB 3.0 контроллер на чипе ASMedia
Либо выглядели настолько сложно, что перспектива реверса такого монстра просто пугала:
Продвинутый сетевой контроллер Fujitsu
Мне же требовалось устройство, которое имеет:
-
встроенный мощный микроконтроллер
-
легко перепрограммируемое ПЗУ
-
отладочные интерфейсы (UART, JTAG)
-
встроенную прошивку (а не загружаемую драйвером)
Ну и в добавок, это должно быть широко поддерживаемым всеми системами устройством, например, SATA или USB контроллером. И, как вы уже догадались из названия статьи, недавно я наткнулся на вот такой SATA контроллер:
А именно, меня привлекла крайняя простота контроллера (только проц, да ПЗУ) и радиатор на нём (а значит, внутри мощный CPU!). Быстрый поиск по названию чипа ещё больше подогрел к нему интерес. Найденная прошивка для него весила аж 500 КБ, имела признаки ARM кода, не была пожата, и имела достаточно текстовых отладочных строк:
Даже по беглому взгляду на прошивку уже можно сказать, что исследовать её будет несложно
И на него была некоторая документация, в которой очень много выводов было не подписано, что давало шанс на наличие отладочных интерфейсов:
Какие подозрительные ряды неиспользуемых пинов, не правда ли?
В скором времени контроллер был куплен, и я взглянул на него повнимательнее:
Без радиатора контроллер выглядит ещё более простым
Контроллер действительно выглядел очень просто и перспективно. Но более подробное изучение даташитов показало, что NC пины — на самом деле NC, и отладки на нём ждать не стоит. Более продвинутый контроллер с поддержкой как SATA, так и IDE, ровно в таком же корпусе имел на этих выводах дополнительные сигналы:
Увы, скорей всего эти выводы не тестовые
Но зато я нашёл даташиты на другие очень похожие контроллеры, с явным упоминанием отладки:
То, что производитель просит не подключать TST2-TST6 ну очень намекает на наличие JTAG, а прямое указание UART на TST0 и TST1 (в другом даташите) это уже джекпот. Засим было решено купить 88SE9215 как самый недорогой из доступных, и издеваться уже над ним:
Проверяем работоспособность
И вот объект изучения у нас в руках, первым делом проверяем, что он работает. Это важный момент, именно тут мы устраняем возможные будущие вопросы «Это я сломал или оно и было нерабочим??»
Для этого мне пришлось купить M2 райзер, поскольку единственный PCI Express слот моего ПК занят видеокартой:
Впервые вживую увидел разъём miniUSB 3.0. Солидно!
В общем, тест прошёл успешно, я даже установил систему на HDD, подключенный к этому контроллеру, всё подцепилось стандартными драйверами:
При запуске ПК мелькает информация о состоянии контроллера, это PCI Option ROM и по идее из этого меню можно что-то настраивать, но мне никак не удалось зайти в настройки:
Чтобы поймать этот момент, пришлось записывать видео
Анализируем компоненты
Теперь нужно определить, что с чем соединено, где процессор, а где ПЗУ, и какие компоненты за что отвечают. В нашем случае анализ крайне простой, особенно учитывая наличие даташитов:
Да, простые компоненты вроде транзисторов и конденсаторов нас не интересуют
Ну а соединения компонентов за нас нарисовал производитель
Точное назначение и тип компонентов узнаём по маркировке и даташитам. В нашем случае список компонентов довольно небольшой:
Получаем прошивку
Вот мы убедились, что устройство работает, проанализировали, из каких компонентов оно состоит, теперь следует его «забэкапить», по максимуму извлечь из него данные, чтобы в будущем можно было его к этому же состоянию и вернуть. Прошивка может находиться как во внешнем ПЗУ, так и в самом контроллере, это тоже следует учитывать и внимательно смотреть документацию.
Обычно прошивку можно получить тремя способами, это:
-
обновления от производителя
-
программатором из ПЗУ / контроллера
-
по отладочным интерфейсам из устройства
В сети прошивка для моего контроллера нашлась на сайте station-drivers, впрочем, оттуда же я брал прошивку и для предыдущего купленного контроллера. Несмотря на то, что это .exe файл, 7-zip его прожевал, и внутри обнаружились .bin файлы самой прошивки:
Я скачал все прошивки, что только смог найти в сети, в том числе и для похожих контроллеров, и это тоже дало результаты — в одном из архивов обнаружился Readme с описанием чтения ПЗУ:
Но для этого нужно было готовить загрузочную DOS флешку, поэтому я просто считал ПЗУ программатором, благо здесь стоит типичная SPI Flash. Самый простой способ — клипсой. Цепляемся к ПЗУ и пытаемся читать:
Самая простая клипса с али, стоит меньше бакса
Мы ещё не раз увидим эту коробочку с надписью PF
И в два клика ПЗУ определилось и прочиталось программатором:
Да, автор программатора очень любит писать «флешь» с мягким знаком
Часто контроллер мешает считыванию и приходится выпаивать микросхему или хотя бы подавать питание на устройство. В этом случае повезло, но на всякий случай считываем несколько раз и сравниваем содержимое, чтобы убедиться, что считалось корректно.
Анализируем прошивку
В качестве объекта для анализа я взял скачанную с сайта прошивку (чтобы начать исследование ещё до того, как купленный контроллер приедет ко мне). Первым делом нужно определить структуру образа прошивки. При беглом просмотре сразу видно, что большую часть образа занимает пустое место, а полезные данные начинаются на некоторых адресах, кратных 0x1000. И по адресу 0x2000 видим достаточно интересный набор данных:
Ну вот, за нас даже структуру прошивки расписали! Итак, согласно описанию, в образе мы имеем:
Мне повезло, что в образе уже было описание разделов. В общем случае, столкнувшись с неизвестным образом, желательно прогнать его через binwalk, эта утилита сразу покажет известные форматы файловых систем и упакованных данных. В этом конкретном случае он нашёл только таблицы коэффициентов для CRC32:
Также полезно разбить прошивку на части и прогнать их через cpu_rec, чтобы знать, с какой программной архитектурой имеем дело. Ещё в начале статьи я предположил, что контроллер должен иметь архитектуру ARM, ну а Option ROM должен быть архитектуры x86, поскольку исполняется он на хосте. Проверим это:
А вот теперь возьмёмся за реверс. Начнем с Loader — он ARM архитектуры и имеет небольшой размер. Скорее всего с него контроллер начинает загрузку. Попробуем его загрузить в дизассемблер:
И видим, что первые инструкции выполняют прыжки на адреса 0xFFFF00**, а это значит, что либо контроллер первым делом при старте прыгает в Mask ROM по адресу 0xFFFF0000 (что сомнительно), либо код Loader сам грузится в этот адрес. Перезагружаем код в дизассемблер по 0xFFFF0000 и действительно, всё корректно парсится:
Функций здесь очень немного (целых четыре), и выяснить что делает код не представляет труда:
-
По адресу 0xF8064000 код обращается к содержимому ПЗУ
-
В ПЗУ происходит поиск сигнатуры «MAGIIMGF»
-
Блок данных с этой сигнатурой парсится и раскидывается по ОЗУ
-
Происходит запуск основной системы прыжком по адресу 0
И да, именно с сигнатуры «MAGIIMGF» начинается Firmware, который мы вырезали ранее! Немного проанализировав загрузчик, получаем вот такой формат блока прошивки:
И теперь мы можем распарсить Firmware и правильно прогрузить его в дизассемблер! Ожидаемо всё идеально прогружается и можно начать анализ основной системы:
Основная задача реверс-инжиниринга — дать имена функциям и понять, что происходит в коде. В этом нам помогут:
-
часто вызываемые стандартные функции (malloc, memset, memcpy)
-
текстовая отладочная информация из прошивки
-
различные уникальные константы
Очень полезно найти функцию Assert, которая выводит в отладочную консоль наименование файла исходников и номер строки с возникшей ошибкой. Например, здесь название файла намекает, что эта функция занимается выделением памяти:
А здесь явно происходит инициализация последовательного порта:
Кстати, немного проанализировав задачи инициализации системы, можно наткнуться на функцию, которая принимает указатель на другую функцию и некоторое имя. Очень похоже на запуск новой задачи! Так и назовём:
В процессе изучения кода задач натыкаемся на интересную функцию, которой часто передаются красивые десятичные значения (100, 1000)… И это очень похоже на функцию задержки исполнения, sleep:
Судя по числам, наш процессор работает на частоте 300 MHz — неплохо так. А ещё из этой функции получаем очень важную информацию — по адресу 0xD0020314 расположен системный таймер. Попытки поискать этот адрес в сети привели к очередному успеху — PDF с детальным описанием другого процессора Marvell:
И вот у нас уже есть какая-никакая, но документация, с помощью которой мы находим то, что искали изначально, а именно функцию отображения (маппинга) адресного пространства ПК в адреса самого контроллера:
Этой функцией контроллер взаимодействует с ПК, к которому он подключен. Достаточно задать нужный адрес в аппаратных регистрах транслятора, и чтение/запись в пределах заданного «окна» по адресу 0x40000000 автоматически приведут к чтению/записи физической памяти ПК!
Теперь осталось найти, как взаимодействовать с прошивкой и подавать команды извне, внедрить в прошивку свой код, который будет лезть в ОЗУ компьютера и … готово?
Ищем отладочные интерфейсы
Надоело смотреть на скриншоты ассемблерного кода? Возвращаемся к железякам! У нас есть набор тестовых контактов, но мы не знаем, где на них какие отладочные интерфейсы (и есть ли они там вообще). Сначала нужно что? Правильно, подпаяться к ним и вывести на гребёнку:
Импровизированное рабочее место инженера
Производитель совсем не заботится о глазах реверс-инженеров, нет бы хоть пятаки сделать!
А дальше — подключаем прибор для поиска JTAG, подрубаем питание и вперед!
PF означает Pin Finder
И ничего не нашлось:
Узнали, какие выводы In, а какие Out…
Ожидаемо, подумал я, и вспомнил про пин «TESTMODE» из даташита. Вероятно, его нужно задействовать и тогда…. Что-ж, паяем ещё проводков, ставим подтяжку на TESTMODE:
И снова ничего не нашлось, правда, картина немного иная, почему-то всё стало Input:
Теперь почему-то все выводы стали Input
Да что ж такое, ну ладно JTAG, хотя бы UART найду, подумал я. Подключаем логический анализатор…
А это уже другая коробочка, хоть и выглядит похоже
И не видим признаков UART ни на одном выводе…
А с поднятым TESTMODE, тест выводы и вовсе колбасит по-черному, и это точно не UART:
Ну, думаю, он просто выключен в прошивке. Нужно его включить! Вношу небольшие изменения и сталкиваюсь с тем, что через клипсу прошивка не хочет записываться. Контроллер питается от программатора и мешает записи. Да что за день-то такой?! Психанул, сделал ПЗУ съёмной:
Но ни одна из моих попыток активировать UART не дала результата. А потом я вспомнил факт, на который сначала не обратил внимания. Прошивка с веб-сайта ARM содержала код, но прошивка, которую я считал программистом, имела только код BIOS. Я думал, что добавления кода ARM для контроллера будет достаточно, чтобы его поднять и запустить. В общем встал вопрос, загружается ли вообще прошивка.
Для этого я подключил логический анализатор прямо к SPI микросхеме:
И проанализировал чтение флешки по SPI:
Здесь уже использую более скоростной анализатор DSLogic
И да, читался только неведомый Autoload (причём трижды) и BIOS! Загрузчик и прошивка и не думали грузиться! Я наконец понял, что мой контроллер имеет только функциональность SATA, а прошивка нужна для RAID, который у меня не поддерживается..
В отчаянии, используя SPI логи, я разреверсил формат Autoload, он оказался очень простой:
Сначала подпись (0xA5A5A5A5), затем пары адрес / значение в конце подписи завершения (0xFFFFFFFF). Подумал, а если эти данные загружаются со встроенного микроконтроллера. После того, как мы испортили стек и адрес возврата из функции, мы перехватим контроллер и перейдем к внешнему загрузчику. И как только процессор переходит на адрес ПЗУ — мы видим в протоколе SPI, он читается!
Я собрал Autoload, в котором во все возможные адреса стека записывался адрес ПЗУ (0xF8064000), запихнул на флешку и… Эта зараза прожевала все 128 КБ и не подавилась, дважды!! (первый раз подавилась, похоже, по таймауту)
Скриншот одной из первых попыток, 128 КБ лог не сохранил
Короче, я сдался. Похоже, нет внутри 88se9215 чипа ARM контроллера, и мне нужен 88se9230 чип. Нет слов. Купил 9230 и … поехали всё заново, что поделать!
Espada FG-EST11B-1
Пайка к ногам без микроскопа)
8 тестпинов + 8 GPIO
В общем, да. На 9230 JTAG нашёлся с пол-пинка и даже без TESTMODE пина:
Кстати, в этом самом TESTMODE неслабо колбасит вообще все пины (как TST, так и GPIO), чип выводит на каждый из них некоторую частоту:
Тест именно физических соединений
Ладно, у нас теперь хотя бы JTAG есть. Продолжаем эпопею!
JTAG
JTAG штука хорошая, с JTAG можно ставить брейкпоинты, читать и писать RAM, в общем, производить полную отладку.
Подключаем JTAG программатор к только что найденным пинам…
Пишем небольшой скрипт для OpenOCD с одним ARM ядром
И подключаемся к контроллеру:
Отладка
Наконец можно проверить, работает ли маппинг памяти! Запускаем UEFI Shell с флешки, читаем адрес 0x100000000:
Теперь по JTAG из контроллера мапим этот же адрес и перезаписываем содержимое:
И проверяем из UEFI Shell:
Получается! Мы действительно можем перезаписать оперативную память ПК с помощью контроллера SATA! Теперь вам нужно включить UART, чтобы позже получать на него команды управления. И наоборот, я обнаружил, что для вывода символа вам нужно записать в UART для регистрации 0xD0072000. Но как я это ни записывал, на анализаторе ничего не происходило.
С помощью отладки по JTAG выяснилось, что до инициализации UART дело не доходит:
Регистр 0xD0071054 имеет значение 0x3F, а для инициализации UART нужно значение 0x2F:
Банальное повторение всего того, что делает процедура serial_init, не помогло, UART не завёлся. Пришлось исследовать более детально. На тот же самый регистр 0xD0071054, по которому код решал, включать UART или нет, ссылалась и другая процедура, i2c_init, только она уже реагировала на значение 1:
Но подождите-ка, на нашей плате пины TST0 и TST1 идут как раз к посадочному месту будто бы под i2c флешку:
А значит, должен быть способ переключить регистр 0xD0071000 (точнее его биты 4-5, которые проверяются в этих функциях) хотя бы в положение «1». И на плате, похоже, для этого предусмотрительно оставлены места под резисторы «на землю» возле некоторых GPIO. А ну-ка подтянем GPIO4 на землю (чтобы в 0x3F обнулить бит №4), не зря же мы все GPIO распаяли:
Читаем снова по JTAG и видим, что изменился регистр, правда не тот, но обнулился нужный бит!
Уже хоть что-то! Попадание совсем рядом. Значит это действительно GPIO. А что если включить контроллер с уже замкнутым GPIO? Может, регистр 0x54 показывает состояние GPIO на момент начального включения? Перезагружаем контроллер:
И да, так и оказалось, эти регистры отвечают за конфигурацию и состояние GPIO. Теперь у нас должен быть инициализирован UART, записываем в 0xD0072000 значение 0x30 и:
Скорость узнали — 115200
Есть UART! RxD на TST0 и TxD на TST1
Более того, подключаем USB-UART адаптер и видим:
Теперь осталось запрограммировать команды и реакцию на них
Расширяем функциональность прошивки
Для начала, напишем довольно простой код, который выдаёт обратно всё, что мы ввели в терминал. В качестве буфера используем верхнюю половину буфера, что прошивка использует под sprintf.
В самом начале кода обязательно добавим прыжок на основную функцию, чтобы потом не заморачиваться с поиском адреса основной функции, а просто вписать адрес, куда код положим. На встроенные системные функции просто будем прыгать по их адресу:
Чтобы скомпилировать код под ARM, нам понадобится GCC. Возьмём его из ARM GNU Toolchain
Поскольку нужно скомпилировать совсем сырой бинарь, добавляем опцию -nostdlib, а после сборки преобразуем полученный файл в binary формат:
arm-none-eabi-gcc.exe echo.c -nostdlib -O2 -o echo.out
arm-none-eabi-objcopy.exe -O binary echo.out echo.bin
К счастью, ARM код (по большей части) использует относительные адреса, поэтому с линковкой по нужному адресу не заморачиваемся. По-хорошему нужно явно указать компилятору, по какому адресу будет расположен код директивой -Wl,—section-start=.text=0x30400000 (последнее — требуемый адрес)
Теперь нужно вставить наш код в прошивку. И снова можно пойти правильным путём — расширить секцию кода, поменять размер в заголовках, добавить код в самый конец файла… А можно поступить проще, найти неиспользуемый участок кода в прошивке и перезаписать его. Помните, мы обнаружили инициализацию I2C, которая теперь точно не нужна?
Нам пока хватит, если что, поищем другое место. Вставляем код по адресу 0x30054EB0 и меняем адрес Idle задачи на этот:
А вот чтобы перешивать контроллер программатором (не делать же снова «внешнее ПЗУ») делаем аппаратную хитрость — «изолируем» питание ПЗУ диодами так, чтобы при подключении клипсы питание не уходило на контроллер:
Проводок нужен, чтобы питание на контроллер поступало в обход ПЗУ
Проверяем и вроде бы работает, после ввода строки и нажатия Enter, в терминал выводится наша введенная строка:
Выводится она без перевода строки и только после нажатия Enter…
Наконец, запрограммируем чтение ОЗУ компьютера! Заодно поправим недостатки, а именно, будем выводить символы, что получили, а ещё избавимся от ассемблерных вставок:
Теперь при вводе в терминал адреса, контроллер считает нам физическую память ПК:
Заключение
Таким образом, цель достигнута, мы модифицируем прошивку и можем считывать физическую память ПК с помощью команд UART т.е. осуществили тонкую настройку SATA контроллера. Конечно, этот пример предельно прост, но при желании можно реализовать абсолютно любой функционал, ведь внутри находится мощный и энергоэффективный ARM-контроллер на 300 МГц. оперативная память компьютера в SSD, подключенном к контроллеру (да, да, там есть функции чтения / записи). Или добавить на карту WiFi-модуль для удаленной отправки команд — хватит фантазии!