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

gipb7e8ucwinvebjerxy1fk2d-8.jpeg

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

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


Предыстория

A long time ago in a galaxy far far away

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

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

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


ebcxb6pxtiypy0s-usvu6i_hdwu.png

Почему 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):


afmu-qpi0ymfpsga-orunargmas.png

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

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


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

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


wxk43r-316cpnsdabb9eydkzus4.png

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

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


9e78tgoterc50rqvmosunzp1u5i.png

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

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


fivij-hshz3izcxoo0lnewnznc8.png
2mcnoqd__te1i0sq5z28jd29mdg.png

Наличие строки «state bypass» говорит о том, что виртуальный Bluetooth-модуль перешёл в режим приёма данных от COM-порта пользователя.

Теперь в Bluetooth-терминале (на рисунке — COM29) можно вводить команды в соответствии с протоколом «Носорога». Например, на команду «MEOW» в Bluetooth-терминал вернётся строка «mur-mur»:


yqtdabgfdd3jlzswsrk3rvbdzds.png

Эмулируй меня не полностью

При построении эмулятора можно выбирать степень детализации/эмуляции того или иного устройства. Так, например, модуль Bluetooth можно эмулировать по-разному:


  • эмулируется полностью устройство с полным набором команд;
  • эмулируются AT-команды, а поток данных принимается с COM-порта основной системы;
  • виртуальное устройство обеспечивает полное перенаправление данных на реальное устройство;
  • в виде простой заглушки, которая всегда возвращает «OK».

В текущей версии эмулятора используется второй подход — виртуальный Bluetooth-модуль выполняет конфигурирование, после чего переходит в режим «проксирования» данных из COM-порта основной системы в UART-порт эмулятора.


e65cm_tl2bggpbg0-q0azjvdz64.jpeg

Рассмотрим возможность простой инструментации кода в случае, если не реализована какая-то часть периферии. Например, если не создан таймер, отвечающий за контроль передачи данных в DMA (проверка выполняется в функции ws2812b_wait, расположенной по адресу 0x08006840), то прошивка будет всегда ждать сброса флага busy, расположенного по адресу 0x200004C4, который показывает занятость линии данных DMA:


ptjjsutfya2dkoqdoshkeyeeqzw.png

Мы можем обойти такую ситуацию путём «ручного» сброса флага 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


ziza-ddx3arqlqfbd-o6jvdmnjc.png

После этого нажимаем Run и закрываем окно скриптов.
Теперь перейдём в код по адресу 0x0800688A, установим breakpoint (клавиша F2), отредактируем его (контекстное меню Edit breakpoint…), не забудем установить тип скрипта — Python:


fopn1vb8fbxqrhn6xoia_rgnsau.png


h6yqb29r1m9sngr3csfimxcxsew.png

Если текущее значение флага busy равно 1, то следует выполнить функцию skip_dma в строке скриптов:


m-t8in2th7ei6neigdzgzq17kxy.png

Если запустить прошивку на выполнение, то срабатывание кода breakpoint-обработчика можно увидеть в IDA в окне Output по строке Skipping wait ws2812.... Теперь прошивка не будет ожидать сброс флага busy.


Взаимодействие с эмулятором

Эмуляция ради эмуляции вряд ли вызовет восторг и радость. Гораздо интереснее, если эмулятор поможет исследователю увидеть данные в памяти или установить взаимодействие потоков.

Покажем, как в динамике установить взаимодействие RTOS-тасков. Предварительно следует приостановить выполнение кода, если оно запущено. Если перейти в функцию bluetooth_task_entry в ветку обработки команды «LED » (адрес 0x080057B8), то можно увидеть, что сначала создается, а потом отправляется в системную очередь ledControlQueueHandle некоторое сообщение.


pobkzmapmust02eaafle7d_pxba.png

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


evwog3vrv7ozxlxjozd4itvve0a.png

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


thnizhzqykfn20l9-btkuqhawlc.png

Таким нехитрым способом можно установить, как таски 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 modules -p firmware=null,tty_dbg=COM26,tty_bt=COM28


Настройка debug-конфигурации

В Eclipse выбираем меню Run — Debug Configurations… В открывшемся окне в разделе GDB Hardware Debugging необходимо добавить новую конфигурацию, после чего на вкладке «Main» указать текущий проект и приложение для отладки:


f-ykeob2i5vg25qbrz_egbqntou.png

На вкладке «Debugger» необходимо указать GDB-команду:
${openstm32_compiler_path}\arm-none-eabi-gdb

А также ввести параметры для подключения к GDB-серверу (хост и порт):


efudfommb7a9gkr4kcvp0vtnqfm.png

На вкладке «Startup» необходимо указать следующие параметры:


  • включить галочку Load image (чтобы выполнялась загрузка в эмулятор собранного образа прошивки);
  • включить галочку Load symbols;
  • добавить команду запуска: set $pc = *0x08000004 (выставить в регистр PC значение из памяти по адресу 0x08000004 — там хранится адрес ResetHandler’а).

Обратите внимание, если вы не хотите загружать файл прошивки из Eclipse, то параметры Load image и Run commands указывать не нужно.


w3q82fvei4c91385rlcipmuiy_i.png

После нажатия Debug можно работать в режиме отладчика:


  • пошаговое выполнение кода
    1h6nax1gpgcdyrwakdmzzlu6_x8.png
  • взаимодействие с точками останова
    4dn15e4hzaukbif58ggwhy_wg18.png

Примечание. В Eclipse есть, хмм… некоторые особенности… и с ними приходится жить. Вот, например, если при запуске отладчика появится сообщение «No source available for »0×0», то выполните команду Step (F5)


rauzko55hgnj1y_uapnqbz8ftrw.png


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

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

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

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

© Habrahabr.ru