STM32 Shellcode: firmware dump over UART

Andrey Voloshin
TechMaker
Published in
3 min readNov 2, 2018

В одній з попередніх публікацій ми розглядали переповнення стека з перезаписом 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 та розповідаємо на курсах

--

--