Среди всех методов в наступательной безопасности (offensive security) можно выделить направленный поиск уязвимостей как наиболее актуальный при исследовании безопасности различных устройств из мира IoT/IIoT. Чаще всего такие устройства анализируются методом черного ящика, когда исследователю практически ничего не известно об объекте исследования. Как правило, это означает, что доступа к исходным кодам прошивки обычно нет, документация также отсутствует, и все, что в лучшем случае есть в распоряжении исследователя – руководство пользователя и несколько веток на каком-нибудь форуме с обсуждением работы устройства.
Поиск уязвимостей в устройствах IoT/IIoT основан на работе с прошивкой и включает в себя несколько этапов – подготовка прошивки (ее извлечение, распаковка), поиск интересных с точки зрения исследователя компонентов, запуск прошивки или ее частей с помощью эмулятора и, наконец, сам поиск уязвимостей. На последнем этапе применяются различные методы, включая статический и динамический анализ и фаззинг.
Пожалуй, чаще всего исследователи при изучении прошивок используют классическую связку из эмулятора QEMU и отладчика GNU Debugger. Мы решили рассказать про менее очевидные инструменты для работы с прошивками, каждый из которых имеет свои особенности, преимущества, и ограничения, которые делают их эффективными для решения тех или иных задач – в частности, о Renode и Qiling.
Renode – это инструмент, позволяющий эмулировать целевую систему полностью, включая эмуляцию микросхем памяти, сенсоров, дисплеев и другой периферии, а также взаимодействия между собой нескольких процессоров (на многопроцессорных устройствах), каждый из которых может иметь свою архитектуру и прошивку. Кроме того, Renode позволяет связать между собой эмулируемое оборудование и реальное «железо», реализованное в микросхеме ПЛИС.
Qiling – это продвинутый кроссплатформенный фреймворк для эмуляции исполняемых файлов, способный эмулировать множество ОС и окружений. С различной степенью зрелости в нем доступна эмуляция Windows, MacOS, Linux, QNX, BSD, UEFI, DOS, MBR и виртуальной машины Ethereum, поддержка архитектур x86, x86_64, ARM, ARM64, MIPS и 8086, поддержка различных форматов исполняемых файлов, а также эмуляция загрузки MBR.
В качестве объекта исследования мы выбрали реальное устройство – сетевой видеорегистратор одного из крупных производителей. Устройство построено на платформе компании HiSilicon, а в качестве операционной системы использует Linux.
Скачанная с сайта производителя прошивка состоит из единственного файла, в котором в результате анализа утилитой binwalk обнаруживается образ файловой системы CramFS. После его распаковки внутри находим комбинированный образ ядра Linux и initramfs — uImage, а также несколько зашифрованных скриптов и tar-архивов.
1 2 3 4 5 6 7 |
DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 uImage header, header size: 64 bytes, header CRC: 0xCA9A1902, created: 2019-08-23 07:16:16, image size: 4414954 bytes, Data Address: 0x40008000, Entry Point: 0x40008000, data CRC: 0xDE0F30AC, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "Linux-3.18.20" 64 0x40 Linux kernel ARM boot executable zImage (little-endian) 2464 0x9A0 device tree image (dtb) 16560 0x40B0 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: -1 bytes 4401848 0x432AB8 device tree image (dtb) |
Рассмотрим, как работают Renode и Qiling на системном уровне.
О том, как использовать эти инструменты на прикладном уровне (на примере прошивки видеорегистратора) смотрите в полной версии статьи.
Эмуляция на системном уровне с помощью Renode
Renode – это инструмент полносистемной эмуляции, который, в первую очередь, позиционируется разработчиками как инструмент, предназначенный для облегчения процесса разработки, отладки и автоматизированного тестирования встроенного ПО. Однако, его возможности позволяют использовать его и для целей динамического анализа поведения исследуемых систем при направленном поиске уязвимостей. С помощью Renode можно запускать как небольшие встраиваемые ОСРВ, так и полноценные операционные системы, такие как Linux и QNX. Большая часть эмулятора написана на языке C#, что позволяет относительно быстро адаптировать его функциональность под потребности исследователя.
Описание эмулируемой платформы
Как правило, периферийные устройства, входящие в состав однокристальных систем, доступны посредством Memory Mapped I/O (MMIO) – регионов физической памяти, на которые отражаются регистры соответствующих периферийных модулей. Renode предоставляет возможность собрать систему на кристалле как конструктор – из блоков, с помощью специального конфигурационного файла с расширением .repl (REnode PLatform), описывающего то, какие устройства следует разместить по каким адресам в памяти.
Информацию о доступных периферийных устройствах, а также карте распределения памяти в применяемой платформе можно почерпнуть из документации на SoC (при ее наличии в открытом доступе), а при ее отсутствии, например, проанализировав содержимое DTB (Device Tree Blob), – блока данных с описанием платформы для ядра Linux, который необходим для запуска Linux на встраиваемых устройствах.
В исследуемой прошивке блок DTB, по информации от того же binwalk, присоединен в конец файла uImage. Преобразовав DTB в читаемый формат (DTS) с помощью утилиты dtc, мы можем на его основе составить описание платформы для Renode.
Запуск эмуляции
Для того чтобы запустить что-то полезное на описанной в REPL-файле платформе, необходимо подготовить скрипт инициализации. Как правило в таком скрипте происходит загрузка исполняемого кода в виртуальную память, настройка регистров процессора, установка дополнительных обработчиков событий, настройка вывода отладочных сообщений (если необходимо) и др.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
:name: HiSilicon :description: To run Linux on HiSilicon using sysbus $name?="HiSilicon" mach create $name machine LoadPlatformDescription @platforms/cpus/hisilicon.repl logLevel 0 ### create externals ### showAnalyzer sysbus.uart0 ### redirect memory for Linux ### sysbus Redirect 0xC0000000 0x40000000 0x8000000 ### load binaries ### sysbus LoadBinary "/home/research/digicap.out/uImage" 0x40008000 sysbus LoadAtags "console=ttyS0,115200 mem=128M@0x40000000 nosmp maxcpus=0" 0x8000000 0x40000100 ### set registers ### cpu SetRegisterUnsafe 2 0x40000100 # atags cpu PC 0x40008040 |
Скрипт загружает uImage-файл в память платформы по адресу, который был получен из вывода binwalk, настраивает аргументы ядра и передает управление по адресу 0x40008040, так как первые 0x40 байт – это заголовок uImage.
Запустив эмуляцию, мы получаем полнофункциональный терминал, с которым можно взаимодействовать как обычно в любой Linux-системе:
Предоставляемых эмулятором Renode возможностей достаточно для быстрого начала работы с исследуемой прошивкой в динамике. В качестве наглядного примера нам удалось частично запустить прошивку сетевого видеорегистратора без самого регистратора. Далее с помощью имеющихся на эмулируемой файловой системе утилит можно расшифровать зашифрованные файлы прошивки, извлечь и проанализировать логику работы модулей ядра, обеспечивающих функциональность регистратора, и т. д.
Благодаря достаточно обширной поддержке эмулятором Renode периферии, часто используемой в системах на кристалле на базе архитектуры ARM, для того чтобы увидеть полнофункциональный терминал Linux часто даже не требуется писать какой-либо дополнительный код. В то же время, в случае возникновения такой необходимости, модульная архитектура самого эмулятора и предоставляемые им возможности написания скриптов и плагинов позволяют с относительно низкими трудозатратами реализовать поддержку отсутствующей функциональности на достаточном для проведения исследования уровне.
Из особенностей данного инструмента стоит отметить его низкоуровневость – эмуляция происходит на системном уровне, поэтому с его использованием сложно провести, например, фаззинг или отладку какого-либо user-space приложения, работающего в эмулируемой операционной системе.
К минусам же можно отнести отсутствие подробной документации – в имеющейся документации описаны лишь самые базовые сценарии работы с инструментом, а в процессе создания чего-то более сложного, как, например, нового периферийного устройства, а также для того, чтобы понять, как работает та или иная встроенная команда, придется неоднократно обращаться к GitHub-репозиторию проекта и изучать исходный код как самого эмулятора, так и имеющихся реализаций периферийных устройств.
Фаззинг с помощью Qiling Framework
Qiling Framework написан на языке Python, что позволяет достаточно легко адаптировать его функциональность под свои нужды. Qiling Framework использует под капотом движок Unicorn для эмуляции. Однако Unicorn – это просто эмулятор машинных инструкций, в то время как Qiling — это надстройка, предоставляющая большое количество высокоуровневых функций, таких как поддержка чтения файлов с файловой системы, загрузка динамических библиотек, и т. д.
В сравнении с QEMU Qiling Framework позволяет эмулировать большее количество платформ, предоставляет возможность гибкой настройки процесса эмуляции, включая, например, изменение выполняющегося кода на лету, и также является кроссплатформенным, то есть позволяет эмулировать, например, исполняемые файлы Windows или QNX на Linux и наоборот.
В рамках демонстрации попробуем запустить с помощью Qiling фаззинг имеющейся в прошивке утилиты hrsaverify с помощью AFL++. Данная утилита предназначена для проверки корректности зашифрованных файлов и принимает в качестве аргумента путь к файлу для проверки. В репозитории Qiling Framework уже имеется несколько примеров запуска фаззера AFL++ в директории examples/fuzzing. Для запуска утилиты hrsaverify мы адаптируем один из них, а именно linux_x8664. Перепишем скрипт запуска фаззера следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
import unicornafl as UcAfl UcAfl.monkeypatch() import os, sys from typing import Any, Optional sys.path.append("../../..") from qiling import Qiling from qiling.const import QL_VERBOSE from qiling.extensions import pipe def main(input_file: str): ql = Qiling(["../../rootfs/hikroot/usr/bin/hrsaverify", "/test"], "../../rootfs/hikroot", verbose=QL_VERBOSE.OFF, # keep qiling logging off console=False, # thwart program output stdin=None, stdout=None, stderr=None) # don't care about stdin/stdout def place_input_callback(uc: UcAfl.Uc, input: bytes, persistent_round: int, data: Any) -> Optional[bool]: """Called with every newly generated input.""" with open("../../rootfs/hikroot/test", "wb") as f: f.write(input) def start_afl(_ql: Qiling): """Callback from inside.""" # We start our AFL forkserver or run once if AFL is not available. # This will only return after the fuzzing stopped. try: if not _ql.uc.afl_fuzz(input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point]): _ql.log.warning("Ran once without AFL attached") os._exit(0) except UcAfl.UcAflError as ex: if ex.errno != UcAfl.UC_AFL_RET_CALLED_TWICE: raise # Image base address ba = 0x10000 # Set a hook on main() to let unicorn fork and start instrumentation ql.hook_address(callback=start_afl, address=ba + 0x8d8) # Okay, ready to roll ql.run() if __name__ == "__main__": if len(sys.argv) == 1: raise ValueError("No input file provided.") main(sys.argv[1]) |
Первое, на что следует обратить внимание, – это базовый адрес исполняемого файла (в нашем случае 0x10000), адрес функции main. Иногда бывает необходимо дополнительно установить перехватчики на другие адреса, факт перехода на которые фаззеру следует рассматривать как падение. Например, в примере запуска AFL в среде QNX (в директории qnx_arm) устанавливается такой дополнительный обработчик на адрес функции SignalKill в libc. В случае с hrsaverify можно обойтись без дополнительных обработчиков. Также следует обратить внимание, что все файлы, которые должны быть доступны для выполняемого приложения, следует помещать в sysroot и передавать пути к ним относительно него (в данном примере это ../../rootfs/hikroot/).
Непосредственно запуск AFL++ выполняется следующей командой:
1 |
AFL_AUTORESUME=1 AFL_PATH="$(realpath ./AFLplusplus)" PATH="$AFL_PATH:$PATH" afl-fuzz -i afl_inputs -o afl_outputs -U -- python ./fuzz_arm_linux.py @@ |
В результате будет запущен фаззер AFL, и через некоторое время мы увидим результат его работы:
Qiling – перспективный инструмент, основными плюсами которого является высокая гибкость, легкая расширяемость, поддержка большого количества архитектур и окружений. Фреймворк может заменить QEMU в случаях, когда использование последнего не представляется возможным (неподдерживаемая целевая ОС или отсутствие необходимых дополнительных возможностей, таких как установка произвольных обработчиков на любые адреса памяти, особая обработка прерываний и т. д.). Платой за высокую гибкость и низкий порог вхождения за счет использования языка высокого уровня Python является относительно низкая скорость эмуляции и фаззинга.
Полная версия статьи опубликована на сайте Kaspersky ICS CERT.
Динамический анализ компонентов прошивок IoT-устройств