В рамках встречи 0x0A DC7831 DEF CON Нижний Новгород 16 февраля мы представили доклад о базовых принципах эмуляции бинарного кода и собственной разработке — эмуляторе аппаратных платформ Kopycat.
В статье мы приведём описание запуска прошивки устройства в эмуляторе, продемонстрируем взаимодействие с отладчиком и выполним небольшой динамический анализ прошивки.
Предыстория
A long time ago in a galaxy far far away
Пару лет назад в нашей лаборатории возникла необходимость исследовать прошивку устройства. Прошивка была сжата, распаковывалась bootloader'ом. Делал он это весьма замороченным способом, несколько раз перекладывая данные в памяти. Да и сама прошивка потом активно взаимодействовала с периферией. И всё это на ядре MIPS.
Имеющиеся эмуляторы по объективным причинам нас не устроили, а хотелось всё-таки запустить код. Тогда решили сделать свой эмулятор, который сделает минимум и позволит распаковать основную прошивку. Попробовали — получилось. Подумали, а что если добавить периферию, чтобы еще и основную прошивку выполнять. Было не очень больно — и тоже получилось. Снова подумали и решили делать полноценный эмулятор.
В итоге получился эмулятор вычислительных систем Kopycat.
Имеет место игра слов.
- copycat (англ., сущ. [ˈkɒpɪkæt]) — подражатель, имитатор
- cat (англ., сущ. [ˈkæt]) — кошка, кот — любимое животное одного из создателей проекта
- Буква "K" — от языка программирования Kotlin
Kopycat
При создании эмулятора ставились совершенно определённые цели:
- возможность достаточно быстро создать новую периферию, модуль, процессорное ядро;
- возможность собрать виртуальное устройство из различных модулей;
- возможность загрузить в память виртуального устройства любые двоичные данные (прошивку);
- возможность работы со снапшотами (снимки состояния системы);
- возможность взаимодействия с эмулятором через встроенный отладчик;
- приятный современный язык для разработки.
В итоге, для реализации был выбран Kotlin, шинная архитектура (это когда модули связываются между собой через виртуальные шины данных), JSON — в качестве формата описания устройства, и GDB RSP — в качестве протокола взаимодействия с отладчиком.
Разработка идёт на протяжении чуть больше двух лет и активно продолжается. За это время были реализованы процессорные ядра MIPS, x86, V850ES, ARM, PowerPC.
Проект растет, и пришло время представить его широкой общественности. Подробное описание проекта сделаем позже, а сейчас сосредоточимся на использовании Kopycat.
Для самых нетерпеливых — промо-версию эмулятора можно скачать по ссылке.
Носорог в эмуляторе
Напомним, ранее для конференции SMARTRHINO-2018 было создано тестовое устройство "Носорог" для обучения навыкам реверс-инжиниринга. Процесс статического анализа прошивки был описан в этой статье.
Теперь же попробуем добавить "динамики" и запустим прошивку в эмуляторе.
Нам понадобятся:
1) Java 1.8
2) Python и модуль Jep для использования Python внутри эмулятора. WHL-cборку модуля Jep под Windows можно скачать тут.
Для Windows:
1) com0com
2) PuTTY
Для Linux:
1) socat
В качестве GDB-клиента можно использовать Eclipse, IDA Pro или radare2.
Как это работает?
Для того, чтобы выполнять прошивку в эмуляторе, необходимо "собрать" виртуальное устройство, которое представляет собой аналог реального устройства.
Реальное устройство ("носорог") можно показать на структурной схеме:
Эмулятор имеет модульную структуру и конечное виртуальное устройство можно описать в JSON-файле.
{
"top": true,
// Plugin name should be the same as file name (or full path from library start)
"plugin": "rhino",
// Directory where plugin places
"library": "user",
// Plugin parameters (constructor parameters if jar-plugin version)
"params": [
{ "name": "tty_dbg", "type": "String"},
{ "name": "tty_bt", "type": "String"},
{ "name": "firmware", "type": "String", "default": "NUL"}
],
// Plugin outer ports
"ports": [ ],
// Plugin internal buses
"buses": [
{ "name": "mem", "size": "BUS30" },
{ "name": "nand", "size": "4" },
{ "name": "gpio", "size": "BUS32" }
],
// Plugin internal components
"modules": [
{
"name": "u1_stm32",
"plugin": "STM32F042",
"library": "mcu",
"params": {
"firmware:String": "params.firmware"
}
},
{
"name": "usart_debug",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_dbg"
}
},
{
"name": "term_bt",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_bt"
}
},
{
"name": "bluetooth",
"plugin": "BT",
"library": "mcu"
},
{ "name": "led_0", "plugin": "LED", "library": "mcu" },
{ "name": "led_1", "plugin": "LED", "library": "mcu" },
{ "name": "led_2", "plugin": "LED", "library": "mcu" },
{ "name": "led_3", "plugin": "LED", "library": "mcu" },
{ "name": "led_4", "plugin": "LED", "library": "mcu" },
{ "name": "led_5", "plugin": "LED", "library": "mcu" },
{ "name": "led_6", "plugin": "LED", "library": "mcu" },
{ "name": "led_7", "plugin": "LED", "library": "mcu" },
{ "name": "led_8", "plugin": "LED", "library": "mcu" },
{ "name": "led_9", "plugin": "LED", "library": "mcu" },
{ "name": "led_10", "plugin": "LED", "library": "mcu" },
{ "name": "led_11", "plugin": "LED", "library": "mcu" },
{ "name": "led_12", "plugin": "LED", "library": "mcu" },
{ "name": "led_13", "plugin": "LED", "library": "mcu" },
{ "name": "led_14", "plugin": "LED", "library": "mcu" },
{ "name": "led_15", "plugin": "LED", "library": "mcu" }
],
// Plugin connection between components
"connections": [
[ "u1_stm32.ports.usart1_m", "usart_debug.ports.term_s"],
[ "u1_stm32.ports.usart1_s", "usart_debug.ports.term_m"],
[ "u1_stm32.ports.usart2_m", "bluetooth.ports.usart_m"],
[ "u1_stm32.ports.usart2_s", "bluetooth.ports.usart_s"],
[ "bluetooth.ports.bt_s", "term_bt.ports.term_m"],
[ "bluetooth.ports.bt_m", "term_bt.ports.term_s"],
[ "led_0.ports.pin", "u1_stm32.buses.pin_output_a", "0x00"],
[ "led_1.ports.pin", "u1_stm32.buses.pin_output_a", "0x01"],
[ "led_2.ports.pin", "u1_stm32.buses.pin_output_a", "0x02"],
[ "led_3.ports.pin", "u1_stm32.buses.pin_output_a", "0x03"],
[ "led_4.ports.pin", "u1_stm32.buses.pin_output_a", "0x04"],
[ "led_5.ports.pin", "u1_stm32.buses.pin_output_a", "0x05"],
[ "led_6.ports.pin", "u1_stm32.buses.pin_output_a", "0x06"],
[ "led_7.ports.pin", "u1_stm32.buses.pin_output_a", "0x07"],
[ "led_8.ports.pin", "u1_stm32.buses.pin_output_a", "0x08"],
[ "led_9.ports.pin", "u1_stm32.buses.pin_output_a", "0x09"],
[ "led_10.ports.pin", "u1_stm32.buses.pin_output_a", "0x0A"],
[ "led_11.ports.pin", "u1_stm32.buses.pin_output_a", "0x0B"],
[ "led_12.ports.pin", "u1_stm32.buses.pin_output_a", "0x0C"],
[ "led_13.ports.pin", "u1_stm32.buses.pin_output_a", "0x0D"],
[ "led_14.ports.pin", "u1_stm32.buses.pin_output_a", "0x0E"],
[ "led_15.ports.pin", "u1_stm32.buses.pin_output_a", "0x0F"]
]
}
Обратите внимание на параметр firmware в разделе params — это имя файла, который можно загружать в виртуальное устройство в качестве прошивки.
Виртуальное устройство и его взаимодействие с основной операционной системой можно представить вот такой схемой:
Текущий тестовый экземпляр эмулятора подразумевает взаимодействие с COM-портами основной ОС (отладочный UART и UART для Bluetooth-модуля). Это могут быть реальные порты, к которым подключены устройства или же виртуальные COM-порты (для этого как раз нужен com0com / socat).
Для взаимодействия с эмулятором извне на данный момент существует два основных способа:
- протокол GDB RSP (соответственно, поддерживающие этот протокол, инструменты — Eclipse / IDA / radare2);
- внутренняя командная строка эмулятора (Argparse или Python).
Виртуальные COM-порты
Для того чтобы взаимодействовать с UART-ом виртуального устройства на локальной машине через терминал, необходимо создать пару связанных виртуальных COM-портов. В нашем случае один порт задействует эмулятор, а второй — программа-терминал (PuTTY или screen):
Использование com0com
Виртуальные COM-порты настраиваются setup-утилитой из комплекта com0com (консольная версия — C:Program Files (x86)com0comsetupс.exe, или GUI-версия — C:Program Files (x86)com0comsetupg.exe):
Следует установить галочки enable buffer overrun для всех созданных виртуальных портов, иначе эмулятор будет ожидать отклика от COM-порта.
Использование socat
На UNIX-системах виртуальные COM-порты автоматически создаются эмулятором при помощи утилиты socat, для этого достаточно при запуске эмулятора в имени порта указать префикс socat:
.
Внутренний интерфейс командной строки (Argparse или Python)
Поскольку Kopycat представляет собой консольное приложение, для взаимодействия со своими объектами и переменными эмулятор предоставляет два варианта интерфейса командной строки: Argparse и Python.
Argparse — это CLI, встроенный в Kopycat, он доступен всегда и всем.
Альтернативный CLI — интерпретатор Python. Для его использования необходимо установить Python-модуль Jep и настроить эмулятор для работы с Python (будет использоваться интерпретатор Python, установленный в основной системе пользователя).
Установка Python-модуля Jep
Под Linux Jep может быть установлен через pip:
pip install jep
Для установки Jep под Windows необходимо предварительно установить Windows SDK и соответствующую Microsoft Visual Studio. Мы немного упростили вам задачу и сделали WHL-сборки JEP под актуальные версии Python для Windows, поэтому модуль можно установить из файла:
pip install jep-3.8.2-cp27-cp27m-win_amd64.whl
Для проверки установки Jep, необходимо выполнить в командной строке:
python -c "import jep"
В ответ должно быть получено сообщение:
ImportError: Jep is not supported in standalone Python, it must be embedded in Java.
В командном файле эмулятора для вашей системы (kopycat.bat — для Windows, kopycat — для Linux) к списку параметров DEFAULT_JVM_OPTS
добавьте дополнительный параметр Djava.library.path
— он должен содержать путь до установленного модуля Jep.
В результате для Windows должна получиться строка следующего вида:
set DEFAULT_JVM_OPTS="-XX:MaxMetaspaceSize=256m" "-XX:+UseParallelGC" "-XX:SurvivorRatio=6" "-XX:-UseGCOverheadLimit" "-Djava.library.path=C:/Python27/Lib/site-packages/jep"
Запуск Kopycat
Эмулятор представляет собой консольное JVM-приложение. Запуск осуществляется через сценарий командной строки операционной системы (sh/cmd).
Команда для запуска под Windows:
binkopycat -g 23946 -n rhino -l user -y library -p firmware=firmwarerhino_pass.bin,tty_dbg=COM26,tty_bt=COM28
Команда для запуска под Linux с использованием утилиты socat:
./bin/kopycat -g 23946 -n rhino -l user -y library -p firmware=./firmware/rhino_pass.bin, tty_dbg=socat:./COM26,tty_bt=socat:./COM28
-g 23646
— TCP-порт, который будет открыт для доступа к GDB-серверу;-n rhino
— имя основного модуля системы (устройство в сборе);-l user
— имя библиотеки для поиска основного модуля;-y library
— путь для поиска модулей, входящих в устройство;firmwarerhino_pass.bin
— путь к файлу прошивки;- COM26 и COM28 — виртуальные COM-порты.
В результате будет выведено приглашение Python >
(или Argparse >
):
18:07:59 INFO [eFactoryBuilder.create ]: Module top successfully created as top
18:07:59 INFO [ Module.initializeAndRes]: Setup core to top.u1_stm32.cortexm0.arm for top
18:07:59 INFO [ Module.initializeAndRes]: Setup debugger to top.u1_stm32.dbg for top
18:07:59 WARN [ Module.initializeAndRes]: Tracer wasn't found in top...
18:07:59 INFO [ Module.initializeAndRes]: Initializing ports and buses...
18:07:59 WARN [ Module.initializePortsA]: ATTENTION: Some ports has warning use printModulesPortsWarnings to see it...
18:07:59 FINE [ ARMv6CPU.reset ]: Set entry point address to 08006A75
18:07:59 INFO [ Module.initializeAndRes]: Module top is successfully initialized and reset as a top cell!
18:07:59 INFO [ Kopycat.open ]: Starting virtualization of board top[rhino] with arm[ARMv6Core]
18:07:59 INFO [ GDBServer.debuggerModule ]: Set new debugger module top.u1_stm32.dbg for GDB_SERVER(port=23946,alive=true)
Python >
Взаимодействие с IDA Pro
В качестве исходного файла для анализа в IDA для упрощения тестирования используем прошивку «Носорога» в виде ELF-файла (там сохранена метаинформация).
Вы также можете использовать основную прошивку без метаинформации.
После запуска Kopycat в IDA Pro в меню Debugger идём в пункт "Switch debugger..." и выбираем "Remote GDB debugger". Далее настраиваем подключение: меню Debugger — Process options...
Устанавливаем значения:
- Application — любое значение
- Hostname: 127.0.0.1 (или IP-адрес удаленной машины, где запущен Kopycat)
- Port: 23946
Теперь становится доступна кнопка запуска отладки (клавиша F9):
Нажимаем её — происходит подключение к модулю отладчика в эмуляторе. IDA переходит в режим отладки, становятся доступны дополнительные окна: информация о регистрах, о стеке.
Теперь мы можем использовать все стандартные возможности работы с отладчиком:
- пошаговое выполнение инструкций (Step into и Step over — клавиши F7 и F8, соответственно);
- запуск и приостановка выполнения;
- создание точек останова как на код, так и на данные (клавиша F2).
Подключение к отладчику не означает запуска кода прошивки. Текущей позицией для выполнения должен быть адрес 0x08006A74
— начало функции Reset_Handler. Если прокрутить листинг ниже, то можно увидеть вызов функции main. Можно установить курсор на этой строке (адрес 0x08006ABE
) и выполнить операцию Run until cursor (клавиша F4).
Далее можно нажать F7, чтобы зайти в функцию main.
Если выполнить команду Continue process (клавиша F9), то появится окно "Please wait" с единственной кнопкой Suspend:
При нажатии Suspend выполнение кода прошивки приостанавливается и может быть продолжено с того же адреса в коде, где было прервано.
Если продолжить выполнение кода, то в терминалах, подключенных к виртуальным COM-портам, можно увидеть следующие строки:
Наличие строки "state bypass" говорит о том, что виртуальный Bluetooth-модуль перешёл в режим приёма данных от COM-порта пользователя.
Теперь в Bluetooth-терминале (на рисунке — COM29) можно вводить команды в соответствии с протоколом "Носорога". Например, на команду "MEOW" в Bluetooth-терминал вернётся строка "mur-mur":
Эмулируй меня не полностью
При построении эмулятора можно выбирать степень детализации/эмуляции того или иного устройства. Так, например, модуль Bluetooth можно эмулировать по-разному:
- эмулируется полностью устройство с полным набором команд;
- эмулируются AT-команды, а поток данных принимается с COM-порта основной системы;
- виртуальное устройство обеспечивает полное перенаправление данных на реальное устройство;
- в виде простой заглушки, которая всегда возвращает "OK".
В текущей версии эмулятора используется второй подход — виртуальный Bluetooth-модуль выполняет конфигурирование, после чего переходит в режим "проксирования" данных из COM-порта основной системы в UART-порт эмулятора.
Рассмотрим возможность простой инструментации кода в случае, если не реализована какая-то часть периферии. Например, если не создан таймер, отвечающий за контроль передачи данных в DMA (проверка выполняется в функции ws2812b_wait, расположенной по адресу 0x08006840
), то прошивка будет всегда ждать сброса флага busy, расположенного по адресу 0x200004C4
, который показывает занятость линии данных DMA:
Мы можем обойти такую ситуацию путём "ручного" сброса флага busy сразу после его установки. В IDA Pro можно создать Python-функцию и вызывать её в breakpoint'е, при этом сам breakpoint поставить в коде после записи значения 1 во флаг busy.
Breakpoint-обработчик
Сначала создадим Python-функцию в IDA. Меню File — Script command...
Добавляем новый сниппет в списке слева, даём ему имя (например, BPT),
в текстовом поле справа вводим код функции:
def skip_dma():
print "Skipping wait ws2812..."
value = Byte(0x200004C4)
if value == 1:
PatchDbgByte(0x200004C4, 0)
return False
После этого нажимаем Run и закрываем окно скриптов.
Теперь перейдём в код по адресу 0x0800688A
, установим breakpoint (клавиша F2), отредактируем его (контекстное меню Edit breakpoint…), не забудем установить тип скрипта – Python:
Если текущее значение флага busy равно 1, то следует выполнить функцию skip_dma в строке скриптов:
Если запустить прошивку на выполнение, то срабатывание кода breakpoint-обработчика можно увидеть в IDA в окне Output по строке Skipping wait ws2812...
. Теперь прошивка не будет ожидать сброс флага busy.
Взаимодействие с эмулятором
Эмуляция ради эмуляции вряд ли вызовет восторг и радость. Гораздо интереснее, если эмулятор поможет исследователю увидеть данные в памяти или установить взаимодействие потоков.
Покажем, как в динамике установить взаимодействие RTOS-тасков. Предварительно следует приостановить выполнение кода, если оно запущено. Если перейти в функцию bluetooth_task_entry в ветку обработки команды "LED " (адрес 0x080057B8
), то можно увидеть, что сначала создается, а потом отправляется в системную очередь ledControlQueueHandle некоторое сообщение.
Следует установить breakpoint на обращение к переменной ledControlQueueHandle, расположенной по адресу 0x20000624
и продолжить выполнение кода:
В результате сначала произойдет останов по адресу 0x080057CA
перед вызовом функции osMailAlloc, далее — по адресу 0x08005806
перед вызовом функции osMailPut, потом через некоторое время — по адресу 0x08005BD4
(перед вызовом функции osMailGet), который принадлежит функции leds_task_entry (LED-таск), то есть произошло переключение тасков, и теперь управление получил LED-таск.
Таким нехитрым способом можно установить, как таски RTOS взаимодействуют друг с другом.
Конечно, в действительности взаимодействие тасков может быть устроено сложнее, но с использованием эмулятора отслеживать это взаимодействие становится менее трудоемко.
Тут можно посмотреть небольшое видео запуска эмулятора и взаимодействия с IDA Pro.
Запуск с Radare2
Нельзя обойти стороной такой универсальный инструмент как Radare2.
Для подключения к эмулятору с использованием r2 команда будет выглядеть так:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elf
Сейчас доступны запуск (dc
) и приостановка выполнения (Ctrl+C).
К сожалению, на данный момент в r2 есть проблемы при работе с хардварным gdb-сервером и разметкой памяти, из-за этого не работают точки останова и Step'ы (команда ds
). Надеемся, в ближайшее время это будет исправлено.
Запуск с Eclipse
Один из вариантов использования эмулятора — отладка прошивки разрабатываемого устройства. Для наглядности будем также использовать прошивку «Носорога». Скачать исходники прошивки можно отсюда.
В качестве IDE будем использовать Eclipse из набора System Workbench for STM32.
Для того, чтобы в эмулятор загружалась прошивка непосредственно собранная в Eclipse, необходимо добавить параметр firmware=null
в команду запуска эмулятора:
binkopycat -g 23946 -n rhino -l user -y modules -p firmware=null,tty_dbg=COM26,tty_bt=COM28
Настройка debug-конфигурации
В Eclipse выбираем меню Run — Debug Configurations... В открывшемся окне в разделе GDB Hardware Debugging необходимо добавить новую конфигурацию, после чего на вкладке "Main" указать текущий проект и приложение для отладки:
На вкладке "Debugger" необходимо указать GDB-команду:
${openstm32_compiler_path}arm-none-eabi-gdb
А также ввести параметры для подключения к GDB-серверу (хост и порт):
На вкладке "Startup" необходимо указать следующие параметры:
- включить галочку Load image (чтобы выполнялась загрузка в эмулятор собранного образа прошивки);
- включить галочку Load symbols;
- добавить команду запуска:
set $pc = *0x08000004
(выставить в регистр PC значение из памяти по адресу0x08000004
— там хранится адрес ResetHandler'а).
Обратите внимание, если вы не хотите загружать файл прошивки из Eclipse, то параметры Load image и Run commands указывать не нужно.
После нажатия Debug можно работать в режиме отладчика:
- пошаговое выполнение кода
- взаимодействие с точками останова
Примечание. В Eclipse есть, хмм… некоторые особенности… и с ними приходится жить. Вот, например, если при запуске отладчика появится сообщение "No source available for "0x0"", то выполните команду Step (F5)
Вместо заключения
Эмуляция нативного кода — дело весьма интересное. Для разработчика устройств появляется возможность отлаживать прошивку без реального устройства. Для исследователя — возможность проводить динамический анализ кода, что не всегда возможно даже при наличии устройства.
Мы хотим предоставить специалистам инструмент, который был бы удобен, в меру прост и не отнимал много сил и времени на свою настройку и запуск.
Напишите в комментариях о своём опыте использования аппаратных эмуляторов. Приглашаем к обсуждению и будем рады ответить на вопросы.
Автор: Павел Русанов