任镇,陈莉君
一种面向Linux内核模块的内存检错方法
任镇,陈莉君
为了方便发现特定内核模块中隐藏的内存访问错误,本文设计了一个面向Linux 内核模块的内存检测框架KMMemcheck。该框架使用可选择性影子内存方法记录内核模块中动态分配的内存块信息,结合二进制分析技术解析内存访问指令,然后借助影子内存提供的信息判断内存访问是否合法。从而解决了内核自带检测工具不能独立、高效地检测特定内核模块内存访问错误的问题。实验结果表明,相比内核自带内存检测工具,KMMemcheck显著地降低了对系统I/O吞吐率的影响。
内核模块;内存访问检测;影子内存
据权威机构US-CERT漏洞数据库[1],在linux内核所有漏洞中内存访问越界约占14%,内存泄露约占4%。内存访问越界很容易破坏正常数据,使程序出错或获得意想不到的结果;内存泄露会不断消耗内存资源,降低系统运行效率,甚至耗尽内存最终导致系统崩溃。值得注意的是以可装载内核模块方式开发的设备驱动[2]代码增长最快,同时也是内核中bug最多的部分[3]。作为发现内存bug,提高软件质量的重要手段,内存检测工具一直以来备受开发者关注。从使用范围来看内存检测工具有用户态和内核态两类。应用开发者使用用户态工具检测应用程序,而内核和驱动开发者使用内核态工具。用户态具有代表性的工具有PREFix[4]和Valgrind[5]中的memecheck[6]等;而内核态具有代表性的工具有Kmemcheck[7]和KEDR[8]等。对国内外多种典型内存检测工具分析之后,发现用户态程序的内存检测工具种类丰富、检测功能齐全可靠,已经发展得相当成熟,而Linux内核态下的内存检测工具数量和质量都明显不足。
内核态下内存检测工具普遍存在功能单一和系统性能消耗大等缺点。其中,Kmemcheck是基于缺页异常处理机制和影子内存技术实现的,每次内存访问指令都会触发页故障处理而且消耗大量内存,使整个系统性能大幅度下降。KEDR是一个面向内核模块的检测框架,由于它是基于系统调用[9]拦截技术实现的,因此功能仅仅局限于内存泄露的检测,而不能检测内存越界等其它常见错误。
针对内核态内存检测工具功能单一、性能差的不足,结合可选性影子内存(Selective Shadow Memory,SSM)方法和二进制分析技术[10],本文设计了一个面向Linux内核模块的内存检测框架(Memory Check for Kernel Module,KMMemcheck)。
1.1 KMMemcheck的设计
KMMemcheck是基于面向内核模块的二进制分析框架Granary[11],并结合可选择性影子内存方式SSM设计的。Granary最大的特点是只对感兴趣的内核模块而不是整个内核进行二进制分析。而SSM是一种轻量级影子内存[12]方法,只监控内核模块中动态分配的内存。系统整体设计框架,如图7示:
图7 KMMemcheck整体框架
Granary主要起两个作用:一方面通过内存分配函数(如kmalloc等)封装层捕获内核模块内存分配行为,为内存对象(内核模块中的阴影部分)分配并初始化元数据,并将元数据添加到SSM组件中;另一方面在每一条内存访问指令执行前,调用由插件实现的钩子函数,根据元数据判断内存访问合法性。
另外,SSM组件负责建立内存对象与影子内存的映射关系,并向内存检错插件提供接口。然后针对不同的内存错误,实现SSM组件提供的接口,便可开发出不同功能的内存检错插件,使系统具有可扩展性。SSM组件的设计,如图2所示:
目前的x86-64架构CPU都遵循AMD的Canonical规范,内核空间地址高16位必须置1。SSM利用高16位作为元数据表索引,这样做的优点是只要通过简单掩码运算就能建立起内存对象与影子内存间的映射关系。但是如果随着元数据分配越来越多,不同的内存对象可能会得到相同的索引,该问题可以通过哈希表链地址法解决。
影子内存结构由两部分组成:元信息和钩子函数表。在分配内存块时,元信息用来保存内存块的特征,如内存块起始地址、大小和是否已初始化等。钩子函数表主要包括插桩和合法性检测两个接口。Granary将二进制代码块解析之后,会调用插桩方法查找内存访问指令并记录内存访问基址和偏移量等信息,然后将这些信息作为参数传递给合法性检测方法。合法性检测方法对比元信息和内存访问指令信息,从而判断内存访问的合法性。
1.2 KMMemcheck的实现
1.2.1 动态添加影子内存
Granary提供了一套内核函数封装机制,使得被调用的函数重定向去执行自身的封装函数。内核具有一系列内存分配函数,都需要生成对应的封装函数。以__kmalloc函数为例说明影子内存的添加过程如图3所示:
图3 内存分配封装函数
封装函数先调用自身__kmalloc进行实际内存对象分配,再调用Add_Shadow_Mem函数分配一块影子内存,用影子内存来存储元数据,然后添加到影子内存表中。
1.2.2 内存越界检测插件
一种常用的内存越界方法是在内存块末尾追加标记内存,如果程序访问了标记内存,说明发生了内存访问越界。但是,这种方法的有效性依赖于标记内存的大小,如果标记内存较小,跨度大的越界就检查不到,如果标记内存较大,那么太浪费存储空间。本文提出的方法能准确地记录内存块的首地址和大小,使检测的有效性不依赖于额外的标记内存大小,因此更适合实现内存越界检测工具。
如下图4所示:
图4 检测内存越界的数据结构
bound_descriptor是用来描述内存对象的结构体,其中base表示内存块的起始地址,uper_bound表示内存块的上界,通过单链表将所有元数据组织起来。
内存访问越界的处理流程,如图5所示:
图5 内存访问越界处理流程
1)指令解析:运用DBT技术,对机器指令进行反汇编,得到指令的操作码和操作数。
2)判断访问地址是否被监控:若给一个内存块添加了影子内存,则称这个内存块的首地址是被监控的。
3)若访问地址是被监控的,则以地址的高16位为索引查找哈希表,获取相应元数据。
4)检查访问合法性:假设我们要向缓存中写入数据,缓存首地址为base,索引为index,缓存元素数据类型大小为type_size,将要写入的操作数大小为opd_size,则判断访问是否越界的公式如公式(1)、(2):
如果公式(1)成立为向下溢出,公式(2)成立为向上溢出,统称内存访问越界。
5)如果检查到内存访问越界,则生成错误报告。
通过实现内存越界检测工具,展现KMMemcheck框架的有效性和扩展性,然后通过性能测试验证其对系统性能影响小的优点。实验使用的PC机配置为Intel® Core™ 2 Duo 2.93 GHz处理器, 4GB内存,安装Ubuntu 64位操作系统,内核版本为linux kernel 3.8。
以内存越界检测为例,进行功能性验证。首先,通过设置编译参数启用内存越界检测插件;然后将以可装载内核模块形式的检测工具kmmemcheck.ko插入内核;接着,将测试模块testmod.ko插入内核,模块中动态分配了一块缓冲区,并对缓冲区进行写溢出操作;然后读取测试结果如下所示:
表示在testmod_init函数偏移0x0e处有1个字节的写溢出,若借助objdump工具便可定位源码位置。
接下来使用IOZone测试工具,对Ext4文件系统I/O吞吐率进行测试来反应内存检错工具对系统性能的影响。实验分3组:native不使用内存检错工具,KMMemcheck使用本文设计的检测框架,kmemcheck是内核自带的内存检错工具。每组实验分别对512M大小的文件进行读写,重复读写和随机读写测试,测试结果,如图6所示:
图6 ext4文件系统I/O吞吐率
测试数据反映使用kmemcheck进行内存检测时I/O吞吐率下降得非常明显,相比之下KMMemcheck对系统性能的影响不大,不会影响对系统的正常使用,而且提高了测试效率。
基于动态二进制指令解析技术和可选择性内存影子方法,设计了一个面向内核模块的内存检错方法,介绍了其基本工作原理和关键实现环节。该方法能够在程序运行过程中实时检测来发现隐性的内存问题,并且基于插件式的开发接口可以构建不同类型的内存错误检测工具。而且,面向内核模块的特点不仅缩小了内存错误的检查范围,也带了减少系统消耗的好处。但是,可选择性内存影子方法只能够为动态分配的内存对象创建元数据,因此,该方法不能检测静态内存错误。随着对影子内存技术的深入研究,把影子内存技术应用到整个内核模块地址空间将克服这个问题。
[1] US-CERT. US-CERT vulnerability notes database [EB/OL]. http://www.kb.cert.org/vuls,2014,05.
[2] 陈莉君.Linux内核的分析及应用[J].西安邮电学院学报,2001,6(1):18-20.
[3] Kernel Bug Tracker.内核中各子系统bug占比报告[EB/OL]. https://bugzilla.kernel.org/report.cgi,2014,04.
[4] William R., Jonathan D.,David J.,et al.A static analyzer for finding dynamic programming errors[J]. Software-Practice & Experience,2000,7(30): 775-802.
[5] Nethercote N.,Seward J.Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation[C] //Jeanne Ferrante. PLDI’07. San Diego, CA, USA:ACM New York,2007: 89-100.
[6] Nethercote N.,Seward J. "How to shadow every byte of memory used by a program[C] //Chandra Krintz. VEE '07. San Diego, California, USA:ACM New York,2007: 65-74.
[7] Nossum V.GETTING STARTED WITH KMEMCHECK [EB/OL].https://www.kernel.org/doc/Documentation/ kmemcheck.txt.,2014,04.
[8] Petrenko A. KEDR - Dynamic Analysis of Linux Kernel Modules[EB/OL]. http://linuxtesting.org/kedr,2014,03.
[9] 苏锦绣,陈莉君.基于系统调用的日志系统的设计与实现[J].西安邮电大学学报,2011,16(4):59-65.
[10] Feiner P.; Brown A. D. and Goel A.Comprehensive Kernel Instrumentation via Dynamic Binary Translation[C] //Tim Harris. ASPLOS’12. London, England, UK: ACM New York, 2012:135-146.
[11] Goodman P. Granary: Comprehensive Kernel Module Instrumentation[OE/OL].http://www.petergoodman.me/d ocs/osdi-2012-poster.pdf,2014,06.
[12] Zhao Qin, Bruening D. Umbra: Efficient and Scalable Memory Shadowing[C] //Andreas Moshovos. CGO’10.Toronto, Ontario, Canada:ACM NewYork,2010:22-31.
TP311.5
A
2015.01.13)
1007-757X(2015)07-0056-03
任 镇(1989-),男,西安邮电大学,计算机学院,硕士研究生,研究方向:嵌入式操作系统,西安,710121
陈莉君(1964-),女,西安邮电大学,计算机学院,教授,硕士,研究方向:Linux操作系统,西安,710121