做者:Hcamael@知道創宇 404 實驗室
時間:2019 年 6 月 26 日
英文版本:https://paper.seebug.org/967/python
前言linux
上週Linux內核修復了4個CVE漏洞[1],其中的CVE-2019-11477感受是一個很厲害的Dos漏洞,不過由於有其餘事打斷,因此進展的速度比較慢,這期間網上已經有相關的分析文章了。[2][3]nginx
而我在嘗試復現CVE-2019-11477漏洞的過程當中,在第一步設置MSS的問題上就遇到問題了,沒法達到預期效果,可是目前公開的分析文章卻沒對該部份內容進行詳細分析。因此本文將經過Linux內核源碼對TCP的MSS機制進行詳細分析。git
測試環境github
內核版本:4.15.0-20-genericubuntu
地址:192.168.11.112服務器
內核源碼:app
$ sudo apt install linux-source-4.15.0
$ ls /usr/src/linux-source-4.15.0.tar.bz2
帶符號的內核:curl
$ 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):tcp
$ cat /etc/default/grub |grep -v "#" | grep CMDLI
GRUB_CMDLINE_LINUX_DEFAULT="nokaslr"
GRUB_CMDLINE_LINUX=""
$ sudo update-grub
裝一個nginx,供測試:
$ sudo apt install nginx
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
地址:192.168.11.111
平常習慣使用Python的,須要裝個scapy構造自定義TCP包
自定義SYN的MSS選項
有三種方法能夠設置TCP SYN包的MSS值
$ 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
$ 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
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(17321024//8)
'0x11000'
hex(17321024//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_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_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
https://paper.seebug.org/959/
https://paper.seebug.org/960/