CVE-2017-13028

Description
Compile
Download
git clone https://github.com/the-tcpdump-group/tcpdump.git && cd tcpdumpgit checkout tcpdump-4.9.2阅读 README 我们可知,要编译 TCPdump 需要先编译 libpcap 。由于 TCPdump-4.9.2 的最后一次提交是九年前的,因此对应的最匹配的 libpcap 应该是十年前的 1.8.1 版本。
git clone https://github.com/the-tcpdump-group/libpcap.git && cd libpcapgit checkout libpcap-1.8.1Build
根据 chall 的说明,我们需要开启 ASAN 来 fuzz,故配置的时候需要加入 AFL_USE_ASAN=1 变量。
CC=clang CXX=clang++ CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-pointer" CXXFLAGS="$CFLAGS" ./configure --enable-shared=no --prefix="$(realpath ../workshop/libpcap-debug)" --disable-bluetooth --disable-dbusmake -j`nproc` && make installmake clean
AFL_USE_ASAN=1 CC=afl-clang-lto CXX=afl-clang-lto++ ./configure --enable-shared=no --prefix="$(realpath ../workshop/libpcap-fuzz)" --disable-bluetooth --disable-dbusAFL_USE_ASAN=1 make -j`nproc` && AFL_USE_ASAN=1 make install接下来编译 tcpdump,遇到如下报错:

原因是它忽略了我们传入的 LDFLAGS 和 CPPFLAGS,强制要求 libpcap 的目录和 tcpdump 在同级,且名字固定为 libpcap 。解决方法是使用 --with-system-libpcap 参数。虽然我们的 libpcap 是安装到自定义路径,而非系统级安装的,但是用了这个参数后,如果系统目录下没找到 libpcap,它就会去我们传入的环境变量里找。
然后又遇到了新的问题:

我们可以通过 config.log 查看详细报错信息:

可见报错原因是 ISO C99 之后不再支持隐式声明导致的。解决方法是通过 -Wno-error=implicit-int 告诉编译器将 implicit-int 的错误当成 warning 处理,而非 error 。
CC=clang \CXX=clang++ \CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-pointer \ -Wno-error=implicit-int" \CXXFLAGS="$CFLAGS" \LDFLAGS="-L$(realpath ../workshop/libpcap-debug/lib)" \CPPFLAGS="-I$(realpath ../workshop/libpcap-debug/include)" \./configure \ --prefix="$(realpath ../workshop/tcpdump-debug)" \ --with-system-libpcap现在 configure 阶段是通过了,make 一下,又是一堆报错:


大概翻了一下,错误种类还不少,一个个解决吧。
首先是部分文件报 incomplete element type 'const struct tok'。这是因为编译器只看到了声明没有看到定义,不知道这个结构体的具体成员有什么,我们只需要在每个报这样错误的文件中加入完整定义即可。
先 grep 一下,可知这个 struct tok 定义在 netdissect.h 中:

那我们直接在报错的文件 l2vpn.h 中加入 #include "netdissect.h" 就好了。
接下来是有些文件报 unknown type name 'uint32_t' 这种错,直接在报错的那个文件里加入 #include <stdint.h> 即可。
对于 incomplete type 'struct in6_addr',我们只要导入 #include <netinet/in.h> 就好了。
然后是 redefinition of 'UNALIGNED' with a different type: 'struct ip6_ext' vs 'struct ip6_hdr' 这种重定义,先 grep 看看 UNALIGNED 是个啥:

可见它就是一个结构体属性宏,同时默认声明为 #define UNALIGNED __attribute__((packed)),既然是重定义,解决方法也很简单,我们在对应的文件顶部加入如下代码即可:
#ifndef UNALIGNED#define UNALIGNED __attribute__((packed))#endif之后 unknown type name 'netdissect_options' 也是一样,找定义它的头文件,然后在缺失的头文件中导入即可,依然是 #include "netdissect.h"。

