转载自http://www.codeceo.com/article/null-pointer-01.html
安全历史上由于空指针所带来的漏洞及攻击数不胜数,但由于其对利用者的编程能力有要求,对分析及防护者来说有更高的要求,所以国内对空指针漏洞及相 关技术的讨论不是很多。今天这篇《空指针漏洞防护技术》,由绿盟科技威胁响应安全专家坐堂讲解,大家可以从中了解空指针漏洞的基础知识,并结合 Windows 8的内存防护机制实例,动手实践空指针漏洞的防护技术。
1 背景
指针对于绝大部分的编程人员来说都不陌生,说起C/C++中指针的使用既带来了编程方面的方便;同时对编程人员来说,也是对个人编程能力的一种考验,不正确的使用指针会直接导致程序崩溃,而如果是内核代码中对指针的错误使用,会导致系统崩溃,后果也是相当严重。
一般情况下我们使用指针时,错误用法集中在三个方面:
- 由指针指向的一块动态内存,在利用完后,没有释放内存,导致内存泄露
- 野指针(悬浮指针)的使用,在指针指向的内存空间使用完释放后,指针指向的内存空间已经归还给了操作系统,此时的指针成为野指针,在没有对野指针做处理的情况下,有可能对该指针再次利用导致指针引用错误而程序崩溃。
- Null Pointer空指针的引用,对于空指针的错误引用往往是由于在引用之前没有对空指针做判断,就直接使用空指针,还有可能把空指针作为一个对象来使用,间接使用对象中的属性或是方法,而引起程序崩溃,空指针的错误使用常见于系统、服务、软件漏洞方面。
对于第一和第二种情况,我们可以通过一些代码审计工具在发布之前就能确定导致内存泄露或是野指针存在的地方。比如常见的工具有fority,valgrind等及时发现指针错误引用导致的问题。
对于第三种情况,空指针(Null Pointer)引用导致的错误,依靠代码审计工具很难发现其中的错误,因为空指针的引用一般不会发生在出现空指针然后直接使用空指针情况。往往是由于代 码逻辑比较复杂空指针引用的位置会比较远,不容易发现;并且在正常情况下不会触发,只有在特定输入条件下才会引发空指针引用。对于排查此类错误也就更加困 难。
本文不会重点讨论内存泄露和野指针的内容,而是通过一些现有的漏洞和实例来分析一下Null Pointer 空指针。从NULL Pointer概念、本质结合静态逆向及内核动态调试技术来了解在win7 32位和win8 32位下系统对Null Pointer处理情况有什么不同;Win8 32位针对Null Pointer添加了哪些防护机制。
本文从浅入深,循序渐进的讲述了NULL Pointer,结合静态逆向分析和内核级动态调试技术深入剖析win8系统对零页内存的保护机制,其中还涉及到了一些内核调试的技巧,对于对NULL Pointer的概念、使用比较模糊的人员值得一读,对于从事安全研究和学习的人员也是巩固、加深、拓展的好素材。
2 空指针由Null Pointer引发的漏洞
首先来看看近年来由于空指针的错误使用导致的系统、服务漏洞:
Microsoft windows kernel ‘win32k.sys’本地权限提升漏洞(CVE-2015-1721)(MS15-061)
该漏洞影响了windows Server 2003 SP2和R2 SP2,Windows Vista SP2, Windows Server 2008 SP2 和R2 SP1,Windows7 SP1,Windows8 ,Windows8.1,Windows Server2012 Gold和R2,Windows RT Gold and 8.1 。利用内核驱动程序 win32k.sys漏洞可以提升权限或是引起拒绝访问服务,主要原因是空指针的错误引用导致。
PHP空指针引用限制绕过漏洞(CVE-2015-3411)
PHP存在安全漏洞,由于程序多个扩展中缺少路径或某些函数的路径参数的空字节检查,允许远程攻击者利用漏洞可绕过目标文件系统访问限制,访问任意文件。
0rg空指针引用拒绝访问漏洞(CVE-2008-0153 )
X.Org是X.Org基金会运作的一个对X Window系统的官方参考实现,是开源的自由软件。libXfont是一个用于服务器和实用程序的X字体处理库。 X.Org libXfont 1.4.9之前版本和1.5.1之前1.5.x版本的bitmap/bdfread.c文件中的’bdfReadCharacters’函数存在安全漏 洞,该漏洞源于程序未能正确处理不能读取的字符位图。远程攻击者可借助特制的BDF字体文件利用该漏洞造成拒绝服务(空指针逆向引用和崩溃),执行任意代 码。
Paragma TelnetServer空指针引用拒绝服务漏洞(BID-27143)
Pragma TelnetServer是一款远程访问和控制Telnet服务器。Pragma TelnetServer处理协议数据时存在漏洞,远程攻击者可能利用此漏洞导致服务器不可用。TelnetServer服务器对每个入站连接启动一个 telnetd.exe进程,该进程在处理TELOPT PRAGMA LOGON telnet选项(138号)期间存在空指针引用,导致进程终止。尽管终止单个进程不会影响其他进程,但终止某些进程会导致拒绝访问服务器。
OpenSSL SSLv2客户端空指针引用拒绝服务漏洞(CVE-2006-4343)
OpenSSL是一种开放源码的SSL实现,用来实现网络通信的高强度加密,现在被广泛地用于各种网络应用程序中。OpenSSL的协议实现在处理 连接请求时存在问题,远程攻击者可能利用此漏洞导致服务器拒绝服务。SSLv2客户端的get_server_hello()函数没有正确地检查空指针。 使用OpenSSL的受影响客户端如果创建了到恶意服务器的SSLv2连接,就会导致崩溃。
Linux Kernel空指针间接引用本地拒绝服务漏洞(CVE-2014-2678)
Linux kernel 3.14版本内,net/rds/iw.c中的函数rds_iw_laddr_check在实现上存在本地拒绝服务漏洞,本地用户通过盲系统调用没有RDS传输的系统上的RDS套接字,利用此漏洞可造成空指针间接引用和系统崩溃。
ISC BIND named拒绝服务漏洞(CVE-2015-5477)
ISC BIND 9.9.7-P2之前版本、9.10.2-P3之前版本,named存在安全漏洞,远程攻击者通过TKEY查询,利用此漏洞可造成拒绝服务(REQUIRE断言失败及程序退出,指针未初始化)。
此类漏洞还有很多,从给出的几个漏洞来看,空指针漏洞主要是以拒绝服务访问漏洞为主,空指针错误引用自然会到底程序出现错误,严重者会崩溃,从而引起拒绝服务访问。
3 基础篇:空指针验证方式
由空指针的错误引用导致的漏洞,其原理本身很简单:错误引用空指针,导致非法访问内存地址。
那到底什么是空指针漏洞呢?在计算机编程过程中使用指针时有两个重要的概念:空指针和野指针。那么在前面我们提到的空指针漏洞是不是就是我们在编程时所说的空指针呢?要弄清这个问题,首先需要知道编程领域中空指针和野指针分别是什么,还要弄清楚什么是空指针漏洞。
3.1 概念性验证
假如 char p,那么p是一个指针变量,该变量还没有指向任何内存空间,如果p = 0; p = 0L; p = ”; p = 3 – 3; p = 0 * 5; 中的任何一种赋值操作之后(对于 C 来说还可以是 p = (void)0;), p 都成为一个空指针,由系统保证空指针不指向任何实际的对象或者函数。反过来说,任何对象或者函数的地址都不可能是空指针。(比如这里的(void)0就是一个空指针。当然除了上面的各种赋值方式之外,还可以用 p = NULL; 来使 p 成为一个空指针。因为在很多系统中#define NULL (void)0。
3.1.1 内存管理之空指针
空指针指向了内存的什么地方呢?标准中并没有对空指针指向内存中的什么地方这一个问题作出规定,也就是说用哪个具体的地址值(0×0 地址还是某一特定地址)表示空指针取决于系统的实现。我们常见的 空指针一般指向 0 地址,即空指针的内部用全 0来表示 (zero null pointer,零空指针)。
对于NULL能表示空指针,是否还有其他值来表示空指针呢?在windows核心编程第五版的windows内存结构一章中,表13-1有提到 NULL指针分配的分区,其范围是从0×00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对 应,所以对这段空间来说,任何读写操作都是会引起异常的。
从图中看出,对NULL指针分配的区域有0×10000之多,为什么分配如此大的空间?在定义NULL的时候,只使用了 0×00000000这么一个值,而在表13-1有提到NULL指针分配的分区包含了0×00000000-0x0000FFFF,是不是有点浪费空间 了;这是和操作系统地址空间的分配粒度相关的,windows X86的默认分配粒度是64KB,为了达到对齐,空间地址需要从0×00010000开始分配,故空指针的区间范围有那么大。
3.1.2知识拓展之野指针
“野指针”又叫”悬浮指针”不是NULL指针,是指向”垃圾”内存的指针。
“野指针”的成因主要有三种:
1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当 被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如: char *p = NULL; char *str = (char *) malloc(100);
2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
free和delete只是把指针所指的内存给释放掉,但并没有把指针本身干掉。free以后其地址仍然不变(非NULL),只是该地址对应的内存 变成垃圾内存,p成了”野指针”。如果此时不把p设置为NULL,会让人误以为p是个合法的指针。如果程序比较长,我们有时记不住p所指的内存是否已经被 释放,在继续使用p之前,通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。
用free或delete释放了内存之后,就应立即将指针设置为NULL,防止产生”野指针”。内存被释放了,并不表示指针会消亡或者成了NULL指针。
3)指针操作超越了变量的作用范围。例如不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放,这种情况让人防不胜防,示例程序如下:
另外需要注意的是如果程序定义了一个指针,就必须要立即让它指向一个我们设定的空间或者把它设为NULL,如果没有这么做,那么这个指针里的内容是 不可预知的,即不知道它指向内存中的哪个空间(即野指针),它有可能指向的是一个空白的内存区域,可能指向的是已经受保护的区域,甚至可能指向系统的关键 内存,如果是那样就糟了,也许我们后面不小心对指针进行操作就有可能让系统出现紊乱,死机了。所以必须设定一个空间让指针指向它,或者把指针设为 NULL。
3.2源码级验证
接下来就结合两个小的实验例子看看空指针,及使用空指针的后果。
3.2.1指针使用前期对比
本例是要验证指针变量在初始化前后的状况,下图是一段代码:
代码很简单只是验证指针变量在赋值与未赋值分别指向的空间。代码中要打印的值:
- 指针变量初始化之前的地址
- 指针变量初始化之前的值
- 指针变量初始化之后的地址
- 指针变量初始化之后的值
编译之后,运行三次程序看运行的结果:
从三次运行结果来看:
- 每次打印指针变量的地址都不同,因为每次运行程序,P指针变量的地址都在变动。
- 同一次打印中指针变量在初始化之前与初始化之后的地址没有变化。因为指针变量也是变量,变量在其生命周期中值可以变动,但是地址不会改变。
- 每次打印中指针变量在初始化之前的值发生了变化。指针变量的值本身也是指向一块内存地址,在初始化之前该变量不指向任何内存地址,所以该变量的值就是一个随机值(也就是指向随机内存地址)。
- 每次打印中指针变量在初始化之后的值没有发生变化,在指针变量初始化之后P=0,所以在内存初始化的指针变量指向了内存地址都为0×00000000的位置。在每次打印中指针变量在初始化之后的值就不会发生变化。
3.2.2指针使用后期对比
针对指针变量在使用完毕后,内存释放之后的情况对比,其源码如下:
代码没什么复杂逻辑,只是想验证一下在指针变量释放后, P=NULL之前的野指针与P=NULL之后的指针状况,打印的内容:
- 指针变量P开始的地址与值
- 指针变量P在分配内存空间之后的地址与值
- 指针变量P在释放内存空间之后的地址与值
- 指针变量P在P=NULL之后的地址与值
下图是在编译之后运行的结果:
a. 指针变量在声明之后一直到最后的程序结束,指针变量的地址一直没有变动为0x35f778。 b. 定义指针变量时其值为0×00000000 c. 在申请新的内存之后,指针变量的值为申请内存的地址0x6e7a78 d. 在内存释放后,P变成野指针,但是此时P的值仍然是申请的内存地址0x6e7a78,但是此时P指针已经不能再使用。 e. 在P=NULL,p重新指向了地址0×00000000处。
由此可见,我们在释放指针变量后,指针变量会变成野指针,如果此时引用该指针会出现非法访问,因此需要在释放指针变量后将指针变量指向空。
3.3可视化内存验证
为了能够更加直观的查看指针变量在内存中的变化,下面就结合动态调试的技术看看指针变量在整个使用过程中的变化情况。下面是一段程序代码:
在这里,我们使用OD动态调试器,来看看调用printf函数的情况:
a. 第一条打印语句
printf("p address :%p, value :%08x\n", &p, p);
在调用printf前其内存情况:
可知指针变量P的值为EAX=[EBP-4],P的地址ECX=EBP-4,此时的寄存器值:
调用printf之后的值刚好是这两个值
b.第二条打印语句printf(“p intialized address :%p, value :%08x\n”, &p, p);在调用printf前其内存情况:
可知指针变量P的值为EDX=[EBP-4],P的地址EAX=EBP-4,此时的寄存器值:
调用printf之后的值刚好是这两个值:
c. 第三条打印语句printf(“p free address :%p, value :%08x\n”, &p, p);在调用printf前其内存情况:
可知指针变量P的值为EAX=[EBP-4],P的地址ECX=EBP-4,此时的寄存器值:
调用printf之后的值刚好是这两个值:
d. 第四条打印语句printf(“p ultimate address :%p, value :%08x\n”, &p, p);在调用printf前其内存情况:
可知指针变量P的值为EDX=[EBP-4],P的地址EAX=EBP-4,此时的寄存器值:
调用printf之后的值刚好是这两个值:
3.4 概念总结之空指针漏洞
前面对什么是空指针,什么是野指针做了讲解和验证。
在编程领域的空指针是指向NULL的指针,也就是说指向零页内存的指针叫空指针。对于未初始化的指针,释放内存而未将指针置为NULL和指针指向超 出范围的情况称为野指针。那么在第二节中列举的空指针漏洞及未列举的空指针漏洞是不是都是由于引用零页内存导致的呢(比如CVE-2014-2678)? 其实不然,有些漏洞是由于引用未初始化的指针或是引用超出范围的指针所导致,而这类漏洞应该说是由于错误的引用了野指针。比如最新的BIND漏洞 (CVE-2015-5477):
但是到目前为止还没用听说过哪个漏洞命名为野指针漏洞,而更多的是空指针漏洞。也就是说在计算机安全领域中由空指针或是野指针导致的漏洞统一叫做空指针漏洞。