做者: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/