Носорог внутри кота — запускаем прошивку в эмуляторе 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)



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


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


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


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

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Для чего Вы используете эмулятор?

    Какой софт Вы используете для эмуляции нативного кода?

    Что бы Вам хотелось улучшить в используемом эмуляторе?

    • +18
    • 2,8k
    • 4
    ИНФОРИОН
    83,00
    Решения ИТ-инфраструктуры и защита информации
    Поделиться публикацией

    Комментарии 4

      0

      На гитхабе ничего кроме редми нет, каковы планы с открытием API?

        +1
        К концу апреля будет релиз community-версии
        На гитхабе будут различные уже готовые модули
        API будет открыто, будут примеры и доки по нему =)
        0
        Какая часть JSON-описания генерируется автоматически, а что пишется вручную?
          0
          На данный момент JSON заполняется вручную по шаблону

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое