+7 (909) 417-77-77 | order@ohm-electronics.ru
Сохранение прошивки из nRF51

Сохранение прошивки из nRF51

Сохранение прошивки из nRF51

Методика сохранения прошивки из ARM Cortex-M0.

В процессе получения копии электронного устройства одной из первых больших целей является копирование прошивок из микросхем этого устройства. Получив доступ к прошивке можно приступать к копирования топологии печатной платы. Иногда файл прошивки можно получить из внешних источников, например, загрузив обновление для устройства. Гораздо чаще прошивку можно только скопировать из уже прошитого чипа. Если у микроконтроллера есть интерфейс программирования или отладки, можно попытаться использовать его для скачивания прошивки. Проблема в том, что большинство современных чипов оснащены системой защиты внутреннего кода, после активации защиты считывание прошивки стандартными методами становится невозможным. В таких ситуация может потребоваться декапирование чипа или глитч-атака по шине питания или тактового сигнала. В этой статье речь пойдёт о технологии скачивания прошивки из конкретной модели SoC с поддержкой Bluetooth. Мы добьемся сохранения прошивки с включенной защитой используя только отладочный интерфейс чипа.

Система на кристалле

Рассмотрим систему на кристалле nRF51822 производства Nordic Semiconductor. Это весьма популярный чип с поддержкой Bluetooth и ядром ARM Cortex-M0 CPU. Система безопасности чипа, предотвращающая чтение содержимого памяти устройства может различаться в конкретных реализациях чипа даже среди модельного ряда одного производителя или одной архитектуры. Механизм защиты nRF51822 позволяет разработчику запретить считывание через отладочный интерфейс либо всей памяти (флэш и RAM), либо конкретных секций памяти. Кроме того, некоторые чипы позволяют полностью отключить отладочный интерфейс. В данном случае, nRF51822 не предоставляет такой возможности; разработчику доступно только запретить доступ к памяти через интерфейс отладки. Чип nRF51822 использует интерфейс отладки SWD (serial wire debug) - двухпроводной интерфейс доступных на многих чипах архитектуры ARM. Этот интерфейс сходен с физическим интерфейсом JTAG, предоставляющим возможности для отладки программного кода и встроенной периферии чипа, доступного на некоторых устройствах с ARM-ядром. Обладая иной физической реализацией, SWD, в общем, реализует те же возможности, что и JTAG. Для работы с чипом через интерфейс SWD можно использовать утилиту OpenOCD. Для скачивания прошивки необходимо подключиться к выводам микросхемы, называющимся SWDIO и SWDCLK при помощи специального SWD-адаптера. OpenOCD поддерживает много адаптеров, из которых многие поддерживают интерфейс SWD. Мы воспользуемся отладочной платой с чипом nRF51822 и встроеным адаптером.

Изучение отладочного интерфейса

После подключения OpenOCD к целевому устройству, можно выполнить команды отладчика, записать или прочитать регистры процессора. Тем не менее, доступ на чтение из флеш-памяти закрыт. В данном примере мы подключимся к целевому устройству с помощью OpenOCD и попытаемся прочитать содержимое памяти. Перезагрузим процессор затем прочитаем адрес 0x00000000 и адрес 0x000114cc на который указывает регистр pc; в обоих случаях отладчик вернёт только нули. Разумеется, нам известно что в этих адресах расположен код, но механизм защиты не позволяет его прочитать:

> reset halt
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114cc msp: 0x20001bd0
> mdw 0x00000000
0x00000000: 00000000
> mdw 0x000114cc 10
0x000114cc: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0x000114ec: 00000000 00000000

Несмотря на это у нас сохраняется возможность чтения и записи регистров CPU, включая счетчик команд (pc) и мы может пошагово исполнять код, мы только не знаем какие инструкции мы исполняем.

