ИНФОРИОН corporate blog
Virtualization
Reverse engineering
Kotlin
Programming microcontrollers
March 29

Носорог внутри кота — запускаем прошивку в эмуляторе Kopycat


В рамках встречи 0x0A DC7831 DEF CON Нижний Новгород 16 февраля мы представили доклад о базовых принципах эмуляции бинарного кода и собственной разработке — эмуляторе аппаратных платформ Kopycat.


В статье мы приведём описание запуска прошивки устройства в эмуляторе, продемонстрируем взаимодействие с отладчиком и выполним небольшой динамический анализ прошивки.


Предыстория


A long time ago in a galaxy far far away


Пару лет назад в нашей лаборатории возникла необходимость исследовать прошивку устройства. Прошивка была сжата, распаковывалась bootloader'ом. Делал он это весьма замороченным способом, несколько раз перекладывая данные в памяти. Да и сама прошивка потом активно взаимодействовала с периферией. И всё это на ядре MIPS.


Имеющиеся эмуляторы по объективным причинам нас не устроили, а хотелось всё-таки запустить код. Тогда решили сделать свой эмулятор, который сделает минимум и позволит распаковать основную прошивку. Попробовали — получилось. Подумали, а что если добавить периферию, чтобы еще и основную прошивку выполнять. Было не очень больно — и тоже получилось. Снова подумали и решили делать полноценный эмулятор.


В итоге получился эмулятор вычислительных систем Kopycat.



Почему Kopycat?

Имеет место игра слов.


  1. copycat (англ., сущ. [ˈkɒpɪkæt]) — подражатель, имитатор
  2. cat (англ., сущ. [ˈkæt]) — кошка, кот — любимое животное одного из создателей проекта
  3. Буква "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-файле.


JSON на 105 строк
{
  "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):


Виртуальные COM-порты

Использование com0com


Виртуальные COM-порты настраиваются setup-утилитой из комплекта com0com (консольная версия — C:\Program Files (x86)\com0com\setupс.exe, или GUI-версия — C:\Program Files (x86)\com0com\setupg.exe):


Настройка виртуальных COM-портов

Следует установить галочки 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:


bin\kopycat -g 23946 -n rhino -l user -y library -p firmware=firmware\rhino_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 — путь для поиска модулей, входящих в устройство;
  • firmware\rhino_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

Настройка подключения к GDB-серверу

Теперь становится доступна кнопка запуска отладки (клавиша F9):



Нажимаем её — происходит подключение к модулю отладчика в эмуляторе. IDA переходит в режим отладки, становятся доступны дополнительные окна: информация о регистрах, о стеке.


Теперь мы можем использовать все стандартные возможности работы с отладчиком:


  • пошаговое выполнение инструкций (Step into и Step over — клавиши F7 и F8, соответственно);
  • запуск и приостановка выполнения;
  • создание точек останова как на код, так и на данные (клавиша F2).

Подключение к отладчику не означает запуска кода прошивки. Текущей позицией для выполнения должен быть адрес 0x08006A74 — начало функции Reset_Handler. Если прокрутить листинг ниже, то можно увидеть вызов функции main. Можно установить курсор на этой строке (адрес 0x08006ABE) и выполнить операцию Run until cursor (клавиша F4).


image


Далее можно нажать F7, чтобы зайти в функцию main.


Если выполнить команду Continue process (клавиша F9), то появится окно "Please wait" с единственной кнопкой Suspend:



При нажатии Suspend выполнение кода прошивки приостанавливается и может быть продолжено с того же адреса в коде, где было прервано.


Если продолжить выполнение кода, то в терминалах, подключенных к виртуальным COM-портам, можно увидеть следующие строки:


image


image


Наличие строки "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 некоторое сообщение.


image


Следует установить breakpoint на обращение к переменной ledControlQueueHandle, расположенной по адресу 0x20000624 и продолжить выполнение кода:



В результате сначала произойдет останов по адресу 0x080057CA перед вызовом функции osMailAlloc, далее — по адресу 0x08005806 перед вызовом функции osMailPut, потом через некоторое время — по адресу 0x08005BD4 (перед вызовом функции osMailGet), который принадлежит функции leds_task_entry (LED-таск), то есть произошло переключение тасков, и теперь управление получил LED-таск.


image


Таким нехитрым способом можно установить, как таски 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 в команду запуска эмулятора:


bin\kopycat -g 23946 -n rhino -l user -y library -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)



Вместо заключения


Эмуляция нативного кода — дело весьма интересное. Для разработчика устройств появляется возможность отлаживать прошивку без реального устройства. Для исследователя — возможность проводить динамический анализ кода, что не всегда возможно даже при наличии устройства.


Мы хотим предоставить специалистам инструмент, который был бы удобен, в меру прост и не отнимал много сил и времени на свою настройку и запуск.


Напишите в комментариях о своём опыте использования аппаратных эмуляторов. Приглашаем к обсуждению и будем рады ответить на вопросы.

Для чего Вы используете эмулятор?
33.3% разрабатываю (отлаживаю) прошивки 4
58.3% исследую прошивки 7
25% запускаю игры (Dendi, Sega, PSP) 3
16.6% что-то другое (напишите в комментарии) 2
Voted 12 users. Passed 5 users.
Какой софт Вы используете для эмуляции нативного кода?
66.6% QEMU 8
0% Unicorn engine 0
16.6% Proteus 2
25% что-то другое (напишите в комментарии) 3
Voted 12 users. Passed 5 users.
Что бы Вам хотелось улучшить в используемом эмуляторе?
15.3% хочется скорости 2
53.8% хочется удобства настройки/запуска 7
46.1% хочется больше возможностей взаимодействия с эмулятором (API, хуки) 6
15.3% меня всё устраивает 2
7.6% что-то другое (напишите в комментарии) 1
Voted 13 users. Passed 2 users.
+18
2.9k 35
Comments 6
Top of the day