Linux内核TCP MSS机制详细分析

前言

上周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

https://paper.seebug.org/959/

https://paper.seebug.org/960/

原文地址:https://www.cnblogs.com/h2zZhou/p/11193916.html

时间: 2024-10-09 14:45:47

Linux内核TCP MSS机制详细分析的相关文章

Linux 内核 TCP MSS 机制详细分析

作者:[email protected]知道创宇 404 实验室时间:2019 年 6 月 26 日英文版本:https://paper.seebug.org/967/ 前言 上周Linux内核修复了4个CVE漏洞[1],其中的CVE-2019-11477感觉是一个很厉害的Dos漏洞,不过因为有其他事打断,所以进展的速度比较慢,这期间网上已经有相关的分析文章了.[2][3] 而我在尝试复现CVE-2019-11477漏洞的过程中,在第一步设置MSS的问题上就遇到问题了,无法达到预期效果,但是目前

Linux内核抢占实现机制分析【转】

Linux内核抢占实现机制分析 转自:http://blog.chinaunix.net/uid-24227137-id-3050754.html [摘要]本文详解了Linux内核抢占实现机制.首先介绍了内核抢占和用户抢占的概念和区别,接着分析了不可抢占内核的特点及实时系统中实现内核抢占的必要性.然后分析了禁止内核抢占的情况和内核抢占的时机,最后介绍了实现抢占内核所做的改动以及何时需要重新调度. [关键字]内核抢占,用户抢占,中断, 实时性,自旋锁,抢占时机,调度时机,schedule,pree

Linux内核抢占实现机制分析

Sailor_forever  [email protected] 转载请注明 http://blog.csdn.net/sailor_8318/archive/2008/09/03/2870184.aspx [摘要]本文详解了Linux内核抢占实现机制.首先介绍了内核抢占和用户抢占的概念和区别,接着分析了不可抢占内核的特点及实时系统中实现内核抢占的必要性.然后分析了禁止内核抢占的情况和内核抢占的时机,最后介绍了实现抢占内核所做的改动以及何时需要重新调度. [关键字]内核抢占,用户抢占,中断, 

Linux内核态抢占机制分析(转)

Linux内核态抢占机制分析  http://blog.sina.com.cn/s/blog_502c8cc401012pxj.html 摘 要]本文首先介绍非抢占式内核(Non-Preemptive Kernel)和可抢占式内核(Preemptive Kernel)的区别.接着分析Linux下有两种抢占:用户态抢占(User Preemption).内核态抢占(Kernel Preemption).然后分析了在内核态下:如何判断能否抢占内核(什么是可抢占的条件):何时触发重新调度(何时设置可抢

Linux 内核的同步机制,第 1 部分 + 第二部分(转)

http://blog.csdn.net/jk198310/article/details/9264721  原文地址: Linux 内核的同步机制,第 1 部分 一. 引言 在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问.尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问.在主流的Linux内核中包含了几乎所有现代的操作系统具有的同步机制,这些同步机制包括:原子操作

linux内核的配置机制及其编译过程

linux内核的配置机制及其编译过程 国嵌第一天第三节:讲解的是内核在X86平台上的配置.安装过程,制作自己的Linux系统,并双系统启动. <Linux系统移植>第四章 http://blog.csdn.net/zhengmeifu/article/details/7682373 Linux内核具有可定制的特点,具体步骤如下: 1.1.1 配置系统的基本结构 Linux内核的配置系统由三个部分组成,分别是: 1.Makefile:分布在 Linux 内核源代码根目录及各层目录中,定义 Lin

大话Linux内核中锁机制之原子操作、自旋锁

转至:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html 很多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实是由于操作系统中存在多进程对共享资源的并发访问,从而引起了进程间的竞态.这其中包括了我们所熟知的SMP系统,多核间的相互竞争资源,单CPU之间的相互竞争,中断和进程间的相互抢占等诸多问题. 通常情况下,如图1所示,对于一段程序,我们的理想是总是美好的,希望它能够这样执行:进程1先对临界区完成操作,

大话Linux内核中锁机制之内存屏障、读写自旋锁及顺序锁

大话Linux内核中锁机制之内存屏障.读写自旋锁及顺序锁 在上一篇博文中笔者讨论了关于原子操作和自旋锁的相关内容,本篇博文将继续锁机制的讨论,包括内存屏障.读写自旋锁以及顺序锁的相关内容.下面首先讨论内存屏障的相关内容. 三.内存屏障 不知读者是是否记得在笔者讨论自旋锁的禁止或使能的时候,提到过一个内存屏障函数.OK,接下来,笔者将讨论内存屏障的具体细节内容.我们首先来看下它的概念,Memory Barrier是指编译器和处理器对代码进行优化(对读写指令进行重新排序)后,导致对内存的写入操作不能

大话Linux内核中锁机制之完成量、互斥量

大话Linux内核中锁机制之完成量.互斥量 在上一篇博文中笔者分析了关于信号量.读写信号量的使用及源码实现,接下来本篇博文将讨论有关完成量和互斥量的使用和一些经典问题. 八.完成量 下面讨论完成量的内容,首先需明确完成量表示为一个执行单元需要等待另一个执行单元完成某事后方可执行,它是一种轻量级机制.事实上,它即是为了完成进程间的同步而设计的,故而仅仅提供了代替同步信号量的一种解决方法,初值被初始化为0.它在include\linux\completion.h定义. 如图8.1所示,对于执行单元A