> reg r0 0x12345678
r0 (/32): 0x12345678
> step
target state: halted
target halted due to single-step, current mode: Thread 
xPSR: 0xc1000000 pc: 0x000114ce msp: 0x20001bd0
> reg pc 0x00011500
pc (/32): 0x00011500
> step
target state: halted
target halted due to single-step, current mode: Thread 
xPSR: 0xc1000000 pc: 0x00011502 msp: 0x20001bd0

Также мы можем прочитать некоторые регистры настроек, имеющие адреса в памяти. Например, прочитаем регистр “RBPCONF” (регистр настройки защиты от записи) из раздела “UICR” (User Information Configuration Registers).

> mdw 0x10001004
0x10001004: ffff00ff

Согласно руководству к чипу, содержимое 0xffff00ff в регистре RBPCONF обозначает “Protect all” (PALL) - защиту от чтения всего содержимого памяти. Биты с 15 по 8, обозначенные B в этой таблице установлены в 0 и защита сектора 0 отключена (биты 7..0 установлены в 1):

Эти настройки защищают содержимое памяти от считывания. Поскольку защита включена, отладчик всегда возвращает нули при попытке чтения памяти. Другая защитная опция PR0 в данном случае отключена. Стоит, однако, отметить, что метод, описанный в этой статье, позволяет также обойти и её. При включении этой опции, чтение из адресов расположенных ниже определённого адреса. Учтите, что флеш-память (и, следовательно, данные, которые мы хотим получить) расположены в адресном пространстве ниже, чем оперативная память. Также настройка PR0 запрещает чтение данных, находящихся в защищенном регионе памяти, любым кодом из незащищенного региона. К сожаление, сброс регистра RBPCONF возможен только при полном стирании чипа в случае чего также теряются все данные в памяти. Хорошая новость в том, что был обнаружен способ обойти механизм защиты от чтения используя отладчик.

Разработка технологии обхода защиты

Первоначальный план выкачивания прошивки через отладочный интерфейс может состоять в исполнении кода, копирующего содержимое флеш-памяти в буфер в оперативной памяти. Но этот план не сработает, если включен хотя бы один из флагов PALL или PR0. Если включен флаг PALL, то блокируется доступ ко всей памяти, включая ОЗУ. Если включен PR0, то становится невозможным считывание защищенных данных через код из незащищенного региона. Таким образом, этот план не подходит. Чтобы обойти защиту нам необходимо считывать защищенные значения и записывать их в какое-то место, к которому у нас есть доступ. В данном случае, только код находящийся в защищенном регионе может считывать значения из защищенной памяти. И так, наш метод считывания данных будет заключаться в нахождении нужной инструкции в защищенном коде и её исполнении. Эта инструкция будет считывать значения из защищенной памяти в регистр CPU после чего мы можем считать значение регистра используя отладчик. Как понять, какая инструкция нам подходит? Нам придётся вслепую искать подходящую инструкцию в защищенной памяти. Искомая инструкция называется LDR и она загружает содержимое ячейки памяти указанной в регистре r0 в регистр r3. После того, как мы найдем такую инструкцию, её можно будет использовать для считывания всего содержимого памяти.

Поиск операции загрузки из памяти

Доступ к отладчику дает возможность записать любое значение в регистр pc и перейти к любой инструкции, а также выполнить единственную инструкцию. Также он открывает возможности чтения и записи в регистры общего назначения. Для того, чтобы считать значение из защищенной ячейки памяти, необходимо найти в коде инструкцию загрузки слова из памяти, использующую регистр в качестве операнда, записать в регистр-операнд адрес интересующей ячейки и выполнить эту инструкцию. Поскольку память с кодом защищена от чтения, невозможно понять где находится какая инструкция, может показаться, что найти нужную инструкцию весьма трудно. На самом деле загрузка значения из памяти по адресу, указанному в одном регистре, в другой регистр довольно часто встречается в коде. Надойдут, например, инструкции LDR или POP. Найти подходящую инструкцию можно методом проб и ошибок. Сначала, в регистр pc записывается адрес по которому может располагаться искомая инструкция. Затем во все регистры записывается адрес, содержимое которого должно быть считано и выполняется шаг программы. Затем проверяются значения регистров. Если в одном из регистров появилось значение, похожее на ожидаемое по указанному адресу - то, возможно, была выполнена подходящая инструкция. Можно начать поиск с вектора перегрузки - по крайней мере точно известно, что там располагается исполняемый код. После перезагрузки CPU, значения всех общих регистров и указателя устанавливаются равными нулю (адрес, который нужно считать) и исполняется один шаг программы, затем проверяется значение регистров:

