恶意意图:利用Mali GPU漏洞实现Android提权(CVE-2022-22706 / CVE-2021-39793)

本文深入分析了ARM Mali GPU驱动中的两个高危漏洞(CVE-2022-22706/CVE-2021-39793),通过技术细节展示了如何从非特权应用获得内核内存写入能力,并逐步实现SELinux绕过和root反向shell获取的全过程。

恶意意图:利用GPU漏洞实现Android提权

想象一下从第三方应用商店下载游戏。您授予它看似无害的权限,但隐藏在应用中的恶意漏洞利用程序允许攻击者窃取您的照片、窃听对话,甚至完全控制设备。这就是CVE-2022-22706和CVE-2021-39793等漏洞带来的威胁,本文将深入剖析这些影响众多Android设备中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, Oct 2021)
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、读取和写入的权限

通过将钩子和有效负载注入到只读共享库中,我们可以操纵特权进程(如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、读取和写入。幸运的是,低特权域如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
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
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计