協議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\n\x00\x13\x01\x04\x00\x05\x01\x0c\x0c\x02\x03\xf7\xa2\x19L)\x00'
inbound '\x81\n\x00\x16\x01\x000\x01\x0c\x0c\x02\x03\xf7\xa2\x19L)\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 = "\x81\x04#\x00\x01\x13\n\x00\xe2\xf3\xef\xbb\xbf\xaf\x81\xa6\x05\x01\x0c)\x00\x02\x19\x03L\xa2"
        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\n");
      word_from_buf = 0;
      break;
    case 1:
      sub_433740("BVLC: Received Write-BDT.\n");
      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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM