驱动安全编码&排查指南
作者: 魏来
什么是驱动
驱动程序是IoT设备能驱使外设的一种代码程序。不常接触底层开发的读者可能会有一个误区,认为驱动程序是运行在外设器件里的。但是,其实驱动程序是运行在IoT 设备主机侧,由主机操作系统内核执行的代码。
举一个常见的驱动执行流程为例:
用户态app 使用系统调用 打开 设备驱动程序,并发送读写数据请求到驱动进行执行。运行在内核态的外设驱动代码 将请求转发到主机控制器驱动的代码片段(同样运行在主机侧内核态里的驱动代码),最终当CPU执行到控制器驱动代码时,将指挥内嵌在SoC 里的控制器设备将请求途经总线转发到外设上。
上述是软件层面的视角,我们再从硬件的控制流视角看一下底层是怎么运作的。
内核驱动风险性
既然驱动代码运行在操作系统内核态,这也就意味着驱动代码是以内核的权限身份执行。一旦驱动存在可被用户程序触发的漏洞,轻则导致系统级复位,重则被攻击者诱导篡改其处在用户态进程的身份权限。这是因为用户进程的身份管理信息(cred) 是由内核进行管理的,内核态出现命令执行时即可修改用户权限。
下面给出一个提权的实例,建立一个直观感受:
在IOT设备上,提权攻击往往是在攻击者从外部攻破设备对外提供服务的业务进程,拿到业务进程shell权限后,进一步权限提升时发生的。
常见安全漏洞
缓冲区溢出/越界写
当内核面向用户态的接口中存在类似如下代码时,即可完成缓存区溢出。
memcpy(dest, src, bigLen); //从src 数组里拷贝bigLen字节到dest数组,bigLen大于dest数组的大小。
copy_from_user(dest, src, len); //len 大于dest 数组大小
...
需要严格校验拷贝字符数量是否大于源/目的缓冲区大小。
上述代码片段,对应如下内存操作
危害
我们知道,函数调用的时候会在栈上创建函数栈帧,如果在子函数中发生缓冲区溢出,则可以覆盖到调用者函数栈帧中保存的数据。如果调用者栈帧中保存的函数返回地址被篡改了,则可以发生ROP 任意代码地址跳转的攻击,或者至少导致内核crash。
下图是一个函数调用对应的 内存栈帧图
举一个典型的内核栈缓冲区溢出攻击的案例,用户进程通过驱动提供的系统调用拷贝数据进内核栈。由于没有严格校验拷贝字符数量,溢出覆盖了内存高地址函数栈帧中的数据。恶意构造了ROP 链,布局了恶意返回地址以及伪造的进程上下文。
引诱内核以高权限执行用户态的一段恶意代码(commit_creds(prepare_kernel_cred(0))) 将进程uid修改为0(root)
最后执行system("/bin/sh"), fork一个新的权限为root的shell进程控制系统。
任意内存读
当内核面向用户态的接口中存在类似如下代码时,即可完成任意内存读。
//src地址 用户入参可控,可以指定任意内核态地址,泄露内核数据
copy_from_user(dest, src, len);
//不限于copy_xxx api, memcpy等都能达到类似效果
因此,一定要校验入参地址范围。