Введение
Ночью я становлюсь хакером всевозможной электроники, но днём работаю разработчиком ПО. В основном моя работа заключается в сидении весь день за компьютером, вводе кода, отладке и т.п. Вероятно, вы могли догадаться, что основным устройством для написания всего этого кода является клавиатура.
Разумеется, жизнь большинства клавиатур не вечна. (Хотя я сильно подозреваю, что моя домашняя IBM Model M практически бессмертна.) Однажды я заметил, что клавиша Shift моей рабочей клавиатуры начала отказывать. Из-за этого мои электронные письма начали выглядеть более ленивыми, а в коде воцарился хаос, поэтому мне понадобилась новая клавиатура. Старая клавиатура была вполне неплохой, но в целом довольно стандартным устройством с резиновыми прокладками переключателей, поэтому у меня появилась неплохая возможность взять что-нибудь получше, например, механическую клавиатуру.
Я вышел в Интернет и поискал хорошую клавиатуру. Я хотел что-нибудь с механическими переключателями Cherry Brown, потому что, скорее всего, никого бы не обрадовал громкими Cherry Blue. Довольно полезным было бы отсутствие лишних десяти клавиш (цифровой клавиатуры справа), потому что я всё равно ими не пользуюсь и это уменьшило бы расстояние, на которое нужно перемещаться моей руке к трекболу.
Самой дешёвой клавиатурой, удовлетворяющей всем этим требованиям, оказалось устройство с довольно длинным названием: Coolermaster Quickfire Rapid-I.
Это красивая чёрная игровая клавиатура без цифрового блока и особых заморочек: единственными действительно «игровыми» особенностями были N-key rollover (что означает отсутствие ghosting клавиатуры), увеличенная до потрясающих уровней частота повторения нажатия клавиш (своего рода «турборежим» клавиатуры) и светодиодная подсветка белым LED под каждой из клавиш.
Светодиодная подсветка — на самом деле довольно интересная функция; клавиатура использует её для включения одного из множества режимов свечения: можно или задать фиксированный паттерн из зажжённых LED, или переключать режимы, реагирующие на ввод, например, подсветку каждой нажатой клавиши и с постепенным угасанием.
После приезда клавиатуры я принёс её на работу. На следующий день я показал её своим коллегам. Все они знали, что у меня есть мания нарушения гарантийных условий своих игрушек, поэтому один из них шутливо мне сказал: «Эта клавиатура у тебя уже 24 часа. На ней есть куча светодиодов и клавиши со стрелками. Я разочарован, что ты ещё не запустил на ней „Змейку“».
Сначала я посмеялся, но потом задумался… Ребята из Coolermaster гордо заявляют, что в клавиатуру встроен процессор ARM Cortex… Может быть…
Железо
Итак, если я хочу это сделать, для начала мне нужно взглянуть на «железо». Производитель утверждает, что внутри находится аж целый ARM, но не будет ли это перебором для простой клавиатуры с несколькими LED? Узнать это можно только одним способом.
Сначала взглянем на великолепные переключатели Cherry. При этом я выяснил и почему клавиатура оказалась довольно тяжёлой: белая пластина, на которую установлены переключатели, на самом деле изготовлена из металла.
Это задняя сторона разобранной клавиатуры, здесь видна печатная плата, соединяющая клавиши и светодиоды. Обратите внимание, что у каждой клавиши есть диод, защищающий её от проблем с ghosting-ом.
Это увеличенный снимок области с чипом контроллера. В левом нижнем углу есть разъём, ведущий к micro-USB. Выше него регулятор напряжения, находящийся рядом с основным процессором. Процессор окружают транзисторы, управляющие всеми светодиодами. Также тут есть EEPROM i2c, предположительно, для хранения настроек свечения.
Установленный на плате контроллер называется HT32F1755, он изготовлен тайваньской компанией Holtek. Это ARM Cortex-M3 с частотой 72 МГц, а также щедрыми 127 КБ флэш-памяти и 32 КБ ОЗУ. Довольно странно, что скоростные микропроцессоры сегодня настолько дёшевы, что экономически целесообразно оказывается установить в клавиатуру подобный процессор, который мощнее, чем первые компьютеры с поддержкой USB-клавиатур.
Контроллер обладает приятными дополнительными особенностями. Во-первых, есть привычный порт SWD/JTAG для отладки выполняемого в контроллере кода при помощи всего нескольких проводов и JTAG-отладчика. Во-вторых, если вам нужно только считывать или записывать флэш, то клавиатура имеет записанный в ПЗУ загрузчик. Достаточно понизить напряжение на входе GPIO и выполнить сброс чипа, а затем с помощью программы под Windows можно будет считывать и записывать флэш через USB. Это сработает, даже если содержимое флэш-памяти повреждено.
Кроме того, чип имеет одну-две функции защиты. Наиболее важной для нас является «бит защиты». Он нужен для того, чтобы мешать людям извлекать содержимое флэш-памяти: он отключает флэш, когда к ней получает доступ что-то, кроме программы, запущенной в самой флэш-памяти. В том числе USB-загрузчик и считывание по JTAG.
Но именно то, чему должен был помешать бит защиты, мне и нужно было сделать первым делом: мне требуется содержимое флэша, чтобы понять, как работает клавиатура и заставить её запускать «Змейку». Если бит защиты установлен, то мне не удастся воспользоваться портом SWD для извлечения содержимого. Так установлен ли он? Есть только один способ выяснить это…
Во-первых, мне нужно было найти простой способ добраться до порта JTAG или SWD. К счастью, на печатной плате они доступны на неиспользуемых разъёмах. GPIO, вызывающий загрузчик USB ROM, тоже вынесен аналогичным образом.
Я создал вот такую систему. Красная плата — это моя FT2232 JTAG. Нативно она не может обращаться с SWD, однако существует хак с одним резистором, позволяющий решить эту проблему. Для обмена данными я использовал OpenOCD, и после создания для него работающей конфигурации я попытался сделать дамп флэш-памяти.
Чёрт, похоже, бит защиты на самом деле установлен…
ПО
Ещё один способ извлечения флэша устройства заключается в изучении обновлений ПО. К счастью, Coolermaster активно работает над прошивками, устраняя баги и добавляя новые режимы свечения. Последнюю версию можно скачать с веб-сайта. На самом деле, веб-сайт содержит две версии: похоже, у прошивки есть европейская и американская версии. Это вызвано тем, что «железо» двух версий немного отличается: у европейской версии есть одна дополнительная клавиша.
Программа обновления прошивки представляет собой единый исполняемый файл, в который каким-то образом засунута сама прошивка. Прежде чем взглянуть на неё, мне как-то нужно было отделить её от остальной части исполняемого файла. Я воспользовался тем, что есть две прошивки. Я рассуждал так: графический интерфейс и функции обновления для обеих версий клавиатуры должны быть одинаковыми; вероятно, нет никаких различий в коде для PC, занимающемся обновлением. Это значит, что бо́льшая часть различий или вообще все различия между двумя исполняемыми файлами прошивок должны находиться в самой прошивке, передаваемой в клавиатуру. Чтобы проверить это, я выполнил двоичный diff этих двух файлов, и действительно, все различия были сконцентрированы в блоке на 16 КБ в конце исполняемого файла, удобно отделённом от остальной части файла отбивкой нулями.
Я вырезал эту часть файла и присмотрелся к ней повнимательнее. Она и в самом деле казалась относящейся к прошивке: например, она начиналась с символов «1.1.7», то есть с номера версии скачанного мной обновления прошивки.
Всё остальное не очень походило на то, что должно находиться во флэш-памяти микроконтроллера. Например, это USB-клавиатура, поэтому следует ожидать, что где-то в прошивке есть строки USB-дескрипторов. Строки USB-дескрипторов — это строки UTF-16, сообщающие ОС о названии устройства, производителе и т.п. Однако здесь вообще не было никаких строк UTF-16. Если это и в самом деле прошивка, то она или сжата, или зашифрована.
Чтобы понять, что происходит, я присмотрелся к байтам в шестнадцатеричном редакторе. Вот очень показательная их часть:
Здесь можно заметить два аспекта. Во-первых, повторяющуюся строку «A5 CA 88 A5». Ни один способ сжатия не оставил бы таких повторений; они сжимаются даже тривиальными способами наподобие RLE. Во-вторых, байт «A5» встречается в этой прошивке ужасно часто, и не только в показанной здесь части. Такой частой встречи с этим байтом не ожидаешь. Обычно вместо него бывает байт «00», в основном потому, что он используется как заполнитель, например, когда мы сохраняем 8-битное значение в 32-битную переменную.
То есть, похоже, здесь происходит явление, одним из эффектов которого является замена 00 на A5. Есть только одно широко используемое шифрование, имеющее побайтовую размерность и оказывающее такой эффект, и это XOR-шифрование. Поэтому я попытался обернуть шифрование, выполнив XOR каждого байта 0xA5, и ура — строки с описаниями в UTF-16 появились! (Позже я обнаружил, что наряду с XOR-шифрованием, были поменяны местами несколько первых блоков по 1 КБ.)
Итак, вроде бы теперь у меня есть работающий дамп прошивки? Ну, забросив расшированную прошивку в дизассемблер, я действительно получил приличный объём того, что выглядит как код ARM. Однако это не всё содержимое флэш-памяти: в нём есть переходы, выходящие за пределы имеющегося у меня фрагмента кода. Кроме того, есть большой блок, который кажется повреждённым: код выполняет переход в него, но внутри нет рабочего кода ARM. Хотя в целом всё это полезно, пока у меня нет прочного фундамента для начала разработки «Змейки».
С исполняемым файлом можно проделать и ещё одно действие, а именно исполнить его. При помощи запущенного на фоне USB-сниффера (в данном случае использовался USBPcap) я смог просмотреть пересылаемые пакеты и разобраться, можно ли что-нибудь из них понять. Прошивка моей клавиатуры имела старую версию, так что обновление всё равно пришлось кстати.
Изучив перехваченные пакеты, я выяснил, что обновление прошивки обрабатывает передачу данных к клавиатуре, передавая пакеты к конечной точке 3 и получая их из конечной точки 4 USB. Один из этих пакетов, казалось, переключает клавиатуру в специализированный режим флэш-памяти: у клавиатуры сменяется PID. Пакеты обновления имеют следующий формат:
- Байт 1-2: команда/субкоманда, сообщающая клавиатуре, что делать
- Байт 3-4: CRC16 всего пакета
- Байт 5-8: 32-битный адрес «откуда» для команды
- Байт 9-12: 32-битный адрес «куда» для команды
- Байт 13-63: данные
Как используются эти пакеты? Вот что происходит во время обновления прошивки, плюс мои догадки о значении пакетов:
Команда | Адрес | Данные | |
1:2 | 0x2800-0x2808 | (kb->pc) «1.1.5» | < — Считать версию |
4:1 | 0 | (Смена PID устройства) | < — Перейти в режим флэш |
0:8 | 0x2800-0x2808 | - | < — Удалить версию |
0:8 | 0x2c00-0x6ad4 | - | < — Удалить прошивку |
1:1 | 0x2c00-0x6ad4 | Несколько, загружает прошивку | < — Записать прошивку |
1:0 | 0x2c00-0x6ad4 | Несколько, загружает прошивку | < — Проверить прошивку? |
1:1 | 0x2800-0x2808 | (с PC->в клав.) «1.1.7» | < — Новая версия |
4:0 | 1 | (Смена PID устройства) | < — Выйти из режима флэш |
Здесь происходит кое-что интересное. Чтобы удалить старую и записать новую версию прошивки, обновление прошивки отправляет одинаковые команды, используемые для стирания и записи самой прошивки, однако в другой области памяти: 0x2800 вместо 0x2c00. Из этого я смог заключить, что версия прошивки просто хранится в отдельном секторе флэш-памяти, что упрощает управление ею: прошивке не требуется реализовывать совершенно другой набор команд только для работы с номером версии.
Но постойте, тот же адрес 0x2800 используется для считывания версии прошивки. Если версия прошивки на самом деле не является ничем иным, кроме хранящихся во флэше данных, то было бы логично, что команда считывания версия прошивки является просто общей командой считывания флэш, применимой для всего объёма флэш. Это будет невероятно полезно для меня: запустив команду для всей флэш-памяти, я смогу, по сути, утянуть все находящиеся в ней данные.
При помощи libusb я набросал небольшую программку, выполняющую именно эту задачу. Программа без проблем выполнилась, однако получил я не то, что ожидал: кроме сектора, в котором была версия, при считывании вернулись одни нули. Похоже, моё предположение о том, что считывания версии была обобщённой командой считывания флэш-памяти, была верной; например, если увеличить адрес на единицу, то я получу все данные со сдвигом на один байт. Похоже, что-то ещё мешало мне считать интересующие меня части. Итак, большая часть расшифрованного мной ранее обновления прошивки по-прежнему была повреждена, но, возможно, я найду какую-нибудь информацию в читаемых частях?
После долгого поиска я нашёл нужный мне код. Этот небольшой фрагмент вызывался при каждом считывании байта из флэш-памяти. По сути, он проверяет, считывается ли байт из допустимой области, и если это так, то выполняет переход к передавшему его коду. Если это не так, то последняя строка фрагмента кода заполняет замену нулями. Этот код действительно создаёт наблюдавшийся мной результат. Теоретически, это легко исправить: если удалить последнюю строку и заменить её на NOP, то считываемый байт будет передаваться без изменений.
Для замены кода на NOP требуется изменить ровно один байт, и немного поэкспериментировав с повторной шифровкой и смещениями, я точно узнал, какой байт нужно изменить в исходной прошивке, чтобы она записалась в клавиатуру. Однако изменив этот байт, я создал повреждённое обновление прошивки. Я не смог найти легко обнаруживаемых проверок CRC, которые бы помешали клавиатуре выполнять код, но я мог пропустить проверку, которая заставит клавиатуру отказаться от обновления. А что будет после? Будет ли она по-прежнему принимать новые прошивки, или я получу «мёртвую» клавиатуру? Есть только один способ это выяснить…
После запуска Windows и запуска исполняемого файла ничего особого не произошло: по крайней мере, казалось, что программа обновления прошивки не проверяет себя. Подключение клавиатуры и запуск обновления тоже сработали без запинки. Спустя какое-то время я вздохнул с облегчением: клавиатура снова загорелась после завершения обновления прошивки. Но сохранилась ли в ней новая прошивка? Я снова запустил свою программу и на этот раз дамп данных выглядел гораздо лучше:
Пропустив образ дампа через дизассемблер, я действительно смог убедиться, что он сильно походил на полный дамп флэш-памяти. Никаких странных повреждённых фрагментов, никаких ссылок на непонятные адреса, ровно всё то, что ожидает от расшифрованного дампа прошивки. Похоже, у меня была резервная копия кода клавиатуры! Кроме того, что это позволяло мне теперь выполнить реверс-инжиниринг всего: если при разработке «Змейки» клавиатура превратится в кирпич, то я смогу использовать ПЗУ-загрузчик, чтобы вернуть флэш-память в рабочее состояние. К тому же это значило, что моя система с JTAG/SWD стала намного более удобной: выполняя полное стирание микроконтроллера и восстановление имеющейся у меня резервной копии, я смогу отключить неприятный бит защиты и, например, снова пошагово пройти по коду флэш-памяти.
Протокол обновления
Прежде чем я смогу начать писать «Змейку», нужно решить ещё один вопрос: надо найти удобный способ записи модифицированной прошивки. Хотя я могу использовать загрузчик ROM USB, необходимая для этого программа работает под Windows и мне придётся вскрывать клавиатуру и подключаться к разъёму каждый раз, когда потребуется её использовать. Поэтому вместо этого мне захотелось посмотреть, смогу ли я решить выдающуюся загадку странной повреждённой прошивки, которую я расшифровал ранее. Если я разберусь, как это работает, то смогу создать инструмент Linux для записи прошивки с моим собственным кодом. Это также упростит распространение хака: мне не придётся распространять защищённые авторским правом код или двоичные файлы Coolermaster, достаточно будет просто попросить людей скачать обновление прошивки и расшифровать его в то, с чем можно работать.
Повреждение не было каким-то дополнительным шифрованием, которое было расшифровано исполняемым файлом обновления прошивки: я убедился в этом, сравнив расшифрованную мной прошивку с пакетами в перехваченном потоке USB. Повреждённая прошивка не оказывается на флэш-памяти: сравнение с дампом флэша продемонстрировало, что повреждённая часть отличается. Сравнение этих двух наборов данных привело к определённому паттерну: я заподозрил, что сам контроллер выполняет некое XOR-шифрование и, возможно, изменяет порядок байтов перед записью их во флэш-память.
Разумеется, теперь я бы мог начать исследовать различия, пока бы не разобрался, в чём заключается хитрость, как это было с шифрованием прошивки. Однако на этот раз у меня уже был загруженный в дизассемблер код, так почему бы не проверить там? И в самом деле, я нашёл в дизассемблированном коде любопытную деталь:
По сути, существует счётчик, подсчитывающий количество флэш-пакетов прошивки, переданных в клавиатуру. Если это количество находится в интервале от 10 до 100, то прошивка предполагает, что пакеты были искажены, и вызывает процедуру для их исправления.
Само искажение, опять-таки, оказалось не особо сложным: это просто XOR с 52-байтным ключом, плюс перемена местами байтов в каждом 32-битном слове, зависящее от упомянутого выше счётчика флэш-пакетов. Я быстренько написал на C процедуру шифрования и дешифровки для имитации того, что увидел в прошивке, и добавил их в ранее написанный код. Теперь он также мог расшифровывать обновление прошивки в данные, которые можно записать во флэш, а также брать содержащий их файл и отправлять его в клавиатуру.
Пишем «Змейку»
Закончив с подготовкой, можно было приступать к написанию «Змейки». Сначала мне нужно было найти неиспользуемые фрагменты флэш-памяти и ОЗУ для хранения моего кода и переменных. Это было несложно: у оригинальной прошивки было более 64 КБ свободной флэш-памяти и 28 КБ ОЗУ. Чтобы модифицировать действия клавиатуры, я изменил хранившиеся в ОЗУ переменные оригинальной прошивки; например, значения PWM и нажатые клавиши — это просто массивы в ОЗУ. Благодаря тому, что порт JTAG/SWD снова работал, было несложно и выяснить, какая из переменных в ОЗУ за что отвечает. Мой код вызывается, потому что я добавил в прошивку несколько хуков: например, в оригинальной прошивке есть функция, вызываемая при нажатии клавиши. Если мне нужно это событие, я просто перенаправлял переход в собственную подпроцедуру, которая затем вызывала исходную функцию.
Немного поработав (и поматерившись, потому что одно-два обновления сломали клавиатуру и мне пришлось восстанавливаться из резервной копии прошивки), мне наконец удалось сыграть в великолепную игру «Змейка»:
Другие возможности использования
Я усовершенствовал ещё некоторые аспекты прошивки.
Во-первых, существует проблема защиты, из-за которой клавиатуру можно обновлять только так. Обновление прошивки от Coolermasters имеет красивое окошко, сообщающее пользователю о выполнении обновления, а моя программа может работать на машине с Linux, которая каким-то образом была скомпрометирована и дала злоумышленнику root-доступ. Взломщик может запросто перепрошить клавиатуру, а пользователь этого даже не заметит. При этом он, например, может установить кейлоггер или выполнить другую атаку в стиле BadUSB.
Чтобы противодействовать этому, мой хак прошивки добавляет необходимость физического условия для включения режима флэш-памяти. По сути, вы можете запустить обновление прошивки только примерно в течение 10 секунд после нажатия сочетания клавиш fn+f; если вы этого не сделали (например, потому что процедуру прошивки запустили не вы), то обновление завершается неудачно.
Ещё одна удобная штука — это возможность использования не всех эффектов или игр, которые должны запускаться на клавиатуре: если я выпущу свой хак, то думаю, что кто-то захочеть писать собственные эффекты, не желая при этом разбирать клавиатуру и забираться в загрузчик, чтобы исправлять поломанную флэш-память. Я заметил, что одна из функций USB отключена, поэтому решил воспользоваться ею: в моей прошивке пакеты с определённой командой можно использовать для переопределения задаваемых прошивкой яркостей светодиодов.
Я написал небольшое демо с плавной анимацией, работающее на PC:
Заключение
Хотя стандартная прошивка Rapid-I сама по себе далеко неплоха, с клавиатурой можно проделать гораздо больше, чем придумала Coolermaster. Благодаря этому хаку, кроме игры в «Змейку» любой программист теоретически может придумать эффекты для запуска на клавиатуре. Надеюсь, я даже сделал эту возможность более безопасной: требование физического нажатия сочетания клавиш должно усложнить злонамеренные действия с клавиатурой.
Как обычно, весь написанный мной код выложен в open source: инструмент разборки и обновления прошивки имеет лицензию GPLv3, хак со «Змейкой» и другой выполняемый в самой клавиатуре код имеет лицензию Beer-Ware. По юридическим причинам мне пришлось вычистить весь код и двоичные файлы Coolermaster, но всё это можно восстановить, скачав обновление прошивки версии 1.1.7 и позволив коду разобрать и пересобрать всё это. Хак пока протестирован только на клавиатурах американской версии, потому что у меня есть только такая. Все исходники можно скачать отсюда.
На правах рекламы
Недорогие серверы для любых задач — это про наши эпичные серверы. Максимальная конфигурация — 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe.