最近海思被爆的驱动代码的漏洞比较多,而且有几个可以用来root安卓设备,所以这两天投入了一下X版本的代码安全审计。结合自己之前分析的一些导致安卓root的CVE总结出了一些小技巧,记录一下。
不能一棒子打死说海思代码写的不好,因为有些模块参数校验做的还是相当不错,不过另有一些模块就根本没有考虑对用户入参的校验了,所以问题出在这里面。
另外一些开发人员,对像remap_pfn_range函数使用可能引发的安全问题认识不足,所以这里总结的都是一些值得开发或安全人员关注的点。
copy_from_user和copy_to_user涉及到用户态与内核态的数据交互,因此数据校验不足会导致比较严重的安全问题。举一个实际例子:
首先从华为官网下载荣耀6移动版的内核代码,地址是传送门,忍不住吐槽一句,有人说"华为用了linux内核的代码自己却不开源,大家快来看,他不要脸。"看看小米做的,其它厂商做的打脸了吧?
在source insight工程中打开,搜索关键字"copy_from_user"
先看一处没有问题的代码,
通过关键字定位到707行有一处copy_from_user调用,实现用户数据到内核内存的拷贝。要关注两个参数,一个是to地址大小,即这里的para_in,另一个是拷贝长度即usr_para_size
分析目的缓冲区para_in大小是否足够容纳usr_para_size大小的数据。
通过分析可以知道,para_in = kzalloc(para_size_in, GFP_KERNEL); ,申请空间大小为para_size_in大小,而para_size_in = usr_para_size + SIZE_CMD_ID; 因此这里不存在缓冲区溢出问题,这是一份校验做的比较好的代码示例。
我们再看一处漏洞代码,
hifi_misc是进行语音处理的模块,其中对应设备节点/dev/hifi_misc权限为system和audio,其内核入口如下
通过不同的cmd,进行不同的分支处理:
当cmd为HIFI_MISC_IOCTL_WRITE_PARAMS时,会调用hifi_dsp_write_param函数,将用户态传入的算法参数拷贝到内核空间:
继续跟踪hifi_dsp_write_para函数,发现其存在堆溢出漏洞:
int hifi_dsp_write_param(unsigned long arg) { ... if (copy_from_user(¶, (void*)arg, sizeof(struct misc_io_sync_param))) { // -> arg to para loge("copy_from_user fail.\n"); ret = ERROR; goto error1; } ... hifi_param_vir_addr = (unsigned char*)ioremap(hifi_param_phy_addr, SIZE_PARAM_PRIV); // -> 分配堆内存,大小SIZE_PARAM_PRIV if (NULL == hifi_param_vir_addr) { loge("hifi_param_vir_addr ioremap fail\n"); ret = ERROR; goto error2; } logd("hifi_param_vir_addr = 0x%x\n", (unsigned int)hifi_param_vir_addr); logd("user addr = 0x%x, size = %d", (unsigned int)para.para_in, para.para_size_in); ret = copy_from_user(hifi_param_vir_addr, para.para_in, para.para_size_in); // -> 未对para校验,堆溢出 ... }
漏洞PoC很简单,为para_in申请超过200*1024的内存传入即可,但前提是首先通过其它漏洞获得system或audio权限。
put_user实现了数据从内核到用户态的拷贝,与copy_to_user类似;get_usr相应的与copy_from_user类似,只不过put_usr/get_user常用于小数据量的拷贝,其效率更高。比如copy_from_user在拷贝4个字节的数据时,其内部其实调用的就是get_usr。
put_user ( x, ptr); x Value to copy to user space. ptr Destination address, in user space.
基于v6k和v7 ARM平台上的Linux kernel 3.5.4及之前的版本中的'get_user'和'put_user' API函数中存在安全漏洞,该漏洞源于程序没有验证目标地址ptr。攻击者可借助特制的应用程序利用该漏洞读取或修改任意内核内存位置的内容。
因此在未打补丁的相应安卓设备上,对get_user和put_user的调用都存在安全问题。下面代码,如果我们传入的para.para_out是一处内核代码,则会导致拒绝服务,精心构造可能导致任意代码执行,业界曾报过此类安卓root漏洞,可以参考CVE-2013-6282。
mmap可以实现将设备内存映射到用户空间,会使用到struct vm_area_struct结构体,当用户空间调用mmap时,系统通过创建一个表示该映射的VMA(虚拟内存区)作为响应。支撑mmap的驱动程序需要帮助进行完成VMA的初始化。
该结构中的重要成员:
unsigned long vm_start;
unsigned long vm_end;
该VMA所覆盖的虚拟地址范围。
struct file * vm_file;
指向与该区域相关联的file结构体指针。
unsigned long vm_pgoff;
以页为单位,文件中该区域的偏移量。当映射一个文件或设备时,它是该区域中被映射的第一页在文件中的位置。
unsigned long vm_flags;
描述该区域的一套标志。驱动程序最感兴趣的标志是VM_IO和VM_RESERVED。 VM_IO将VMA设置成一个内存映射IO区域。VM_IO会阻止系统将该区域包含在进程的核心转存中。VM_RESERVED告诉内存管理系统不要将该VMA交换出去;大多数设备映射中都设置该标志。
struct vm_operations_struct * vm_ops;
内核能调用的一套函数,用来对该内存区进行操作。它的存在表示内存区域是一个内核“对象”。
一个驱动程序只映射与其外围设备相关的一小段地址,而不是映射全部地址的例子。为了向用户空间只映射部分内存的需要,驱动程序只需要使用偏移量即可。下面的代码揭示了驱动程序如何对起始于物理地址simple_region_start(页对齐),大小为simple_region_size字节的区域进行映射的工作过程:
(注:“外围设备” 即是设备文件。 vm_pgoff; 为要映射区域在“外围设备”文件中的偏移量(以页为单位),“外围设备”文件即是指外围设备的一段内存。这个偏移量即使相对于这段内存开始的偏移量)
unsigned long off = vma->vm_pgoff << PAGE_SHIFT; //以页为单位转为以字节为单位 unsigned long physical = simple_region_start + off; //计算相对于文件开始的偏移 unsigned long vsize = vma->vm_end - vma->vm_start; //要映射的区域大小 unsigned long psize = simple_region_size - off; //偏移之后文件的剩余空间 if (vsize > psize) //是否偏移之后文件的剩余空间不足以满足要映射的空间大小 return -EINVAL; /* 跨度过大*/ remap_pfn_range(vma, vma_>vm_start, physical, vsize, vma->vm_page_prot);
做代码审计时,需重点关注remap_pfn_range的两个参数physical和vsize,physical代表设备中映射的起始物理地址,而vsize表示映射的大小。这两个参数设置不当,会导致映射任意地址和大小内存,从而任意内存的读写。
仍通过一个例子看一下,
hifi_misc_mmap内核函数中,虽然phys_page_addr使用的是固定地址,但size却使用用户传入的vma->vm_end-vma->vm_start计算,由外部输入控制。因此可以实现对phys_page_addr之后任意设备内存的读写。
漏洞利用的一个重要步骤,就是获取一些敏感内核态内存地址,通常会通过Information Leak等漏洞获取。而代码中大量使用的%p为攻击者打开方便之门,对root漏洞来说,通过动态解析这些内核指针,可以实现通用root方案。
因此建议打印内核指针时,使用%pK加以保护,这块是个人理解。