> reset halt
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114cc msp: 0x20001bd0
> reg r0 0x00000000
r0 (/32): 0x00000000
> reg r1 0x00000000
r1 (/32): 0x00000000
> reg r2 0x00000000
r2 (/32): 0x00000000
> reg r3 0x00000000
r3 (/32): 0x00000000
> reg r4 0x00000000
r4 (/32): 0x00000000
> reg r5 0x00000000
r5 (/32): 0x00000000
> reg r6 0x00000000
r6 (/32): 0x00000000
> reg r7 0x00000000
r7 (/32): 0x00000000
> reg r8 0x00000000
r8 (/32): 0x00000000
> reg r9 0x00000000
r9 (/32): 0x00000000
> reg r10 0x00000000
r10 (/32): 0x00000000
> reg r11 0x00000000
r11 (/32): 0x00000000
> reg r12 0x00000000
r12 (/32): 0x00000000
> reg sp 0x00000000
sp (/32): 0x00000000
> step
target state: halted
target halted due to single-step, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114ce msp: 00000000
> reg
===== arm v7m registers
(0) r0 (/32): 0x00000000
(1) r1 (/32): 0x00000000
(2) r2 (/32): 0x00000000
(3) r3 (/32): 0x10001014
(4) r4 (/32): 0x00000000
(5) r5 (/32): 0x00000000
(6) r6 (/32): 0x00000000
(7) r7 (/32): 0x00000000
(8) r8 (/32): 0x00000000
(9) r9 (/32): 0x00000000
(10) r10 (/32): 0x00000000
(11) r11 (/32): 0x00000000
(12) r12 (/32): 0x00000000
(13) sp (/32): 0x00000000
(14) lr (/32): 0xFFFFFFFF
(15) pc (/32): 0x000114CE
(16) xPSR (/32): 0xC1000000
(17) msp (/32): 0x00000000
(18) psp (/32): 0xFFFFFFFC
(19) primask (/1): 0x00
(20) basepri (/8): 0x00
(21) faultmask (/1): 0x00
(22) control (/2): 0x00
===== Cortex-M DWT registers
(23) dwt_ctrl (/32)
(24) dwt_cyccnt (/32)
(25) dwt_0_comp (/32)
(26) dwt_0_mask (/4)
(27) dwt_0_function (/32)
(28) dwt_1_comp (/32)
(29) dwt_1_mask (/4)
(30) dwt_1_function (/32)

В регистре r3 появилось значение 0x10001014. Может ли это быть значение из памяти по нулевому адресу? Попробуем загрузить в регистры значение 4:

> reset halt
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114cc msp: 0x20001bd0
> reg r0 0x00000004
r0 (/32): 0x00000004
> reg r1 0x00000004
r1 (/32): 0x00000004
> reg r2 0x00000004
r2 (/32): 0x00000004
> reg r3 0x00000004
r3 (/32): 0x00000004
> reg r4 0x00000004
r4 (/32): 0x00000004
> reg r5 0x00000004
r5 (/32): 0x00000004
> reg r6 0x00000004
r6 (/32): 0x00000004
> reg r7 0x00000004
r7 (/32): 0x00000004
> reg r8 0x00000004
r8 (/32): 0x00000004
> reg r9 0x00000004
r9 (/32): 0x00000004
> reg r10 0x00000004
r10 (/32): 0x00000004
> reg r11 0x00000004
r11 (/32): 0x00000004
> reg r12 0x00000004
r12 (/32): 0x00000004
> reg sp 0x00000004
sp (/32): 0x00000004
> step
target state: halted
target halted due to single-step, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114ce msp: 0x00000004
> reg
===== arm v7m registers
(0) r0 (/32): 0x00000004
(1) r1 (/32): 0x00000004
(2) r2 (/32): 0x00000004
(3) r3 (/32): 0x10001014
(4) r4 (/32): 0x00000004
(5) r5 (/32): 0x00000004
(6) r6 (/32): 0x00000004
(7) r7 (/32): 0x00000004
(8) r8 (/32): 0x00000004
(9) r9 (/32): 0x00000004
(10) r10 (/32): 0x00000004
(11) r11 (/32): 0x00000004
(12) r12 (/32): 0x00000004
(13) sp (/32): 0x00000004
(14) lr (/32): 0xFFFFFFFF
(15) pc (/32): 0x000114CE
(16) xPSR (/32): 0xC1000000
(17) msp (/32): 0x00000004
(18) psp (/32): 0xFFFFFFFC
(19) primask (/1): 0x00
(20) basepri (/8): 0x00
(21) faultmask (/1): 0x00
(22) control (/2): 0x00
===== Cortex-M DWT registers
(23) dwt_ctrl (/32)
(24) dwt_cyccnt (/32)
(25) dwt_0_comp (/32)
(26) dwt_0_mask (/4)
(27) dwt_0_function (/32)
(28) dwt_1_comp (/32)
(29) dwt_1_mask (/4)
(30) dwt_1_function (/32)

Видно, что в регистре r3 появилось то же самое значение. Значит первая инструкция не подходит. Проверим следующую:

> reg r0 0x00000000
r0 (/32): 0x00000000
> reg r1 0x00000000
r1 (/32): 0x00000000
> reg r2 0x00000000
r2 (/32): 0x00000000
> reg r3 0x00000000
r3 (/32): 0x00000000
> reg r4 0x00000000
r4 (/32): 0x00000000
> reg r5 0x00000000
r5 (/32): 0x00000000
> reg r6 0x00000000
r6 (/32): 0x00000000
> reg r7 0x00000000
r7 (/32): 0x00000000
> reg r8 0x00000000
r8 (/32): 0x00000000
> reg r9 0x00000000
r9 (/32): 0x00000000
> reg r10 0x00000000
r10 (/32): 0x00000000
> reg r11 0x00000000
r11 (/32): 0x00000000
> reg r12 0x00000000
r12 (/32): 0x00000000
> reg sp 0x00000000
sp (/32): 0x00000000
> step
target state: halted
target halted due to single-step, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114d0 msp: 00000000
> reg
===== arm v7m registers
(0) r0 (/32): 0x00000000
(1) r1 (/32): 0x00000000
(2) r2 (/32): 0x00000000
(3) r3 (/32): 0x20001BD0
(4) r4 (/32): 0x00000000
(5) r5 (/32): 0x00000000
(6) r6 (/32): 0x00000000
(7) r7 (/32): 0x00000000
(8) r8 (/32): 0x00000000
(9) r9 (/32): 0x00000000
(10) r10 (/32): 0x00000000
(11) r11 (/32): 0x00000000
(12) r12 (/32): 0x00000000
(13) sp (/32): 0x00000000
(14) lr (/32): 0xFFFFFFFF
(15) pc (/32): 0x000114D0
(16) xPSR (/32): 0xC1000000
(17) msp (/32): 0x00000000
(18) psp (/32): 0xFFFFFFFC
(19) primask (/1): 0x00
(20) basepri (/8): 0x00
(21) faultmask (/1): 0x00
(22) control (/2): 0x00
===== Cortex-M DWT registers
(23) dwt_ctrl (/32)
(24) dwt_cyccnt (/32)
(25) dwt_0_comp (/32)
(26) dwt_0_mask (/4)
(27) dwt_0_function (/32)
(28) dwt_1_comp (/32)
(29) dwt_1_mask (/4)
(30) dwt_1_function (/32)

На этот раз в регистре r3 появилось значение 0x20001BD0. Может ли это быть значение из памяти по нулевому адресу? Попробуем исполнить вторую инструкцию с операндом 4:

> reset halt
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114cc msp: 0x20001bd0
> step
target state: halted
target halted due to single-step, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114ce msp: 0x20001bd0
> reg r0 0x00000004
r0 (/32): 0x00000004
> reg r1 0x00000004
r1 (/32): 0x00000004
> reg r2 0x00000004
r2 (/32): 0x00000004
> reg r3 0x00000004
r3 (/32): 0x00000004
> reg r4 0x00000004
r4 (/32): 0x00000004
> reg r5 0x00000004
r5 (/32): 0x00000004
> reg r6 0x00000004
r6 (/32): 0x00000004
> reg r7 0x00000004
r7 (/32): 0x00000004
> reg r8 0x00000004
r8 (/32): 0x00000004
> reg r9 0x00000004
r9 (/32): 0x00000004
> reg r10 0x00000004
r10 (/32): 0x00000004
> reg r11 0x00000004
r11 (/32): 0x00000004
> reg r12 0x00000004
r12 (/32): 0x00000004
> reg sp 0x00000004
sp (/32): 0x00000004
> step
target state: halted
target halted due to single-step, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114d0 msp: 0x00000004
> reg
===== arm v7m registers
(0) r0 (/32): 0x00000004
(1) r1 (/32): 0x00000004
(2) r2 (/32): 0x00000004
(3) r3 (/32): 0x000114CD
(4) r4 (/32): 0x00000004
(5) r5 (/32): 0x00000004
(6) r6 (/32): 0x00000004
(7) r7 (/32): 0x00000004
(8) r8 (/32): 0x00000004
(9) r9 (/32): 0x00000004
(10) r10 (/32): 0x00000004
(11) r11 (/32): 0x00000004
(12) r12 (/32): 0x00000004
(13) sp (/32): 0x00000004
(14) lr (/32): 0xFFFFFFFF
(15) pc (/32): 0x000114D0
(16) xPSR (/32): 0xC1000000
(17) msp (/32): 0x00000004
(18) psp (/32): 0xFFFFFFFC
(19) primask (/1): 0x00
(20) basepri (/8): 0x00
(21) faultmask (/1): 0x00
(22) control (/2): 0x00
===== Cortex-M DWT registers
(23) dwt_ctrl (/32)
(24) dwt_cyccnt (/32)
(25) dwt_0_comp (/32)
(26) dwt_0_mask (/4)
(27) dwt_0_function (/32)
(28) dwt_1_comp (/32)
(29) dwt_1_mask (/4)
(30) dwt_1_function (/32)

На этот раз в регистре r3 появилось значение 0x00014CD. Очень похоже на действительно значение из памяти. Почему? Это значение и является вектором перезагрузки. Согласно документации на Cortex-M0, вектор перезагрузки располагается по адресу 4 и после того, как мы перезагружаем чип, регистр pc устанавливается в значение 0x000114CC (наименее значащий байт при записи в вектор перезагрузки изменяется с C на D, поскольку Cortex-M0 работает в режиме Thumb). Попробуем считать те две инструкции, которые мы только что проверяли:

reset halt
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114cc msp: 0x20001bd0
> step
target state: halted
target halted due to single-step, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114ce msp: 0x20001bd0
> reg r0 0x000114cc
r0 (/32): 0x000114CC
> reg r1 0x000114cc
r1 (/32): 0x000114CC
> reg r2 0x000114cc
r2 (/32): 0x000114CC
> reg r3 0x000114cc
r3 (/32): 0x000114CC
> reg r4 0x000114cc
r4 (/32): 0x000114CC
> reg r5 0x000114cc
r5 (/32): 0x000114CC
> reg r6 0x000114cc
r6 (/32): 0x000114CC
> reg r7 0x000114cc
r7 (/32): 0x000114CC
> reg r8 0x000114cc
r8 (/32): 0x000114CC
> reg r9 0x000114cc
r9 (/32): 0x000114CC
> reg r10 0x000114cc
r10 (/32): 0x000114CC
> reg r11 0x000114cc
r11 (/32): 0x000114CC
> reg r12 0x000114cc
r12 (/32): 0x000114CC
> reg sp 0x000114cc
sp (/32): 0x000114CC
> step
target state: halted
target halted due to single-step, current mode: Thread
xPSR: 0xc1000000 pc: 0x000114d0 msp: 0x000114cc
> reg r3
r3 (/32): 0x681B4B13

Регистр r3 принимает значение 0x681B4B13. Это соответствует двум инструкциям LDR, из которых первая относится к регистру pc, а вторая к регистру r3:

$ printf "x13x4bx1bx68" > /tmp/armcode

$ arm-none-eabi-objdump -D --target binary -Mforce-thumb -marm /tmp/armcode




/tmp/armcode: 	file format binary

Disassembly of section .data:

00000000 <.data>:
   0:   4b13        	ldr 	r3, [pc, #76]   ; (0x50)
   2:   681b        	ldr 	r3, [r3, #0]

На случай, если вы не знакомы с ассемблером Thumb, вторая инструкция - это инструкция загрузки значения из памяти в регистр (LDR). Она берет адрес из регистра r3, прибавляет к нему смещение 0 и загружает значение из памяти по этому адресу в регистр r3. Мы нашли инструкцию загрузки, которая позволяет нам считать значение из указанного адреса в памяти. Повторим, что это необходимо, поскольку только код из защищенной памяти может считывать данные из защищенной памяти. Хитрость состоит в том, что возможность считывать и записывать значения регистров CPU позволяет выполнять эти инструкции когда угодно. Нам повезло найти инструкцию загрузки близко к вектору перезагрузки, в противном случе, можно было бы записать произвольное значение в регистр pc и продолжить выполнение программы с другого места.

Сохранение прошивки

После того, как мы нашли инструкцию загрузки, которая может считывать значения из произвольно адреса памяти, процесс сохранения защищенной прошивки заключается в следующем: Перезагрузка CPU Выполнение шага программы (первая инструкция не важна) Запись в регистр r3 адреса, из которого нужно считать значение Выполнение шага программы (загружает значение по адресу r3 в регистр r3) Чтение регистра r3 Вот скрипт на ruby, автоматизирующий данный процесс:

#!/usr/bin/env ruby

require 'net/telnet'

debug = Net::Telnet::new("Host" => "localhost", 
                         "Port" => 4444)

dumpfile = File.open("dump.bin", "w")

((0x00000000/4)...(0x00040000)/4).each do |i|
  address = i * 4
  debug.cmd("reset halt")
  debug.cmd("step")
  debug.cmd("reg r3 0x#{address.to_s 16}")
  debug.cmd("step")
  response = debug.cmd("reg r3")
  value = response.match(/: 0x([0-9a-fA-F]{8})/)[1].to_i 16
  dumpfile.write([value].pack("V"))
  puts "0x%08x:  0x%08x" % [address, value]
end

dumpfile.close
debug.close

Скрипт подключается к интерфейсу OpenOCD, доступному через telnet. Затем перебирает адреса, кратные четырем, при помощи инструкции загрузки считывает данные из этих адресов.

Copyright © 2024 Ohm Electronics - All Rights Reserved
Ohm Electronics