这样一路 patch 下来,就解决的差不多了,直接编译;
make clean && make -j`nproc` && make install
至于这个奇怪的 libpcap 1.9.0,是因为它 VERSION 文件里写的 1.9.0,但 release tag 打的确实是 1.8.1,不重要。
总结:修这种头文件风暴,有的时候就像打地鼠一样,修好一个蹦出来 19 个都是常有的事……边修边骂好吧(
然后编译一份用来 fuzz 的:
make cleanAFL_USE_ASAN=1 \CC=afl-clang-lto \CXX=afl-clang-lto++ \CFLAGS="-Wno-error=implicit-int" \CXXFLAGS="$CFLAGS" \LDFLAGS="-L$(realpath ../workshop/libpcap-fuzz/lib)" \CPPFLAGS="-I$(realpath ../workshop/libpcap-fuzz/include)" \./configure \ --prefix="$(realpath ../workshop/tcpdump-fuzz)" \ --with-system-libpcapAFL_USE_ASAN=1 make -j`nproc` && AFL_USE_ASAN=1 make installSamples
要准备报文样本不容易,也很容易。
不容易在,我们不可能自己去用它抓包弄点报文,也不确定 AI
能不能生成(应该可以),容易在,tcpdump 提供的测试用例里有各种各样的报文:

这里我直接用它提供的测试报文作为初始语料库。
此外,既然都是抓包工具,那大名鼎鼎的 wireshark 是不是也提供了这种测试报文?

确实有,不过由于 tcpdump 的测试报文库已经很丰富了,如果我 fuzz 不出来再去用 wireshark 的。
Fuzzing
开始之前先确定一下怎么用,随便传一个测试数据包看看:

执行需要很长时间,所以我们 fuzz 的时候需要手动添加 -t 1000+ 将超时时间增加到一秒钟,+ 表示让 afl++ 根据平均时间动态缩放这个 timeout 值,但是始终保持在我们设定的上限之内,AFL_TMPDIR=/dev/shm 是为了保护我们的硬盘寿命,而 -m none 则是因为 ASAN 会占用大量内存,虽然有 OOM 的风险,但是建议开启。当然也有其它解决方案,比如:Notes for using ASAN with afl-fuzz。
ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:symbolize=0" AFL_TMPDIR=/dev/shm afl-fuzz -i corpus -o outs -s 1337 -t 1000+ -m none -- ./tcpdump-fuzz/sbin/tcpdump -vvvvXX -ee -nn -r @@fuzzer 开始工作,我们也该去打游戏了,让它在后台慢慢跑吧~

结果跑了半天毛都没出,受不了了,直接看官方题解,官方题解用的是 1.8.0,虽然 1.8.1 显然是更接近的版本,不懂啊,不懂,但是不妨碍我试试……也有可能版本号是以 VERSION 文件中定义的为准?或许是这个原因吧,不管了……
把之前的都删了重头来过,这里我只写编译 libpcap 1.8.0 的指令,其它的不变。
configure 后 make 依旧遇到很多报错,这次我不打算禁用 dbus 和 bluetooth 了,手动 patch 一下好了。

查看报错信息,发现都是因为不知道 pcap_t 和 pcap_if_t 导致的,并且 grep 发现它们定义在同一个文件中,那我们在每一个报错的文件头加入 #include "pcap.h" 就行。
CC=clang CXX=clang++ CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-pointer" CXXFLAGS="$CFLAGS" ./configure --enable-shared=no --prefix="$(realpath ../workshop/libpcap-debug)"make -j`nproc` && make installmake clean
AFL_USE_ASAN=1 CC=afl-clang-lto CXX=afl-clang-lto++ ./configure --enable-shared=no --prefix="$(realpath ../workshop/libpcap-fuzz)"AFL_USE_ASAN=1 make -j`nproc` && AFL_USE_ASAN=1 make install编译完 libpcap 后开开心心去编译 tcpdump,本以为会很顺遂,然后一 make:

我还能说什么呢?还是得把 dbus 禁用啊,还得再重新编译一遍 libpcap,我……草……
还能咋办,加上 --disable-dbus 再来一遍呗!/骂骂咧咧
结果呢?结果我发现只关 dbus 不够,还要解决一下 libusb 和 canusb……
AFL_USE_ASAN=1 CC=afl-clang-lto CXX=afl-clang-lto++ ./configure --enable-shared=no --prefix="$(realpath ../workshop/libpcap-fuzz)" --disable-dbus --disable-usb --disable-canusb现在总算是解决了,让它慢慢跑去吧~祈祷可以跑出点 crashes 来。
难过了,早上起来一看,11 个小时,毛都没出,然后又过了一个早八,依旧无果。非但无果,还又发现了一些新的路径……合着我 fuzz 一晚上一直在探路啊。

后来,我发现了俩个傻逼……

解决方法是除了 configure 的时候需要 AFL_USE_ASAN=1 外,make 的时候也要。
现在这样才算是编译进去了,之前不显示 Compiled with AddressSanitizer/CLang.……

然后继续挂机,看看这次能不能出点货来。
历经两个小时,终于出现 crash 了!

Analysis
分析之前先把 debug 版本编译出来:
CC=clang \CXX=clang++ \CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-pointer \ -fsanitize=address" \CXXFLAGS="$CFLAGS" \./configure \ --enable-shared=no \ --prefix="$(realpath ../workshop/libpcap-debug)" \ --disable-dbus --disable-usb \ --disable-canusb
CC=clang \CXX=clang++ \CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-pointer \ -Wno-error=implicit-int \ -fsanitize=address" \CXXFLAGS="$CFLAGS" \LDFLAGS="-L$(realpath ../workshop/libpcap-debug/lib)" \CPPFLAGS="-I$(realpath ../workshop/libpcap-debug/include)" \./configure \ --prefix="$(realpath ../workshop/tcpdump-debug)" \ --with-system-libpcap然后看看第一个 crash:

很遗憾,并不是我们要复现的那个 CVE,继续挂机……
又是跑了一晚上加一个早八的时间,一晚上跑出来六百多个重复的 crashes,早上关掉去重后继续跑,跑出来十个:

但是用 gdb 这么一查,并没有我们要复现的那个 CVE 的 crash……

检测脚本如下:
#!/usr/bin/env bash
TARGET="./tcpdump-debug/sbin/tcpdump"CRASH_DIR="./outs_v3/default/crashes"OUTPUT_LOG="crash_reports.log"
>"$OUTPUT_LOG"
echo "[+] Analysing crashes in $CRASH_DIR..."
export ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:symbolize=1"
for crash_file in "$CRASH_DIR"/id:*; do [ -e "$crash_file" ] || continue
echo "--------------------------------------------------" >>"$OUTPUT_LOG" echo "File: $(basename "$crash_file")" >>"$OUTPUT_LOG"
gdb --batch --quiet \ --ex "run" \ --ex "bt" \ --ex "quit" \ --args "$TARGET" -vvvvXX -ee -nn -r "$crash_file" >>"$OUTPUT_LOG" 2>&1
echo -e "\n" >>"$OUTPUT_LOG"done
echo "[+] Done! Result in: $OUTPUT_LOG"有点烦,直接让 AI 再生成几个样本一起加进去:
#!/usr/bin/env python3
import osimport binasciifrom scapy.all import *
# 加载扩展协议load_contrib("bgp")load_contrib("ospf")
# 创建语料目录CORPUS_DIR = "corpus"if not os.path.exists(CORPUS_DIR): os.makedirs(CORPUS_DIR)
def save_pcap(name, pkts): wrpcap(os.path.join(CORPUS_DIR, f"{name}.pcap"), pkts)
def generate_corpus(): print("正在生成初始语料...")
# 1. 基础以太网 + IPv4 + TCP (带有各种标志) save_pcap("tcp_flags", [ Ether()/IP(dst="1.2.3.4")/TCP(dport=80, flags="S"), Ether()/IP(dst="1.2.3.4")/TCP(dport=80, flags="PA"), Ether()/IP(dst="1.2.3.4")/TCP(dport=80, flags="F"), Ether()/IP(dst="1.2.3.4")/TCP(dport=80, flags="R"), ])
# 2. UDP + DNS 查询 save_pcap("dns_query", [ Ether()/IP(dst="8.8.8.8")/UDP(dport=53)/DNS(rd=1, qd=DNSQR(qname="www.google.com")) ])
# 3. ICMP (Ping, Unreachable) save_pcap("icmp", [ Ether()/IP(dst="1.2.3.4")/ICMP(type=8), # Echo Request Ether()/IP(dst="1.2.3.4")/ICMP(type=3, code=3), # Port Unreachable ])
# 4. IPv6 基础数据包 save_pcap("ipv6_base", [ Ether()/IPv6(dst="2001:4860:4860::8888")/TCP(dport=443), Ether()/IPv6(dst="2001:4860:4860::8888")/ICMPv6EchoRequest(), ])
# 5. ARP 请求与应答 save_pcap("arp", [ Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.1.1"), Ether()/ARP(op=2, psrc="192.168.1.1", hwsrc="00:11:22:33:44:55") ])
# 6. HTTP 请求 (简单应用层负载) http_payload = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" save_pcap("http_get", [ Ether()/IP(dst="1.2.3.4")/TCP(dport=80, flags="PA")/Raw(load=http_payload) ])
# 7. 带有选项的 IP/TCP (测试解析器对选项的处理) save_pcap("options", [ Ether()/IP(dst="1.2.3.4", options=[IPOption(b'\x83\x03\x10')])/TCP(dport=80), Ether()/IP(dst="1.2.3.4")/TCP(dport=80, options=[('MSS', 1460), ('NOP', None), ('WScale', 7)]) ])
# 8. 分片包 (Fragmentation) payload = "A" * 100 pkts = fragment(IP(dst="1.2.3.4")/UDP(dport=123)/payload, fragsize=40) save_pcap("fragments", pkts)
# 9. BOOTP & DHCP (丰富样本) # 9.1 基础 BOOTP 请求与应答 save_pcap("bootp_base", [ Ether(dst="ff:ff:ff:ff:ff:ff")/IP(src="0.0.0.0", dst="255.255.255.255")/UDP(sport=68, dport=67)/BOOTP(op=1, chaddr="00:11:22:33:44:55"), Ether(src="00:11:22:33:44:55")/IP(src="192.168.1.1", dst="192.168.1.100")/UDP(sport=67, dport=68)/BOOTP(op=2, yiaddr="192.168.1.100", siaddr="192.168.1.1", chaddr="00:11:22:33:44:55") ])
# 9.2 DHCP 完整交互 (Discover, Offer, Request, Ack) chaddr = "00:de:ad:be:ef:00" transaction_id = 0x12345678 save_pcap("dhcp_full_flow", [ # Discover Ether(dst="ff:ff:ff:ff:ff:ff")/IP(src="0.0.0.0", dst="255.255.255.255")/UDP(sport=68, dport=67)/BOOTP(xid=transaction_id, chaddr=chaddr)/DHCP(options=[("message-type", "discover"), "end"]), # Offer Ether(dst=chaddr)/IP(src="192.168.1.1", dst="192.168.1.100")/UDP(sport=67, dport=68)/BOOTP(op=2, xid=transaction_id, yiaddr="192.168.1.100", siaddr="192.168.1.1", chaddr=chaddr)/DHCP(options=[("message-type", "offer"), ("server_id", "192.168.1.1"), ("lease_time", 86400), "end"]), # Request Ether(dst="ff:ff:ff:ff:ff:ff")/IP(src="0.0.0.0", dst="255.255.255.255")/UDP(sport=68, dport=67)/BOOTP(xid=transaction_id, chaddr=chaddr)/DHCP(options=[("message-type", "request"), ("requested_addr", "192.168.1.100"), ("server_id", "192.168.1.1"), "end"]), # Ack Ether(dst=chaddr)/IP(src="192.168.1.1", dst="192.168.1.100")/UDP(sport=67, dport=68)/BOOTP(op=2, xid=transaction_id, yiaddr="192.168.1.100", siaddr="192.168.1.1", chaddr=chaddr)/DHCP(options=[("message-type", "ack"), ("server_id", "192.168.1.1"), ("lease_time", 86400), "end"]) ])
# 9.3 带有复杂选项的 DHCP (测试解析器健壮性) save_pcap("dhcp_options", [ Ether()/IP(src="0.0.0.0", dst="255.255.255.255")/UDP(sport=68, dport=67)/BOOTP(chaddr=chaddr)/DHCP(options=[ ("message-type", "discover"), ("hostname", "fuzz-target-node"), ("param_req_list", [1, 3, 6, 12, 15, 28, 42]), ("vendor_class_id", b"MSFT 5.0"), ("client_id", b"\x01" + binascii.unhexlify(chaddr.replace(':',''))), "end" ]) ])
# 10. BGP (历史上漏洞极多) # 模拟一个 BGP Keepalive 和 Open 消息 save_pcap("bgp", [ Ether()/IP(dst="1.1.1.1")/TCP(sport=179, dport=179)/BGPHeader(type=4), # Keepalive Ether()/IP(dst="1.1.1.1")/TCP(sport=179, dport=179)/BGPHeader(type=1)/BGPOpen(my_as=65000, hold_time=180) # Open ])
# 11. SNMP (复杂编码,易出洞) save_pcap("snmp", [ Ether()/IP(dst="1.2.3.4")/UDP(sport=161, dport=161)/SNMP(community="public", PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID("1.3.6.1.2.1.1.1.0"))])) ])
# 12. OSPF (路由协议解析器很复杂) save_pcap("ospf", [ Ether()/IP(dst="224.0.0.5")/OSPF_Hdr(type=1)/OSPF_Hello(router="1.1.1.1", mask="255.255.255.0") ])
# 13. 畸形数据包 (Fuzz 核心:故意破坏长度字段) # 构造一个 IP 长度字段远大于实际数据的包 malformed_ip = IP(len=100, dst="1.2.3.4")/TCP(dport=80) save_pcap("malformed_len", [Ether()/malformed_ip])
# 14. 802.1Q VLAN 嵌套 save_pcap("vlan", [ Ether()/Dot1Q(vlan=10)/Dot1Q(vlan=20)/IP()/TCP() ])
print(f"语料生成完成,保存在 {CORPUS_DIR} 目录下。")
if __name__ == "__main__": generate_corpus()并且我加入 AFL_LLVM_CMPLOG=1 编译了 CMPLOG 的版本,然后使用下面的指令重新 fuzz 。此外,这次用两个线程,我就不信这次还跑不出来/mad
mkdir -p /dev/shm/asanmkdir -p /dev/shm/asan_cmplog
AFL_TMPDIR=/dev/shm/asan \afl-fuzz -i clean_seeds_v4 \ -o outs_v4 \ -s 1337 \ -m none \ -M asan \ -- ./tcpdump-fuzz/sbin/tcpdump -vvvvXX -ee -nn -r @@
AFL_TMPDIR=/dev/shm/asan_cmplog \afl-fuzz -i clean_seeds_v4 \ -o outs_v4 \ -m none \ -c ./tcpdump-fuzz-cmplog/sbin/tcpdump \ -S asan_cmplog \ -- ./tcpdump-fuzz-cmplog/sbin/tcpdump -vvvvXX -ee -nn -r @@传下去,我放弃了。
哥们 fuzz 了三天出了一堆别的 CVE,就是没有能和 CVE-2017-13028 对上的……反正这个 chall 主要是教我们使用 ASAN 的,我们已经学会了,那就到此为止吧!
btw ASAN 的 backtrace 还是很帅的(
