Reverse engineering STM32 firmware
14 September 2018
Виробники мікроконтролерів надають функції захисту енергонезалежної пам’яті від зчитування інструментами зневадження. На перший погляд, основна проблема, яка вирішується - клонування прошивки пристрою. Проте, маючи на руках незашифровану прошивку зацікавлена особа може проаналізувати хід її виконання і навіть змінити його на свій розсуд.
Для прикладу візьмемо 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
Візуалізація файлу дає змогу оцінити ентропію різних ділянок файлу та виявити присутність текстової інформації. Скористайтеся сервісом 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 документація дає наступну інформацію про хід виконання та точку входу
Код, з якого починається виконання - 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), дозволяємо редагування прошивки та власне вказуємо шлях до неї.
Команда ааа
- проаналізувати виклики, переходи та символи і роздати їм автогенеровані імена.
Команда pd 32
- вивести 32 команди по поточному зміщенню. Поточне зміщення вказано лівіше від курсора (жовтим, 0х08000000 - початок файла).
Переходимо до аналізу ResetHandler. Зміщуємося на 0x08003ac4 та виводимо дизассемблінг:
s aav.0x08003ac4
pd 32
Одразу бачимо 3 виклики функцій (bl fcn.08003xxx). Що це за функції? Заглянемо в проект, в файл /startup/startup_stm32f103xb.s
і знайдемо відповідності.
Три виклики функцій, це SystemInit
(налаштування тактування та ініціалізація flash), libc_init
та main
. Зміщуємося до main
та виводимо лістинг функції:
s fcn.0800348c
pdf
На початку зберігається вказівник адреси виклику, виділяється стек для локальних змінних (росте вниз, тому від вказівника стека просто віднімається необхідна кількість байт).
Далі йдуть виклики функцій, котрі ініціалізують різну периферію. Після чого, організовано два цикли і умовний перехід на один з них. Простіше їх розібрати на візуалізації графу переходів:
VV
Якщо в регістрі r0 після виклику функції 33а4 ненульове значення, то хід виконання піде праворуч. Якщо нуль - ліворуч.
0x03 Patch #
Зліва в регістр r0 завантажується вказівник на щось… (aav.0x08004430) Гляньмо, що ж там. Виводимо 32 значення по зміщенню aav.0x08004430:
x 32 @ aav.0x08004430
Таким чином, йдучи ліворуч по графу викликів ми потрапляємо в цикл, що блокує роботу пристрою. Нам потрібно змінити умовний перехід.
Варіанти:
- інвертувати його (
cbnz
->cbz
) - замінити на безумовний перехід на зміщення
0x80034c8
(там код виконання основного функціоналу пристрою)
Безумовний перехід зробить прошивку робочою незалежно від виконання умови, а інверсія - буде працювати лише при відсутності ключа.
Запишемо безумовний перехід на місце cbnz
:
wa b 0x80034c8 @ 0x080034ac
wa - записати опкоди, що відповідають асемблерному коду “b 0x80034c8” по зміщенню 0x080034ac.
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 щоб вона повертала ненульове значення незалежно від наявності ключа. Спробуйте зробити це самостійно 😎