Hardware Hacking: Исключительный провал – Взлом защиты от чтения STM32F1

Перевод подготовлен при поддержке Inside chat в честь дня рождения @N3M351DA 2020

Прошивка микроконтроллеров зачастую содержит ценные данные, такие как интеллектуальная собственность, а иногда и ключи шифрования (криптографические материалы). Для их защиты в большинстве микроконтроллеров используют механизмы защиты от чтения. Задача такого механизма – ограничить чтение данных из внутренней флэш памяти злоумышленником, обладающим физическим доступом к устройству. Однако, как исследователи безопасности, так и любители, регулярно показывают, что эти механизмы можно обойти. В данном исследовании мы рассмотрим защиту от чтения флеш памяти в семействе микроконтроллеров STM32F1 STMicroelectronics. Мы обсудим новую уязвимость, эксплуатация которой является первым неинвазивным способом обхода защиты. Причиной этой уязвимости является недостаточное ограничение доступа: чтение данных из флэш-памяти через отладочный интерфейс блокируется, но система прерываний ЦПУ имеет доступ к этим данным по шине ICode. Мы подробно рассмотрим почему и как эта уязвимость раскрывает значительную часть внутренней памяти, тем самым компрометируя устройство.

[Видео]

Введение

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

Отключение отладочного интерфейса является частым способом противодействия злоумышленникам, однако его реализация отличается у разных микроконтроллеров. К примеру в семействе STM32F0 этот интерфейс можно полностью отключить. В то же время, семейство STM32F1 такой возможностью не располагает, предлагая несколько альтернатив. Одной из них является защита флэш-памяти от чтения (read-out protection, RDP). Этот механизм блокирует доступ к любым данным во флэш-памяти через отладочный интерфейс. Это значит, что атакующий может подключить отладчик к микроконтроллеру, но прочитать из него данные он не сможет. Однако исследования показали, что у некоторых микроконтроллеров этот механизм имеет изъяны. Например, в своем исследовании "Shedding too much Light on a Microcontroller's Firmware Protection”, Йоханнес Обермайер и Штефан Тачнер описали атаку на семейство STM32F0. Некоторые исследователи предположили, что эта уязвимость может также затрагивать другие семейства, такие как STM32F1. Однако один из авторов это опроверг. До сего момента механизм защиты семейства STM32F1 считался безопасным, поскольку не было свидетельств, что его можно обойти. В этой статье мы рассматриваем уязвимость (CVE-2020-8004), которая приводит к первой неинвазивной атаке на систему защиты флэш-памяти STM32F1.

Обнаружение уязвимости

Семейство STM32F1 не предоставляет механизма перманентного отключения отладочного интерфейса. По этой причине атакующий с физическим доступом всегда имеет возможность получить контроль над ним. Однако встроенный механизм защиты от чтения пресекает любые запросы к данным при подключенном отладчике. Для того, чтобы изучить систему защиты мы использовали отладочную плату STM32 Nucleo-64 с установленным на ней микроконтроллером STM32F103RB. Защита от чтения на нём была включена и запрещала доступ к данным флэш-памяти по отладочному интерфейсу. Через интерфейс SWD этот микроконтроллер был подключён к отладчику SEGGER J-Link, как изображено на рисунке 1.

Рисунок 1. Отладочная плата STM32 Nucleo-64 и подключенный к ней отладчик SEGGER J-Link

Мы начали наше исследовать с установки соединения с целевым микроконтроллером. Для этого мы запустили OpenOCD следующей командой:

openocd -f interface/jlink.cfg -c "transport select swd" -f target/stm32f1x.cfg

Затем мы открыли Telnet-сессию через OpenOCD, что дало контроль над микроконтроллером. Наконец, мы перезагрузили микроконтроллер командой перезагрузки с остановом, получив такой вывод:

target halted due to debug-request, current mode: Thread

xPSR: 0x01000000 pc: 0x08000268 msp: 0x20005000

На первый взгляд в нём нет ничего особенного. Однако, если внимательно прочитать вторую строчку, одно из значений должно вас заинтересовать: счетчик инструкций (program counter, PC) имеет значение 0x08000268, что является корректным адресом, расположенным во флэш-памяти. Это очень важно, поскольку перезагрузка является особым видом исключения. При обработки каждого исключения, в процессор загружается соответствующий адрес обрабатывающей функции из таблицы векторов в PC. Эта процедура иногда называется запросом вектора (vector fetch). После перезагрузки устройства, таблица векторов располагается во флэш-памяти. Это наблюдение показывает, что процессор получил вектор обработчика перезагрузки из флэш-памяти при условии того, что защита от считывания включена. Почему же процесс обработки исключений смог прочитать адрес? Руководство к STM32F1 даёт подсказку:

ЦПУ Cortex®-M3 всегда получает значение вектора перезагрузки по шине ICode, что подразумевает, что загрузчик должен располагаться только в области кода (обычно, флэш-памяти).

Запрос вектора перезагрузки происходит через шину ICode и таким образом обрабатывается как запрос инструкции, который разрешен несмотря на включенную защиту. По всей видимости, она следит только за обращениями к данным, что позволяет беспрепятственно запрашивать вектор перезагрузки по ICode. Справочное руководство на Cortex-M3 предоставляет дополнительную информацию по запросам векторов:

Запрос вектора производится либо по системной шине, либо по шине ICode в зависимости от местонахождения таблицы векторов [...]

В конечном счёте, защита от чтения семейства STM32F1 не блокирует доступ к памяти по шине ICode. При возникновении исключения, соответствующий адрес, расположенный во внутренней памяти запрашивается по ICode, тем самым раскрывая содержимое памяти через PC.

Эксплуатация

Мы заметили, что при возникновении исключения запрос вектора раскрывает защищенные данные через PC. Далее мы рассмотрим, как такое поведение можно эксплуатировать для обхода системы защиты.

Мы уже упоминали таблицу векторов: она содержит инициализирующее значение главного указателя на стек (main stack pointer, MSP), за которым следуют указатели на обработчики всех исключений. Её содержимое, определённое архитектурой ARMv7-M, показано в Таблице 1. В ней содержатся исключения и их смещения относительно начала таблицы. Первые 16 исключений являются обязательными и определены архитектурой ARMv7-M. Остальные, так называемые "внешние прерывания", опциональны и зависят от устройства.

Номер прерывания

Прерывание

Смещение

-

Инициализирующее значение MSP

0x00

1

Reset

0x04

2

NMI

0x08

3

HardFault

0x0c

4

MemManage

0x10

5

BusFault

0x14

6

UsageFault

0x18

7-10

Reserved

0x1c

11

SVCall

0x20

12

DebugMonitor

0x24

13

Reserved

0x28

14

PendSV

0x2c

15

SysTick

0x30

16

Внешнее прерывание 1

0x34

Таблица 1. Таблица векторов для микроконтроллеров архитектуры ARMv7-M

Главная идея эксплуатации заключается в намеренной генерации исключений, при этом соответствующая ячейка таблицы будет запрашиваться из флэш-памяти в регистр PC. Однако в Таблице 1 видно, что некоторые элементы зарезервированы и не имеют привязки к исключению. В частности это элементы с 7 по 10-й, а также 13-й. К тому же инициализирующее значение MSP также не связано с исключением. В связи с этим рассматриваемый метод не позволяет извлечь данные по этим смещениям. Мы ненадолго отвлечемся от этой проблемы, но вернёмся к ней позже.

Смещение таблицы векторов

На данном этапе может возникнуть вопрос: зачем мне беспокоиться о безопасности содержимого таблицы векторов?

Действительно, её содержимое обычно не является конфиденциальным, поскольку содержит только указатели на обработчики прерываний. Однако архитектура ARMv7-M имеет регистр смещения таблицы векторов (Vector Table Offset Register, VTOR). Он определяет расположение таблицы в адресном пространстве. Обычно его используют, когда на микроконтроллере должно работать несколько приложений, например, загрузчик (bootloader) и основное приложение. С его помощью каждое из приложений может иметь свою собственную таблицу прерываний и свои обработчики. Ключевым моментом является то, что отладочный интерфейс предоставляет доступ ко всему устройству за исключением флэш-памяти. При помощи VTOR мы можем перемещать таблицу по всей внутренней памяти, и таким образом получать доступ к большей части данных.

Для того, чтобы изменить расположение таблицы векторов, мы разделим всё адресное пространство флэш-памяти на одинаковые блоки по 32 слова каждый, как изображено на Рисунке 2. Размер блока определяется размером самой таблицы и согласно спецификации ARMv7-M должен быть степенью 2. К примеру, STM32F103 имеет 59 исключений. Таким образом его таблица векторов имеет размер 64. Тем не менее, это значит, что у нас не хватает исключений, чтобы получить доступ ко всем элементам таблицы. Поэтому мы будем использовать самую большую таблицу размером менее 59, то есть 32.

