本节摘要:驱动跑在内核态,天然是整个系统的攻击面。本节讲清驱动成为高危目标的原因、输入校验的完整清单、三类高频漏洞的机理与修法(整数溢出、越界访问、信息泄漏),以及权限模型在设备层的落地。
内核安全圈流传一句行话:应用漏洞丢数据,驱动漏洞丢内核。一个越界的用户态程序顶多读到自己的内存,一个越界的驱动能把整台机器交给攻击者。历史上通过驱动接口提权攻陷系统的案例屡见不鲜——声卡接口的整数溢出、显卡接口的越界映射、摄像头驱动的信息泄漏,都曾是真实攻击链的起点。原因不复杂:驱动接口是用户态能触到的最高权限代码入口,而驱动作者往往只想着"让硬件跑起来",很少想着"有人会拿接口当武器"。
三个结构性原因。其一,权限错位:任何用户都能打开的设备文件,背后是内核态代码——接口校验稍有疏漏,普通用户的输入就直接指挥内核内存了。其二,输入面宽:设备控制命令参数多、结构复杂,解析代码量大,出错点多。其三,测试盲区:功能测试覆盖"正常用法",攻击者专挑"参数极限值与畸形组合"下手,常规测试根本不走这些路径。
防御的思路因此明确:把每一个入口都当成"不怀好意的输入"来处理。
第一关:长度与范围。所有来自用户态的长度、偏移、索引,先验证再使用。经典的整数溢出漏洞长这样:
/* 脆弱写法:count 传超大值时加法回绕,分配极小、拷贝巨大 */ struct config *cfg = kmalloc(sizeof(*cfg) + count, GFP_KERNEL); copy_from_user(cfg->data, ubuf, count); /* 缓冲区溢出 */ /* 稳妥写法:先卡上限,再分配,再拷贝,逐环校验 */ if (count > MAX_CONFIG_SIZE) /* 上限白名单 */ return -EINVAL; if (check_add_overflow(sizeof(*cfg), count, &total)) /* 溢出显式检查 */ return -EINVAL; cfg = kmalloc(total, GFP_KERNEL); if (!cfg) return -ENOMEM; if (copy_from_user(cfg->data, ubuf, count)) return -EFAULT;
第二关:值合法性。寄存器编号要查表、数组索引要卡界、枚举值要白名单。这条铁律源自一个真实案例:某驱动允许用户态指定"寄存器编号",直接拿去当偏移量访问——攻击者传个超大编号,就获得了对整块映射区的任意读写。
/* 白名单式校验:编号必须落在已注册命令表内 */ static const struct reg_def reg_table[] = { { REG_ID, 1, true }, /* 编号、宽度、可写标志 */ { REG_CONFIG, 1, true }, { REG_STATUS, 1, false }, /* 状态寄存器只读 */ }; static int safe_reg_access(u32 num, bool write) { for (int i = 0; i < ARRAY_SIZE(reg_table); i++) { if (reg_table[i].num == num) { if (write && !reg_table[i].writable) return -EACCES; return i; } } return -EINVAL; /* 不在表内一律拒绝 */ }
第三关:状态一致性。并发到达的命令组合可能把设备推进非法状态——6.1 节的双进程对撞就是状态一致性问题的 benign 版本;恶意版本则是故意用并发命令组合试探"先关电源再提交传输"这类窗口。对策是把状态迁移集中到一处、全部持锁执行。
信息泄漏不崩溃、不报错,只是把内核内存"顺"给用户态。两个高发点。
未初始化内存回传:向用户态拷贝结构体时,为对齐填充的字段没有清零,内核栈上的旧数据——可能包含指针、密钥残片——原样出库。修法是拷贝前整体清零,或只拷贝有效字段:
struct status_report st; memset(&st, 0, sizeof(st)); /* 结构体变化也不漏填充字节 */ st.version = hw_version; st.state = led_state; if (copy_to_user(ubuf, &st, sizeof(st))) return -EFAULT;
越界读当功能用:read 回调对偏移量不加边界检查,用户态把偏移推到天上去,读回来的就是映射区之外的内核数据。1.3 节的责任表里"写入成功但行为怪异"一栏,安全视角下要重新审视:每个偏移、每次长度协商都可能是探测。
💡 关键直觉:防御性驱动的通用心法是"对输入做最坏假设"——长度可能溢出、指针可能为空、并发可能对撞、状态可能非法。把校验写在函数最前面,业务逻辑只处理"已消毒"的数据。
校验防"数据坏",权限防"人不该来"。设备文件的传统门禁是属主与模式位,配合 capabilities 细分特权——比如访问存储类设备需要特权能力,访问普通输入设备只需相应组员身份。驱动侧的配合点有二:open 回调里按需做 capability 检查;控制命令分级——只读查询放行普通用户,改变设备状态的命令要求更高权限。udev 规则(2.4 节)在设备层收紧属组与模式,是权限落地的运维侧抓手。
| 校验对象 | 威胁 | 防御手段 |
|---|---|---|
| 长度与偏移 | 溢出与越界 | 上限白名单、溢出显式检查、卡界 |
| 命令与索引 | 任意寄存器访问 | 白名单表、只读位保护 |
| 回传缓冲 | 内核数据泄漏 | 清零后回填、按有效字段拷贝 |
| 并发命令组合 | 状态机击穿 | 状态迁移集中持锁 |
| 设备访问者 | 越权使用 | 属主模式、capability、命令分级 |
拿三段典型缺陷代码练手(均为真实缺陷的简化),每段先自己找,再看答案。
/* 片段一 */ static long demo_copy(unsigned long arg, unsigned long len) { char kbuf[64]; copy_from_user(kbuf, (void __user *)arg, len); /* 隐患在哪 */ return 0; } /* 片段二 */ static long demo_index(unsigned long arg) { u32 idx; get_user(idx, (u32 __user *)arg); iowrite32(table[idx], base); /* 隐患在哪 */ return 0; }
片段一的 len 是用户值且未设上限——传一百兆直接把内核栈犁穿,修复是先卡上限再拷贝(还要检查返回值);片段二的 idx 未验证范围,恶意值等于任意偏移写寄存器,修复是白名单或卡界。两个片段的共同点:缺陷不在语法而在"用户输入直接参与了内存运算"——审自己的代码时,追着每个用户值走一遍"它最后用在哪",安全隐患基本无处藏身。
安全守住底线,下一节把目光转向另一端——让设备在守住底线的前提下跑得更快。