afl 實戰
前言
像 libFuzzer
, afl
這類 fuzz
對於 從文件 或者 標准輸入 獲取輸入的程序都能進行很好的 fuzz
, 但是對於基於網絡的程序來說就不是那么方便了。
這篇文章介紹用 afl
來 fuzz
網絡應用程序。
介紹
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_accept
和modbus_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
里面
所以我們就可以利用 preeny
來 fuzz 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 + preeny
來 fuzz
網絡應用 速度還行, 關鍵的還是要找到好的樣本,從程序自帶的測試用例中抓取也是一個不錯的思路。
參考
Fuzzing nginx - Hunting vulnerabilities with afl-fuzz
AFL Persistent Mode
在介紹一個 在 fuzz
一些網絡程序時可能用到的特性, AFL
的 persistent
模式。
persistent
模式就是在程序的某個代碼位置不斷喂生成的變異數據 進行 fuzz
, 而不用每次喂數據都得重新 fork
一個程序。
要使用這個特性,首先得編譯 llvm_mode
cd afl-2.52b/
cd llvm_mode/
make
cd ..
sudo make install
此時就會有 afl-clang-fast
和 afl-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/
總結
如果使用 afl
來 fuzz
網絡應用,有兩種方式
- 利用
preeny
把從socket
獲取數據,轉變為 從stdin
獲取數據 - 利用
afl
的persistent
模式
其實還有第三種,網上有個修改版的 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