• 协议Fuzz技术


    文章一开始发表在微信公众号

    https://mp.weixin.qq.com/s?__biz=MzUyNzc4Mzk3MQ==&mid=2247486230&idx=1&sn=01809f7e700869ad7083d810bc119c1a&chksm=fa7b0a5acd0c834c5b8c18a99382e1867f83b33f75d69e521f6d19cd96e775c9a4d70ce07ca1&scene=21#wechat_redirect
    

    协议Fuzz

    本节以一个Bacnet server为例介绍如何进行协议Fuzz, 软件的下载地址

    https://sourceforge.net/projects/bacnetserver/
    

    boofuzz

    boofuzz是一个基于生成的协议Fuzz工具,它通过python语言来描述协议的格式。BACnet协议是由美国采暖、制冷和空调工程师协会(ASHRAE)制定的用于楼宇自动控制技术的标准协议,BACnet协议最根本的目的是提供一种楼宇自动控制系统实现互操作的方法。

    Bacnet Server是一个BACnet协议的仿真工具,它在UDP协议栈上实现了BACnet协议。我们可以使用BACnet协议的客户端(比如BACnetScan)和Bacnet Server交互来生成协议数据,然后用Wireshark分析协议,下面是一个BACnet数据包的细节

    image-20191027221654359

    可以看到协议的格式比较简单,由一些简单的字段组成。为了使用boofuzz来Fuzz协议,首先需要根据协议的格式用boofuzz的语法来描述协议,上面的BACnet数据包用boofuzz描述的结果如下

    s_initialize("bacnet_packet")
    if s_block_start("block"):
        s_byte(0x81, name='type')
        s_byte(0x0a, name='function')
        s_word(0x19, name='bvlc-length', endian=BIG_ENDIAN)
        s_byte(0x01, name="version")
        s_byte(0x00, name="control")
        s_byte(0x30, name="type_flag")
        s_byte(0x03, name="id")
        s_byte(0xc, name="sc")
        s_byte(0xc, name="tag")
        s_dword(0x0203f7a2, name="type_number", endian=BIG_ENDIAN)
        s_byte(0x19, name="CT")
        s_byte(0x4c, name="PI")
        s_word(0x290b, name="PAI", endian=BIG_ENDIAN)
        s_byte(0x3e)
        s_byte(0xc4)
        s_dword(0x00800001, endian=BIG_ENDIAN)
        s_byte(0x3f)
    s_block_end()
    

    s_initialize表示描述的开始, s_block_start用于组合各个字段,s_byte表示一个字节, s_word表示两个字节,s_dword表示4个字节。描述好数据结构后使用boofuzz提供的发包器和fuzz引擎就可以开始fuzzing了

    session = Session()
    # 创建目标
    target = Target(connection=SocketConnection(target_ip, 47808, proto='udp'))
    target.procmon = boofuzz.instrumentation.External(pre=None, post=target_alive, start=reset_target, stop=None)
    session.add_target(target)
    
    s_initialize("bacnet_packet")
    ...................
    ...................
    ...................
    session.connect(s_get("bacnet_packet"))
    session.fuzz()
    

    通过boofuzz.instrumentation.External我们可以自定义服务存活检测函数以及服务重启函数。这里我们通过发送一个正常的BACnet请求并查看服务端能否正常返回数据来判断服务是否存活。

    def target_alive():
        try:
            client = socket.socket(type=socket.SOCK_DGRAM)
            decode_hex = codecs.getdecoder("hex_codec")
            send_data = decode_hex("810a001301040005010c0c0203f7a2194c2900")[0]
            client.sendto(send_data, (target_ip, 47808))
    
            client.settimeout(3.0)
            recv_data, address = client.recvfrom(1024)
            client.settimeout(None)
            client.close()
    
            if len(recv_data) > 0:
                return True
            else:
                return False
        except:
            return False
    

    写完boofuzz脚本后,直接运行python文件就可以开始Fuzz了

    image-20191027224228808

    当Fuzz结束后,boofuzz会在本地起一个http服务,用于让用户查看Fuzz运行的状态信息

    image-20191027224352068

    mutiny-fuzzer

    decept是一个代理软件,它支持多种协议的代理比如TCP、UDP等。mutiny-fuzzer是一款基于变异的协议Fuzz工具,它通过对pcap文件中保存的数据包以及decept捕获的数据包变异来生成测试数据,使用decept和 mutiny-fuzzer来进行协议Fuzz的示意图如下:

    image-20191029195435579

    工作流程如下:

    1. 首先使用decept来监听客户端和服务端的通信数据,生成一个 .fuzzer 文件。
    2. 然后 mutiny-fuzzer 基于 .fuzzer 文件来进行协议Fuzz。

    下面以BACnet Server为例介绍如何使用decept和mutiny-fuzzer来进行协议Fuzz。首先我们用decept起一个代理用于监控客户端与服务端的通信并生成相应的.fuzzer文件

    python decept.py 127.0.0.1 12245 192.168.245.133 47808 -l udp
    # 192.168.245.133为 Bacnet Server的IP地址
    

    执行之后decept会在 127.0.0.1:12245 监听udp服务并会把接收到的数据转发到192.168.245.133:47808。然后我们写个脚本发送 bacnet 数据包到 127.0.0.1:12245和bacnet server进行一次通信

    import socket
    import codecs
    
    target_ip = "127.0.0.1"
    port = 12245
    def main():
        client = socket.socket(type=socket.SOCK_DGRAM)
        decode_hex = codecs.getdecoder("hex_codec")
        send_data = decode_hex("810a001301040005010c0c0203f7a2194c2900")[0]
        client.sendto(send_data, (target_ip, port))
        client.settimeout(10.0)
        recv_data, address = client.recvfrom(1024)
        client.settimeout(None)
        client.close()
        print(recv_data.hex())
        if len(recv_data) > 0:
            return True
        else:
            return False
    
    if __name__ == "__main__":
        main()
    

    执行完后decept会在当前目录下生成bacnet.fuzzer文件,后面会用于Fuzz。

    ~/workplace/Decept$ python decept.py 127.0.0.1 12245 192.168.245.133 47808 -l udp -r udp --fuzzer bacnet.fuzzer
    [<_<] Decept proxy/sniffer [>_>]
    
    [*.*] Listening on 127.0.0.1:12245
    [$.$] local:udp|remote:udp
    0000   81 0a 00 13 01 04 00 05 01 0c 0c 02 03 f7 a2 19    ................
    0010   4c 29 00                                           L).
    [o.o] 07:57:48.730920 Sent 19 bytes to remote (192.168.245.133:47808)
    
    0000   81 0a 00 16 01 00 30 01 0c 0c 02 03 f7 a2 19 4c    ......0........L
    0010   29 00 3e 21 38 3f                                  ).>!8?
    [o.o] 07:57:50.735225 Sent 22 bytes to local from 192.168.245.133:47808
    
    ^CFile bacnet.fuzzer already exists, using bacnet.fuzzer-1 instead
    [^.^] Thanks for using Decept!
    

    我们需要修改 bacnet.fuzzer的 proto 字段为 udp, 修改后的脚本如下

    # Directory containing any custom exception/message/monitor processors
    # This should be either an absolute path or relative to the .fuzzer file
    # If set to "default", Mutiny will use any processors in the same
    # folder as the .fuzzer file
    processor_dir default
    # Number of times to retry a test case causing a crash
    failureThreshold 3
    # How long to wait between retrying test cases causing a crash
    failureTimeout 5
    # How long for recv() to block when waiting on data from server
    receiveTimeout 1.0
    # Whether to perform an unfuzzed test run before fuzzing
    shouldPerformTestRun 1
    # Protocol (udp or tcp)
    proto udp
    # Port number to connect to
    port 47808
    # Port number to connect from
    sourcePort -1
    # Source IP to connect from
    sourceIP 0.0.0.0
    
    # The actual messages in the conversation
    # Each contains a message to be sent to or from the server, printably-formatted
    outbound fuzz 'x81
    x00x13x01x04x00x05x01x0cx0cx02x03xf7xa2x19L)x00'
    inbound 'x81
    x00x16x01x000x01x0cx0cx02x03xf7xa2x19L)x00>!8?'
    

    然后使用mutiny加载生成的.fuzzer文件开始Fuzz测试。

    python mutiny.py bacnet.fuzzer 192.168.245.133
    # 192.168.245.133为 Bacnet Server的IP地址
    

    得到POC如下

    import socket
    
    def target_alive():
        try:
            client = socket.socket(type=socket.SOCK_DGRAM)
            send_data = "x81x04#x00x01x13
    x00xe2xf3xefxbbxbfxafx81xa6x05x01x0c)x00x02x19x03Lxa2"
            client.sendto(send_data,('192.168.46.128',47808))
    
            client.settimeout(10.0)
            re_Data,address = client.recvfrom(1024)
            client.settimeout(None)
    
            print(re_Data.encode('hex'))
            client.close()
    
            if len(re_Data) > 0:
                return True
            else:
                return False
        except:
            return False
    
    print target_alive()
    

    image-20191028215954737

    基于Hook的Fuzzer

    对于一个网络协议而言,协议的客户端和服务端之间的通信结构图如下

    image-20191029200603240

    流程如下

    • 客户端会生成协议数据,然后可能会对数据进行加密,压缩等后处理操作,最后把数据通过网络发送到服务端。
    • 服务端接收到数据,然后对数据进行预处理比如数据解密、解压缩等,最后对数据进行具体的处理。

    对于一些自定义加密、压缩算法的私有协议或者交互比较复杂协议,分析协议并构造一个fuzzer会投入很大的工作量。在这种情况下,我们采用“中间人”的方式来Fuzz协议,使用“中间人”的方式Fuzz协议服务端程序时的协议交互图如下:

    image-20191029203716611

    其关键的思路是在服务端接收完数据并对数据进行预处理后,对预处理后的协议数据进行变异来实现Fuzz。这种方式的优点在于可以用较少投入Fuzz出更深层次的问题,本节将介绍如何用hook的方式来对bacnet server进行Fuzz。

    通过前面的分析我们知道bacnet server会在47808端口监听udp服务,从udp端口获取数据一般是使用recvfrom函数,在IDA中搜索recvfrom函数的交叉引用可以找到调用 recvfrom 的位置

    .text:00432D1E                 push    edx             ; len
    .text:00432D1F                 mov     eax, [ebp+buf]
    .text:00432D22                 push    eax             ; buf
    .text:00432D23                 call    sub_41B415
    .text:00432D28                 push    eax             ; s
    .text:00432D29                 call    ds:recvfrom
    .text:00432D2F                 cmp     esi, esp
    .text:00432D31                 call    j___RTC_CheckEsp
    .text:00432D36                 mov     [ebp+recvlen], eax
    .text:00432D3C                 jmp     short loc_432D45
    

    对应的伪代码如下

      v36 = sub_41B415(v5);
      if ( select(v36 + 1, &readfds, 0, 0, &timeout) <= 0 )
        return 0;
      s = sub_41B415(&from);
      recvlen = recvfrom(s, buf, len, 0, &from, &fromlen); // 读取 udp 数据
      if ( recvlen < 0 )
        return 0;
      if ( !recvlen )
        return 0;
      if ( *buf != 129 )
        return 0;
      byte_from_buf = buf[1]; // 从读取的buf中取出一个字节
      extract_word_from_buf((buf + 2), &word_from_buf); // 从 buf 里面取出2个字节
      word_from_buf -= 4;
      v16 = byte_from_buf;
      switch ( byte_from_buf ) // 根据 byte_from_buf 决定后续的数据的处理
      {
        case 0:
          extract_word_from_buf((buf + 4), &v21);
          dword_4D8648 = v21;
          sub_433740("BVLC: Result Code=%d
    ");
          word_from_buf = 0;
          break;
        case 1:
          sub_433740("BVLC: Received Write-BDT.
    ");
          v19 = sub_433960(buf + 4, word_from_buf);
          ..............................
          ..............................
          ..............................
    

    可以看到程序通过recvfrom读取数据之后就开始对数据进行处理,没有预处理操作,因此我们可以在recvfrom函数返回后对数据进行变异。这里使用Frida框架来实现这个任务, Frida框架是跨平台的hook框架,它支持 windows,linux,android等主流操作系统平台。hook部分的主要代码如下

    //设置异常
    Process.setExceptionHandler(function (details) {
    
        if (details.type != "system") {
            console.log(details.context);
            console.log(details.type);
            console.log(details.address);
            console.log(details.memory);
            var backtrace = Thread.backtrace(details.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n\t');
            console.log(backtrace);
        }
    
        return false;
    });
    
    //生成从minNum到maxNum的随机数
    function generate_random_number(minNum, maxNum) {
        switch (arguments.length) {
            case 1:
                return parseInt(Math.random() * minNum + 1, 10);
                break;
            case 2:
                return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
                break;
            default:
                return 0;
                break;
        }
    }
    
    
    var fuzz_count = 1;
    function fuzz(buf, size) {
        var fuzz_size = parseInt(size * 0.3, 10);
        console.log("fuzz size: " + fuzz_size);
        var offset = generate_random_number(0, size - fuzz_size - 1);
    
        for (var i = 0; i < fuzz_size; i++) {
            buf.add(offset + i).writeU8(generate_random_number(0, 255));
        }
    
        console.log("fuzz count: " + fuzz_count);
        fuzz_count += 1;
        console.log(hexdump(buf, {
            offset: 0,
            length: size,
            header: true,
            ansi: true
        }));
    }
    
    var recv_from = Module.findExportByName(null, "recvfrom");
    Interceptor.attach(recv_from, {
        onEnter: function (args) {
            // console.log("socket: " + args[0]);
            // console.log("buffer: " + args[1]);
            this.buffer = args[1];
        },
        onLeave: function (retval) {
            // console.log("recvfrom length: " + retval);
            fuzz(this.buffer, retval.toInt32());
        }
    });
    

    脚本首先获取recvfrom函数的地址,然后用Interceptor.attach来hook recvfrom函数,当函数进入recvfrom函数前会调用onEnter对应的回调函数,在这里面把recvfrom参数中的buf参数保存下来,后面由于Fuzz.

    function (args) {
    	this.buffer = args[1];
    }
    

    当程序从recvfrom函数返回时会调用onLeave对应的回调函数,在这里把存储接收到的数据的指针和接收到的数据大小传入fuzz函数对数据进行变异。

    function (retval) {
    	fuzz(this.buffer, retval.toInt32());
    }
    

    fuzz函数的变异策略比较简单,就是随机修改%30的数据内容,为了调试方便这里还会把变异后的数据用hexdump打印出来。

    function fuzz(buf, size) {
        var fuzz_size = parseInt(size * 0.3, 10);
        console.log("fuzz size: " + fuzz_size);
        var offset = generate_random_number(0, size - fuzz_size - 1);
    
        for (var i = 0; i < fuzz_size; i++) {
            buf.add(offset + i).writeU8(generate_random_number(0, 255));
        }
    
        console.log(hexdump(buf, {
            offset: 0,
            length: size,
            header: true,
            ansi: true
        }));
    }
    

    用Frida把函数hook好之后,还需要弄一个脚本或者用一个客户端不断和服务端进行正常的通信,这里我们使用python脚本来模拟正常的通信

    def send_loop():
        import time
        client = socket.socket(type=socket.SOCK_DGRAM)
        decode_hex = codecs.getdecoder("hex_codec")
        send_data = decode_hex("810a001301040005010c0c0203f7a2194c2900")[0]
    
        while True:
            client.sendto(send_data, (target_ip, port))
            time.sleep(0.1)
    
        client.close()
    

    一次执行的crash信息如下

    image-20191029220933602

  • 相关阅读:
    宁波工程学院2020新生校赛C
    宁波工程学院2020新生校赛B
    宁波工程学院2020新生校赛A -恭喜小梁成为了宝可梦训练家~(水题)
    POJ 1611
    牛客算法周周练11E
    牛客算法周周练11C
    牛客算法周周练11A
    CodeForces 1176C
    CodeForces 445B
    UVALive 3027
  • 原文地址:https://www.cnblogs.com/hac425/p/14278971.html
Copyright © 2020-2023  润新知