4、权限控制基础:Linux权限模型(DAC、capabilities、LSM)、文件操作权限检查、ioctl权限校验
Linux内核的权限控制是驱动安全的第一道防线。驱动开发者常犯的错误是“只关注功能逻辑,忽略权限边界”,导致用户态程序通过设备节点直接读写内核内存、篡改寄存器或绕过安全策略。本章从Linux权限模型的三个层次(DAC、capabilities、LSM)出发,深入剖析文件操作与ioctl调用中的权限校验机制,并结合实战案例说明如何在驱动中构建纵深防御体系。
上图展示了驱动权限校验的完整链路:用户态发起系统调用后,VFS层首先进行DAC检查,随后LSM钩子介入强制访问控制,最终到达驱动实现的file_operations。驱动开发者必须理解:DAC是基础门槛,capabilities是特权细分,LSM是系统级强制策略。三者协同工作,任何一层被绕过都可能导致权限提升漏洞。
4.1 核心原理:DAC、capabilities与LSM的协同机制
DAC(自主访问控制)是Linux最基础的权限模型,基于文件所有者、属组和其他用户的读/写/执行权限位(rwx)。驱动设备节点通常由root创建,默认权限为crw-rw----(如/dev/mem),这意味着只有root和所属组(通常为kmem)可以访问。DAC的局限性在于:一旦进程获得文件描述符,后续操作不再重新检查权限;且root用户拥有所有DAC权限,无法细分。
capabilities机制将root的超级权限拆分为约40种独立能力,如CAP_SYS_ADMIN(系统管理)、CAP_DAC_OVERRIDE(绕过DAC检查)、CAP_NET_RAW(原始套接字)。驱动开发中常用capable(CAP_SYS_ADMIN)来校验调用者是否具备特定特权。与uid==0判断相比,capabilities更精细——非root进程也可以被赋予特定capability,而root进程也可能被剥夺某些capability。
LSM(Linux安全模块)提供强制访问控制(MAC)框架,通过钩子函数在关键路径上插入安全策略。SELinux和AppArmor是主流实现,它们基于安全上下文或路径规则对进程访问设备节点进行额外限制。驱动开发者可以通过security_file_permission钩子感知LSM的决策结果,但通常无需直接调用LSM接口——内核会在VFS层自动调用。
关键认知:DAC是“所有者决定”,capabilities是“特权细分”,LSM是“系统策略强制”。驱动代码中应优先使用capabilities而非uid==0判断,同时尊重LSM的决策结果,不要尝试绕过。
4.2 判断方法:文件操作权限检查的完整流程
当用户态进程执行open("/dev/mydrv", O_RDWR)时,内核执行以下权限检查序列:
- VFS路径解析:遍历路径各分量,检查执行权限(x)
- inode权限检查:调用
inode_permission(),比较当前进程的fsuid/fsgid与inode的uid/gid/mode - ACL检查:如果文件系统支持POSIX ACL,则检查扩展访问控制列表
- LSM钩子:调用
security_inode_permission(),SELinux等模块在此执行MAC策略 - 打开文件:驱动
open()方法被调用,此时可执行额外的权限校验(如capabilities检查)
驱动开发者在open()中应避免重复DAC检查(内核已完成),但应针对设备特有资源进行校验。例如,一个只允许特定组访问的调试接口,应在open()中检查current_fsuid()或使用inode_permission()辅助函数。
| 检查阶段 | 内核函数 | 驱动开发者操作 | 典型错误 |
|---|---|---|---|
| VFS路径解析 | link_path_walk() | 无需干预 | 设备节点路径权限过宽 |
| inode权限 | inode_permission() | 可调用该函数做二次校验 | 依赖open()时uid判断 |
| LSM钩子 | security_inode_permission() | 无需干预,但需了解策略 | 忽略SELinux拒绝日志 |
| 驱动open() | drv_open() | 检查capabilities/资源互斥 | 仅检查uid==0 |
4.3 ioctl权限校验:从命令号到参数的双重验证
ioctl是驱动控制通道的核心,也是权限漏洞的高发区。一个安全的ioctl实现必须包含三层校验:
第一层:命令号校验。Linux内核提供_IOC()宏族(_IO、_IOW、_IOR、_IOWR)生成命令号,其中编码了方向、大小和魔数。驱动应使用_IOC(dir,type,nr,size)宏定义命令,并在ioctl()中通过_IOC_DIR(cmd)、_IOC_TYPE(cmd)等宏验证命令合法性。未识别的命令应返回-ENOTTY,而不是静默忽略。
第二层:调用者权限校验。根据命令类型决定所需权限:
- 只读命令(
_IOR):通常无需特权,但需确保数据不泄露敏感信息 - 写命令(
_IOW):需检查capable(CAP_SYS_ADMIN)或文件打开模式(FMODE_WRITE) - 特权命令(如固件升级、寄存器读写):必须使用
capable()或自定义capability
第三层:参数指针校验。使用copy_from_user()/copy_to_user()时,内核已自动处理地址合法性,但驱动仍需验证参数内容。例如,一个设置缓冲区大小的ioctl,必须检查size是否在合理范围内,防止后续内存分配溢出。
static long mydrv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
int ret = 0;
// 第一层:命令号校验
if (_IOC_TYPE(cmd) != MYDRV_MAGIC) return -ENOTTY;
if (_IOC_NR(cmd) < MYDRV_IOCTL_MIN || _IOC_NR(cmd) > MYDRV_IOCTL_MAX) return -ENOTTY;
// 第二层:权限校验
switch (cmd) {
case MYDRV_SET_MODE:
if (!capable(CAP_SYS_ADMIN)) return -EPERM;
break;
case MYDRV_GET_STATUS:
if (!(filp->f_mode & FMODE_READ)) return -EPERM;
break;
default:
return -ENOTTY;
}
// 第三层:参数校验
if (_IOC_DIR(cmd) & _IOC_WRITE) {
void __user *p = (void __user *)arg;
if (!access_ok(p, _IOC_SIZE(cmd))) return -EFAULT;
}
// ... 实际处理
return ret;
}
风险警示:切勿使用int或long直接解引用用户指针(如*(int *)arg),这会导致内核崩溃或安全漏洞。必须使用copy_from_user()系列函数。同时,ioctl命令号中的size字段应使用sizeof()而非硬编码数值,以适配32/64位兼容。
4.4 实战案例:字符设备驱动的权限加固
某物联网设备驱动提供固件升级功能,初始实现仅检查uid==0,导致任何root权限进程(包括被攻破的root服务)都能升级固件。我们进行以下加固:
初始现象:驱动open()中仅判断current_uid()==0,ioctl的升级命令无额外校验。安全审计发现,一个以root运行的Web服务存在命令注入漏洞,攻击者借此调用升级ioctl写入恶意固件。
加固方案:
- 引入专用capability:定义
CAP_SYS_ADMIN作为升级权限,在ioctl中调用capable(CAP_SYS_ADMIN),而非uid判断 - 增加命令号魔数校验:使用
_IOW(MYDRV_MAGIC, 0x01, struct fw_data),拒绝未知命令 - 参数范围检查:验证
fw_data.size不超过固件缓冲区上限(如1MB),防止整数溢出 - LSM集成:在SELinux策略中,仅允许特定域(如
fw_update_t)访问设备节点,Web服务域被拒绝
结果对比:加固后,即使Web服务被攻破,攻击者也无法调用升级ioctl——因为capability检查失败,且SELinux策略阻止了设备节点访问。固件升级功能仅对明确授权的管理进程开放。
复盘经验:权限校验应遵循“最小特权”原则:使用capabilities而非uid==0;命令号必须严格校验;参数内容必须验证;LSM策略作为最后防线,即使驱动代码有漏洞,系统级策略仍可阻断攻击。
最佳实践:在驱动中实现一个统一的mydrv_check_permission()函数,集中管理所有权限校验逻辑。该函数接收命令号和参数指针,返回0或错误码。这样便于代码审查和后续安全加固。
4.5 常见误区与总结复盘
误区一:依赖uid==0判断特权。在容器或用户命名空间环境中,uid==0并不等于全局root。应使用ns_capable()或capable(),它们会正确判断当前命名空间的能力。
误区二:ioctl命令号不校验魔数。如果驱动接受任意cmd值,攻击者可能构造特殊命令触发未预期的代码路径。必须校验_IOC_TYPE()和_IOC_NR()范围。
误区三:忽略LSM的存在。某些驱动开发者认为“SELinux是系统管理员的事”,在代码中尝试绕过LSM检查(如直接调用sys_setuid())。这会导致安全策略失效,且可能引发内核警告。
误区四:open()中做过多校验。open()时检查权限是必要的,但不应重复DAC检查。例如,如果设备节点权限为0640,open()中再检查uid==0是冗余的,且可能引入TOCTOU(时间-of-check-to-time-of-use)竞态。
总结:驱动权限控制的核心是“分层防御”:DAC提供基础隔离,capabilities细分特权操作,LSM实施系统级强制策略。驱动开发者应专注于capabilities校验和参数验证,同时理解并尊重LSM的决策。在实现ioctl时,务必遵循“命令号→权限→参数”的三层校验模式,并使用copy_from_user()安全访问用户数据。通过本章的实战案例,可以看到单一权限检查的不足,以及纵深防御体系的有效性。