作者:Hcamael@知道創宇 404 實驗室
時間:2019 年 6 月 26 日
英文版本:https://paper.seebug.org/967/
前言
上周Linux核心修複了4個CVE漏洞[1],其中的CVE-2019-11477感覺是一個很厲害的Dos漏洞,不過因為有其他事打斷,是以進展的速度比較慢,這期間網上已經有相關的分析文章了。[2][3]
而我在嘗試複現CVE-2019-11477漏洞的過程中,在第一步設定MSS的問題上就遇到問題了,無法達到預期效果,但是目前公開的分析文章卻沒對該部分内容進行詳細分析。是以本文将通過Linux核心源碼對TCP的MSS機制進行詳細分析。
測試環境
-
存在漏洞的靶機
作業系統版本: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值
- 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
-
直接發包設定
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(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_output.c
tcp_current_mss -> tcp_sync_mss:
mss_now = tcp_mtu_to_mss(sk, pmtu);
tcp_mtu_to_mss:
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
https://paper.seebug.org/959/
https://paper.seebug.org/960/