在《空指针漏洞防护技术-初级篇》中我们介绍了空指针及空指针漏洞的概念,在这次高级篇中介绍空指针利用及相应的防护机制。
1 提高篇之:空指针的利用
前面主要介绍了空指针的一些概念和相关的知识,了解了什么是空指针,对于由野指针导致的空指针漏洞不是今天的重点。接下来主要就针对指向零页内存的空指针漏洞做详细的介绍。
此类漏洞利用主要集中在两种方式上:
- 利用NULL指针。
- 利用零页内存分配可用内存空间
对于第一种情况可以利用NULL指针来绕过条件判断或是安全认证。比如X.0rg空指针引用拒绝访问漏洞(CVE-2008-0153 ),如下图对比修改补丁前后的对比:
从代码补丁可以看出该漏洞利用NULL指针改变程序流程来触发漏洞。
针对第二种情况,在某些情况下零页内存也是可以被使用,比如下面两种情况:
- 在windows16系统或是windows16虚拟系统中,零页内存是可以使用的;在windows 32位系统上运行DOS程序就会启动NTVDM进程,该进程就会使用到零页内存。
- 通过ZwAllocateVirtualMemory等系统调用在进程中分配零页内存(win7系统之前)。
接下来结合ZwAllocateVirtualMemory API函数的调用直观感受在win7与win8系统中零页内存分配的差异。
1.1 ZwAllocateVirtualMemory基本介绍
zwAllocateVirtualMemory函数在指定进程的虚拟空间中申请一块内存,那是不是只要在内存申请空间就会调用zwAllocateVirtualMemory函数呢?调用zwAllocateVirtualMemory需要根据实际的情况。
堆的分配、使用、回收都是通过微软的API来管理的,最常见的API是malloc和new。在底层调用是HeapAlloc,同时通过HeapCreate来创建堆。对zwAllocateVirtualMemory函数的调用情况是:
. HeapCreate->RtlCreateHeap->ZwAllocateVirualMemory,这里会直接申请一大块内存,至于申请多大的内存,由进程PEB结构中的字段决定,HeapSegmentReserve字段指出要申请多大的虚拟内存,HeapSegmentCommit指明要提交多大内存。
图中看出默认申请大小是0×100000,默认提交大小是0×2000。下图是利用Heapcreate函数调用zwAllocatevirtualMemory时的函数调用栈关系。
图中展示了函数的调用过程。
- HeapAlloc->RtlAllocateHeap-> ZwAllocateVirualMemory,这里的内存申请是由Heapcreate已经提交的内存中申请。堆管理器从这片内存中划分一块出来以满足申请的需要,仅当申请的内存不够的时候,才会再次调用ZwAllocateVirualMemory函数。 **也就是说,我们平时使用**** malloc ****或是**** new *\*在堆上申请一块小的内存是不会调用该函数的。** 这点大家要注意。下图中展示了利用HeapAlloc调用ZwAllocateVirualMemory的过程:
c. 直接利用zwAllocateVirtualMemory分配内存,对于zwAllocateVirtualMemory函数,微软没有给出公开的文档,但是可以通过相关资料或是逆向来了解该函数的使用方式。直接利用zwAllocateVirtualMemory函数分配内存首先加载函数所在模块,同时获取该函数的符号地址。由于该函数提供了比较全面的参数,利用此函数来分配内存空间更加灵活多变。
1.2 ZwAllocateVirtualMemory函数知识
zwAllocateVirtualMemory该函数在指定进程的虚拟空间中申请一块内存,该块内存默认将以64kb大小对齐。
NTSYSAPI NTSTATUS NTAPI **ZwAllocateVirtualMemory** ( IN HANDLE *ProcessHandle*, IN OUT PVOID **BaseAddress*, IN ULONG *ZeroBits*, IN OUT PULONG *RegionSize*, IN ULONG *AllocationType*, IN ULONG *Protect* );
两个主要参数:
返回值
如果内存空间申请成功会返回0,失败会返回各种NTSTATUS码。
从zwAllocateVirtualMemory说明来看,本想利用BaseAddress参数在零页内存中分配空间,但是当BaseAdress指定为0时,系统会寻找第一个未使用的内存块来分配,而不是在零页内存中分配。那么如何才能分配到零页内存呢?
1.3 零页内存分配之实例win7 vs win8
了解了zwallocatevirtualmemory的用法,就结合实例来看看如何利用该函数进行零页内存分配。前面介绍将BaseAdress设置为0时,并不能在零页内存中分配空间,就需要利用其它方式在零页内存分配空间。在AllocateType参数中有一个分配类型是MEM_TOP_DOWN,该类型表示内存分配从上向下分配内存。那么此时指定 BaseAddress为一个 低地址,例如 1,同时指定分配内存的大小 大于这个值 ,例如8192(一个内存页),这样分配成功后 地址范围就是 0xFFFFE001(-8191) 到 1把0地址包含在内了,此时再去尝试向 NULL指针执行的地址写数据,会发现程序不会异常了 。通过这种方式我们发现在0地址分配内存的同时,也会在高地址(内核空间)分配内存(当然此时使用高地址肯定会出错,因为这样在用户空间)。
下面就举个例子看如何在零页分配内存
了解windows编程的情况下,对上面的代码不难理解,获取模块句柄,获取zwAllocateVirtualMemory函数地址,并传递参数调用该函数,其中baseaddress的值为4,参数类型中包含了MEM_TOP_DOWN,即内存由上向下分配。所以如果成功的话,将把零页内存中的地址4-0都会分配出去;函数返回0表示内存分配成功。然后再给0地址赋值打印,对0地址重新赋值后再次打印,查看结果。
为了能对比win7与win8系统运行结果的差异,需要准备两个干净的系统win7和win8,这里采用的都是32位系统。
在win7系统中直接编译运行该程序,运行结果如下
从显示结果来看,利用zwAllocateVirtualMemory函数,确实在零内存页分配了4-0地址的空间,也就是说我们可以利用zwAllocateVirtualMemory在零页内存中成功分配空间。
同时在win8系统中同时编译该程序,如果F5调试运行结果如下图:
或是CTRL+F5非调试运行,其结果如下图:
在win8系统中在调用zwallocatevirtualmemory后,函数返回了非0,返回值为0Xc00000f0。最终没能在零页内存分配空间。也就是说在win8系统中对零页内存做了安全防护,导致在零页地址分配内存时失败。
接下来看看win8系统中到底对NULL Pointer也就是零页内存做了哪些防护。
2 提高篇:windows零页内存防护机制
win8系统对零页内存防护机制通过搜索引擎可以查到: **在**** WIN8 ****系统中利用了内核进程结构**** EPROCESS ****中的**** flags ****字段的**** Vdmallowed *\*标志来判断是否允许访问零页内存。** 但是知其然而不知其所以然不是我们追求的目标。我们要做的就是在不依靠其他文献的条件下通过逆向和动态调试技术来剖析win8对零页内存的防护机制。
2.1 搭建内核调试环境
探索Win8的零页内存保护机制在内核中的实现,首先需要搭建一个能调试内核的环境。利用上面给出的例子结合内核调试来分析安全机制。关于如何搭建内核环境在之前的文章中已经介绍过,这里再累赘一下。
5.1.1 虚拟机及调试环境
搭建内核调试环境,采用双机调试模式,在虚拟机中安装win8系统同时配置win8调试模式。
Vmware安装win8系统就不在详解。系统启动后:
管理员权限打开CMD
按照上面的步骤配置好后,关闭虚拟机,打开win8虚拟机配置选项,删除并行端口,添加串行端口,同时配置串行端口,如下图:
配置好vmware后启动虚拟机进入调试模式,如下图:
5.1.2 配置启动windbg
双机模式调试内核需要配置windbg工具,现在主机中安装windbg;创建一个windbg的快捷方式,并在属性->目标中填入如下内容:
"C:\program file\WinDbg(x64)\windbg.exe" -b -k com:pipe,port=\.\pipe\com_1,baud=115200,resets=0,pipe
Windbg路径根据安装路径不同而调整。
启动windbg,由于我的主机是win7系统如果直接双击windbg快捷方式启动
这是缺少必要的权限,所以在启动windbg快捷方式时,需要以管理员的权限启动,启动完成后就可以和虚拟机中的win8系统进行内核级调试,启动之后的结果如下图:
到此内核调试的基本环境创建好了,为了能够更加直观的查看windows系统函数及调用关系需要为windbg配置符号表,这样windbg可以识别出windows标准的导出符号,同时为了能够更方便的调试nullpointer,也需要加载该程序的符号表。
到此内核调试环境搭建完成。
2.2 用户态及内核态跨栈调试
该部分是动态调试的关键部分,也是注意事项最多的一节。
5.2.1 内核调试用户态程序
在上面配置好环境后,在windbg中运行G命令,启动调试。启动程序后,同时在windbg中按住CRTL+BREAK断到调试器中。此时win8系统处于中断状态,不会再运行任何指令。
调试器断下来
我们知道在win7与win8中调用zwallocatevirtualmemory时由于零页内存保护机制的原因,win8系统不能在零页内存分配空间。那么就从调试nullpointer入手,通过调用zwallocatevirtualmemory调试内核态程序来剖解安全机制。
但是现在windbg处于内核调试状态,查看所有启动的进程
需要切换到nullpointer进程中。加载了nullpointer符号表。查看main函数的反汇编如下图所示:
5.2.2 用户态进入内核态
跟进该函数,最后进入
进入了sysenter指令之前,对windows系统有所了解的话,就知道该函数利用该调用进入快速系统调用也就是说此时将由用户态进入内核态。
SYSENTER用来快速调用一个0层的系统过程。SYSENTER是SYSEXIT的同伴指令。该指令经过了优化,它可以使将由用户代码(运行在3层)向操作系统或执行程序(运行在0层)发起的系统调用发挥最大的性能。
在调用SYSENTER指令前,软件必须通过下面的MSR寄存器,指定0层的代码段和代码指针,0层的堆栈段和堆栈指针:
- IA32_SYSENTER_CS:一个32位值。低16位是0层的代码段的选择子。该值同时用来计算0层的堆栈的选择子。
2.IA32_SYSENTER_EIP:包含一个32位的0层的代码指针,指向第一条指令。
3.IA32_SYSENTER_ESP:包含一个32位的0层的堆栈指针。
MSR寄存器可以通过指令RDMSR/WRMSR来进行读写。寄存器地址如下表。这些地址值在以后的intel 64和IA32处理器中是固定不变的。
为了能保证我们调试的**** NT!NtAllocatevirtualMemory ****函数刚好是**** nullpointer ****调用的,需要给**** NT!NtAllocatevirtualMemory ****函数下断点,意思是只在指定进程调用该函数时才断下来。** 待函数断下来后,同时查看函数调用栈如下图 **:
从图中调用栈可知,此时断下来的NT!NtAllocatevirtualMemory刚好是nullpointer引用的内核函数。此时已经进入内核调试状态。
5.3 逆向分析nt!NtAllocateVirtualMemory
进入win8内核调试,就要结合静态分析和动态调试来挖掘有用的信息。首先找到win8内核文件。 **需要注意的是此时分析的是虚拟机中**** win8 ****系统的内核文件,不是**** win7 *\*主机的内核文件。**
5.3.1 NtAllocatevirtualMemory参数确认
在前面的调试中,windbg断在了NT! NtAllocatevirtualMemory函数的入口处。对比NtAllocatevirtualMemory函数在windbg和IDA反编译的结果:
可知,在运行完指令call __SEH_prolog4_GS,后EBP+8为第一个参数,EBP+C为第二个参数,那就看看在调用call __SEH_prolog4_GS后EBP的值,如下图:
第一个参数是进程句柄,本进程句柄刚好是-1,第二个参数基地址指针,之前我们在程序中基地址是4,也就是0×00000004,查看基地址指针的值:
刚好是0×00000004,用户态传入的第三个参数是0,这里的第三个参数也刚好是0。其他参数不在一一列举;也就是是说内核函数NtAllocatevirtualMemory与用户态函数zwallocatevirtualmemory参数是一致的。
5.3.2 查找NtAllocatevirtualMemory 零页内存安全机制
到了最重要的时刻,接下来动态调试NtAllocatevirtualMemory, 一路单步执行,看到eax=0xc00000f0时停下:
从图中可知并EAX来自于ESI的赋值。那就继续往上看,看ESI的值是从哪里来的
从图中看出,在执行test[edi+0C4],1000000h指令后,如果相等就执行了mov esi,0xc00000f0。
5.3.3 确认NtAllocatevirtualMemory零页内存安全机制
前面已经找到了返回0xc00000f0的地方,接下来就要看看条件判断的地方, EDI=0x84b2ac80是EPROCESS进程的地址,查看EPROCESS结果可知:
在结构eprocess偏移0xc4的位置刚好是标志Vdmallowed,该值是0,并且[edi=0xc4] 与上1000000后刚好是零。导致ESI=0xc00000f0,进而导致EAX=0xc00000f0。
**到此基本上已经确认了**** NtAllocateVirtualMemory ****中对**** NULLPage ****的安全机制,就是检查**** EPROCESS ****中的**** VdmAllowed *\*的标志位。**
确定条件判断确定位置
从判断语句来看V76应该是参数中的基地址,V14应该是EPROCESS的地址。看一下这两个值的来源
从上图可知v76就是baseaddress; V14追溯到keGetCurrentThread,在内核模式下FS却指向KPCR(Kernel’s Processor Control Region)结构。即FS段的起点与KPCR结构对齐。看一下KPCR结构偏移124的位置
图中可知偏移124的位置刚好的当前线程的结构地址。通过查看函数KeGetCurrentThread的实现也能证实这一点,如下图:
线程结构地址+128(0×80)的位置刚好是当前进程的内核地址,如下图所示:
之后NtAllocateVirtualMemory函数在将进程结构地址偏移0xC4值与0×1000000做比较判断做安全检查。
到目前为止,NtAllocateVirtualMemory函数对零页内存的保护机制剖析完成。总结一下:
- 判断基地址是否小于**** 0×1000000,
- 判断内核结构**** EPROCESS ****的**** Vdmallowed ****标志是否为**** 0
5.3.4 查找内核中其他对零页内存保护的函数
通过对NtAllocateVirtualMemory逆向分析和动态跟踪了解了win8中对零页内存的保护机制;那么除了NtAllocateVirtualMemory函数,在内核中是否还有其他的函数也对零页内存进行了安全检查呢?
通过在整个NT内核文件中查找零页内存保护机制,又发现了几个包含零页内存包含的函数,搜索结果如下图:
也就是说在内核文件中除了NTAllocateVirtualMemory外,还有四个函数对零页内存做检测:
- MiIsVaRangeAvailable:
- MiMapViewOfPhySicalSection
- MiMapLockedPagesInUserSpace
- MiCreatePebOrTeb
这四个函数不都是导出函数,也就是说在内核编程中有可能我们使用的一些导出函数中内部调用了这四个函数中的某一个,其内部已经做了零页内存检测。
6 总结
本文先是介绍了什么是空指针漏洞,之后对windows系统的零页内存保护机制做了剖析。
空指针漏洞:
对零页内存的安全防护:
- 检测分配页是否在零页内存。
- **检测**** EPROCESS ****结构中的**** VdmAllowed *\*标志。**
Win8**** 以后的零页内存防护也只是保证了不会在零页内存分配空间,缓解分配零页内存空间来利用漏洞;从上图可知,利用零页内存分配导致的空指针漏洞也只是众多空指针漏洞类型中的一种。通过零页内存保护机制并不能缓解所有的空指针漏洞。