
CVE-2022-48872这个编号我估计很多人第一眼看到它是在自己发行版的安全公告里扫一眼发现是Linux内核的漏洞然后又扫一眼发现和高通平台、DSP相关就直接跳过了。说实话也能理解毕竟FastRPC这个驱动对绝大多数搞服务器、搞云原生的朋友来说确实很远但如果你碰过Android内核、做过车机、搞过物联网设备那你大概率绕不开它。这个洞的本质是释放后使用Use-After-Free加竞态条件Race Condition的组合拳出在fastrpc驱动管理内存映射的生命周期上。攻击者一旦稳定触发就能拿到内核态任意读写直接提权到root。坦白讲这类洞在驱动里不算罕见但FastRPC这个驱动特别典型因为它把“用户态直接操作DSP共享内存”这种需求做到了极致攻击面大、触发路径多、修复起来还容易顾此失彼。这篇文章我就把整个漏洞的前因后果、触发路径、利用思路和修复验证从头到尾捋一遍既有原理也有实操希望对正在做内核安全或者嵌入式BSP开发的朋友有点用。1. 漏洞背景FastRPC驱动是干什么的1.1 一个和DSP打交道的特殊驱动FastRPC不是某个开源社区随手写的玩具驱动它是高通平台里负责应用处理器AP也就是主CPU和DSP数字信号处理器比如Hexagon之间通信的核心组件。你在手机上用到的语音唤醒、摄像头计算、音频后处理、传感器融合这些重计算任务很多都是丢给DSP去做的因为DSP在低功耗下算这些任务比CPU效率高得多。问题来了主CPU上的用户程序怎么把数据交给DSP传统思路是走内核驱动的ioctl把用户态buffer拷贝进内核再拷贝给DSP一来一回全是拷贝性能稀烂。FastRPC的做法简单粗暴——直接在用户态和DSP之间共享物理内存两边读写同一块地址省掉拷贝。为了实现这个目标驱动必须提供mmap能力把DSP侧的内存映射到用户态进程的地址空间里。正是这种“用户态直接映射”的设计把FastRPC推到了一个非常敏感的位置。映射的创建、销毁、引用计数管理任何一环出了纰漏都会直接演变成内核态的内存破坏漏洞。CVE-2022-48872就是在这种背景下被挖出来的它出现在mmap路径和buffer释放路径的交叉点上两条执行流谁也没等谁结果把一块内存给搞悬空了。1.2 这个CVE公开了哪些信息CVE-2022-48872在2022年就被上报了但公开的时间比发现时间晚不少这也是内核安全公告的常态。它影响的是Linux内核5.10、5.15、6.1几个长期维护分支里的fastrpc驱动修复补丁随后被打进5.10.163、5.15.88、6.1.10这些版本。公开公告里的描述很短大意是在drivers/misc/fastrpc.c的fastrpc_release_current_app函数中存在一个释放后使用漏洞本地攻击者可以利用它造成内核内存破坏进而提权。很多人看到这里就划走了觉得“我用的内核版本没这么老”但实际上大量Android设备、车机、IoT设备的内核虽然版本号新却长期在企业级BSP里沿用同一套fastrpc驱动源码根本没同步上游补丁。所以判断自己是否受影响不能只看uname -r得实际去看驱动代码。从触发条件来看这个漏洞需要攻击者已经有能力在目标设备上执行代码——哪怕是个低权限的app或者一个shell。它不需要物理接触设备也不需要用户交互是典型的本地提权漏洞。对攻击者来说拿到了这个洞就等于从普通用户直接变身root接下来什么SELinux策略、文件访问控制都拦不住了。2. 漏洞原理深度拆解UAF和竞态是怎么凑到一起的2.1 释放后使用的本质先从最基础的概念说起。释放后使用英文叫Use-After-Free意思是某一块内存已经被释放归还给系统了但程序里还有一个指针指向它后续又通过这个悬空指针访问了内存。这块内存可能已经被别人重新分配也可能处于不可用的状态你往里面写数据轻则数据错乱重则直接改写内核关键结构体拿到控制流劫持能力。拿生活场景打比方。你租了一间房房东在你退租后又租给了别人可你手里还留着钥匙。某天半夜你拿钥匙开门进去把行李放人家客厅了人家醒来懵了你也懵了。如果进来的是一个攻击者他可能会故意把某个东西放在特定的位置给下一个租客挖坑。内核UAF利用的思路就是这个——你释放了内存攻击者赶紧申请一块同样大小的内存占住位置然后驱动还把这块内存当成原来的结构体来用攻击者精心伪造的数据就变成了内核里的合法对象。在FastRPC这个案例里悬空的对象是buffer相关的结构体。驱动在释放buffer时如果没有确保所有引用它的映射都被清理干净随后另一个执行流通过已经失效的映射去访问这块内存UAF就发生了。关键点在于这个访问不一定是直接读写buffer的内容也可能是驱动重新使用了这个结构体里的字段比如函数指针、链表节点、长度字段。一旦这些字段被攻击者控制那基本就是内核沦陷。2.2 竞态条件为什么总在mmap附近出现竞态条件Race Condition指的是多个执行流同时访问共享资源执行顺序不同会导致结果不一致。在FastRPC的mmap路径里有两种典型的并发场景一个线程在mmap创建映射另一个线程在关闭fd或者释放buffer或者多个线程同时打开同一个远程进程的session大家都在操作同一个底层对象。为什么竞态总爱出现在mmap周围因为mmap的语义横跨了虚拟内存区域VMA和驱动程序内部的私有结构这两者的生命周期管理是分开的。VMA的销毁由内核的内存管理子系统触发而驱动私有结构体的销毁由驱动自己控制。当用户进程主动munmap或者进程退出导致VMA被清理时驱动需要在自己的结构体里找到对应的映射记录并释放它。可如果此时另一个线程正好在mmap的传入路径里正在使用这个记录两个线程就会在同一个对象上打架一个读一个释放释放后的那个读就是UAF。在CVE-2022-48872里具体的触发方式是驱动在计算mmap传输大小、创建新的内存映射时会先查找或者创建一个session/context对象。在某个特定时间窗口里另一个线程通过release操作把这个context对象释放了前一个线程继续拿着已经释放的指针做后续操作。竞态的窗口可能只有几个指令周期但通过用户态反复开线程、反复触发总能在某一轮踩中这个窗口。内核漏洞利用的一个基本法则就是窗口不需要大只要触发次数够多概率再小也能被轰开。2.3 根因定位fastrpc_release_current_app公开信息把这个漏洞的根因指向fastrpc_release_current_app函数但这只是一个表象真正的根子是驱动里映射引用计数和生命周期管理缺乏同步保护。看fastrpc驱动的历史代码会发现整个驱动从头到尾都在和session、context、buffer这些结构体打交道它们之间的互相引用非常复杂。具体来说每次用户态调用open()打开/dev/fastrpc设备时驱动会创建一个新的session上下文调用FASTRPC_IOCTL_INVOKE时会创建buffer并映射到用户态调用mmap时会在驱动内部记录对应的远程内存句柄。问题就出在这个“记录”上——驱动使用了一个名为fastrpc_mmap的结构体来保存映射信息其中有个字段是引用计数还有一个字段指向所属的session。当用户进程关闭设备fd时驱动调用fastrpc_release_current_app来清理该进程的所有映射和session它会遍历进程对应的session链表逐个release buffer。释放的顺序如果和另一个线程分配的顺序冲突就会出问题。举个例子线程A在mmap里新建了一个fastrpc_mmap还没来得及把它链入session的链表线程B已经开始了release流程遍历链表发现没有这个新节点于是认为所有对象都清理完了直接把session本身也给释放了。等线程A再往后走要访问session字段来保存映射信息时它拿到的session指针已经是一个悬空指针。补丁的做法不难猜就是在释放路径上加锁让整个清理过程串行化同时保证新建的mmap在发布到链表之前不会被人半路释放。但就是要补这么一层纸还是要靠一次commit专门修复可见并发问题一旦牵扯上复杂的对象关系是真的很考验代码功底。3. 攻击者视角这个漏洞怎么被利用3.1 攻击路径与前置条件要利用这个漏洞攻击者需要具备几个条件。首先系统里必须存在/dev/fastrpc设备节点并且当前用户有权限打开它。在很多Android设备上这个节点的权限是0666也就是所有用户可读可写这就成了绝佳的入口。其次目标设备必须使用了受影响的fastrpc驱动代码版本对应上前面说的那几个分支。攻击者拿到入口之后怎么操作大致流程是先打开设备节点然后准备好多个线程。一个线程反复执行mmap/ioctl操作诱导驱动不停地分配新的buffer和映射另一个线程则快速关闭fd触发fastrpc_release_current_app清理路径。两个线程一攻一守目标就是在驱动创建一个映射对象、但还没完全初始化完成的那个时间窗口内让cleanup线程把对应的父对象释放掉。一旦踩中用户态就能在映射的虚拟地址上进行读写而这些地址背后对应的可能已经是内核重新分配的其他对象比如cred结构体。我个人的经验是这类竞态窗口的命中率一开始往往很低可能需要跑几万次甚至几十万次才能稳定触发一次但攻击者可以利用CPU亲和性把两个线程绑到同一个核上通过特定的系统调用顺序来扩大窗口。比如在mmap之后不立刻做后续操作而是用户态主动触发一次调度让出CPU这样release线程就有更大的概率在mmap后续路径执行之前插入进来。3.2 从UAF到内核代码执行成功触发UAF只是第一步从“内存被释放后又访问”到“拿到root”之间还有一段路。攻击者需要想办法把悬空的内存重新占回来再通过驱动对这个对象的后续操作来劫持执行流。这就是所谓的堆喷射Heap Spraying内核利用里的标准操作。FastRPC驱动允许用户态通过ioctl创建各种大小的buffer攻击者就可以反复创建不同大小的buffer来占位。占位之后怎么劫持比较经典的思路是覆盖fastrpc_mmap对象中的某些指针字段比如指向session的指针。当驱动后续执行到通过这个指针去访问session内容时就会跳到攻击者伪造的地址。更直接的办法是伪造对象里的函数指针。不过现代内核开启了CFI等多种防护函数指针劫持的难度比以前高了很多所以现在的利用往往转向参数伪造、权限结构体覆盖这类方式。再说个细节漏洞利用过程中攻击者和内核之间其实在进行一场信息博弈。攻击者需要精准地知道当前内核对象布局、目标结构体的偏移量这些信息通常通过设备内核符号表或者同型号其他设备上的泄露来获得。如果一个设备的/sys/kernel/notes或者/proc/kallsyms能够读取利用难度会降低不少反之如果这些信息被封锁攻击者就需要靠盲打的方式猜布局成功率大幅下降。这也是为什么很多厂商在安全更新里除了修漏洞本身还会顺带收紧内核信息泄露的口子双管齐下。3.3 为什么说这类漏洞防不胜防我总跟朋友讲内核驱动漏洞和白盒代码审计不同前者最大的难点在于你怎么也想不到某个路径和某个路径会在真实负载下以什么顺序交织在一起。写驱动的人当然知道要保护共享对象但保护的程度、加锁的粒度、锁的顺序这些都是在代码演进的漫长过程中逐步补出来的。FastRPC这个驱动功能多、历史包袱重、设备树又复杂补丁打得再勤也难免有盲区。从漏洞披露的时间线也能看出这类驱动问题并不是某一家厂商独有的而是整个行业都在面对的课题。Android、车机、路由器、智能音箱只要用了高通芯片又带了DSP就都受这个驱动的管辖。攻击者只要在一个目标上打通了利用链就可以批量复用到所有同芯片平台的设备上这个攻击面是相当可观的。4. 修复方案与补丁分析4.1 官方补丁到底改了什么我先明确一点不同内核分支的补丁可能略有差异但核心思路是一致的给缓冲区对象和session的生命周期管理加上同步机制。以fastrpc驱动修复的常见写法来看修复补丁主要做了两件事一是在释放路径上引入互斥锁保护mmap列表的遍历和删除操作确保释放过程中不会有一个新的mmap对象被并发地添加进来二是调整引用计数的获取和释放顺序保证任何时刻访问session对象都持有有效的引用。具体到代码层面修复后的驱动会在创建新映射时先持锁检查session是否已经被标记为dead状态如果是就直接拒绝。这个检查必须和mmap列表的插入操作放在同一个临界区里。很多修复不彻底的原因就出在这里检查归检查插入归插入中间还是脱了锁等于白修。所以你看内核安全补丁的review最看重的就是临界区的完整性和锁的覆盖范围不是说你加了把锁就完事了。补丁backport到不同分支时通常会遇到冲突因为5.10分支和6.1分支的驱动代码已经有差异。这也是为什么我一直建议BSP团队切分支要慎重长期不跟上游的话真出安全补丁时你只能手工移植移植过程中还可能引入新问题。4.2 上游版本和发行版修复情况以2023年初的时间点为参考Linux上游在5.10.163、5.15.88、6.1.10等版本合入修复。对普通Linux用户来说升级内核到包含修复的版本是最直接的解决办法。但对Android或嵌入式设备来说事情就没这么简单了因为这些设备的BSP通常基于某个固定内核版本不会随便跳大版本需要厂商手动合入修复补丁。如果你是设备用户怎么判断自己是否安全两条路一是跟踪高通和OEM厂商的安全公告看是否包含CVE-2022-48872的修复二是自己在设备上检查fastrpc驱动的具体代码。前者简单但依赖厂商响应速度后者才是真正靠谱的办法。内核版本新并不代表修复到了很多设备把内核版本从5.10升到5.15但fastrpc.c还是老代码漏洞照样存在。4.3 临时缓解措施如果暂时没法升级内核或打补丁还有没有别的办法有但都会牺牲部分功能你可以按实际情况取舍。最直接的做法是修改设备节点的权限。在Android设备上把/dev/fastrpc的权限从0666改成0660并且归属到system组或特定的DSP服务组普通app就无法直接打开了。副作用是某些依赖DSP的第三方应用可能无法正常工作。在Linux系统上还可以用UDEV规则对节点权限做限制。其次是SELinux策略加固。如果设备启用了SELinux可以收紧fastrpc相关域的策略禁止普通untrusted_app域访问fastrpc设备节点。这不能修复漏洞本身但能大幅提高利用门槛。攻击者即便有代码执行能力想要打开设备节点也得先过他本身的SELinux策略这一关。现代化的Android系统对内核驱动的SELinux策略管得已经很严了但很多IoT设备出厂的SELinux是permissive模式等于没有这种就特别危险。最后补充一点即便修复了CVE-2022-48872FastRPC的安全问题也远没有结束。DSP侧固件的安全边界和主CPU侧同样重要但从系统角度说无论DSP多强大用户态的访问入口必须经过内核驱动的校验这个校验过程是无论如何都不能省略的。5. 自查操作如何判断你的设备受影响5.1 检查内核版本与驱动代码最简单的自查方式是看内核版本号跑一下uname -r看看你的内核版本是否高于修复版本。如果你的系统是5.10.163之上、5.15.88之上、6.1.10之上的对应分支版本说明内核主线侧已经包含了修复。但就像前面说的嵌入式设备光看这个不够还得实际确认驱动代码有没有同步合并。在设备上找到fastrpc驱动的实际代码路径通常在drivers/misc/fastrpc.c。如果你有内核源码包直接去看驱动代码里查找是否存在fastrpc_release_current_app等关键函数确认它是否有同步锁保护。看完代码如果不放心我建议写一个小内核模块来检查fastrpc驱动的procfs或debugfs接口看看映射列表的引用计数是否正常——不过这需要设备开启调试支持生产设备一般没有所以更多还是靠代码审计。5.2 检查设备节点与访问权限检查/dev/fastrpc节点是否存在以及它的权限位是什么样的。运行ls -l /dev/fastrpc看第一列的权限位。如果是crw-rw-rw-说明所有用户都可以读写这个节点这就是比较容易利用的条件如果是crw-rw----或者权限位更严格攻击门槛会高一些。从做安全的习惯来说设备节点的权限、所属用户组、SELinux标签这些都应该在设备出厂前就规划清楚。很多厂商在这方面确实做得不错但总有漏网之鱼。建议无论设备是否打了补丁都把fastrpc节点的权限梳理一遍能不给的权限坚决不给。5.3 监控可疑调用行为如果设备还能运行日志分析和审计工具可以关注是否有异常的mmap/ioctl操作大量出现。攻击者为了触发竞态必然会在短时间内高频调用设备节点的操作这种突发的调用模式在系统日志或者审计日志里多少会留痕。具体监控哪些点一个是调用频率正常的DSP服务调用保持在一个稳定的水平攻击触发的调用频率往往高出好几个数量级。另一个是异常的线程模式攻击者通常会让多个线程反复执行创建和销毁操作这种行为特征也很明显。当然真正老练的攻击者会刻意降低触发频率来规避检测但至少对于普通终端设备来说这种监控已经能拦住不少脚本小子级别的攻击。6. 实操经验怎么给老内核手工修这个洞6.1 动手前的准备我们经常遇到一种情况设备很老没法升级内核厂商又迟迟不出补丁但漏洞就摆在眼前不能装作看不见。这时候可以尝试自己手工backport补丁。动手之前请做好三件事——备份内核源码确认设备上有编译内核的工具链和解包打包工具准备一个能随时恢复的刷机方案。手工打补丁最怕的不是代码本身而是不知道这个补丁依赖哪些前置改动。fastrpc驱动在5.10到5.15之间经历了不少重构你在老内核上合并新补丁时很可能发现上下文对不上。这时候不要强行硬合先把需要修改的函数在旧代码里的异同点找出来搞清楚新旧逻辑的差异再决定怎么移植。我见过很多人一看到补丁冲突就退缩其实只要理解了改动的意图移植起来并没有多难。6.2 核心改动思路在不依赖官方补丁的情况下自己修这个洞的核心是两件事把mmap列表的访问串行化以及让session的释放必须等到所有引用结束。具体做法上可以看以下几点。件一是用一个mutex或者spinlock来保护fastrpc_session的mmap链表。凡是涉及到对mmap链表进行修改的地方比如添加映射、删除映射、遍历链表释放映射都要在锁的保护下进行。二是释放session时先把session标记为清理中再把session从全局链表摘下最后才逐项释放映射对象。为了防止清理中又被其他线程拿到新的引用驱动的查找逻辑要先判断session的清理标记发现标记就直接拒绝。三是引用计数用原子操作并且在进行可能有并发风险的长流程操作时持有引用再执行。移植时要特别小心锁的顺序问题。FastRPC驱动里已经存在的锁不止一把如果新加的锁和旧锁的获取顺序不一致会直接导致死锁。这个在代码评审的时候要反复对照确保任何路径都不会出现互相等待的情况。我自己实践下来最好把锁顺序写在一张注释表格里摆在头文件里方便大家维护时查对。6.3 编译验证与回归测试改完代码后编译之前先做一次静态检查建议跑一下内核自带的sparse或者smatch扫一遍新增代码很多低级错误在这步就能暴露。然后正常编译内核用目标设备能识别的方式打包刷机。刷完机后先做基础功能验证打开设备节点跑一遍正常的DSP调用确认语音、图像这类依赖DSP的功能没有异常。接着验证修复效果。有条件的话可以运行高速并发的压力测试程序模拟攻击者手法多线程同时执行mmap和close操作跑十万次以上看看能不能触发异常。如果修复有效压力测试跑完之后dmesg里应该没有任何使用已释放内存的警告系统不会crash也不会卡死。如果测试中出现异常指针访问或者Oops说明修复还不完整需要重新审视锁的覆盖范围。这个循环可能要重复几次才能收敛到稳定的修复版本做内核安全修复要有耐心这不是一次就能搞定的。最后不要忘记做完整回归测试。特别是fastrpc驱动涉及远程调用、缓存同步、dma buf共享这些复杂功能如果修复影响了正常路径的性能或者功能那成本就太大了。我个人的准则安全修复不能破坏功能基线否则没人敢用你的内核。7. 几个容易踩的坑7.1 别把内核版本更新当成补丁之前说过了很多设备的内核版本号看起来在新但驱动代码可能是旧版。现在的Linux发行版在backport安全补丁时为了减小回归风险往往只摘取必要的改动驱动代码的结构可能和上游大版本完全不同。反过来厂商在合入新内核时也常常把驱动代码从旧内核原封不动地搬过来。所以判断是否安全最可靠的方法还是直接对比代码而不是看版本号。7.2 加锁不是万能药对于这类UAF加竞态的漏洞加锁是最直接的修复方式但很容易产生新问题。最典型的是锁粒度太大把整个设备操作都串行化性能直接崩掉还有就是锁顺序不对A路径先拿锁1再拿锁2B路径先拿锁2再拿锁1并发时双双卡死。内核社区对这种补丁的评审非常严格因为一旦合入一个死锁补丁影响面比漏洞本身还大。7.3 别忽视用户态的作用很多人在分析这个漏洞时只盯着内核代码却忽略了用户态的作用。实际上攻击者能够稳定触发竞态靠的往往是用户态精心设计的线程调度策略。同理做修复验证时也不能只用简单的线性测试必须编写高并发的压力测试程序尽量模拟攻击者的行为模式否则验证效果会大打折扣。FastRPC这种驱动属于那种“平时不响响则致命”的组件它服务的是DSP这种系统里最核心的算力单元暴露的是用户态可以直通的通路。CVE-2022-48872只是这条通路上被发现的漏洞之一后续肯定还会有类似的洞被挖出来。对这个驱动做过完整代码审计的人应该能感受到作者在功能膨胀和安全性之间挣扎的痕迹这种挣扎在每一个历史悠久的驱动里都能看到。回顾我自己处理这个漏洞的过程最大的体会是内核安全没有银弹UAF和竞态这两座大山只能靠严谨的代码评审、充分的对并发场景理解和一轮又一轮的压力测试慢慢去填平。新入行的朋友如果觉得内核安全门槛太高不妨从FastRPC这类功能明确、边界清晰的驱动入手把它们当成练手项目比直接啃整个内核要友好得多。