# CVE-2017-13028
## Description
CVE: https://www.cve.org/CVERecord?id=CVE-2017-13028
# Compile
## Download
```shellsession
git clone https://github.com/the-tcpdump-group/tcpdump.git && cd tcpdump
git checkout tcpdump-4.9.2
```
阅读 README 我们可知,要编译 TCPdump 需要先编译 libpcap 。由于 TCPdump-4.9.2 的
最后一次提交是九年前的,因此对应的最匹配的 libpcap 应该是十年前的 1.8.1 版本。
```shellsession
git clone https://github.com/the-tcpdump-group/libpcap.git && cd libpcap
git checkout libpcap-1.8.1
```
## Build
根据 chall 的说明,我们需要开启 ASAN 来 fuzz,故配置的时候需要加入 `AFL_USE_ASAN
=1` 变量。
```shellsession
CC=clang CXX=clang++ CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-poi
nter" CXXFLAGS="$CFLAGS" ./configure --enable-shared=no --prefix="$(realpath ../
workshop/libpcap-debug)" --disable-bluetooth --disable-dbus
make -j`nproc` && make install
make 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
-dbus
AFL_USE_ASAN=1 make -j`nproc` && AFL_USE_ASAN=1 make install
```
接下来编译 tcpdump,遇到如下报错:
原因是它忽略了我们传入的 `LDFLAGS` 和 `CPPFLAGS`,强制要求 libpcap 的目录和 tcpd
ump 在同级,且名字固定为 libpcap 。解决方法是使用 `--with-system-libpcap` 参数。
虽然我们的 libpcap 是安装到自定义路径,而非系统级安装的,但是用了这个参数后,如
果系统目录下没找到 libpcap,它就会去我们传入的环境变量里找。
然后又遇到了新的问题:
我们可以通过 `config.log` 查看详细报错信息:
可见报错原因是 ISO C99 之后不再支持隐式声明导致的。解决方法是通过 `-Wno-error=im
plicit-int` 告诉编译器将 `implicit-int` 的错误当成 warning 处理,而非 error 。
```shellsession
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__((pac
ked))`,既然是重定义,解决方法也很简单,我们在对应的文件顶部加入如下代码即可:
```c
#ifndef UNALIGNED
#define UNALIGNED __attribute__((packed))
#endif
```
之后 `unknown type name 'netdissect_options'` 也是一样,找定义它的头文件,然后在
缺失的头文件中导入即可,依然是 `#include "netdissect.h"`。
这样一路 patch 下来,就解决的差不多了,直接编译;
```shellsession
make clean && make -j`nproc` && make install
```
至于这个奇怪的 libpcap 1.9.0,是因为它 VERSION 文件里写的 1.9.0,但 release tag
打的确实是 1.8.1,不重要。
总结:修这种头文件风暴,有的时候就像打地鼠一样,修好一个蹦出来 19 个都是常有的事
……边修边骂好吧(
然后编译一份用来 fuzz 的:
```shellsession
make clean
AFL_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-libpcap
AFL_USE_ASAN=1 make -j`nproc` && AFL_USE_ASAN=1 make install
```
# Samples
<s>要准备报文样本不容易,也很容易。</s>
不容易在,我们不可能自己去用它抓包弄点报文,也不确定 AI
能不能生成(应该可以),容易在,tcpdump 提供的测试用例里有各种各样的报文:
这里我直接用它提供的测试报文作为初始语料库。
此外,既然都是抓包工具,那大名鼎鼎的 wireshark 是不是也提供了这种测试报文?
确实有,不过由于 tcpdump 的测试报文库已经很丰富了,如果我 fuzz 不出来再去用 wire
shark 的。
# Fuzzing
开始之前先确定一下怎么用,随便传一个测试数据包看看:
执行需要很长时间,所以我们 fuzz 的时候需要手动添加 `-t 1000+` 将超时时间增加到一
秒钟,`+` 表示让 afl++ 根据平均时间动态缩放这个 timeout 值,但是始终保持在我们设
定的上限之内,`AFL_TMPDIR=/dev/shm` 是为了保护我们的硬盘寿命,而 `-m none` 则是
因为 ASAN 会占用大量内存,虽然有 OOM 的风险,但是建议开启。当然也有其它解决方案
,比如:[Notes for using ASAN with afl-fuzz](https://aflplus.plus/docs/notes_for
_asan/)。
```shellsession
ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:symbolize=0" AFL_TMPDIR=/dev/shm a
fl-fuzz -i corpus -o outs -s 1337 -t 1000+ -m none -- ./tcpdump-fuzz/sbin/tcpdum
p -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"` 就行
。
```shellsession
CC=clang CXX=clang++ CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-poi
nter" CXXFLAGS="$CFLAGS" ./configure --enable-shared=no --prefix="$(realpath ../
workshop/libpcap-debug)"
make -j`nproc` && make install
make 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……
```shellsession
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 版本编译出来:
```shellsession
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……
检测脚本如下:
```bash
#!/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 再生成几个样本一起加进去:
```python
#!/usr/bin/env python3
import os
import binascii
from 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.go
ogle.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.1rnHost: example.comrnrn"
save_pcap("http_get", [
Ether()/IP(dst="1.2.3.4")/TCP(dport=80, flags="PA")/Raw(load=http_payloa
d)
])
# 7. 带有选项的 IP/TCP (测试解析器对选项的处理)
save_pcap("options", [
Ether()/IP(dst="1.2.3.4", options=[IPOption(b'x83x03x10')])/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=6
7, 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=6
7, 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)/BG
POpen(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="publ
ic", 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
```shellsession
mkdir -p /dev/shm/asan
mkdir -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 对上的……反正这个 c
hall 主要是教我们使用 ASAN 的,我们已经学会了,那就到此为止吧!
btw ASAN 的 backtrace 还是很帅的(