前言
上周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服務。
攻擊者向靶機發送請求:
GET / HTTP/1.1。靶機接收到請求后,首先計算出
tcp_header_len,默認等於20字節,在內核配置sysctl_tcp_timestamps開啟的情況下,增加12字節,如果編譯內核的時候選擇了CONFIG_TCP_MD5SIG,會再增加18字節,也就是說tcp_header_len的最大長度為50字節。隨后需要計算出mss_now = 48 – 50 + 20 = 18
這里假設一下該漏洞可能利用成功的場景:有一個TCP服務,自己設定了TCP可選參數,並且設置滿了40字節,那么攻擊者才有可能通過構造SYN包中的MSS值來對該服務進行Dos攻擊。
隨后我對Linux 2.6.29至今的內核進行審計,mss_now的計算公式都一樣,tcp_header_len長度也只會加上時間戳的12字節和md5值的18字節。
參考
https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-001.md