Рисунок 2. Перемещение таблицы векторов во флэш-памяти. Подсвечены недоступные элементы.

Семь подсвеченных ячеек на Рисунке 2 невозможно извлечь предлагаемым способом. Первые две - инициализирующее значение MSP и вектор перезагрузки. Остальные соответствуют зарезервированным областям. Инициализирующее значение MSP и вектор перезагрузки могут быть извлечены только когда сама таблица расположена по умолчанию в начале флэш-памяти. Причиной этому является то, что при перезагрузке нужно получить эти значения, но при этом перезагрузка сбрасывает значение VTOR, возвращая таблицу векторов на стандартное место. Эти ограничения можно обойти, используя исключения, которые превосходят размеры таблицы. В нашем случае те, что больше 31. Согласно разделу 4.4.4 инструкции по программированию STM32F1 (STM32F1 Programming Manual), адрес расположения таблицы векторов должен быть выровнен в соответствии с размером таблицы (64 в случае STM32F103). Но что происходит, когда таблица не выровнена, а мы генерируем исключения, номера которых выходят за границы таблицы? Мы выяснили, что эти исключения возвращаются в начало таблицы. На Рисунке 3 изображен этот возврат значений на не выровненной таблице, содержащей 32 элемента. С левой стороны - обычная таблица с подсвеченными не доступными элементами. Справа - положение позволяющее получить к ним доступ. Теперь к подсвеченным ячейкам можно получить доступ через прерывания, расположенные за пределами таблицы.

Рисунок 3. Перемещение значений в не выровненной таблице векторов. Недоступные ячейки (красные) становятся доступными (синие) при генерации исключения, выходящего за пределы таблицы.

Обратите внимание на недоступные части таблицы векторов на Рисунке 3. Остальные элементы также могут быть извлечены с помощью подобного "оборота", хотя в этом нет необходимости, потому что они доступны обычным способом. С помощью "оборота" мы получили доступ к недоступным ранее элементам. Это касается и первых двух элементов, доступных только когда таблица расположена в начале флэш-памяти. Единственным ограничением осталось лишь то, что этот подход можно использовать только для не выровненных таблиц векторов. Так или иначе, мы сократили количество недоступных элементов вдвое. Заметьте, что этот способ является лишь одним вариантом использования внешних прерываний. Мы использовали его, потому что его просто реализовать.

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

Генерация исключений

Чтобы генерировать исключения мы пойдём следующим путём. Каждое исключение потребует трёх шагов:

  1. Произвести перезагрузку устройства, чтобы микроконтроллер находился в определенном, заранее известном, состоянии и чтобы восстановить его в случае сбоя или зависания.

  2. Добиться того, чтобы следующим шагом вызывалась обработка требуемого исключения.

  3. Выполнить шаг, чтобы запустить обработчик.

Таких прерываний, как Non-Maskable Interrupt (NMI), PendSV и SysTick несложно добиться установкой битов NMIPENDSET, PENDSVSET и PENDSTSET соответственно. Эти биты находятся в регистре состояния и управления прерываниями (Interrupt Control and State Register, ICSR). То же самое относится к исключению DebugMonitor, который можно активировать установкой бита MON_PEND в регистре управления и мониторинга исключения отладки (Debug Exception and Monitor Control Register, DEMCR).

К примеру, следующая команда OpenOCD вызовет прерывание PendSV:

mww 0xe000ed04 0x10000000

Поскольку процессор остановлен и находится в режиме отладки, мы должны дать ему шанс вызвать исключение. Для этого мы делаем отладчиком один шаг, выполняя одну инструкцию. Для того, чтобы избежать побочных эффектов от неё, мы используем инструкцию nop, расположенную в SRAM по адресу 0x20000000.

mwh 0x20000000 0xbf00

reg pc 0x20000000

Необходимо отключить маскировку прерываний. Она включена по умолчанию. Чтобы её выключить, используйте следующую команду:

cortex_m maskisr off

Эта команда меняет поведение при пошаговой отладке и управляет битом C_MASKINTS в регистре статуса и управления остановкой отладки (Debug Halting Control and Status Register, DHCSR). Он определяет, будут ли PendSV, SysTick и прочие внешние прерывания маскироваться. По умолчанию команда maskisr имеет значение auto. В этом режиме при шаге отладчиком сначала отрабатываются ожидающие обработки прерывания, а затем производится шаг на следующую инструкцию.

