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



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


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


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


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

    Only registered users can participate in poll. Log in, please.

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

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

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

    • +18
    • 3.2k
    • 6
    ИНФОРИОН
    79.73
    Решения ИТ-инфраструктуры и защита информации
    Share post

    Comments 6

      0

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

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

        Only users with full accounts can post comments. Log in, please.