有段时间我调一台一体机的触摸板,现象很邪门:机器放在桌上没人碰,光标偶尔自己跳一下,甚至出现一次“无人参与的点击”,刚好选中了桌面上的一个文件。我一开始以为是硬件接触不良,拆机、换排线、加屏蔽,折腾一轮都没用。最后把事件流打出来一看,物理触点那里干干净净,问题出在驱动自己生成的逻辑事件上。后来翻内核CVE列表,发现这类问题比想象中常见——CVE-2023-52840就是其中典型:触摸板驱动里存在一处内存生命周期缺陷,可以在设备重连或休眠唤醒时触发悬垂指针,让系统在没有任何真实触碰的情况下上报点击。安全圈把这个现象叫“幽灵点击”,听起来带点玄学,本质却是一个非常经典的use-after-free漏洞。
这类漏洞值得聊,不只是因为影响面覆盖大量笔记本和一体机设备,更因为它动摇了系统里“输入事件可信”的假设。触摸板在主流操作系统里被当作高可信输入设备,锁屏解锁、权限确认、支付弹窗都依赖对物理按压的信任。驱动一旦在无人触碰时伪造出点击信号,就等于给攻击者开了一扇GUI层面注入的窗户。这篇文章我不打算只贴一个公告链接,而是按照我实际排查的思路把整条链路拆开:漏洞怎么定性、驱动代码里哪一步写错了、攻击者要付出多大代价才能利用、我们用什么手段复现和追证据,最后是补丁和缓解方案。适合内核驱动开发、设备安全测试和做系统运维的朋友参考。
1. 定性:一个输入设备CVE为什么值得上心
1.1 CVE编号背后的标准流程
CVE编号不是随便分配的,它由编号机构在厂商或研究员提交详细报告后审核发布。拿到一个CVE意味着漏洞已经被社区确认,不再是“可能存在问题”,而是“确实存在、且需要排期修复”的官方认证。CVE-2023-52840从编号年份看是2023年报出的问题,类型属于Linux内核触摸板驱动模块的释放后使用,也就是我们常说的UAF。
从评分上看,这类输入设备漏洞的CVSS基础分通常不会高得吓人,一般落在中危到中高危区间。原因也直白:触发它往往需要本地物理接触或特定设备事件配合,不像网络漏洞那样能隔着网线直接打。但我在实际评估中一直有个观点——CVSS对输入设备的低估是结构性的,因为它衡量的是“直接可利用性”,而不是“信任被破坏后的连锁反应”。一个能伪造点击的驱动缺陷,在真实攻击链条里往往不是终点,而是绕过前端安全校验的“钥匙”。
1.2 操作系统把触摸板当可信输入设备
我们平时不会意识到一个底层假设:操作系统安全机制默认“触摸板/触摸屏上报的事件是可信的”。锁屏界面的密码输入、权限弹窗的“是/否”按钮、指纹验证后的确认步骤,本质上都是把“存在一次物理输入”当作“用户本人同意”的证据。
这个假设在正常硬件下是成立的。物理设备只有人用手触碰时才会产生电信号,信号变成内核事件,再变成用户态的一次点击,中间链路相对干净。但UAF打破的正是这里:驱动在无人触碰时凭空生成一个点击事件,而且这个事件走的是完全正常的input子系统通道,桌面环境根本无法区分它和真实手指点击的差别。这就像门禁系统的门把手,在没人触碰时自己转了一下——门禁读卡器再安全,门把手自己动了,整个信任链就出了问题。
我在评估这类漏洞时,最常提醒团队的一句话是:不要问“攻击者能不能直接拿到root”,而要问“如果锁屏界面上的某个按钮被自动点击,会发生什么”。这样一想,幽灵点击的价值立刻清晰起来。
1.3 幽灵点击和普通触控误触是两种病
很多朋友看到“幽灵点击”第一反应是“我电脑触摸板也有乱跳问题”,这里必须把两类现象严格分开。
| 特征 | 普通物理误触 | 驱动UAF幽灵点击 |
|---|---|---|
| 触发来源 | 电容信号噪声、手掌误贴、电磁干扰 | 驱动逻辑层凭空生成 |
| 物理层证据 | evtest能看到触点坐标漂移、压力值异常 | evtest显示无物理信号,但BTN_TOUCH被置位 |
| 复现稳定性 | 与环境、手势相关,不稳定 | 与热插拔/休眠时序相关,可脚本化复现 |
| 危害 | 误操作,影响体验 | 可被利用绕过输入可信假设 |
我遇到过不少设备被说“触摸板闹鬼”,最后查明是电源适配器接地不好造成电容噪声。那种情况evtest里坐标数值会贴着某个角落轻微跳动,和UAF产生的完整点击事件有明显区别。下一节我们进代码里看看真正的UAF长什么样。
2. 漏洞根因:一块被提前释放的触摸状态内存
2.1 一条点击从物理到应用要过几道门
一条触摸板点击事件从手指到应用,至少要经过五个环节:触摸控制器采集物理信号,固件把它组装成HID报告,内核HID驱动解析报告,input子系统分发事件,最后由桌面环境的libinput之类组件转成GUI操作。
中间每个环节都有结构体在传递状态,最关键的叫“设备私有数据”,通常由一个结构体指针挂在驱动和input设备之间。这个结构体保存当前触点数量、slot状态、坐标缓存、工作队列等运行时信息。驱动正常工作时,每次触摸事件都会读写这块内存,并在合适的时机调用input_report_key和input_sync上报。
问题恰恰出在“合适时机”这四个字上。很多触摸板驱动为了提高吞吐,会把事件上报放到异步工作队列里,而不是在中断上下文里立即执行。这就引入了一个时间差:事件处理函数可能还排在队列里,设备却已经被拔出或进入低功耗模式。时间差意味着“什么先发生”不再是确定性的,一旦设备先销毁了状态结构体,队列里的处理函数再跑起来,就会踩进一块已经释放的内存。
2.2 缺陷模式:释放后的悬垂引用
下面是这类驱动中非常典型的UAF模式,我以通用代码示意,不是某个厂商的原样代码,但结构几乎一致:
struct touch_dev { int slot_count; int *slot_state; // 触点状态数组 struct input_dev *input; struct work_struct report_work; }; static void touch_report_worker(struct work_struct *work) { struct touch_dev *ts = container_of(work, struct touch_dev, report_work); // 访问已被释放的 slot_state,就是这一行踩进了悬垂内存 input_report_key(ts->input, BTN_TOUCH, ts->slot_state[0] ? 1 : 0); input_report_abs(ts->input, ABS_MT_POSITION_X, ts->slot_state[1]); input_report_abs(ts->input, ABS_MT_POSITION_Y, ts->slot_state[2]); input_sync(ts->input); } static void touch_dev_release(struct touch_dev *ts) { // 注意:这里只 cancel 了当前正在执行的 work,但队列里后续的 work 还在 cancel_work_sync(&ts->report_work); kfree(ts->slot_state); kfree(ts); }问题核心在kfree(ts)之后,touch_report_worker仍然可能执行。驱动的某个断开路径或电源管理回调在释放设备状态时,只取消了当下正在执行的一个工作项,但异步机制下的竞态让使用了同样函数的工作项仍可能被调度执行。于是ts->slot_state变成悬垂指针,读出来的值完全取决于这块内存被释放后有没有被其他数据覆盖。
说得直白点:快递员一直在往一个已经挂牌“暂停营业”的中转站投递包裹,系统照常签收。用户看到的就是一个凭空出现、完全没有物理来源的点击事件。
2.3 触发窗口:重连与休眠唤醒的竞态
UAF不是每次设备断开都会发生,它需要卡中一个很窄的竞态窗口。最常见的触发条件是两个:
第一个是热插拔。触摸板如果是USB接口方案,用户或系统拔插设备时,驱动会走disconnect流程释放私有数据。如果正好有事件报告work已经入队但还没执行,释放后work一跑就是UAF。第二个是休眠唤醒。现代笔记本的触摸板在系统进入待机状态时会经历一个完整的电源管理回调序列,驱动如果在这个序列里做了“部分释放”,而唤醒过程中又有残留的触摸报告在处理,同样会撞上。
竞态窗口可能只有几个毫秒到几十毫秒,所以用户看到的不是“每次插拔必现”,而是“偶尔乱点一下”。我见过不少故障单,工程师把这种偶发现象当成玄学问题处理,最后靠加日志才定位到。说白了,这类驱动漏洞比网络漏洞更难养眼,因为它不触发则以,一触发就是逻辑层凭空多了个动作,而这个动作可能正好落在危险的GUI按钮上。
3. 攻击者视角:一个幽灵点击如何变成一次越权操作
3.1 利用路径先要克服的物理门槛
说清楚一点,CVE-2023-52840这类漏洞不是那种攻击者能在公网上一键利用的远程漏洞。要触发UAF,要么让目标设备经历一次触摸板热插拔,要么诱导系统进入特定电源状态。
听起来门槛不高,但实际操作中还要满足几个前提。目标设备必须使用受影响驱动,系统里不能已经打了补丁,触发时序还得稳定复现。对于共享工位、机房一体机、公共查询终端这类无人值守设备,攻击者确实有物理接触机会,可以反复插拔触摸板设备来撞概率。对于个人笔记本,攻击者更多是借助“诱导”而非“物理接触”——比如通过恶意软件模拟一次USB设备重连,或者调用系统电源管理接口强制休眠唤醒。
我不建议把“需要物理接触”当作高枕无忧的借口。真实攻击场景里,公共设备和半可信环境是最容易中招的地方,而这类环境的运维往往又对内核补丁不够敏感。
3.2 从“漂移点击”到“定点确认”的距离
UAF产生的事件,坐标和状态是读悬垂内存的残留数据,一开始是“随机鬼画符”。攻击者真正想要的是让点击落在屏幕上的特定按钮——比如支付确认、权限提升确认、锁屏界面的回车键。这中间的距离叫“堆布局控制”。
常规思路是堆风水:在释放触摸状态结构体之前,攻击者大量分配同类型、同大小的内存对象,让系统把刚释放的内存重新分配给攻击者可控的数据。这样后续slot_state[0]、slot_state[1]这些字段读出来的就不再是随机残留,而是攻击者精心构造的“坐标点”和“触点状态”。严格说,要做到把一次UAF变成精准点击,已经属于内核漏洞利用的进阶水平,比单纯触发一次崩溃要难得多。
但即使不做完整堆布局控制,幽灵点击仍有实际风险。哪怕是随机坐标的乱点,只要攻击者反复触发,点击落在高风险区域的概率仍然存在,尤其在一些按钮很大的界面里。安全评估时我们把这种场景叫“概率型利用”,不一定完美,但足够给业务造成实质影响。
3.3 风险权重最高的场景:锁屏与支付确认
我把这类漏洞的风险权重排序为三个场景,从上到下依次递减:
- 锁屏界面:如果驱动在锁定状态下生成一次键盘Enter或触摸点击,攻击者可能直接跳过锁屏认证,进入桌面。这是影响最严重的一类。
- 权限确认弹窗:攻击者借幽灵点击“替”用户点了提权确认,后续配合社工可以完成隐性的权限提升。
- 支付/核身机制:一些支付软件在安全环境里把“用户点击确认”作为风控因子之一,幽灵点击会污染这个因子。
一句话,安全体系越是把“物理输入”当成信任根,这类驱动漏洞的价值就越高。对个人用户来说,发现设备乱跳一定要当回事,别只当成驱动不完善。
4. 复现与追踪:像排查疑难杂症一样定位幽灵点击
4.1 复现环境搭建:硬件和内核配置怎么选
要复现这类问题,虚拟机基本不行,因为触摸事件过不了虚拟化这层,必须用物理机。选一台使用受影响触摸板驱动的设备,系统内核换成漏洞版本范围内的版本,最好再重新编译一个开启KASAN的内核来跑。KASAN是内核的内存越界检测工具,它能在每次访问释放内存的地方立刻停下来,并打印完整栈回溯。
编译调试内核时几个关键开关:
# 内核 .config 中确保这些选项开启 CONFIG_KASAN=y CONFIG_KASAN_GENERIC=y CONFIG_DEBUG_INFO=y CONFIG_FRAME_POINTER=y内核编译可以稍慢,但调试体验好很多,不用靠猜。同时准备一个稳定的触发脚本,循环做热插拔和休眠唤醒:
#!/bin/bash # 循环触发休眠唤醒,观察输入事件 for i in $(seq 1 100); do echo "== round $i ==" rtcwake -m mem -s 2 sleep 1 done4.2 用系统自带工具抓事件铁证
先确认事件节点。触摸板在Linux下通常是一个input event节点,可以通过/proc/bus/input/devices找到对应设备:
cat /proc/bus/input/devices然后开一个终端跑evtest,选择触摸板对应的event设备。紧接着执行复现脚本,观察输出。正常情况下,没人触摸时屏幕应该一动不动;如果UAF被触发,你会看到类似:
Event: time 1690000000.123456, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 982 Event: time 1690000000.123456, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 532 Event: time 1690000000.123456, type 1 (EV_KEY), code 330 (BTN_TOUCH), value 1 Event: time 1690000000.123456, type 4 (EV_SYN), code 0 (SYN_REPORT), value 0看到BTN_TOUCH value 1,而此刻你根本没有碰触摸板,这就是“幽灵点击”的铁证。用另一台干净系统做同样的循环测试,如果事件消失,基本坐实是驱动UAF导致。
4.3 KASAN让内存错误现形
拿到input事件证据后,再回来看内核日志。KASAN环境下,UAF触发瞬间的dmesg里会打印类似这样的报告:
BUG: KASAN: use-after-free in touch_report_worker+0x3c/0x90 Read of size 4 at addr ffff88802345abc0 by task kworker/u8:2/1234 ... Call Trace: touch_report_worker process_one_work worker_thread kthread栈回溯会把我们从touch_report_worker到工作队列调度器的调用关系完整拉出来,一眼就能看出是哪条路径访问了已释放内存。配合addr2line工具,还能把函数地址转换成源文件行号,直接定位代码bug位置。
我一般把KASAN报告截图存档,再补一段完整输入事件日志,这样无论是报给上游内核社区还是写内部漏洞报告,都具备完整的证据链。
4.4 一份实用的鉴别表,别把所有“乱跳”都当成CVE
做设备维护和驱动调试的朋友,拿到“触摸板乱跳”工单后,建议先按这张表对症排查,别一上来就认定是CVE:
| 现象 | 优先怀疑 | 验证手段 |
|---|---|---|
| 光标轻微漂移,随环境变化 | 电容噪声、接地干扰 | 换电源、脱离金属桌面后复测 |
| 手掌贴边时误触 | 防误触算法失效 | libinput配置调整 palm rejection |
| 无人触碰出现完整点击事件 | 驱动UAF或固件bug | evtest + KASAN + 固件更新对比 |
| 插拔/休眠后偶发乱点 | 驱动生命周期问题 | 循环热插拔/休眠脚本复现 |
| 点击事件重复多次 | 固件去抖逻辑问题 | 查看固件版本和事件时间戳 |
经验是:先看事件时间戳和eytest输出能不能对应上物理动作,如果对不上,再往驱动生命周期层面深挖。
5. 修复与缓解:把补丁补在生命周期上
5.1 上游修复的正确写法
UAF修复的核心不是“在worker函数开头加个if”,而是把设备状态的生命周期和异步事件的生命周期彻底绑死。比较标准的手法是给touch_dev加引用计数,并在提交异步工作时自动持有引用:
static void touch_report_worker(struct work_struct *work) { struct touch_dev *ts = container_of(work, struct touch_dev, report_work); // 正常使用 ts 和 ts->slot_state if (ts->slot_state[0]) { input_report_key(ts->input, BTN_TOUCH, 1); } input_sync(ts->input); // 工作完成,释放引用 kref_put(&ts->refcnt, touch_dev_free); } static int touch_dev_submit_report(struct touch_dev *ts) { kref_get(&ts->refcnt); if (!queue_work(ts->wq, &ts->report_work)) { kref_put(&ts->refcnt, touch_dev_free); return -EBUSY; } return 0; }配合释放函数做完整清理:
static void touch_dev_free(struct kref *ref) { struct touch_dev *ts = container_of(ref, struct touch_dev, refcnt); kfree(ts->slot_state); kfree(ts); }这样设计的核心逻辑是:异步任务在运行期间一定持有设备对象的引用,设备不会在任务执行过程中被释放;任务结束后再通过引用计数触发真正的释放。与此同时,驱动移除路径要保证调用cancel_work_sync,把还没执行完的任务先排空。只说一句“修复是释放后置空”是不完整的,真正的要点是“让对象死掉之前,所有对它的引用都已消失”。
5.2 运维侧可以立刻做的临时缓解
对还没拿到补丁、又不能在第一时间升级内核的环境,可以做几件临时处置:
- 优先方案:升级到厂商修复后的内核版本,或者打上发行版安全团队发布的热补丁。热补丁一般只替换内核函数,不需要重启系统,适合生产设备。
- 临时缓解:在受影响的设备上禁用内置触摸板,改用外接USB鼠标/键盘。做法可以根据驱动模块名,写入模块黑名单。这要以管理员权限操作,并且提前确认外接输入设备可用,否则机器会失去输入手段。
- 物理加固:公共设备如果无法接受关闭触摸板,就在BIOS层面禁用触摸板,或者用硬件开关隔离。这类设备通常以键鼠为主,禁用触摸板对日常使用影响不大。
- 脚本监控:在关键终端上部署一个输入事件监控脚本,检测到无人触碰且无进程操作时出现BTN_TOUCH事件就告警,至少能第一时间发现被利用。
需要强调的是,临时缓解只是争取时间的方案,最终还是要走内核升级或补丁回归。只靠禁用设备不是长久之计,因为触摸板是这些设备的基础交互方式。
5.3 补丁回归验证清单
我每次验证输入设备驱动修复,都会按下面的清单逐项做回归,而不是只看漏洞用例是否消失:
| 检查项 | 验证方法 |
|---|---|
| 基础触控功能 | 单指移动、双指滚动、点击、拖拽,逐一确认 |
| 休眠唤醒稳定性 | 循环休眠唤醒100轮,全程monitor input事件 |
| 热插拔稳定性 | 对可插拔设备做100轮插拔,确认无UAF报告 |
| 锁屏输入可信 | 锁屏后循环触发复现脚本,确认无幽灵事件 |
| 内存检测 | 用KASAN内核跑复现用例,确认无任何use-after-free报告 |
第4项是我特别加上的,因为锁屏是这类漏洞最敏感的攻防场景。如果修复后锁屏状态下不再出现幽灵点击,才算真正意义上的安全回归完成。
我自己排查这类问题时最深的感受是:触摸板这种每天被摸上千次的设备,很少有人在驱动代码上多看一眼。它不像网络协议栈那样到处都是攻击面,所以生命周期管理的松懈往往藏得很深。如果你手头也有“没人碰却乱点”的设备,我的建议是先别急着换硬件,把event节点的时间戳抓出来对一下,确认是物理信号还是驱动逻辑生成的。再一个值得养成的习惯:把“休眠唤醒+热插拔”写进驱动和固件的例行回归测试里,这类UAF几乎都藏在电源和连接管理回调的夹缝中。
如果这篇文章能让你下次看到“幽灵点击”不再先怀疑硬件,而是想到“这会不会是一个驱动生命周期问题”,那我的目的就达到了。设备输入可信这件事,比大多数人想象的重要得多。