Заметка

Команда maskisr недоступна на так называемых высокоуровневых адаптерах (high-level adapters, HLA) OpenOCD. По этой причине мы не используем в качестве отладчика программатор ST-LINK, находящийся на плате Nucleo-64.

При генерации исключения можно заметить, что адрес в таблице векторов и содержимое PC отличаются. Младший значащий бит (LSB) может не совпадать. Причиной тому является то, что LSB этого адреса не загружается в PC, а используется как индикатор режима Thumb. Поскольку состояние режима Thumb также содержится в регистре состояния выполнения программы (Execution Program State Register, EPSR), мы можем восстановить адрес целиком, комбинируя PC и бит режима Thumb.

Теперь у нас есть вся информация, требующаяся для извлечения элементов таблицы векторов. Далее мы расскажем, как генерировать остальные исключения. Для простоты с этого моменты мы будем опускать первый и последний шаги процесса генерации.

BusFault

Исключение BusFault происходит, когда инструкция обращается к некорректному региону памяти. Таким образом, мы можем вызвать его, выполнив инструкцию загрузки, которая считает из некорректного региона памяти.

Для этого мы запишем инструкцию ldr r0, [r1, #0] в SRAM по адресу 0x20000000, а регистрам r1 и PC присвоим такие значения:

mwh 0x20000000 0x0868

reg r1 0xf0000000

reg pc 0x20000000

Адрес 0xf0000000 хранящийся в r1 - это часть памяти, зависящая от изготовителя. В случае STM32F1, этот адрес не отображается на память, поэтому каждая операция по нему некорректна и подходит для генерации исключения BusFault. Все остальные адреса, не имеющие отображения в памяти также будут работать.

Прежде, чем мы сможем сгенерировать BusFault, его необходимо включить, установив бит BUSFAULTENA в регистре статуса и контроля системного обработчика (System Handler Control and State Register, SHCSR):

mww 0xe000ed24 0x20000

Этот шаг необходим. В противном случае будет произведено повышение привилегий и процессор сгенерирует прерывание HardFault вместо BusFault.

MemManage

Это исключение генерируется при возникновении любой ошибки доступа к памяти. Также, оно происходит, когда процессор пытается выполнить код из региона памяти, отмеченного как XN. В нашем случае мы выбрали первый адрес в системной памяти, то есть 0xe0000000.

reg pc 0xe0000000

Как и BusFault, исключение MemManage должно быть включено. Это делается установкой бита MEMFAULTENA в регистре SHCSR:

mww 0xe000ed24 0x10000

UsageFault

Исключения этого типа возникают по разным причинам. Одной из них является исполнение несуществующей инструкции, другой - чтение или запись по невыровненному адресу.

Однако простейшим способом будет исполнение неопределённой инструкции, например 0xffff. Мы поместим её в SRAM по адресу 0x20000000 и установим соответствующее значение PC:

mwh 0x20000000 0xffff

reg pc 0x20000000

Как и другие исключения UsageFault должен быть включен. Для этого установите бит USGFAULTENA в регистре SHCSR:

mww 0xe000ed24 0x40000

HardFault

HardFault это общее исключение. Оно генерируется, когда ошибка не может быть обработана другими исключениями.

Существуют разные способы вызвать его вручную. Простейшим будет вызов любого другого исключения из описанных выше, не включая их. Как уже сказано выше, это вызывает повышение привилегий, после которого генерируется HardFault.

SVCall

Вызов супервизора (Supervisor Call) используется программой для вызова операционной системы. Оно генерируется при исполнении процессором инструкции svc.

Мы поместим svc #0 в SRAM и сделаем шаг отладчиком.

mwh 0x20000000 0xdf00

reg pc 0x20000000

В связи с тем, что нас интересует только генерация исключения SVCall, значение параметра #imm не играет роли и может быть произвольным. В отличие от двух предыдущих исключений, SVCall постоянно включено.

Внешние прерывания

Все внешние прерывания обрабатывает контроллер вложенных векторных прерываний (Nested Vectored Interrupt Controller, NVIC). Для этого он использует два регистра: один для включения внешних прерываний и второй для флагов требования обработки каждого из них. Для того, чтобы сгенерировать одно из прерываний, мы должны установить соответствующие биты в обоих регистрах, тем самым включая прерывание и запрашивая его обработку. Каждый бит соответствует одному внешнему прерыванию.

В случае семейства STM32F1, первым внешним прерыванием является прерывание по сторожевому таймеру (window watchdog, WWDG). Чтобы его сгенерировать, установим первый бит в обоих регистрах:

mww 0xe000e100 0x1

mww 0xe000e200 0x1

Остальные внешние прерывания генерируются аналогично.

Количество этих прерываний сильно отличается в семействе STM32F1 и зависит от микроконтроллера. К тому же, документация не всегда бывает не точна. К примеру, согласно справочному руководству (reference manual) к STM32F1 внешние прерывания с номерами с 43-го до 49-го в устройствах из линейки с дополнительными интерфейсами связи (connectivity line) зарезервированы. Тем не менее мы обнаружили, что их можно генерировать точно так же, как и остальные. Важность наличия внешних прерываний будет показана в следующей части.

Производительность

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

Для оценки серьезности уязвимости был разработан скрипт на языке Python, который самостоятельно генерирует исключения и извлекает содержимое памяти из микроконтроллера, защищённого от чтения. Мы испытали описанную атаку на трёх различных устройствах семейства STM32F1. Во всех случаях нашей задачей было извлечение 128 Кб флэш-памяти. Результаты испытаний приведены в Таблице 2.

Устройство

Внешних прерываний

Время на извлечение

Покрытие

STM32F100

55

48.8 мин

91.4%

STM32F103

43

48.2 мин

89.1%

STM32F107

68

51.0 мин

94.5%

Таблица 2. Время, потраченное на извлечение, и объем извлеченной памяти для трех устройств STM32F1

Результаты показывают, что объем извлеченной памяти зависит от количества внешних прерываний. Больше всего данных удалось получить из STM32F107, который имеет 68 внешних прерываний. По таблице также видно, что время для извлечения слегка возрастает при увеличении количества извлекаемых данных.

Использовался отладчик SEGGER J-Link, на котором для повышения производительности была установлена скоростью 3500 kHz. Заметьте, что скорость отладчика - это важный фактор, влияющий на время извлечения. Помимо этого, на него могут влиять и другие вещи, например ваш компьютер.

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

Заключение

В этой статье мы рассказали об уязвимости в системе защиты флэш-памяти от чтения в семействе микроконтроллеров STM32F1. Было показано, что для её эксплуатации достаточно неинвазивного доступа к отладочному интерфейсу микроконтроллера. В связи с этим данная атака явно подходит под типичную модель атак на системы защиты флэш-памяти от чтения.

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

Доступность

Чтобы развить обсуждение и сделать результаты нашего исследования понятными и воспроизводимыми, мы публикуем исходный код, разработанный в рамках исследования. Он лицензирован GPLv3+ и находится по адресу https://gitlab.zapb.de/zapb/stm32f1-firmware-extractor.

Координированное раскрытие

Публикации наших исследований предшествовал скоординированный процесс раскрытия уязвимости. Мы проинформировали STMicroelectronics более чем за 100 дней до публикации этой статьи.

28 ноября 2019:

Технические детали уязвимости отправлены STMicroelectronics.

8 декабря 2019:

Пока нет никакого ответа от STMicroelectronics. Отправлено напоминание с техническими деталями.

23 декабря 2019:

Всё еще никакой реакции со стороны STMicroelectronics. Отправлено напоминание без технических деталей.

6 января 2020:

CERT Bund отправил напоминание. Получен немедленный ответ от STMicroelectronics.

10 января 2020:

Групповой звонок с STMicroelectronics и CERT Bund.

С 15 января по 7 февраля 2020:

Дальнейшие обсуждения между STMicroelectronics и CERT Bund.

1 февраля 2020:

Публичный анонс уязвимости без технических деталей.

17 марта 2020:

Публикация этой статьи и сопутствующих материалов.

Об авторах

Марк Шинк (mail(a)marcschink.de) и Йоханнес Обермайер (mail(a)obermaier-johannes.de) - исследователи безопасности встраиваемых систем, имеющие опыт в информационных технологиях и электронике. Они фокусируются на микроконтроллерах, поскольку встречаются с ними каждый день. Своими исследованиями и публикациями они пытаются улучшать безопасность с помощью открытого обсуждения уязвимостей и желания добиться улучшений систем безопасности.

P.S. Согласно комментариям на Hackaday, исходный код POC работал не стабильно. Прикрепляю еще один его вариант: https://github.com/doegox/stm32f1-firmware-extractor