最近,在 Linux 内核 TEE 子系统中发现了一个释放后使用漏洞,影响版本直至并包括 5.15.11,已分配编号 CVE-2021-44733 [1]。
乍一看,由于多种原因,它似乎不可利用;然而,在进一步分析漏洞代码路径并实现一个粗糙的概念验证漏洞利用后,可以覆盖内核中的函数指针。本文不提供权限提升载荷,但运行 OPTEE 和该漏洞利用的完整环境可供进一步测试,请参阅“设置环境”部分。
TEE(可信执行环境,Trusted Execution Environment)是一种运行在安全环境中的可信操作系统,例如 ARM CPU 上的 TrustZone。TEE 驱动程序负责处理与 TEE 通信所需的细节。驱动程序的一些更重要的职责是基于 Globalplatform TEE Client API 规范 [3] 向 TEE 提供通用 API,同时还管理 Linux 与 TEE 之间的共享内存。该子系统可以通过在 ARM 架构的内核配置中启用 CONFIG_OPTEE 来开启。
安全世界包含被记为 OP-TEE OS 的可信操作系统 [4]。在该操作系统之上,可以运行所谓的可信应用(Trusted Applications,TA),它们可以在隔离环境中执行某些操作,参见图 1。
图 1:TEE 概览——来自 Linaro 的演示 [5]
普通世界(Linux 用户空间/内核)可以使用客户端应用程序(CA)以及 TEE 子系统暴露的 API 与这些应用程序交互。CA 可以打开一个到特定 TA 的会话,并调用该 TA 实现的函数。TA 与 CA 之间任何参数的传递都是通过共享内存完成的。 下面描述使用所有相关系统调用的 CA 与 TA 之间的交互。
CA 打开 /dev/tee[0-9] 与驱动程序通信。注意,对于这些 API 的常规使用方式,这是通过 libteec 隐式完成的。
CA 可以使用 TEE_IOC_SHM_ALLOC IOCTL 注册共享内存。这会分配共享内存并返回一个文件描述符,用户空间可以将其用作 mmap 的一部分。
下一步是使用 TEE_IOC_OPEN_SESSION IOCTL 建立会话,并指定特定 TA 的 uuid。该 uuid 在 TA 编译期间被硬编码。
为了调用 TA 中的任何特定函数,CA 通过指定函数标识符以及任何输入参数来调用,这通过 TEE_IOC_INVOKE 完成。
当 CA 完成所有请求后,可以使用 TEE_IOC_CLOSE_SESSION 关闭会话。
图 2:CA 与 TA 之间的会话——来自 Linaro 的演示 [5]
客户端与 TEE 之间的大部分通信对驱动程序来说是不透明的。驱动程序的主要工作是管理上下文、接收来自客户端的请求、将其转发给 TEE,并将结果返回 [2]。
CVE-2021-44733 是通过使用 syzkaller 进行模糊测试发现的。用于此目的的描述文件如下所示。请注意,ioctl$TEE_SHM_REGISTER_FD 仅属于 Linaro(维护者)内核树,而不在上游中。如果根据 syzkaller 文档 [6] 正确配置,“设置环境”中提供的环境可用于模糊测试。```
#include <uapi/linux/tee.h>
resource fd_tee0[fd] resource session_resource[int32]
openat$tee0(fd const[AT_FDCWD], dev ptr[in, string["/dev/tee0"]], flags flags[open_flags], mode flags[open_mode]) fd_tee0 ioctl$TEE_OPEN_SESSION(fd fd_tee0, cmd const[0x8010a402], arg ptr[inout, tee_ioctl_buf_data_session]) ioctl$TEE_INVOKE(fd fd_tee0, cmd const[0x8010a403], arg ptr[inout, tee_ioctl_buf_data_invoke]) ioctl$TEE_CANCEL(fd fd_tee0, cmd const[0x8008a404], arg ptr[in, tee_ioctl_buf_data_cancel]) ioctl$TEE_CLOSE_SESSION(fd fd_tee0, cmd const[0x8004a405], arg ptr[in, tee_ioctl_buf_data_close]) ioctl$TEE_VERSION(fd fd_tee0, cmd const[0x800ca400], arg ptr[out, tee_ioctl_buf_data_version]) ioctl$TEE_SHM_ALLOC(fd fd_tee0, cmd const[0xc010a401], arg ptr[inout, tee_ioctl_buf_data_shm_alloc]) ioctl$TEE_SHM_REGISTER(fd fd_tee0, cmd const[0xc018a409], arg ptr[inout, tee_ioctl_buf_data_shm_register]) ioctl$TEE_SHM_REGISTER_FD(fd fd_tee0, cmd const[0xc018a408], arg ptr[inout, tee_ioctl_buf_data_shm_register_fd]) ioctl$TEE_SUPPL_RECV(fd fd_tee0, cmd const[0x8010a406], arg ptr[inout, tee_ioctl_buf_suppl_recv]) ioctl$TEE_SUPPL_SEND(fd fd_tee0, cmd const[0x8010a407], arg ptr[inout, tee_ioctl_buf_suppl_send])
#=======================================================
define TEE_IOCTL_UUID_LEN 16
tee_ioctl_param_struct { attr flags[TEE_IOCTL_PARAM_ATTR_TYPE, int64] a int64 b int64 c int64 }
TEE_IOCTL_PARAM_ATTR_TYPE = 0, 1, 2, 3, 5, 6, 7 TEE_LOGIN = 0, 1, 2, 4, 5, 6
#=======================================================
tee_ioctl_buf_data_session { buf_ptr ptr64[inout, tee_ioctl_open_session_struct] buf_len len[buf_ptr, int64] }
tee_ioctl_open_session_struct { uuid array[int8, TEE_IOCTL_UUID_LEN] (in) clnt_uuid array[int8, TEE_IOCTL_UUID_LEN] (in) clnt_login flags[TEE_LOGIN, int32] (in) cancel_id int32 (in) session session_resource (out) ret int32 (out) ret_origin int32 (out) num_params len[params, int32] (in) params array[tee_ioctl_param_struct] (in) }
#=======================================================
tee_ioctl_buf_data_invoke { buf_ptr ptr64[inout, tee_ioctl_invoke_struct] buf_len len[buf_ptr, int64] }
tee_ioctl_invoke_struct { func int32 (in) session session_resource (in) cancel_id int32 (in) ret int32 (out) ret_origin int32 (out) num_params len[params, int32] (in) params array[tee_ioctl_param_struct] (in) }
#=======================================================
tee_ioctl_buf_data_cancel { cancel_id int32 (in) session session_resource (in) }
#=======================================================
tee_ioctl_buf_data_close { session session_resource (in) }
#=======================================================
tee_ioctl_buf_data_version { impl_id int32 (out) impl_caps int32 (out) gen_caps int32 (out) }
#=======================================================
tee_ioctl_buf_data_shm_alloc { size int64 (inout) flags const[0, int32] (inout) id int32 (out) }
#=======================================================
tee_ioctl_buf_data_shm_register { addr int64 (in) length int64 (inout) flags const[0, int32] (inout) id int32 (out) }
#=======================================================
tee_ioctl_buf_data_shm_register_fd { fd int64 (in) size int64 (out) flags const[0, int32] (in) id int32 (out) } [align[8]]
#=======================================================
tee_ioctl_buf_suppl_recv { func int32 (in) num_params len[params, int32] (inout) params array[tee_ioctl_param_struct] (inout) }
#=======================================================
tee_ioctl_buf_suppl_send { ret int32 (out) num_params len[params, int32] (in) params array[tee_ioctl_param_struct] (in) }
在模糊测试期间,引起注意的崩溃与在持有互斥锁时 task_struct 对象的释放后使用(use-after-free)有关:```
==================================================================
BUG: KASAN: use-after-free in __mutex_lock.constprop.0+0x118c/0x11c4
Read of size 4 at addr 863b0714 by task optee_example_r/244
CPU: 0 PID: 244 Comm: optee_example_r Tainted: G D 5.14.0 #151
Hardware name: Generic DT based system
[<8012b204>] (unwind_backtrace) from [<8011f460>] (show_stack+0x20/0x24)
[<8011f460>] (show_stack) from [<81cf0108>] (dump_stack_lvl+0x5c/0x68)
[<81cf0108>] (dump_stack_lvl) from [<80650f04>] (print_address_description.constprop.0+0x38/0x304)
[<80650f04>] (print_address_description.constprop.0) from [<80651548>] (kasan_report+0x1c0/0x1dc)
[<80651548>] (kasan_report) from [<81d0a9b4>] (__mutex_lock.constprop.0+0x118c/0x11c4)
[<81d0a9b4>] (__mutex_lock.constprop.0) from [<81d0ada4>] (mutex_lock+0x128/0x13c)
[<81d0ada4>] (mutex_lock) from [<817424b0>] (tee_shm_release+0x4b0/0x6cc)
[<817424b0>] (tee_shm_release) from [<81303674>] (dma_buf_release+0x1b8/0x2f0)
[<81303674>] (dma_buf_release) from [<806d5ac0>] (__dentry_kill+0x4c4/0x678)
[<806d5ac0>] (__dentry_kill) from [<806d8a68>] (dput+0x630/0xba4)
[<806d8a68>] (dput) from [<8067d890>] (__fput+0x3b4/0x900)
[<8067d890>] (__fput) from [<801dd1d8>] (task_work_run+0x15c/0x230)
[<801dd1d8>] (task_work_run) from [<80172b70>] (do_exit+0x103c/0x3770)
[<80172b70>] (do_exit) from [<80179aec>] (do_group_exit+0x134/0x3ac)
[<80179aec>] (do_group_exit) from [<801a7658>] (get_signal+0x7d8/0x2f28)
[<801a7658>] (get_signal) from [<8011dea4>] (do_work_pending+0x984/0x154c)
[<8011dea4>] (do_work_pending) from [<801000d0>] (slow_work_pending+0xc/0x20)
Exception stack(0x85743fb0 to 0x85743ff8)
3fa0: 00023108 00000080 00000000 00000000
3fc0: 66bca2d0 66bca2d0 66bca2d0 000000f0 66bca2d0 66bca340 00000000 6ec00b0c
3fe0: 66bc9cc8 66bc9cb8 00011655 66c80c20 000e0130 00023108
Allocated by task 242:
set_alloc_info+0x48/0x50
__kasan_slab_alloc+0x48/0x58
kmem_cache_alloc+0x14c/0x314
copy_process+0x2014/0x7b18
kernel_clone+0x244/0xfc8
sys_clone+0xc8/0xec
ret_fast_syscall+0x0/0x58
0x6ec00a10
Freed by task 67:
kasan_set_track+0x28/0x30
kasan_set_free_info+0x20/0x34
__kasan_slab_free+0xdc/0x108
kmem_cache_free+0x80/0x394
__put_task_struct+0x2b4/0x35c
delayed_put_task_struct+0x104/0x384
rcu_core+0x91c/0x2a68
__do_softirq+0x2fc/0xfb8
Last potentially related work creation:
kasan_record_aux_stack+0xb8/0xc0
call_rcu+0x9c/0xfd0
put_task_struct_rcu_user+0x9c/0xbc
finish_task_switch+0x534/0xa10
__schedule+0x934/0x1adc
schedule_idle+0x9c/0x120
do_idle+0x2ec/0x434
cpu_startup_entry+0x18/0x1c
start_kernel+0x3ec/0x430
The buggy address belongs to the object at 863b0700
which belongs to the cache task_struct of size 1664
The buggy address is located 20 bytes inside of
1664-byte region [863b0700, 863b0d80)
The buggy address belongs to the page:
page:f09c9565 refcount:1 mapcount:0 mapping:00000000 index:0x0 pfn:0x463b0
head:f09c9565 order:3 compound_mapcount:0 compound_pincount:0
flags: 0x10200(slab|head|zone=0)
raw: 00010200 00000000 00000122 82802e00 00000000 80120012 ffffffff 00000001
page dumped because: kasan: bad access detected
Memory state around the buggy address:
863b0600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
863b0680: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>863b0700: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
^
863b0780: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
863b0800: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
==================================================================
这是通过在另一个线程向(在我们的案例中)一个不存在的 TA 打开会话时,关闭来自 TEE_IOC_SHM_ALLOC 的所有文件描述符而触发的。Syzkaller 成功复现了该问题,并通过试验复现代码以及稍微延迟调用 TEE_IOC_OPEN_SESSION,导致了另一个 UAF,涉及一个属于 kmalloc-64 缓存的对象:```
================================================================== BUG: KASAN: use-after-free in tee_shm_put+0x8c/0x98 Read of size 4 at addr 86467020 by task optee_example_h/216
CPU: 0 PID: 216 Comm: optee_example_h Not tainted 5.14.0 #21 Hardware name: Generic DT based system [<80122584>] (unwind_backtrace) from [<80117fd4>] (show_stack+0x10/0x14) [<80117fd4>] (show_stack) from [<819d57a0>] (dump_stack_lvl+0x40/0x4c) [<819d57a0>] (dump_stack_lvl) from [<819ced74>] (print_address_description.constprop.0+0x5c/0x2d8) [<819ced74>] (print_address_description.constprop.0) from [<805a12c4>] (kasan_report+0x1b4/0x1d0) [<805a12c4>] (kasan_report) from [<814cc6b0>] (tee_shm_put+0x8c/0x98) [<814cc6b0>] (tee_shm_put) from [<814c9b2c>] (tee_ioctl+0x1578/0x2e44) [<814c9b2c>] (tee_ioctl) from [<806038ec>] (sys_ioctl+0x918/0x1e70) [<806038ec>] (sys_ioctl) from [<80100060>] (ret_fast_syscall+0x0/0x58) Exception stack(0x86417fa8 to 0x86417ff0) 7fa0: 00000080 00000000 00000003 8010a402 200001c0 00000003 7fc0: 00000080 00000000 00423018 00000036 66c562d0 66c55e10 66c562d0 6ebebafc 7fe0: 66c55cb0 66c55ca0 004114bd 66cebd72
Allocated by task 216: tee_shm_alloc+0x15c/0x7e8 tee_ioctl+0x8d0/0x2e44 sys_ioctl+0x918/0x1e70 ret_fast_syscall+0x0/0x58 0x66c55ca0
Freed by task 215: kasan_set_free_info+0x20/0x34 __kasan_slab_free+0xdc/0x108 kfree+0x98/0x294 tee_shm_release+0x1dc/0x610 dma_buf_release+0x180/0x2a0 __dentry_kill+0x488/0x6ac __fput+0x2f0/0x7b4 task_work_run+0x178/0x230 do_work_pending+0xaf8/0x10a8 slow_work_pending+0xc/0x20 0x66d5bd16
The buggy address belongs to the object at 86467000 which belongs to the cache kmalloc-64 of size 64 The buggy address is located 32 bytes inside of 64-byte region [86467000, 86467040) The buggy address belongs to the page: page:(ptrval) refcount:1 mapcount:0 mapping:00000000 index:0x0 pfn:0x46467 flags: 0x200(slab|zone=0) raw: 00000200 00000000 00000122 82401200 00000000 00200020 ffffffff 00000001 page dumped because: kasan: bad access detected
Memory state around the buggy address: 86466f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 86466f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
86467000: fa fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc ^ 86467080: fa fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc 86467100: fa fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc ==================================================================
该漏洞是通过在未与系统上运行的现有 TA 建立任何会话的情况下对 TEE 驱动进行模糊测试而发现的。这可以进一步通过 syzkaller 中所谓的伪系统调用进行扩展,以便建立并启动与某个 TA 的会话。
## 根因分析
结论是 `tee_shm:dmabuf` 对象的生命周期跟踪存在设计问题。该驱动的设计是让用户空间在调用 `tee_ioctl_shm_alloc()` 后保持唯一的引用计数。
其假设是,如果该对象仍能在驱动的 IDR 对象中找到,则对 dmabuf 的引用仍然有效,并且可以递增其引用计数。事实证明这只部分正确。dmabuf 内存仍归 dmabuf 驱动所有,但它可能正处于销毁过程中,而这种销毁无法通过再次将引用计数变为非零来阻止。
触发该问题的场景是一个多线程应用程序:一个线程关闭 dmabuf 文件描述符的同时,另一个线程调用了引用该共享内存的 IOCTL 命令 `TEE_IOC_OPEN_SESSION` 或 `TEE_IOC_INVOKE`。
跟踪用户空间关闭 fd 时 dmabuf 的销毁过程,会在内核中执行以下代码:
1. `fput()`
2. `fput_many()` >> 文件引用计数降为零。竞态窗口打开。
3. `[task_work 被调度]`
4. `__fput`
5. `dput`
6. `dma_buf_release`
7. `tee_shm_release`
8. `mutex_lock(teedev->mutex)`
9. `idr_remove(teedev->idr, shm->id)` >> 现在用户空间无法再引用 shm 对象。竞态窗口关闭。
10. `mutex_unlock()`
这意味着 IDR 表及其互斥锁无法保证 dmabuf 和相应的 `tee_shm` 仍然存活。一个进程通过调用 `tee_shm_get_from_id()` 与 `fput()` 竞争,可以获得一个即将失效的 shm 的引用。```
/**
* tee_shm_get_from_id() - Find shared memory object and increase reference
* count
* @ctx: Context owning the shared memory
* @id: Id of shared memory object
* @returns a pointer to 'struct tee_shm' on success or an ERR_PTR on failure
*/
struct tee_shm *tee_shm_get_from_id(struct tee_context *ctx, int id)
{
struct tee_device *teedev;
struct tee_shm *shm;
if (!ctx)
return ERR_PTR(-EINVAL);
teedev = ctx->teedev;
mutex_lock(&teedev->mutex);
shm = idr_find(&teedev->idr, id);
if (!shm || shm->ctx != ctx)
shm = ERR_PTR(-EINVAL);
else if (shm->flags & TEE_SHM_DMA_BUF)
get_dma_buf(shm->dmabuf);
mutex_unlock(&teedev->mutex);
return shm;
}
为了利用这一点,必须在对象被释放之后、触发 UAF 之前进行一次重分配(reallocation)。在调用 tee_shm_get_from_id() 之后,会调用函数 tee_shm_put()(syzkaller 报告的第二次 UAF 崩溃即发生在此函数中),该函数会解引用作为输入参数传递给 dma_buf_put() 的 tee_shm:dmabuf 对象。```
/**
`tee_shm` 对象属于 kmalloc-64 缓存,因此可能在 UAF 之前被重新分配。必须使用以下对象进行重新分配:
1. 伪造的 `tee_shm`、`tee_shm:dmabuf`、`dma_buf:file` 对象
2. 将 `file->f_count` 设为 1
3. 构造一个 `file:file_operations` 对象,将其 `fasync` 函数指针设置为任意地址
当 `file->f_count` 降为零时,该函数会在 `dma_buf_put()` 调用之后于 `__fput()` 中被调用。
PAN(特权访问从不,Privileged Access Never)可缓解此问题,因为必须在用户空间内存中引用伪造对象,才能在 `file:f_ops` 结构中设置任意函数指针。因此,要使其生效,必须禁用 `CONFIG_CPU_SW_DOMAIN_PAN`,而在所提供的环境中该选项正是被禁用的。关于 PAN 是否可以在该漏洞中被绕过(例如使用 ret2dir)仍存在一些未决问题。
此外,要成功重新分配已释放的 shm 对象,必须有一个线程抢占 `TEE_IOC_OPEN_SESSION` 或 `TEE_IOC_INVOKE` 的 IOCTL 调用,该线程执行文件描述符关闭操作,并且堆喷射线程会填充 kmalloc-64 缓存。要做到这一点,内核必须配置 `CONFIG_PREEMPT`。在此 PoC 中,使用了 Nicolas Fabretti 博客文章 [7] 中基于阻塞 `sendmsg()` 的堆喷射方法。
总而言之,利用方面的难点在于 free 和 UAF 必须发生在同一个系统调用内。此外,释放操作难以触发,因为它需要在系统调用内进行竞争。释放后,从释放到实际 UAF 之间的时间窗口很小,必须在此期间执行堆喷射以重新分配已释放的对象。下图展示了利用代码中涉及的线程及其作用。
<p align="center">
<img src="https://raw.githubusercontent.com/pjlantz/pjlantz.github.io/master/docs/assets/Threads.png?raw=true" alt="涉及的线程" width="50%" height="50%"/>
<br /><em>图 3:利用代码中涉及的线程</em>
</p>
有三类线程在持续运行。为了抢占发起系统调用的线程,该线程以最低可能优先级 `SCHED_IDLE` 运行,而其他线程的优先级设置为 `SCHED_OTHER`。由于我们使用的是阻塞 `sendmsg()`,每次喷射尝试都必须在其自己的线程中运行,并且必须运行在触发 UAF 的同一个 CPU 核心上,因为每个核心都有自己的 kmalloc 缓存。还有一些释放线程,用于关闭步骤 1b) 中共享内存分配得到的文件描述符。该 UAF 触发和函数指针覆盖的完整源代码可在 [10] 中找到。
## 搭建新环境
若要复现带有易受攻击内核和 OPTEE 的环境,可以从以下仓库克隆并使用以下命令构建:```
$ mkdir optee-qemu && cd optee-qemu
$ repo init -u https://github.com/pjlantz/optee-qemu.git
$ repo sync
$ cd build
$ make toolchains -j2
$ make run
构建成功后,会生成三个控制台,一个用于 QEMU - 在 QEMU 控制台中按 'c' 以引导启动。第二个控制台显示来自安全世界的输出,最后一个控制台将引导进入 Linux。以 root 身份登录(无密码)。
运行漏洞利用代码,直到 file_operations 结构的 fasync 函数指针被设置为 0x22000000。```
until optee_exploit | grep "0x22000000" /var/log/messages; do sleep 0.01; done
这将因特权执行永不(PXN)阻止在 `PC=0x22000000` 处的执行而停止。从这里开始,利用策略可能因内核版本而异,但有可能执行内核 ROP 并进行栈迁移,或者使 vDSO 区域可写并将载荷放置在那里。未来工作也值得研究是否可以通过 ret2dir 和某种 physmap 喷洒来绕过 PAN。可以通过在 `linux/.config` 中设置 `CONFIG_CPU_SW_DOMAIN_PAN=y` 在内核中启用 PAN。在真实硬件上,它在 ARMv8.1 和 AArch64 上默认启用;对于 ARMv7 和 AArch32,可以使用此设置 [8] 实现软件模拟的 PAN。
**注意**:此漏洞利用程序未经过很好的优化,如果过早释放共享内存对象,有时可能会导致驱动程序挂起,此时 PC 将位于 `tee_shm_get_from_id()`。如果发生这种情况,请在 QEMU 控制台中执行 `system_reset` 以重启环境。
## 致谢
感谢 Axis Communications 的 Lars Persson 在根因分析方面提供的帮助,以及 Linaro 的 Jens Wiklander(TEE 子系统的维护者)在问题沟通和快速解决方面的协助 [9]。
## 参考
[1] CVE-2021-44733 - https://nvd.nist.gov/vuln/detail/CVE-2021-44733
[2] TEE 子系统 - https://www.kernel.org/doc/html/latest/staging/tee.html
[3] Globalplatform TEE API - https://globalplatform.org/specs-library/?filter-committee=tee
[4] OP-TEE OS - https://github.com/OP-TEE/optee_os
[5] BKK16-110: 可信执行与 OP-TEE 的温和介绍 - https://connect.linaro.org/resources/bkk16/bkk16-110/
[6] Syzkaller - https://github.com/google/syzkaller
[7] Lexfo 的安全博客,作者 Nicolas Fabretti:CVE-2017-11176:Linux 内核利用的分步指南 - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part3.html
[8] Linux 内核安全子系统:利用方法/用户空间数据使用 - http://kernsec.org/wiki/index.php/Exploit_Methods/Userspace_data_usage
[9] [PATCH v2] tee: 处理引用计数为 0 的 shm 查找 - https://lore.kernel.org/lkml/[email protected]/T/
[10] 概念验证漏洞利用 - https://github.com/pjlantz/optee_examples/tree/master/exploit/host