[Перевод] Дамп прошивки беспроводной Logitech K360 с помощью GreatFET
Недавно чисто ради спортивного интереса я решил хакнуть клавиатуру, и в качестве первой подопытной выбрал самую дешевую беспроводную модель Logitech K360. Она уже несколько устарела, а ее основная микросхема также, как и используемый протокол беспроводной связи, ранее подробно разбирались. Можете посмотреть презентацию Mousejack Марка Ньюлина, почитать работу Трэвиса Гудспида по сниффингу nRF24, а также ознакомиться с упоминаемой в этих материалах разработкой KeyKeriki.
Я же занялся всем этим больше в качестве тренировки нежели нового исследования и никак не знал, что в итоге обнаружу. Тем не менее мне показалось, что подобная затея станет интересным небольшим примером извлечения из встроенной флэш-памяти голой прошивки.
Разборка и первый осмотр
Реверс-инжиниринг клавиатуры я начинаю с аппаратной части, поэтому первым шагом будет извлечение прошивки. После снятия с передней панели приклеивающейся пластины разборка клавиатуры сложностей не представляет, так как остается лишь выкрутить скрепляющие корпус шурупы. Внутри находится матрица клавиш с резиновым куполом, а также небольшая плата с несколькими компонентами. Из основного на ней распложен только чип nRF24LE1:
Лицевая сторона платы
Обратная сторона платы
Слева от микросхемы расположено шесть контактных площадок для тестирования. Я решил, что это удачный интерфейс и начал с их проверки при помощи логического анализатора и щупов прижимных контактов. TP8 отправлял короткий такт при загрузке, а TP4 являлся землей, но пассивное «прощупывание» не выявляло здесь никакой интересной активности. Тут мне стало ясно, что группа из шести слов справа от микросхемы как раз описывала эти контактные площадки, так как их порядок размещения соответствовал.
Подключение к GreatFET и пробное тестирование
Чтобы прозондировать интерфейс SPI активно я подключил эти площадки к гребенке с помощью эмальпровода, а с ними и контакты TP5 и TP7. Согласно спецификации nRF24LE1, TP7 ведет к контакту PROG, используемому для «активации программирования флэш-памяти», а TP5 к RESET (подписан сразу рядом с TP5). Reset в данном случае активируется сигналом низкого уровня.
Схема распиновки nRF24LE1
Контактные площадки с 10 по 13 я трогать не стал, потому что они идут к общим контактам ввода/вывода и расположены на дорожках, ведущих к матрице клавиш.
Провода SPI
Я подключил площадки SPI к SPI-контактам на GreatFET, GND к одному из контактов земли и привязал контакт ввода/вывода к площадке RESET. При этом я также задействовал на GreatFET контакт 3V3 для прямой подачи питания к микросхеме nRF24 через площадку VMCU.
gf = GreatFET()
reset_pin = gf.gpio.get_pin('J1_P4')
reset_pin.high()
Далее я попробовал отправить простые тестовые команды при помощи встроенного кода SPI, но в таком состоянии в ответ ничего не получил:
In [6]: gf.spi.transmit([0x05], receive_length=1)
Out[6]: b'\xff'
In [7]: gf.spi.transmit([0x03, 0x00, 0x00], receive_length=4)
Out[7]: b'\xff\xff\xff\xff'
Тогда я вновь обратился к спецификации и выяснил, что шина SPI для программирования флэш-памяти, активируемая контактом PROG, имеет собственный набор выделенных контактов. Эти контакты не совпадали с площадками для тестирования, к которым я подпаялся, поэтому было решено проверить, не ведут ли они к другим площадкам.
В этом мне помог мультиметр, но вы также можете проследить дорожки на фото самой платы. К счастью, оказалось, что к этим контактам (P1.2, P1.5, P1.6 и P2.0) ведут те самые оставленные мной изначально TP10-TP13. Так как потребовалось задействовать всего четыре новых площадки, я не стал паять, а снова использовал щупы PCBite.
Проверка интерфейса флэш-программирования
Для активации SPI нужно удерживать контакт PROG на высоком уровне и сбросить устройство подачей на RESET сигнала низкого уровня. Я настроил отдельный контакт ввода/вывода GreatFET для PROG, а затем добавил в скрипт функцию для отправки на RESET сигнала низкого уровня.
#!/usr/bin/env python3
import hexdump
import time
from greatfet import GreatFET
def reset(gf, reset_pin):
reset_pin.low()
time.sleep(0.001)
reset_pin.high()
time.sleep(0.001)
def main():
gf = GreatFET()
reset_pin = gf.gpio.get_pin('J1_P4')
prog_pin = gf.gpio.get_pin('J1_P6')
# Reset is active low
reset_pin.high()
# Enter prog mode
prog_pin.high()
time.sleep(0.01)
reset(gf, reset_pin)
# ...
if __name__ == '__main__':
main()
После сброса устройства я попробовал отправить одну из SPI-команд для получения статуса флэш-памяти и регистров состояния ее защиты, а также выполнить тестовое считывание.
fsr = ord(gf.spi.transmit([0x05], receive_length=1))
fpcr = ord(gf.spi.transmit([0x89], receive_length=1))
print(f'flash status register: {fsr:#02x}')
print(f'flash protect register: {fpcr:#02x}')
# test read
print('test read:')
data = gf.spi.transmit([0x03, 0x00, 0x00], receive_length=256)
hexdump.hexdump(data)
Но и теперь я по-прежнему не получал никакого ответа (лишь все ту же последовательность байтов
0xFF
). В итоге, вооружившись осциллографом, я взялся проверять работоспособность всех контактов. С помощью простого скрипта для GreatFET я продолжал сбрасывать устройство и проверял осциллографом контакт RESET, возвращающийся на высокий уровень, чтобы увидеть состояние сигнала Reset на щупе после загрузки.Помимо проверки контактов RESET и PROG я сделал простой замер потребления питания с помощью шунтирующего резистора, чтобы понять, выполняет ли устройство вообще сброс. Обратите внимание на разницу в энергопотреблении, когда устройство успешно входит в режим PROG:
Сброс при низком уровне PROG
Сброс при высоком уровне PROG
Один из щупов SPI, возможно, был отключен, но когда осциллограф показал, что все в порядке, я повторил процесс и на этот раз смог считать из памяти уже вразумительные данные:
$ ./test.py
flash status register: 0x80
flash protect register: 0x0
test read:
00000000: 80 A3 A3 02 00 03 78 FF E4 F6 D8 FD 90 00 00 7F ......x.........
00000010: 00 7E 04 E4 F0 A3 DF FC DE FA 75 81 7E 02 07 82 .~........u.~...
00000020: FC 00 FF D9 00 11 01 FF E0 FF E0 00 01 02 FF E1 ................
00000030: FF E1 00 01 02 FF E2 FF E6 00 01 02 FF E7 FF EB ................
00000040: 00 01 02 FF EC FF EF 00 04 02 FF F0 FF FF 00 01 ................
00000050: 00 57 69 72 65 6C 65 73 73 20 4B 65 79 62 6F 61 .Wireless Keyboa
00000060: 72 64 20 00 34 D9 1D F0 40 01 00 00 00 61 02 20 rd .4...@....a.
...
Обратите внимание на строку с
Wireless keyboard
. Затем я выполнил одно считывание 18432 байтов — допустимый максимум, если верить спецификации. На выводе получился толковый дамп флэш-памяти программы. В nRF24LE1 используется набор инструкций от 8051, поэтому для подтверждения, что это код, я загрузил его в Ghidra в виде BLOB-объекта 8051. В начале объекта находится подпрограмма инициализации, значит это действительно код.
Тестовое дизассемблирование в Ghidra
Чтобы проверить настройки активации аппаратной отладки и защиты от эхосчитывания флэш-памяти я также решил считать упомянутую в спецификации InfoPage:
Раздел InfoPage спецификации nRF24LE1
Для считывания InfoPage необходимо установить бит INFEN
в регистре состояния флэш-памяти, для чего достаточно было отправить перед считыванием команду write flash status register
:
def read_fsr(gf):
fsr = gf.spi.transmit([0x05], receive_length=1)
return ord(fsr)
def write_fsr(gf, fsr):
fsr &= 0xff
gf.spi.transmit([0x01, fsr])
def read_flash(gf, address, count):
command = struct.pack('>BH', 0x03, address)
data = gf.spi.transmit(command, receive_length=count)
return data
def get_infoblock(gf):
flash_stat_reg = read_fsr(gf)
# INFEN is bit 3 (2^3)
write_fsr(gf, flash_stat_reg | 8)
time.sleep(0.001)
infoblock = read_flash(gf, 0, 512)
# Unset INFEN bit
write_fsr(gf, flash_stat_reg & (~8 & 0xff))
return infoblock
Теперь я могу считывать InfoPage, чтобы проверять установку аппаратной отладки и защиты от эхосчитывания. Возвращение программных данных в ходе изначальных попыток чтения уже указывало на то, что защита от эхосчитывания отключена, но выполнение описанных шагов помогло мне убедиться в правильности использования интерфейса программирования. Думаю, что также будет нелишним сделать бэкап InfoPage на случай, если позднее я сотру флэш-память.
При этом в код достаточно внести всего несколько простых изменений и его можно будет использовать для дампирования памяти программы и/или InfoPage в файлы:
$ ./k360_spi.py --dump flashdump.bin
flash status register: 0x80
flash protect register: 0x0
InfoBlock content:
00000000: 00 A3 A3 48 31 57 54 79 70 14 0A 12 FF FF 98 04 ...H1WTyp.......
00000010: 79 7C 88 23 B1 50 0F 05 FF FF FF FF 82 79 FF FF y|.#.P.......y..
00000020: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................
00000030: FF FF FF 4C 45 31 4F FF FF FF FF FF FF FF FF FF ...LE1O.........
...
000001F0: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................
Flash readback protection: False (ff)
HW debug enabled: False (ff)
wrote flash dump to flashdump.bin
Весь исходный код для
k360_spi.py
находится здесь: https://gist.github.com/jamchamb/b2892a22ac0760346d4d617fedf9b541. Следующим шагом будет анализ прошивки.