Перейти к основному содержимому
  1. Статьи/

Расчехляем Qiling: эмуляция вместо железа

·3 минут·

В проектах по анализу защищённости одним из самых “неблагодарных” дел является фаззинг прошивок. Подготовка стендов и окружений может занимать довольно много времени, а разнообразие архитектур, baremetal-реализаций и RTOS точно не способствует фаззингу “по методичке”.

Если же исследователь оказался завален сдампленными прошивками от множества устройств (а на некоторых проектах приходится работать с целым зоопарком железа), неизбежно возникает вопрос о расстановке приоритетов. Выбор, естественно, делается в пользу наиболее критичных подсистем и интерфейсов, однако и остальным пациентам хочется уделить должное внимание.

Фреймворк для эмуляции Qiling, названный в честь мифического китайского зверя Цилиня, решает объёмный спектр проблем за специалиста, подступающего к вышеописанной нелёгкой задаче.

Qiling берёт своё начало в небезызвестном фреймворке Unicorn, что обещает нам обилие эмулируемых архитектур (8086, X86, X86_64, ARM, ARM64, MIPS, RISC-V, PowerPC). Архитектурное разнообразие Unicorn имеет обратную сторону в виде отсутствия возможностей по работе с кодом, предполагающим запуск в операционных системах (syscalls, FS, etc.).

Здесь-то Qiling и проявляет себя во всей красе. Он не просто имеет хороший набор уже реализованных сущностей, но и предоставляет минималистичные rootfs-ы для многих наборов архитектур и операционных систем. А ещё всё это имеет реализацию на Python, что позволяет практически безболезненно реализовать что-то отсутствующее прямо “на ходу”.

Итак, переходим к фаззингу!

(1) Локализуйте интересующий код (Code of Interest). Qiling в связке с AFL++ позволит нам профаззить не весь код, а только интересующее место, что сильно уменьшает время подготовки:

   ql.run(start=0xaddr1, end=0xaddr2) # исполняемся откуда и докуда хотим

(2) Соберите информацию о требующихся CoI-сущностях. Обращения к периферии чипа, общение с интерфейсами, файловой системой и буквально всё остальное, что может помещать CoI успешно выполниться. Отличным выходом будут итеративные попытки запуска эмуляции:

ql = Qiling([r'path/to/target'], r'path/to/target/rootfs')
  ql.run(start=0xaddr1, end=0xaddr2)

(3) Реализуйте/захукайте места, найденные в п.2. Серьёзный арсенал хуков:

  ql.hook_address(_callback_: Callable, _address_: int) # просто на адресе
  ql.hook_code(_callback_: Callable, _user_data_: Any = None) # каждая инструкция
  ql.hook_block() # каждый basic block
  ql.hook_insn() # инструкция с настройкой особенностей
  ql.hook_mem_unmapped() # на доступ к незамапленной памяти
  #  и т.д.

(4) Соберите информацию об ограничениях входных данных. CoI может не проверять, к примеру, длину ввода - но это совсем не значит, что вызывающая функция “двумя этажами выше” тоже этого не делает.

(5) Соберите rootfs, если ваш бинарник касается операционных систем:

Available rootfs

(6) Выделите память для входов, генерируемых AFL++. Легко:

  ql.mem.map(addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str] = None) -> None # выделение памяти
  ql.mem.write(address, data) # запись

(7) Включите встроенный sanitizing или реализуйте его сами. Но помните, что каждый лишний хук портит скорость работы AFL++.

  heap = QlSanitizedMemoryHeap(ql, ql.os.heap, fault_rate=fault_rate) # для некоторых OS доступен, к примеру, такой вариант

(8) Анализируя логику исполнения, найдите лучший из возможных момент исполнения, в который начнёт свою работу forkserver. Требуется встроить всего несколько строк, чтобы наш код стал target-ом для фаззера:

  ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point]) # внутри коллбека start_afl() 
  ...
  ql.hook_address(callback=start_afl, address=frksrv_addr) # устанавливаем хук в выбранный адрес для forkserver-а 

(9) Отладьте весь сетап вместе с AFL++ и начинайте фаззить! GDB включается очень просто:

Checking with GDB

Результаты его работы:

GDB Results

(10) Обратите внимание, что AFL++ может не воспринять ситуацию краша как краш, что фатально для процесса. Необходимым решением будет “переопределение” крашей для AFL:

force crash patching
force crash patching

Enjoy responsibly! Перед применением описанных методик убедитесь в соответствии ваших действий местному законодательству, а при обнаружении уязвимостей в коммерческих продуктах следуйте принципам ответственного разглашения.

Related

2025

·9 минут