Reverse engineering STM32 firmware

Reverse engineering STM32 firmware

14 September 2018
Embedded, AppSec

BlackPill з подругою
BlackPill з подругою

Виробники мікроконтролерів надають функції захисту енергонезалежної пам’яті від зчитування інструментами зневадження. На перший погляд, основна проблема, яка вирішується - клонування прошивки пристрою. Проте, маючи на руках незашифровану прошивку зацікавлена особа може проаналізувати хід її виконання і навіть змінити його на свій розсуд.


Для прикладу візьмемо BlackPill та невеличку прошивку, що перевіряє секретний ключ (передається по UART) та або блокує пристрій (рядки 30–37) або виконує основний функціонал - щосекунди відправляє інформацію з АЦП по USB.

Повний код проекту для STM32CubeIDE знаходиться тут.

Шматок нашого embedded crackme :)
uint8_t isUnlocked() {
	char code[31] = { 0 };

	if (HAL_UART_Receive(&huart1, code, 30, 100) != HAL_OK) {
		// code not received, we are locked
		return 0;
	}

	if (strcmp(code, "SECRET_CODE_TOKEN") == 0) {
		// secret token is valid, unlocking
		return 1;
	}

	return 0;
}

int main(void) {
	/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
	HAL_Init();

  /* Configure the system clock */
	SystemClock_Config();

	/* Initialize all configured peripherals */
	MX_GPIO_Init();
	MX_ADC1_Init();
	MX_USART1_UART_Init();
	MX_USB_DEVICE_Init();

	if (!isUnlocked()) {
		CDC_Transmit_FS("device locked\r\n", 15);
		// halt
		while (1) {
			HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_12);
			HAL_Delay(100);
		}
	}

	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, 0); // LED ON

	char buf[100];
	while (1) {
		HAL_ADC_Start(&hadc1);
		HAL_ADC_PollForConversion(&hadc1, 100);

		sprintf(buf, "ADC Value: %d\r\n", HAL_ADC_GetValue(&hadc1));

		CDC_Transmit_FS(buf, strlen(buf));
		HAL_Delay(1000);
	}
}

0x00 Firmware dump #

Першим кроком ми отримуємо дамп пам’яті пристрою. Це можливо зробити завантаживши оновлення прошивки з офіційного сайту або вичитуванням flash пам’яті самого пристрою. Наступна команда OpenOCD викачає перші 32кБ флеш пам’яті пристрою.

openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg -c "init" -c "reset init" -c "flash read_bank 0 firmware.bin 0 0x8000" -c "exit"

Результат виконання команди
Результат виконання команди

Спробуйте змінити команду таким чином, щоб вигрузити усі 64кБ флеш пам’яті BlackPill. Між іншим, там насправді 128кБ а обмеження в 64кБ можна обійти ;)

0x01 Quick analysis #

Швидкий аналіз дасть нам розуміння:

  • чи зашифровано вміст flash?
  • які строкові літерали присутні в прошивці?

З цією роботою справляться strings, binwalk, https://binvis.io .

strings firmware.bin

strings firmware.bin
😳 секретний токен не такий вже й секретний

Візуалізація файлу дає змогу оцінити ентропію різних ділянок файлу та виявити присутність текстової інформації. Скористайтеся сервісом https://binvis.io/ та завантажте firmware.bin для візуального аналізу.

0x02 Disassembly #

Нам підійде radare2 в якості дизасемблера. ARM Cortex-M використовують Thumb набір інструкцій. З офіційної документації ARM:

The Thumb instruction set is a subset of the most commonly used 32-bit ARM instructions. Thumb instructions are each 16 bits long, and have a corresponding 32-bit ARM instruction that has the same effect on the processor model. Thumb instructions operate with the standard ARM register configuration, allowing excellent interoperability between ARM and Thumb states. On execution, 16-bit Thumb instructions are transparently decompressed to full 32-bit ARM instructions in real time, without performance loss. Thumb code is typically 65% of the size of ARM code, and provides 160% of the performance of ARM code when running from a 16-bit memory system.

Іншими словами, інструкції 16-бітні, хардварно розширюються у 32-бітні. Освіжимо в пам’яті як виглядають виклики функцій та умовні переходи в ARM:

+------------------+------------+
|   Instruction    | Relativity |
+------------------+------------+
| b label          | Relative   |
| bx register      | Absolute   |
| bl label         | Relative   |
| blx label        | Relative   |
| blx register     | Absolute   |
| pop {..., pc}    | Absolute   |
| ldr pc, =address | Absolute   |
+------------------+------------+

