初步理解内核漏洞:(内核开发人员必读)
内核开发人员,如果不了解内核漏洞的话,容易在开发时引入漏洞,轻则导致内核崩溃,影响上面的所有应用程序;重则导致内核提权,即可以突破应用层的沙箱,进入内核,并在内核里面为所欲为。
本文章就是讲解一下常见的内核漏洞类型,让内核开发人员有个初步的了解,从而在开发时就会有潜意识,不会制造比较明显的漏洞了。(对于专门挖过内核漏洞的人来说,该文章就没必要看了:)。
常见的内核漏洞有下面几种:栈溢出(stack overflow)、堆溢出(heap overflow)、整型溢出漏洞(integer overflow)、释放后重用(use-after-free)、双重释放(double free)、线程竞争(race condtion).
下面通过例子,一一进行简单地介绍:
栈溢出:
例子:
static size_t deviceA_write(struct file *filp, const char __user *buf, size_t len, loff_t *data) {int ret = 0;char tmp[100] = { 0 };/* write data to the buffer */if (copy_from_user(tmp, buf, len)) {return -EFAULT;}
上述是一个设备驱动的代码,提供了write的接口。即用户在应用层可以调用write函数,向该设备写数据。 内核开发人员都知道copy_from_user是从应用层往内核层拷贝数据,其第一个参数tmp为拷贝的目的地址;第二个参数buf 为拷贝的源地址;第三个参数为拷贝的数据的长度。在这里目标地址的长度为100,但是由于没有对拷贝的长度(len)做限制,当用户传入的长度大于100时,用户的数据就会覆盖掉tmp的100个字节以外的数据。由于栈是从高往低的,因此覆盖的数据会是比tmp的高地址的数据。程序的返回地址一般在进行函数调用时放到栈上,在这里如果将返回地址覆盖掉了,那么就可以控制指令计数器IP,从而可以在内核层任意执行代码了。(这里暂时不考虑内核的缓解措施)
堆溢出:
堆溢出和栈类似,只不过溢出的对象为堆而已。比如将上述例子中的char tmp[100] 换成char *tmp = kmalloc(100); 那么就变成堆溢出漏洞了。
整型溢出:
整型溢出,就是在进行整数运算时没有考虑到整数本身的上下限。比如unsigned int的下限是0, 上限是0xffffffff. 当a,b,c都是unsgined int 类型时, a = b + c,就有可能发生溢出,溢出的结果会导致a比b或者c都小。 比如b = 0xffffffff, c = 2, 那么a = b + c = 0xffffffff + 2 = 1, 如果对整型溢出不了解,是不是觉得很神奇? 两个数相加的和竟然变小了!
例子:
1 int driver_create_buffer(Command *userCommand)2 {3 ...4 uint32_t totalCount = 0;5 uint32_t *regAddrBuf = NULL;7 uint32_t kernelCount = 1000;8 const uint32_t userCount = userCommand->count;9 ...10totalCount = kernelCount + userCount;11regAddrBuf = kzalloc(totalCount * sizeof(uint32_t), GFP_KERNEL);12copy_from_user(regAddrBuf, userCommand->buffer, userCount*sizeof(uint32_t));13 ...14 }
上述代码的第10行, 将正整数kernelCount与userCount相加,存储到totalCount中。 而userCount是用户可以控制的,当将该值设为很大时,就会导致溢出,比如userCount= 0xffffffff - 1000 + 1时, 那么totalCount的大小就为1。在11行,分配的空间时按照totalCount 进行分配的,在而在12行拷贝的内容长度为userCount, 而userCount明显比totalCount大。因此会导致溢出漏洞。
释放后重用:
顾名思义,就是一个内核对象在释放后又使用该对象。这种的漏洞的利用技术已经很成熟了,这里先不介绍利用了:)
1. static int dealloc_session(void *core_handle,2.struct session *session)3. {4....5.deregister(session->handle);6.mutex_destroy(&session->lock);7.sessions[session->id] = NULL;8.kfree(session);9.session = NULL;10....11. }
初一看,没什么问题, 将session的所有内容成员都释放以后,然后对session本身进行kfree以后,并且马上设置为NULL。 但是这里没有考虑多线程的问题。比如一个线程在第8行,将session内核对象释放以后,此时另外一个线程正在第5行访问该对象。那么就造成了释放后重用漏洞。
双重释放:
双重释放,顾命思义,就是将一个对象释放两次。那么开发人员说了,释放两次就释放两次,那能利用吗? 那么我要说了: 可以利用! 双重释放的漏洞是可以转化为释放后重用漏洞的。并且技术也很成熟。 下面看一个例子:
1. static long deviceB_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)2 {3....4.if (__copy_from_user(idc, (void __user *)arg, tmp)) {5. kfree(idc);6. err = -EFAULT;7.} else {8. err = spi_message(spi, idc, n_ioc);9.}10. kfree(ioc);11. return 0;13. }
ioctl是内核开发人员很熟悉的,通过ioctl可以给设备驱动添加新的功能。用户层可以直接调用ioctl来使用设备的新功能。 在上述例子中,第5行,将idc内核对象释放了,但是没有返回。在第10行又被释放了一次,造成了内核对象idc的双重释放。
线程竞争:
线程竞争漏洞并不是独立的漏洞,一般由于线程竞争会导致释放后重用漏洞或者双重释放漏洞或者堆溢出漏洞等。比如上述的释放后重用漏洞就是由于线程竞争导致的,这里不再进一步说明啦!
Linux 内核学习经验总结
学习内核,每个人都有自己的学习方法,仁者见仁智者见智。以下是我在学习过程中总结出来的东西,对自身来说,我认为比较有效率,拿出来跟大家交流一下。
这份是整理的学习路线思维导图,下面有Linux内核相关视频学习资料:
Linux内核相关学习视频,清晰版导图可以点击:学习资料 获取
如果觉得《理解Linux内核漏洞:(内核开发人员必读)》对你有帮助,请点赞、收藏,并留下你的观点哦!