Stack Buffer Overflow in STM32

Stack Buffer Overflow in STM32

26 September 2018
AppSec, Embedded

Nucleo64 STM32446RE
Nucleo64 STM32446RE

Сучасних мікроконтролери схожі з комп’ютерами 10–20ти річної давнини не лише потужністю але й своїми вразливостями. Далі ми розповімо про майже забутий клас вразливостей, котрі з академічних переходять у нову хвилю популярності.


0x00 Buffer overflow

В якості кукли візьмемо наступний код, що очікує від користувача пароль та вирішує чи надавати доступ далі.

Smash this for fun and profit
void callme() {
	HAL_UART_Transmit(&huart2, sWelcome, strlen(sWelcome), 100);

	while (1) { // just keep led blinking
		HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin); 
		HAL_Delay(100);
	}
}
	
void CheckUART() {
	uint8_t byte;
	int offset = 0;
	char buffer[20] = { 0 };

	while (1) {
		if (HAL_UART_Receive(&huart2, &byte, 1, 0) == HAL_OK) {
			if (byte == '\r' || byte == '\n') {
				buffer[offset] = 0; // null terminated string
				if (strcmp(sPassword, buffer) == 0) {
					DeviceLocked = 0;
				}
				offset = 0;
				return;
			} else {
				buffer[offset] = byte;
				offset++;
			}
		}
	}
}

int main(void) {
	HAL_Init();
	SystemClock_Config();
	MX_GPIO_Init();
	MX_USART2_UART_Init();

	char buffer[30] = { 0 };
	sprintf(buffer, "callme() pointer: 0x%08x \r\n", callme); // so you dont need reverse firmware for now
	HAL_UART_Transmit(&huart2, buffer, strlen(buffer), 100);
	
	while (1) {
		CheckUART();
		if (!DeviceLocked) {
			callme();
		}
		HAL_Delay(100);
	}
}

В функції main в рядках 38–40 виводиться вказівник на функцію callme, яку ми хочемо викликати.

Вразливою є функція CheckUART, в якій ми очікуємо від користувача пароль. Кінець вводу визначається символом \r або \n (натиснення Enter).

Основна проблема в тому, що потік інформації може перевищити розмір буфера. Куди буде записано байти, що виходять за межі буфера?

0x01 Memory Map

Reference Manual STM32F446
Reference Manual STM32F446

Оперативна пам’ять STM32 починається з адреси 0x2000 0000. Частину займають різні константи та глобальні змінні/масиви. Далі йдуть частини для stack та heap.

Stack росте вниз
Stack росте вниз

Stack є не лише програмною концепцією. Всередині обчислювального ядра існує спеціальний регістр, в котрому зберігається вказівник на поточний кадр (frame) стеку — stack pointer (sp).

Регістри ARM
Регістри ARM

При виклику функції на стеку зберігається інформація про

  • вказівник звідки викликали функцію. Ця інформація доступна в спеціальному регістрі link register (lr),
  • зміст деякіх регістрів загального вжитку,
  • виділяється місце під локальні змінні.

Коли ми виділяємо місце на стеку під все вищезазначене (створюємо новий кадр), то ми декрементуємо sp (росте донизу, від більших адрес до менших) на необхідну кількість байт. Наприкінці роботи функції ми повертаємо, інкрементуючи sp, таку ж кількість байт та повертаємо збережені значення назад в регістри.

Збережене значення регістру lr записується в pc (program counter) — регістр, що вказує на наступну інструкцію, котру необхідно виконувати. Іншими словами, перезаписавши збережене на стеку значення регістру lr ми в кінці функції можемо скерувати виконання програми на власний розсуд.

0x02 Assembler

Розглянемо це на прикладі CheckUART, лістинг якої подано нижче. Як його отримати ви можете прочитати у нашій попередній статті про reverse engineering stm32 firmware

Асемблерний лістинг CheckUART
Асемблерний лістинг CheckUART

Описаний вище механізм виглядає ось так (тіло функції видалили зі світлини)

Виділення стекфрейму та його знищення
Виділення стекфрейму та його знищення

ARM має інструкції, котрі одним тактом зберігають значення регістрів на стек та завантажують їх назад (push та pop відповідно).

  • Виділення місця на стеку — sub sp, 0x18.
  • Звільнення місця — add sp, 0x18

0x03 Exploit

В якості девайсу візьмемо Nucleo64 з мікроконтролером STM32f446RE. UART2 через програматор надсилає інформацію в символьний пристрій COMx або /dev/tty.* та очікує пароль **pass123** після чого вітає нас та починає весело блимати світлодіодом.

Вказівник на функцію нам спростить завдання :)
Вказівник на функцію нам спростить завдання :)

Пароль нам невідомий, а от механізм перезапису адреси повернення на стеку ми якраз розібрали. Спробуємо надіслати більше 20 байт, на які розрахований буфер. Експериментально — для даного коду та рівню оптимізації Os необхідна кількість це 32 байти, після яких ще 4 байти — адреси, куди хочемо направити виконання програми. Направимо на callme() (0x08001435). Для таких речей добре підходить Python та два пакети для роботи з serial та struct. Лістинг сплойту подано нижче:

Затримка під час пересилання байта необхідна для надійної передачі
Затримка під час пересилання байта необхідна для надійної передачі

Запускаємо скрипт, натискаємо reset на платі та через секунду насолоджуємося блиманням світлодіода без вводу пароля:

izi gg wp
izi gg wp

В той же час, в реальному світі
В той же час, в реальному світі

0x04 What have I done

what have i done
¯\(ツ)

Під час розробки виникає необхідність зберігати та компонувати інформацію. Інтерфейси здебільшого передають байти, а прикладний рівень потребує агрегації та обробки байт у вигляді пакетів. Помилка в момент перевірки виходу за межі масиву може обернутися у Remote Code Execution (RCE) або Arbitrary Code Execution що нівелюють спроби захистити пристрій та інформацію в ньому.

Розмістивши в буфері не сміття (накшталт 32 x “.”), а інструкції та передавши їм виконання актор може виконати власний код (shellcode).

0x05 Empire Strikes Back

Дієвими механізмами захисту є:

  • ASLR — при кожному запуску адреси функцій та стеку різні. Реалізація потребує повноцінної ОС та MMU. Активно використовується у всіх десктопних та серверних системах. Технологія відсутня у більшості роутерів та інших вбудованих пристроїв з ОС GNU/Linux
  • Stack Canary — більшість сучасних тулчейнів мають ключ, що дозволяє їм генерувати спеціальні значення на стеку, що перевіряються під час виходу з функції. Якщо значення було перезаписане, то виконання перейде у функцію обробки такої події. Для gcc це -fstack-protector. Очевидно, що статичне незмінне значення може бути легко помічене і увійти до шелкоду для запобігання спрацювання механізму.
  • XN (eXecute Never)— технологія дозволяє відмітити області пам’яті, що не можуть містити інструкцій. Використовує MPU мікроконтролерів, в яких він присутній. Для нашого STM32F446 код ініціалізації буде наступним
void MPU_Init() {
	MPU_Region_InitTypeDef MPU_InitStruct;

	HAL_MPU_Disable();

	MPU_InitStruct.Enable = MPU_REGION_ENABLE;
	MPU_InitStruct.BaseAddress = 0x20000000;
	MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
	MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
	HAL_MPU_ConfigRegion(&MPU_InitStruct);

	HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

0x06 DYI

Спробуйте написати власний шелкод та запихнувши його в буфер запустити виконання. Після цього повторіть з увімкненим MPU.

0x07 Moar?


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