Дві інструкції, котрі ми зустрінемо, використовуються для організації умовних переходів та циклів:

  • cbnz (compare, branch on non-zero),
  • cbz (compare, branch on zero).

Entry point #

Для мікроконтролерів stm32f1xx документація дає наступну інформацію про хід виконання та точку входу

datasheet: entry point
datasheet: entry point

Код, з якого починається виконання - reset handler. В таблиці векторів переривань знаходиться по зміщенню 0x04 від початку флеша (Reset_Handler @ 0x0800 0004).

Talk is cheap. Show me the code! #

r2 -a arm -b 16 -m 0x08000000 -w firmware.bin

Radare2 запускаємо та вказуємо архітектуру ARM, 16-бітний набір інструкцій, зміщення 0x08000000 (початок flash memory), дозволяємо редагування прошивки та власне вказуємо шлях до неї.

Перші 32 інструкції прошивки
Перші 32 інструкції прошивки

Команда ааа - проаналізувати виклики, переходи та символи і роздати їм автогенеровані імена.

Команда pd 32 - вивести 32 команди по поточному зміщенню. Поточне зміщення вказано лівіше від курсора (жовтим, 0х08000000 - початок файла).

NVIC table. Знайшли зміщення, де розташовано ResetHandler
NVIC table. Знайшли зміщення, де розташовано ResetHandler

Переходимо до аналізу ResetHandler. Зміщуємося на 0x08003ac4 та виводимо дизассемблінг:

s aav.0x08003ac4
pd 32

ResetHandler закінчується по зміщенню 0x08003af6 (bx lr)
ResetHandler закінчується по зміщенню 0x08003af6 (bx lr)

Одразу бачимо 3 виклики функцій (bl fcn.08003xxx). Що це за функції? Заглянемо в проект, в файл /startup/startup_stm32f103xb.s і знайдемо відповідності.

startup
startup

Три виклики функцій, це SystemInit (налаштування тактування та ініціалізація flash), libc_init та main. Зміщуємося до main та виводимо лістинг функції:

s fcn.0800348c
pdf

Лістинг main
Лістинг main

На початку зберігається вказівник адреси виклику, виділяється стек для локальних змінних (росте вниз, тому від вказівника стека просто віднімається необхідна кількість байт).

Далі йдуть виклики функцій, котрі ініціалізують різну периферію. Після чого, організовано два цикли і умовний перехід на один з них. Простіше їх розібрати на візуалізації графу переходів:

VV

main graph
main graph

Якщо в регістрі r0 після виклику функції 33а4 ненульове значення, то хід виконання піде праворуч. Якщо нуль - ліворуч.

0x03 Patch #

Зліва в регістр r0 завантажується вказівник на щось… (aav.0x08004430) Гляньмо, що ж там. Виводимо 32 значення по зміщенню aav.0x08004430:

x 32 @ aav.0x08004430

А там - строкові літерали.
А там - строкові літерали.

Таким чином, йдучи ліворуч по графу викликів ми потрапляємо в цикл, що блокує роботу пристрою. Нам потрібно змінити умовний перехід.

Compare and Branch on Non-Zero
Compare and Branch on Non-Zero

Варіанти:

  • інвертувати його (cbnz -> cbz)
  • замінити на безумовний перехід на зміщення 0x80034c8 (там код виконання основного функціоналу пристрою)

Безумовний перехід зробить прошивку робочою незалежно від виконання умови, а інверсія - буде працювати лише при відсутності ключа.

Запишемо безумовний перехід на місце cbnz:

wa b 0x80034c8 @ 0x080034ac

wa - записати опкоди, що відповідають асемблерному коду “b 0x80034c8” по зміщенню 0x080034ac.

Patched main. 0x080034ac тепер 0ce0 замість 60b9
Patched main. 0x080034ac тепер 0ce0 замість 60b9

0x04 Upload & test #

Завантажимо змінену прошивку в пристрій та переконаємося в її працездатності.

openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg -c "program path/to/firmware.bin verify reset exit 0x08000000"
screen /dev/cu.usbmodem1421

Пристрій працює
Пристрій працює

0x05 DIY #

Функція перевірки ключа може викликатися в різних місцях прошивки. Тому ефективніше пропатчити саме функцію isUnlocked щоб вона повертала ненульове значення незалежно від наявності ключа. Спробуйте зробити це самостійно 😎

Materials #


Подібні матеріали ми також пишемо на нашій сторінці TechMaker в Facebook та розповідаємо на наших курсах