导语:OP-TEE是跑在Arm手机、机顶盒和嵌入式设备上的TrustZone可信执行环境。它把系统切成两块:Normal World跑Linux和普通应用;Secure World跑OP-TEE内核(S-EL1)和受信应用(TAs,S-EL0),保管设备密钥、安全存储、DRM物料、远程证明凭证。ByteRay安全团队向OP-TEE上游提交了三个漏洞的修复——其中一个能把一次普通的RSA调用变成任意地址写入的写啥写哪(write-what-where)原语;另一个用一次SMC调用就能让整个Secure World崩溃。
漏洞一:RSA NOPAD的下溢写入
RSA NOPAD是”教科书式”的无填充RSA:调用者传入一个与模数等长的数据块,内核直接做模幂运算。当OP-TEE用mbedTLS作为加密后端时,软件加密路径会把输入左对齐到一块与模数等长的临时缓冲区,再做模幂:
rsa.len = crypto_bignum_num_bytes((void *)&rsa.N); /* 模数长度 */
blen = CFG_CORE_BIGNUM_MAX_BITS / 8;
buf = malloc(blen);
memset(buf, 0, blen);
memcpy(buf + rsa.len - src_len, src, src_len); /* 没有长度检查 */
目标地址是 buf + rsa.len - src_len。意图是做一次右对齐拷贝:对于一个比模数短的输入,rsa.len - src_len 是一个小的正偏移,数值正好落在缓冲区末端紧贴末尾。代码完全没检查 src_len <= rsa.len 是否成立。这些都是 size_t 类型——一旦输入比模数还长,减法会下溢,”偏移”变成一个巨大的无符号数,buf + (那个值) 落到 buf 之前的某个地址上。memcpy 再从那里开始把整个攻击者控制的输入往回拷。
预期形态(输入 ≤ 模数):
buf
|
v
+-----------------------------+
| RSA scratch |
+-----------------------------+
触发后(输入 > 模数):
dest = buf - k
|
v
+----------+-----------------------------+
| under- | RSA scratch |
| write | |
+----------+-----------------------------+
^
|
buf
在 buf 之前写入的字节数等于攻击者让输入超出模数的那么多字节,每一字节都是攻击者可控的。Normal World客户端走的是一条完全普通的路径:它对一个TA打开会话,TA用RSA NOPAD密钥调用 TEE_AsymmetricEncrypt,内核就触发了那次拷贝。不需要调试器,不需要人工入口。恶意长度跨过TrustZone边界,内核就把自己的堆搞坏了。
Heap Grooming到写任意地址(Write-What-Where)原语
OP-TEE内核的堆由BGET管理——一个boundary-tag分配器(lib/libutils/isoc/bget.c)。每个块在前面带一个很小的头部,记录块大小和一个 prevfree 字段,分配器用它来找下方相邻块并与之合并。空闲块被串到一条空闲链表上,释放时与相邻空闲空间合并。
一个BGET块:
+--------+-----------------------------+
| header | payload |
+--------+-----------------------------+
^ ^
| |
size, prevfree 返回给调用者的指针
两个特性决定了如何利用。第一,BGET从空闲块的高端切出请求,因此一连串分配返回的地址是降序的,后分配的对象位于先分配对象的下方——也就是低地址方向。第二,任何活跃块紧邻下方的字节,要么是下一个对象的载荷,要么是那个对象的头部。
A、B、C 依次从一个空闲块中分配出来:
低地址 高地址
+----------------+------+------+------+
| free space | C | B | A |
+----------------+------+------+------+
^
最新分配位置最低
把这一点对齐到下溢:拷贝会从RSA scratch缓冲区的前端开始反向写,往下、向低地址方向写进它下面的对象。
把受害者铺到缓冲区下方
目标就是让RSA scratch缓冲区正好落在我们选中的对象之上,这样下溢能正好写进去。BGET是确定性的,因此只要能控制内核何时做哪些分配(每一条都能通过普通的TA调用触发内核分配与释放),布局就是可重复的。
- 把堆填紧,让目标尺寸类是连续且可预测的,去掉历史空洞
- 交叉铺下我们想命中的对象,以及同尺寸的占位对象
- 释放掉一个占位,让一个空洞正好位于目标受害者之上
- 触发RSA运算。它的scratch缓冲区是同一个尺寸类,于是复用了那个空洞,下溢就正好写进受害者
步骤2:用占位P交叉铺受害者V
低 高
... +-----+-----+-----+-----+ ...
| V | P | V | P |
+-----+-----+-----+-----+
步骤3:释放受害者V上方的占位 → 形成空洞
... +-----+------+-----+----- ...
| V | hole | V | P |
+-----+------+-----+-----+
步骤4:RSA scratch缓冲区复用空洞;拷贝方向向左进入V
... +-----+------+ ...
| V | buf |
+--^--+------+
<-- 下溢
写坏buf下方的受害者
我们针对的目标
缓冲区下方有两类目标值得打。
第一类是一个相邻对象,里面含有函数指针或”ops表”,内核稍后会调用它。把那个指针覆盖成某个选定的Secure World地址,下一次间接调用就会跳转到我们选的位置。在RSA缓冲区下方放一个被插桩的对象,把布局调整好让下溢覆盖到它稍后的字段,把函数指针覆盖成某个标记例程的地址,然后看内核通过它派发、在S-EL1跑起我们的目标路径:
受控的下溢 → 覆盖相邻对象的字段
→ 把函数指针置为选定地址
→ 间接调用派发过去
→ Secure World跑起攻击者选定的代码路径
第二类是分配器本身。损坏相邻块的boundary tag(它的大小、prevfree或空闲链表指针),下一次该块的释放或合并会让BGET按攻击者指定的指针去unlink。分配器替你做这次写入——把一个”有限长度的线性下溢”放大成一个通用的写啥写哪(攻击者选的值写到攻击者选的地址)原语。我们在写利用代码时走的就是这条路。
从写到读,再绕过ASLR
CFG_CORE_ASLR 把内核的加载地址随机化,因此函数指针覆盖需要一个”本不应该知道”的地址。同一个下溢可以同时拿来做信息泄漏原语——瞄向一个读,而不是代码指针。
挑一个受害者对象,让它的长度字段或源指针字段最终被复制回Normal World,然后把它”撑大”。一个把输出返回给调用者的操作于是从目标对象开始拷贝,并继续走完相邻的堆区域,把Secure World的内存交出去。
受害者:{len, data},后续操作把它的字节返回给Normal World
before +-----+---------------------+
| len | data |
+-----+---------------------+
下溢后 +-----+---------------------+ . . . 相邻堆 . . .
增长len → | LEN | data | pointers | keys | headers |
+-----+---------------------+
后续操作拷出LEN字节:
Normal World拿到:data || 相邻Secure世界内存
^ 含一个活的S-EL1指针
leaked_pointer - known_static_offset = 内核基址
一个被泄漏的指针就能干掉 CFG_CORE_ASLR。两个原语按显然的顺序组合:先读恢复基址,再把代码指针或分配器元数据写到你现在已经知道正确的地址。
漏洞二:Widevine PTA——一次普通世界调用就能跨边界让Secure World崩溃
第二个漏洞更小,但从普通世界直接一次调用就能触发。Widevine伪TA把自己限制在几个允许的调用者UUID里,为此要读取调用会话:
struct ts_session *session = ts_get_calling_session();
/* 确保我们是被一个TA调用的 */
if (!is_user_ta_ctx(session->ctx))
return TEE_ERROR_ACCESS_DENIED;
这个检查假定调用会话一定存在。当普通世界直接对PTA打开会话时,线程的会话栈里没有发起调用的TA,于是 ts_get_calling_session() 返回NULL。紧接着下一行从NULL指针读 session->ctx,在S-EL1触发fault,整个内核panic。一次来自普通世界非特权进程的SMC就足以拿下整个Secure World。影响所有开启 CFG_WIDEVINE_PTA 的构建——也就是用上DRM路径的场景。
漏洞三:一个未被强制执行的标志,最后变成释放后使用
第三个漏洞是一个并发标志,内核本不该信任它。TA_FLAG_CONCURRENT 在文档里被标注为”仅伪TA可用”,但它落在用户TA允许在头部声明的标志位掩码里,而且内核真的就接受了。一旦设上,串行化行为就变了:
static bool tee_ta_ta_try_set_busy(struct tee_ta_ctx *ctx)
{
if (ctx->flags & TA_FLAG_CONCURRENT)
return true; /* 直接跳过busy锁 */
...
于是单个多会话、单实例用户TA的两个会话可以在同一个共享上下文上同时跑。两个会话对同一份地址空间区域链表(uctx->vm_info.regions)做memref参数的映射与解映射,全程没有锁保护。并发地插入、移除、释放会破坏链表,并释放仍在被使用的 vm_region 节点。这是一次S-EL1中的释放后使用(use-after-free),由一个攻击者自己编写并加载的TA驱动。在使用测试密钥签名的构建上,这一步是免费的;在锁了签名的设备上,则需要TA的签名密钥。
当前状态
三个漏洞都已带着概念验证代码上报给OP-TEE项目,修复已在2026年提交上游。其中RSA NOPAD下溢由我们与Ramtine Tofighi Shirazi(ramtine@secmate.dev,Secmate.dev)独立发现。
参考链接
- OP-TEE #7898: crypto: rsa: reject RSA NOPAD input longer than the modulus
- OP-TEE #7899: core: pta: widevine: reject a NULL calling session in open_session
- OP-TEE #7900: core: ldelf: reject TA_FLAG_CONCURRENT for user TAs
出处:本文翻译自 ByteRay Blog 发布的《An RSA Heap Underwrite Into OP-TEE’s Secure World》一文,原文链接:https://blog.byteray.co.uk/blog/optee-rsa-nopad-heap-underwrite.html。原文中的 ASCII 图示与”Write-what-where through corrupted BGET metadata”示意图由 ByteRay 团队创作,版权归属原作者。
版权声明:本文由华盟网翻译整理,保留所有权利。原文版权归 ByteRay 所有。














暂无评论内容