STM32 Shellcode: firmware dump over UART
1 November 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