原文地址:http://www.yulv.net/archives/370
1.什么是DEP
DEP,全称是Data Excution Protection,中名叫数据执行保护,是XP+sp2,Win2K03+sp1中加入的对内存的一种保护,用来防止恶意程序对系统的攻击,如溢出。
现在只有两种设置方式:
一、是只为系统关键进程和服务提供DEP保护,这也是默认选项。
二、是为所有程序和服务提供DEP保护,除去用户手动指定的程序。
其设置在”我的电脑”->右键菜单”属性”->”高级”->”性能 – 设置”->”数据执行保护”
2.DEP能保护什么?
个人认为DEP就是专门为防止溢出设计的,当然这么说是有点狭隘了,毕竟这种机制完善了CPU的内存管理机制。感觉每一个指令执行之前都进行内存页属性的检测有点太费资源了。
一句话,DEP可以使指定的内存页不具有可执行属性。
这样一来,如果指定栈所在的内存页不可执行,那么,当我们要栈上溢出时,我们的Shellcode将难以被执行。
所以,DEP保护的就是内存,保护指定的内存上的代码不能被执行,这样就可以达到反溢出的目的。
当然,这是微软的一厢情愿罢了。因为越过这个限制也并不是一件难事。
3.可以绕过DEP吗?如果可以,如何绕过呢?
答案是肯定的,可以绕过。即使是硬件上的DEP保护也是可以的。
有一个API,改变指定内存页的属性的,VirtualProtect(),当然,还有其它的API,如
VirtualProtectEx(),ZwProtectVirtulMemory(),不过都是封闭在VirtualProtect()中。
所以,我们溢出的时候,把返回地址设计为这个API的地址,再精心构造一个栈为调用这个API的栈,就可以
改变当前栈的内存页的属性,使其从”不可执行”变成”可执行”.
这个过程的示例如下面的一段代码
_test proc
;push 04
;invoke VirtualProtect,esp,30h,
PAGE_EXECUTE_READWRITE,esp;pop eax
mov dword ptr [esp+18h],4
mov eax,esp
add eax,18h
mov dword ptr [esp+14h],eax;lpOldProtection
mov dword ptr [esp+10h],40h;dwNewProtection
mov dword ptr [esp+0ch],30h;dwRegionSize
mov dword ptr [esp+8],esp;dwStartAddress
mov dword ptr [esp],VirtualProtect;func addr
mov eax,esp
add eax,1ch
mov dword ptr [esp+4],eax;return address from VirtualProtect
mov dword ptr [esp+1ch],90909090h;our shellcode
ret
_test endp
4.DEP在内核中到底是如何实现的?
这个问题我曾经费不了少时间去找答案,从内存操作的API上来看,
有标准内存管理API,虚拟内存管理API,堆管理API,内存映射文件API
从内存管理的结构上来看,有VAD,有PFN,有PTE,PDE,有段
一开始我认为windows可能会在任何一个层面上做文章,可能是VAD,也可能是PFN,也可能是PTE
并且我认为VAD的可能性比较大,因为PTE没有相关的bit位表示此页有没有可执行属性。PFN也没有。
VAD倒是有相关的位表示页的可执行属性。
经过一点点的测试和排除,发现VAD与此并没有关系,用VirtualProtect()改变页的属性时,此页对应
的VAD的flag位竟然毫无变化。
那么只剩下PFN和PTE了,发现这个API调用前后,PFN的restore pte这个字段有变化,PTE的低双字却
没有一点变化,高双字我时候我没有关心,我一直认为高双字是用于寻址4G以外的物理内存地址的。
然后我手动把PFN的restore pte改变成上面提到的API改成的值,但是结果却让人失望,我拷入shellcode
的页还是不可执行。
尽管这时,我还是没有意识到PTE的高双字发生的变化,并为此付出了代价,那就是二夜一天的对VirtualProtect()
相关API的反汇编。
VirtualProtect()调用了VirtualProtectEx()
VirtualProtectEx()调用了ZwProtectVirtualMemory()
ZwProtectVirtualMemory()通过sysenter进入内核,EAX中存放的服务号是0x89,对应的服务是NtProtecVirtualMemory()
NtProtectVirtualMemory()又调用了MiProtectVirtualMemory()
在MiProtectVirtualMemory()内部,计算出要改变其属性的内存页的PTE的地址,新的属性,然后调用
MiFlushTbAndCapture()改变PTE的属性,但是我当时也只是看到把PTE的属性从067变成了027,和可执行属性还是
没有关系,然后我再深入到MiFlushTbAndCapture()中,发现它主要又是调用KeFlushSingleTb()来改变指定PTE
的属性的,深入到KeFlushSingleTb()内部,它主要是调用KeInterlockedSwapPte()来改变指定PTE的属性。
而KeInterlockedSwapPte()的内部是比较简单的,来看看它的反汇编代码:
nt!KeInterlockedSwapPte:
80541c08 53 push ebx
80541c09 56 push esi
80541c0a 8b5c2410 mov ebx,dword ptr [esp+10h] ;ebx=新的PTE值所在变量的地址
80541c0e 8b74240c mov esi,dword ptr [esp+0Ch] ;esi=PTE地址
80541c12 8b4b04 mov ecx,dword ptr [ebx+4] ;ecx=新PTE值高双字
80541c15 8b1b mov ebx,dword ptr [ebx] ;ebx=新PTE值低双字
80541c17 8b5604 mov edx,dword ptr [esi+4] ;edx=旧PTE值高双字
80541c1a 8b06 mov eax,dword ptr [esi] ;eax=旧PTE值低双字
80541c1c 0fc70e cmpxchg8b qword ptr [esi]
看到关键点了吧,就是一个cmpxch8b指令,这个指令是干什么的呢?
执行的操作:edx,eax与DST相比较
如果 (edx,eax)=(dst)
则 ZF=1,(dst)<-(ecx,ebx)
否则 ZF=0,(edx,eax)<-dst
很简单,如果新的PTE属性和旧的不等,把PTE属性设置为新的属性。如果相等,则实际上等于不进行操作。
从这里我才发现,他把PTE的高双字设置为0了,以前的值是0x80000000.
所以,DEP是通过PTE的高双字的最高bit即bit63来实现的,这个位置位了,表示此页不可执行。没有置位,
表示此页可以执行。而win2k下面的页目录和页表项只有32个bit,所以不可能提供DEP的这个保护位,因此
DEP只有在64bit的PTE上才能实现。而只有cr4的bit5即PAE启用的时候,PTE才为64bit。
所以,可以这么说吧,intel的奔腾CPU是早就有PAE属性位的,可以支持64位的页表项,而windows系统
只有win2k的某个版本,winxp和win2003的某个server pack版本的内核才支持64的页表项。
不管怎样,PTE的第63位控制着页的可执行属性,我后来查了IA,在IA的修正说明中,才提到这一点,并
把这个位叫做EXB,却对此位的作用一字为提。
但是我们是不能在ring3下操作PTE的,所以,绕过DEP还是得用return-to-lib的经典方式返回到VirtualProtect()
来改变当前栈的属性。