fuzz系列之afl


afl 實戰

前言

libFuzzer, afl 這類 fuzz 對於 從文件 或者 標准輸入 獲取輸入的程序都能進行很好的 fuzz, 但是對於基於網絡的程序來說就不是那么方便了。

這篇文章介紹用 aflfuzz 網絡應用程序。

介紹

afl 是一個非常厲害的 fuzz,最近幾年炒的火熱。它是基於代碼插樁來生成測試用例,這樣生成的樣本就比較的好,而且針對 linux 做了許多性能優化使得速度也非常快。

使用 afl 的常規步驟

  • 如果有源碼,用 afl-gcc 或者 afl-clang-fast 編譯源碼,afl 會利用這些工具在編譯期間對代碼進行插樁,為后面的測試提供代碼覆蓋率,測試樣本的變異則會基於代碼覆蓋率進行。無源碼的話可以使用 qemu 進行插樁
  • 搜集好 初始樣本集,如果必要的話使用 afl-cmin 把樣本集進行精簡
  • 然后用 afl-fuzz 開始 fuzz.

下面對一些常見的命令給個示例

精簡樣本集

afl-cmin -i in/ -o out/  /path/to/program

in/ 是初始樣本集目錄

out/ 是 精簡后的樣本集存放的目錄

開啟 fuzz

afl-fuzz -i in/ -o out/ /path/to/program

in/ 是初始樣本集目錄

out/ 用於保存 fuzz 過程中的一些文件

afl-fuzz 默認是往 stdin 中寫測試數據,它同時支持從文件喂 測試數據給目標程序,只要把設置文件的參數修改為 @@fuzz 過程中 afl-fuzz 會把它替換成 文件名。

比如 ./a 這個程序的 第二個參數是要處理的文件的名稱, 那么相應的 afl-fuzz 的命令就是

afl-fuzz -i in/ -o out/ ./a arg1 @@

更多內容請看

http://lcamtuf.coredump.cx/afl/QuickStartGuide.txt
http://lcamtuf.coredump.cx/afl/README.txt

Fuzz 網絡程序

這里以 libmodbus 這個庫為目標進行 fuzz 。

構建 Modbus TCP Server

庫的官網地址如下

http://libmodbus.org/documentation/

這是一個用於 modbus 通訊的庫, 通過這個庫可以很方便的實現 modbus 服務器 和 客戶端。 這里以 modbus tcp 的服務端作為 fuzz 的對象。

首先在官網下載好源碼

http://libmodbus.org/releases/libmodbus-3.1.4.tar.gz

源碼目錄下的 tests 目錄里面有一些示例程序, 其中 tests/bandwidth-server-one.c 就實現了一個 modbus tcp server , 把它做一些精簡得到

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <modbus.h>

int main(int argc, char *argv[])
{
    int s = -1;
    modbus_t *ctx = NULL;
    modbus_mapping_t *mb_mapping = NULL;
    int rc;
    int use_backend;
    ctx = modbus_new_tcp("127.0.0.1", 1502);
    s = modbus_tcp_listen(ctx, 1);
    modbus_tcp_accept(ctx, &s);
    mb_mapping = modbus_mapping_new(MODBUS_MAX_READ_BITS, 0,
                                    MODBUS_MAX_READ_REGISTERS, 0);
    if (mb_mapping == NULL) {
        modbus_free(ctx);
        return -1;
    }
    uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
    memset(query, 0, MODBUS_TCP_MAX_ADU_LENGTH);
    rc = modbus_receive(ctx, query); // 獲取客戶端的請求數據
    if (rc > 0) {
    	modbus_reply(ctx, query, rc, mb_mapping); // 處理並響應之
    } 
    modbus_mapping_free(mb_mapping);
    if (s != -1) {
        close(s);
    }
    /* For RTU, skipped by TCP (no TCP connect) */
    modbus_close(ctx);
    modbus_free(ctx);
    return 0;
}

代碼邏輯簡單理一下

  • modbus_new_tcp 初始化 modbus_t 結構體
  • modbus_tcp_acceptmodbus_tcp_listen 就是調用 socket 監聽端口
  • modbus_mapping_new 初始化一個緩沖區,用於模擬寄存器信息
  • 然后 modbus_receive 接收客戶端的請求和輸入
  • 獲取輸入后就 通過 modbus_reply 處理 請求,以及構造響應數據包, 同時返回響應
  • 然后就是釋放掉分配的一些內存

利用 preeny 庫輔助

afl 默認只能 fuzz 通過 stdin 和 文件 獲取輸入的程序, 要 fuzz 網絡相關的程序,需要使用一個庫

https://github.com/zardus/preeny

這個庫利用 LD_PRELOAD 機制,重寫了 很多庫函數, 其中 desock.c 這個文件負責重寫 socket 相關的函數,其實現的功能就是當應用從 socket 獲取輸入時,其實是從 stdin 獲取輸入。 (具體實現以后再看_)

首先下載編譯下

git clone https://github.com/zardus/preeny.git
cd preeny/
make

然后會在 x86_64-linux-gnu 目錄下生成編譯好的 lib

寫個測試腳本,測試一下 (根據 tests 目錄里面的 sock.c 改造)

#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main()
{
	int s = socket(AF_INET, SOCK_STREAM, 0);
	char buf[1024]={0};
    char send_msg[] = "hello, send by send() :\n";
	send(s, send_msg, strlen(send_msg), 0);
	recv(s, buf, 1024, 0);
    printf("recv from recv() : %s\n", buf);
}

編譯運行

gcc sock_test.c -o sock_test
LD_PRELOAD="/home/haclh/vmdk_kernel/preeny/x86_64-linux-gnu/desock.so" ./sock_test

socket 調用 send , 成功往 stdout 輸出了 字符串。

stdin 輸入 I am Tester ,可以看到成功寫入 buf 里面

所以我們就可以利用 preenyfuzz modbus tcp server

編譯 modbus server

首先使用 afl-gcc 編譯 libmodbus ,對 libmodbus 插樁。

unzip libmodbus-master.zip 
cd libmodbus-master/
./autogen.sh 
CC=afl-gcc CXX=afl-g++ ./configure --enable-static
make -j4

--enable-static : 用於生成靜態庫

然后在 src/.libs 下就可以看到編譯好的庫

03:45 haclh@ubuntu:libmodbus-master $ ls src/.libs/
libmodbus.a  libmodbus.la  libmodbus.lai  libmodbus.so  libmodbus.so.5  libmodbus.so.5.1.0  modbus-data.o  modbus.o  modbus-rtu.o  modbus-tcp.o

libmodbus.a 就是編譯好的靜態庫

然后使用我們修改過的 bandwidth-server-one.c 編譯 和 fuzz

cd tests/
vim bandwidth-server-one.c 
afl-gcc bandwidth-server-one.c -I../src ../src/.libs/libmodbus.a -o server
mkdir in
echo 11111 > in/1
LD_PRELOAD="/home/haclh/vmdk_kernel/preeny/x86_64-linux-gnu/desock.so" afl-fuzz -i in -o out ./server

這里 直接用 echo 生成了一個 測試文件,如果直接用這個去測的話會發現速度非常的慢。

獲取樣本數據

一組好的樣本數據對 fuzzer 的影響還是非常大的,一般我們可以去網上搜索樣本,比如圖片,視頻文件等。對於我們這次的目標 libmodbus , 它自帶了很多的測試程序,我們可以利用這些測試程序測試,然后用 tcpdump 抓包, 最后在把其中的請求數據保存下來,作為測試樣本集。

首先使用 random-test-server 在 127.0.0.1:1502 起一個 modbus tcp 服務

04:09 haclh@ubuntu:libmodbus-master $ cd tests/
04:09 haclh@ubuntu:tests $ ./random-test-server 

然后開啟 tcpdump , 保存數據包到 ~/modbus.pcap

04:09 haclh@ubuntu:~ $ sudo tcpdump -i lo -w ~/modbus.pcap
[sudo] password for haclh: 
tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes

最后使用 random-test-client 隨機發送各種 modbus 請求到 127.0.0.1:1502

cd tests/
./random-test-client 

然后寫一個腳本把 ~/modbus.pcap 中 由客戶端發送的數據包 (也就是目的地為 127.0.0.1:1502的數據包) 的內容提取出來,每個數據包內容保存為一個單獨的文件。

from scapy.all import *
save_path = "/tmp/seeds/"
uuid = 0

if not os.path.exists(save_path):
    os.system("mkdir %s" %(save_path))

def save_to_file(data):
    global uuid
    with open("{}{}".format(save_path, uuid), "w") as fp:
        fp.write(str(data))
    uuid += 1
    print "write test file: {}".format(uuid)

modbus_session = ''
pg = rdpcap("modbus.pcap")
session = pg.sessions()
for k in session.keys():
    if k.endswith("127.0.0.1:1502"):
        modbus_session = session[k]

for s in modbus_session:
    payload = s[TCP].payload
    if len(payload) > 4:
        save_to_file(payload)

print "Total: %d tests" %(uuid)

使用獲取的樣本再次 fuzz

然后以生成的樣本集作為初始樣本集進行 fuzz

LD_PRELOAD="/home/haclh/vmdk_kernel/preeny/x86_64-linux-gnu/desock.so" afl-fuzz -i /tmp/seeds/  -o out ./server

速度有一定的提升,而且 總路徑數 直接 1000+

最后 fuzz 了兩個多小時

那個唯一的 crash 還是誤報 (_)

總結

afl + preenyfuzz 網絡應用 速度還行, 關鍵的還是要找到好的樣本,從程序自帶的測試用例中抓取也是一個不錯的思路。

參考

Fuzzing nginx - Hunting vulnerabilities with afl-fuzz

AFL Persistent Mode

在介紹一個 在 fuzz 一些網絡程序時可能用到的特性, AFLpersistent 模式。

persistent 模式就是在程序的某個代碼位置不斷喂生成的變異數據 進行 fuzz , 而不用每次喂數據都得重新 fork 一個程序。

要使用這個特性,首先得編譯 llvm_mode

cd afl-2.52b/
cd llvm_mode/
make
cd .. 
sudo make install

此時就會有 afl-clang-fastafl-clang-fast++ 兩個命令, 要使用這個模式,就要用這兩個命令來編譯目標應用。

afl 的 作者有一篇 文章 介紹這個特性。

下面還是用 afl 自帶的 測試文件 experimental/persistent_demo/persistent_demo.c 來看看。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
int main(int argc, char** argv) {
  char buf[100]; 
  while (__AFL_LOOP(1000)) {
    memset(buf, 0, 100);
    read(0, buf, 100);
    if (buf[0] == 'f') {
      printf("one\n");
      if (buf[1] == 'o') {
        printf("two\n");
        if (buf[2] == 'o') {
          printf("three\n");
          if (buf[3] == '!') {
            printf("four\n");
            abort();
          }
        }
      }
    }
  } // end of while (__AFL_LOOP(1000))
  return 0;
}

最關鍵的 就是 AFL_LOOP(1000) 這個宏, 其中的參數指定循環的次數

每一次循環 afl 都會生成 測試數據,然后喂到 stdin , 這樣 fuzzer 就可以在 AFL_LOOP 宏 包圍的內部,通過 read(0,buf, size) 來獲取測試數據,然后喂給目標程序的數據處理的代碼,這樣可以減少 fork 等操作的開銷。

對應到上面的程序,就是 afl 會在

 while (__AFL_LOOP(1000)) {
 ........
 ........
 }

里面 fuzz 1000 次,即生成 1000 次測試數據, 然后會 return 0 . 程序結束,然后 afl 會重新起一個程序。繼續這樣的 fuzz .

while (__AFL_LOOP(1000)) 包圍的代碼,就是不斷的 從 stdin 獲取測試數據,然后進入下面的 if 判斷邏輯。

編譯 然后用 afl-fuzz

afl-clang-fast persistent_demo.c -o persistent_demo
afl-fuzz  -i in/ -o out/ ./persistent_demo

一會就能拿到 crash 了。( abort 會被 afl 檢測為 crash) 。

對於 libmodbus , 不會用這種方式進行 fuzz。(如果有人成功,望悉知,感激不盡)

不過網上還是有一些案例

https://www.fastly.com/blog/how-fuzz-server-american-fuzzy-lop
https://sensepost.com/blog/2017/fuzzing-apache-httpd-server-with-american-fuzzy-lop-%2B-persistent-mode/

總結

如果使用 aflfuzz 網絡應用,有兩種方式

  • 利用 preeny 把從 socket 獲取數據,轉變為 從 stdin 獲取數據
  • 利用 aflpersistent 模式

其實還有第三種,網上有個修改版的 afl 可以用來 fuzz 網絡應用,不過版本比較老,貌似也沒啥人使用(:~

https://github.com/jdbirdwell/afl

此外 , afl 還有各種擴展模式,比如 利用 qemu 可以無源碼 fuzz。 17 年 還有一個 afl-unicorn ,貌似可以 fuzz 任意架構的代碼(:沒來得及看~。

https://hackernoon.com/afl-unicorn-fuzzing-arbitrary-binary-code-563ca28936bf
https://hackernoon.com/afl-unicorn-part-2-fuzzing-the-unfuzzable-bea8de3540a5


免責聲明!

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



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