4. 驱动文件操作权限检查
在 Linux 驱动开发中,文件操作接口(open、release、read、write、ioctl)是用户空间与内核空间交互的主要通道。如果这些接口缺乏严格的权限校验,攻击者可以通过普通用户态程序直接操作硬件寄存器、读取敏感内核数据或破坏系统稳定性。本章深入分析每个接口的权限检查要点,并结合 SELinux 策略给出适配方案。
核心原则:驱动层的权限校验不能依赖用户空间传递的“可信”标识,必须在内核态重新验证凭证(credential)和访问模式。SELinux 策略则提供强制访问控制(MAC)的第二道防线。
4.1 权限校验的整体架构
驱动文件操作的权限校验分为三个层次:
- DAC(自主访问控制):基于文件 inode 的 uid/gid/mode,由 VFS 层自动完成,驱动通常无需干预。
- Capability 检查:驱动在关键操作前调用
capable()或ns_capable()检查进程是否拥有特定特权。 - LSM(Linux Security Module)钩子:SELinux 等 LSM 通过安全钩子函数(如
selinux_file_open、selinux_file_permission)实施细粒度策略。
4.2 open 方法中的权限校验
open 是驱动暴露给用户空间的第一个入口。VFS 层在调用驱动 open 回调之前,已经完成了 DAC 检查(根据文件权限位判断是否允许打开)。但驱动 open 内部仍需做以下校验:
| 检查项 | API / 方法 | 说明 |
|---|---|---|
| 文件标志验证 | filp->f_flags |
检查是否包含 O_RDONLY、O_WRONLY、O_RDWR,拒绝非法组合 |
| 设备独占检查 | 原子变量 / mutex | 防止同一设备被多个进程同时打开(如串口独占) |
| Capability 检查 | capable(CAP_SYS_RAWIO) |
操作敏感硬件时要求特权 |
| SELinux 上下文 | selinux_file_open 钩子 |
检查进程安全上下文是否允许打开该设备文件 |
实战案例:某 GPU 驱动在
open 中未检查 O_RDWR 标志,导致普通进程以读写模式打开只应被 root 使用的控制节点,进而通过 mmap 映射 GPU 寄存器实现权限提升。修复方案是在 open 开头增加标志位校验。
static int my_dev_open(struct inode *inode, struct file *filp)
{
// 1. 检查文件打开模式
if ((filp->f_flags & O_ACCMODE) == O_RDWR) {
if (!capable(CAP_SYS_RAWIO))
return -EPERM;
}
// 2. 设备独占检查
if (!atomic_add_unless(&dev->open_count, 1, 1))
return -EBUSY;
// 3. 初始化私有数据
filp->private_data = dev;
return 0;
}
4.3 release 方法中的权限校验
release 在文件描述符关闭时调用。虽然 release 本身不执行新的权限检查,但必须确保:
- 资源清理不泄露敏感信息:释放前清零内核缓冲区,防止后续通过
ptrace或内存转储读取残留数据。 - 引用计数正确递减:避免因计数错误导致设备被意外卸载或内存泄漏。
- SELinux 上下文重置:如果驱动在
open时修改了进程安全上下文,release时应恢复。
常见漏洞:驱动在
release 中释放了共享内存,但未通知其他仍在访问该内存的进程,导致释放后重用(UAF)。权限校验不能只关注入口,出口同样需要安全。
4.4 read/write 方法中的权限校验
read 和 write 是数据交换的核心路径。权限校验要点如下:
| 校验维度 | read 场景 | write 场景 |
|---|---|---|
| 访问模式匹配 | 检查 filp->f_mode & FMODE_READ |
检查 filp->f_mode & FMODE_WRITE |
| 缓冲区安全 | 使用 copy_to_user(),验证用户地址范围 |
使用 copy_from_user(),防止内核栈/堆溢出 |
| 数据范围检查 | 偏移量 + 长度不超过设备容量 | 偏移量 + 长度不超过设备容量 |
| Capability 检查 | 读取敏感寄存器时需 CAP_SYS_RAWIO |
写入关键配置时需 CAP_SYS_ADMIN |
| SELinux 检查 | selinux_file_permission 钩子 |
selinux_file_permission 钩子 |
static ssize_t my_dev_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_dev *dev = filp->private_data;
// 1. 检查读权限
if (!(filp->f_mode & FMODE_READ))
return -EPERM;
// 2. 检查偏移和长度
if (*f_pos >= dev->data_size)
return 0;
if (*f_pos + count > dev->data_size)
count = dev->data_size - *f_pos;
// 3. 安全拷贝到用户空间
if (copy_to_user(buf, dev->data + *f_pos, count))
return -EFAULT;
*f_pos += count;
return count;
}
4.5 ioctl 方法中的权限校验
ioctl 是驱动中最容易引入权限漏洞的接口,因为它支持自定义命令码。每个命令码都应独立进行权限校验。
4.5.1 命令码分类与校验策略
| 命令类型 | 示例 | 推荐校验方式 |
|---|---|---|
| 只读查询 | 获取设备状态、版本号 | 检查 FMODE_READ,无需特权 |
| 只写配置 | 设置参数、启动/停止 | 检查 FMODE_WRITE,可能需要 CAP_SYS_ADMIN |
| 读写控制 | 读写寄存器、DMA 操作 | 检查 FMODE_READ|FMODE_WRITE,需要 CAP_SYS_RAWIO |
| 危险操作 | 固件升级、复位、解锁 | 必须 capable(CAP_SYS_ADMIN) 或更高 |
4.5.2 ioctl 权限校验模板
static long my_dev_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct my_dev *dev = filp->private_data;
int ret = 0;
// 通用检查:命令码方向与文件打开模式匹配
if (_IOC_DIR(cmd) & _IOC_READ)
if (!(filp->f_mode & FMODE_READ))
return -EPERM;
if (_IOC_DIR(cmd) & _IOC_WRITE)
if (!(filp->f_mode & FMODE_WRITE))
return -EPERM;
switch (cmd) {
case MY_IOCTL_GET_STATUS:
// 只读,无需额外特权
ret = put_user(dev->status, (int __user *)arg);
break;
case MY_IOCTL_SET_CONFIG:
// 写操作,需要 CAP_SYS_ADMIN
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
ret = get_user(dev->config, (int __user *)arg);
break;
case MY_IOCTL_WRITE_REG:
// 读写操作,需要 CAP_SYS_RAWIO
if (!capable(CAP_SYS_RAWIO))
return -EPERM;
// 执行寄存器写入
break;
default:
return -ENOTTY;
}
return ret;
}
关键点:使用
_IOC_DIR(cmd) 宏自动提取命令码方向,与 filp->f_mode 比对,可以防止用户通过 ioctl 绕过 read/write 的权限检查。这是很多驱动容易遗漏的校验。
4.6 SELinux 策略适配要点
为驱动文件操作编写 SELinux 策略时,需要定义设备文件的类型和访问向量:
| SELinux 对象 | 类型定义 | 允许的访问向量 |
|---|---|---|
| 设备文件 | my_device_t |
open read write ioctl |
| 设备目录 | my_device_dir_t |
search add_name remove_name |
| 进程域 | my_app_t |
my_device_t:chr_file { open read write ioctl } |
# SELinux 策略示例:允许 my_app_t 域访问 my_device_t 设备文件
type my_device_t;
type my_device_dir_t;
dev_node(my_device_t)
# 允许应用进程打开并操作设备
allow my_app_t my_device_t:chr_file { open read write ioctl };
allow my_app_t my_device_dir_t:dir { search };
# 允许内核模块加载设备
allow kernel_t my_device_t:chr_file { create setattr };
适配建议:在驱动
open 回调中,可以通过 selinux_check_access 函数进行更细粒度的运行时检查,例如根据 ioctl 命令码动态判断是否需要额外的 SELinux 权限。这比静态策略更灵活,但会增加复杂度。
4.7 常见权限漏洞与防护清单
| 漏洞类型 | 触发场景 | 防护措施 |
|---|---|---|
| open 绕过 | 通过 ioctl 直接操作设备而不经过 open |
在 ioctl 中检查 filp->private_data 是否已初始化 |
| ioctl 命令码劫持 | 使用合法命令码传递恶意参数 | 严格校验 arg 指向的用户地址和长度 |
| 权限提升 | 普通进程通过驱动获取 root 权限 | 所有特权操作必须调用 capable(),不能依赖 uid==0 判断 |
| SELinux 绕过 | 驱动未调用 LSM 钩子 | 确保驱动使用标准文件操作接口,不要自己实现系统调用 |
| 竞争条件 | 并发 open 导致权限检查失效 |
使用互斥锁保护权限检查与资源分配之间的临界区 |
4.8 本章小结
驱动文件操作的权限校验是 Linux 安全体系中的重要一环。开发者需要在 open、release、read、write 和 ioctl 中分别实施 DAC、Capability 和 SELinux 三层检查。其中 ioctl 由于命令码的多样性,是权限漏洞的高发区,必须为每个命令码独立设计校验逻辑。结合 SELinux 策略,可以实现即使 root 进程也无法绕过强制访问控制的安全目标。