STM32 Shellcode: firmware dump over UART

STM32 Shellcode: firmware dump over UART

1 November 2018
Embedded, AppSec

Nucleo64 STM32446RE
Nucleo64 STM32446RE

В одній з попередніх публікацій ми розглядали переповнення стека з перезаписом stack pointer адресою на необхідну нам функцію — Stack Buffer overflow in STM32

Повноцінною атакою з використанням такої вразливості можна назвати RCE — remote code execution. Для цього в буфер записується shellcode а в stack pointer вказівник на сам буфер.
Результат — виконання коду, що записано в буфер.

І тут починаються веселощі. Справа в тому, що при підготовці матеріалу не було на меті написання повноцінного shellcode і тому розмір буфера було обрано довільно. Довільно малим)))

void CheckUART() { 
uint8_t byte; 
int offset = 0; 
char buffer[20] = { 0 };
...
}

О_о 20 байт власне буфера та ще трохи місця на стеку від локальних змінних. Як виявилось пізніше, загалом — 32 байта (оті 32*”.” в пайтон коді)

Що можна запихнути у 32 байта? 🤔 #

Виклик прийнято, пишемо власний shellcode. Яка його основна функція? Спробуємо злити всю прошивку назовні. З ініціалізованих інтерфейсів у нас USB та UART. Простіше працювати з UART, його і оберемо в якості каналу зливу.

Алгоритм роботи з UART наступний:

  • чекаємо на прапорець UART_FLAG_TXE (Transmit Data Register Empty)
  • записуємо в UART->DR (data register) наступний байт прошивки
  • інкрементуємо вказівник на наступний байт прошивки
  • повертаємось до п1

Окрім цього нам потрібно забезпечити працездатність нашого коду:

  • виділити собі місце на стеку (декремент stack pointer)
  • вказати валідне значення link register

Щоб оминути організацію циклу перевірки UART_FLAG_TXE можна викликати HAL_Delay(1). Наш UART працює на швидкості 115200 кбіт/с і затримки в 1мс якраз досить для відправки одного байта.

На перший погляд, ми могли б знайти та використати функцію HAL_UART_Transmit() але в такому разі наш код буде залежати від зміщення у пам’яті конкретної функції. Залежність від HAL_Delay() можна прибрати тим же циклом.

Працюючи напряму з регістрами периферії ми отримуємо код, що не залежить від наявності тих чи інших функцій в прошивці. Буквально налаштувавши 2–3 регістри ми можемо увімкнути той же UART та почати передачу інформації.

Отож, фінальна версія шелкоду буде виглядати приблизно наступним чином:

sub sp, 0x54 ; виділяємо собі трохи місця на стеку
movs r0, 1 ; перший аргумент функції HAL_Delay(1)
ldr     r2, [pc, #8] ; в регістрі r2 буде адреса UART2->DR
mov.w r3, =0x8000000 ; в регістрі r3 значення 0х08000000 (початок Flash)
ldrb.w  r1, [r3], #1 ; завантажуємо байт прошивки в регістр r1
str     r1, [r2, #0] ; значення з r1 записуємо у UART2->DR
subw    lr, pc, #9 ; повертаємось з HAL_Delay одразу на {ldrb r1, [r3], 1}
ldr.w   pc, [pc, #4] ; викликаємо HAL_Delay
; після коду буде розміщено два значення, котрі ми завантажуємо в регістри
0x40004404 ; UART->DR address, інформація з Reference Manual
0x0800067b ; HAL_Delay address

Код простіше написати (навіть на С з asm вставками), скомпілювати та підглянути дизасемблером результат після чого модифікувати його під себе:

Запакувавши в скрипт на пітоні та трохи потестивши, отримуємо наступне:

Результат його виконання можна переглянути нижче.

Враження та висновки #

Написання свого shellcode це досить цікавий спосіб пізнання нутрощів роботи архітектури MCU чи CPU. Для більшості популярних комбінацій архітектур та ОС можливо самостійно знайти готовий код (наприклад, https://www.exploit-db.com/shellcode/). Але коли справа доходить до вбудованих систем з нішевою ОС (QNX, VxWorks, NuttX) може виникнути необхідність власноруч спробувати підготувати shellcode.

Нещодавно була цікава презентація з висвітленням поточного стану захисту QNX. Рекомендуємо для самостійного опрацювання та подальшого дослідження :)

https://recon.cx/2018/brussels/resources/slides/RECON-BRX-2018-Dissecting-QNX.pdf

Materials #


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