Stack Buffer Overflow in STM32
26 September 2018
Сучасних мікроконтролери схожі з комп’ютерами 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 #
Оперативна пам’ять STM32 починається з адреси 0x2000 0000. Частину займають різні константи та глобальні змінні/масиви. Далі йдуть частини для stack та heap.
Stack є не лише програмною концепцією. Всередині обчислювального ядра існує спеціальний регістр, в котрому зберігається вказівник на поточний кадр (frame) стеку — stack pointer (sp).
При виклику функції на стеку зберігається інформація про
- вказівник звідки викликали функцію. Ця інформація доступна в спеціальному регістрі link register (lr),
- зміст деякіх регістрів загального вжитку,
- виділяється місце під локальні змінні.
Коли ми виділяємо місце на стеку під все вищезазначене (створюємо новий кадр), то ми декрементуємо sp (росте донизу, від більших адрес до менших) на необхідну кількість байт. Наприкінці роботи функції ми повертаємо, інкрементуючи sp, таку ж кількість байт та повертаємо збережені значення назад в регістри.
Збережене значення регістру lr записується в pc (program counter) — регістр, що вказує на наступну інструкцію, котру необхідно виконувати. Іншими словами, перезаписавши збережене на стеку значення регістру lr ми в кінці функції можемо скерувати виконання програми на власний розсуд.
0x02 Assembler #
Розглянемо це на прикладі CheckUART, лістинг якої подано нижче. Як його отримати ви можете прочитати у нашій попередній статті про reverse engineering stm32 firmware
Описаний вище механізм виглядає ось так (тіло функції видалили зі світлини)
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 на платі та через секунду насолоджуємося блиманням світлодіода без вводу пароля:
0x04 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.