内容简介:在当日得到了消息,然后呼哈大笑,忽然想起奇书名著《黑客大曝光》的调调,知道这这么回事,简单的一个POC说明,却没有EXP,呵呵了。没意思。其实这个公告只是一个POC说明,证明了
几天前,为了备注,2019年的6月17号吧,一个Linux/FreeBSD系统的漏洞爆出,就是CVE-2019-11477,Netflix的公告为:
https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-001.md
Redhat的链接为:
https://access.redhat.com/security/vulnerabilities/tcpsack在当日得到了消息,然后呼哈大笑,忽然想起奇书名著《黑客大曝光》的调调,知道这这么回事,简单的一个POC说明,却没有EXP,呵呵了。
没意思。
其实这个公告只是一个POC说明,证明了 代码那么写确实是有问题的! 具体如何来触发问题,却至今还未知。
POC的证明如下:
- gso_segs是一个u16类型的数字,其最大值为65535;
- gso_segs的计算方法为 ;
- 由所有skb的片段总长构成,总片段最大17个,每一个片段最长32KB;
- mss最小值为48,减去TCP选项头40字节,raw data的最小值为8;
- 根据gso_segs的计算方法, 会溢出。
确实是会溢出,然而想要制造这么个溢出,却是另一回事。POC和EXP是两回事,很多人往往混淆。
先说结论, CVE-2019-11477漏洞的危害并没有那么严重,不必惊慌。
在阅读下面的内容之前,还是希望把上面我发的链接仔细梳理梳理,此外还需要把 Linux 系统协议栈TCP关于SACK的处理流程仔细梳理一番,不然下面的内容可能会不知所云,就更别说有多么好玩了。
看了很多关于CVE-2019-11477的POC/EXP报文流程,几乎总是纠结于 一次性send出32k,重复17次或者更多,然后就被堵在 如何一次性发出32k这么大的数据 这个难题上了。
但实际上,触发SACK Panic漏洞的关键根本就不在 “一次性发多少数据” 这里,而是在 “发送队列中有多少数据可供合并!” 这里的数据是以字节为单位的,而不是包。
这个CVE-2019-11477漏洞的关键是 SACK段的合并操作 ,最终使得被合并SACK段的总大小超过了阈值:
Multiple such SKB in the list are merged together into one to efficiently process different SACK blocks. It involves moving data from one SKB to another in the list. During this movement of data, the SKB structure can reach its maximum limit of 17 fragments and ‘tcp_gso_segs’ parameter can overflow and hit the BUG_ON() call below resulting in the said kernel panic issue.
注意,sack段被合并(merge)的目标是 “to efficiently process different SACK blocks” 这是为了抑制另外一种DDoS,即伪造畸变的SACK序列,让发送端的CPU消耗于发送队列的链表操作,排序,遍历等。在4.15内核,TCP的发送队列被重构成了红黑树,进一步抑制了这种DDoS攻击。
然而,这种善良的本意却无心插柳了一个漏洞,即CVE-2019-11477。
CVE-2019-11477的攻击EXP要点主要有以下三点:
- 发送队列中保持 字节的数据,这是合并溢出的前提。
- 被攻击侧发送TCP报文中要携带40字节的option,这才能使得raw data的mss成为8,这是实施除法的前提。
- 需要超时重传,从而完成两个目标,即 “给ICMP Need Frag以间隙” 以及 “tcp_fragment实施除法” 导致溢出。
其中,第一个要点已经有办法解决,难的是第二个。如何让被攻击侧携带40个字节的选项呢?
我们能列举出的选项,时间戳,SACK…如何才能 驱使 被攻击的发送端携带40字节之多的选项呢?用 诱导 这个词比较合适。
我们知道,TCP选项最大40字节,SACK选项最多容纳4个段,满4个段的SACK就能占掉 字节的选项空间,我们还知道,TCP时间戳选项一共10个字节,那么除却时间戳,还需要30字节就够了,我建议以下的TCP选项组合:
时间戳+1个SACK选项+1个MD5选项=10字节+20字节+10字节=40字节
然而这需要被攻击者内核支持TCP MD5选项。我的手改版是现编译的,特意支持的,但是互联网线上的内核是否大规模支持MD5选项,不得而知。
接下来,诱导被攻击侧发送1个SACK选项也有办法,比如攻击者可以主动给被攻击侧发送带有一个空洞的数据序列,诱使被攻击侧SACK空洞之后的数据。
但其实,非常难。
本文将给出一例EXP报文的触发逻辑。但我是改了内核的,手改了内核的初始拥塞窗口,加入了TCP MD5选项的支持,且增加了超时容忍时间。
如果使用原生的内核,想要实施攻击就更加不易了。
在本例中,若想实施攻击,首先有个前置条件,假设当前的mss为1400,则必须让被攻击的发送端的发送队列里必须至少有 个数据包,这里的 指的是 发送队列里面已经发送尚未确认的数据包数量必须至少18个,17个用来合并,1个为UNA空洞。
我们算一下 大概需要多大的拥塞窗口,以包计数,也就是 左右的拥塞窗口。
这并不难,攻击者可以拉取被攻击方的一个大文件, 不断伪造正常的ACK确认报文,诱导被攻击的发送端拥塞窗口不断张开,如果怕ACK丢失,可以每个ACK发两遍 ,只有发送端的拥塞窗口涨得足够大,其发送队列中才能容纳至少 这么多字节的数据包。
同时攻击者伪造通告非常的接收窗口使的被攻击的发送端的发送窗口不断增加:
snd_wnd=min(songestion_wnd, receive_wnd);
两个措施,便可诱导被攻击侧发送更多的 尚未确认的包 ,以被攻击者伪造包序列以利用漏洞。
如果我们一开始就构建mss为48而不是1400的握手包去连接被攻击者的话,我们就需要诱导它的拥塞窗口达到 这么大,这就困难了。
那么既然漏洞在mss为48时才会触发,现在为了快速增长拥塞窗口,攻击者的mss协商成了1400,那么如何让其在攻击前置条件(即发送队列里已经堆积了总大小为 字节的包)满足后,如果让mss变成48而实施攻击呢?
答案是伪造ICMP Need Frag序列,迫使被攻击者自己重置mss。
好了,在满足了前置条件后,此时被攻击的发送端的发送队列情况是:
接下来,一个合理的触发操作序列为:
- 攻击者伪造sack序列, <sack una+mss~end, rwnd 0>
被攻击的发送端收到该sack序列后,会将这些 连续的被sack的段 合并成为一个大的GSO段,一共 字节,且阻止被攻击的发送端发送任何数据。
rwnd=0 这个很重要,为了让序列起作用,则必须阻止被攻击侧的任何发包,即憋住它。
插一句。为什么会合并?很简单,为了处理发送队列更高效。不管怎么说,发送队列要么为链表( 操作),要么为红黑树(4.15以后, 操作),元素合并会减少数量,提高效率。
此时,被攻击的发送端的发送队列情况是:
- 伪造 ICMP Need Fragment [mtu=48+协议头长] 包
迫使TCP连接的mss减少到48,留下8字节给raw data。进而raw data的mss成为8。 - 等待超时,攻击者只需等待。
被攻击的发送端超时后会重置所有的发送队列skb为LOST,包括那个已经被合并的大小为 的包。
此时,被攻击的发送端的发送队列情况是:
- 被攻击端超时重传
当重传到被合并的超大skb时,由于mss已经变小,且拥塞窗口此时还比较小,会调用tcp_fragment将大的skb进行分割,分为大小分别为8(即raw data为8的skb)以及 ,首先重传第一个大小为8的包,大小为 的包留在后面等待下次重传。
tcp_fragment函数中,gso_segs字段就会溢出。由于mss已经变小,会重新计算gso_segs,算法为: 其值等于 ,超过了u16的最大值65535,回绕成4095(记住这个值)。
此时,被攻击的发送端的发送队列情况是:
- 攻击者收到重传包后,再次伪造sack序列 <sack una+1~end, rwnd 32768>
被攻击的发送端收到该sack序列后会尝试将超时重传时分开的两个大小分别为8(即raw data为8的skb)以及 的包重新合并为一个。此时打开了接收窗口。因为通告接收窗口憋住期间,攻击已经一气呵成。
合并过程中,后面 字节大小的包要和前面8字节大小的包合并,合并的函数如下:
static struct sk_buff *tcp_shift_skb_data(struct sock *sk, struct sk_buff *skb, struct tcp_sacktag_state *state, u32 start_seq, u32 end_seq, bool dup_sack) { … len = end_seq - TCP_SKB_CB(skb)->seq; // len就是后面将要被合并到前面那个8字节小包的大包的长度,即int型值 (17*32*1024-8)=557048 pcount = len / mss; // pcount就是即将被合并的大包的以mss为单位的段数,即int型的(17*32*1024-8)/8=69631 if (!skb_shift(prev, skb, len)) // 大包顺利合入第一个小包 goto fallback; // tcp_shifted_skb 中pconut为大包段数69631,skb的gso_segs为在超时重传中tcp_fragmeng中被重置的溢出值4095。 // 4095<69631!命中BUG_ON,gg! if (!tcp_shifted_skb(sk, skb, state, pcount, len, mss, dup_sack)) goto out;
插播一段tcp_fragment的逻辑。
在GSO启用的情况下,当重传一个长度len大于mss的包时,重传逻辑会将其分为两部分:
- 一个长度和mss大小相同的包;
- 一个长度为len-mss的包。
分割逻辑如下:
// __tcp_retransmit_skb 在发现当前mss已经小于skb的len时,将会调用tcp_fragment。 /* Initialize TSO segments for a packet. */ static void tcp_set_skb_tso_segs(const struct sock *sk, struct sk_buff *skb, unsigned int mss_now) { struct skb_shared_info *shinfo = skb_shinfo(skb); /* Make sure we own this skb before messing gso_size/gso_segs */ WARN_ON_ONCE(skb_cloned(skb)); if (skb->len <= mss_now || !sk_can_gso(sk) || skb->ip_summed == CHECKSUM_NONE) { /* Avoid the costly divide in the normal * non-TSO case. */ shinfo->gso_segs = 1; shinfo->gso_size = 0; shinfo->gso_type = 0; } else { // 这里的除法将会是:(17*32*1024-8)/8,使得u16的gso_segs溢出! shinfo->gso_segs = DIV_ROUND_UP(skb->len, mss_now); shinfo->gso_size = mss_now; shinfo->gso_type = sk->sk_gso_type; } } int tcp_fragment(struct sock *sk, struct sk_buff *skb, u32 len, unsigned int mss_now) { … /* Fix up tso_factor for both original and new SKB. */ // skb的raw data大小为raw data的mss,也就是8 tcp_set_skb_tso_segs(sk, skb, mss_now); // buff的大小为合并后的原始skb的大小17*32768字节减去8. tcp_set_skb_tso_segs(sk, buff, mss_now); … }
在描述完一个EXP触发逻辑后,这里再给出一个。
这个是我昨夜值班时想了的,并没有亲自试,所以也只是个想法。该EXP触发逻辑是概率性的大摆拳,没有上一个那么精心Trick。
如果不使用超时重传呢?
- 同前文的触发逻辑第1步。
- 伪造窗口通告 <rwnd 2000> 。
- 被攻击侧重传合并后的GSO大包。
由于rwnd此时只有1000,故而只重传UNA,不涉及合并后的包。 - 恰好该GSO大包被塞入了SCH队列。
这一步是概率性的,一旦GSO大包被塞入队列,tcp_retransmit_skb将会逐栈返回,TCP连接解锁。 - 解锁期间,伪造ICMP Need Frag报文,强制被攻击侧raw data的mss为8。
注意,这一步设置mss为48,因此依然依赖凑40字节的TCP选项。 - 伪造rwnd打开,重传继续进行。
此时会重传合并后的GSO大包,按照重传逻辑,由于合并后的GSO大包长度为 字节,远大于mss,因此会执行tcp_fragment(请务必熟读该函数!),此时gso_segs字段会溢出。 - 继续按照前文的EXP步骤,伪造SACK段,促使一次新的合并,gg。
注意,这个步骤中没有采用超时重传的间隙,而是采用Linux发包流程中的队列隔断了同步流程,获取概率性的TCP锁空档,进而改变mss。
当然了,这个我并没有测试,根本没有时间。所以,这只是一个想法。
恕不能提供源码和fixed attacked kernel,因为很多人的目的不是学习,而是干坏事。
皮鞋:mans_shoe:湿了,不会胖。
经理可以扣篮,但不经常,也不绝对
经理穿着皮鞋能扣篮,但不经常,也不绝对。
正如时间无限的前提下,总会有一立方的空气分子朝着一个方向运动的概率,虽然概率很低。
所以,经理一定有概率可以穿着皮鞋扣篮。虽然这并不意味着经理扣篮就一定能成功。
经理穿皮鞋扣篮可以用泊松分布来建模(无论跳多少次,高度几乎总是一个固定值),把经理的弹跳高度15公分填入兰姆达,然后让k等于扣篮所需的弹跳高度比如100公分,只要概率不等于0,就是有可能。越大越经常,越绝对。如果很小,就是但不经常,也不绝对。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【技术分享】WebSocket漏洞与防护详解
- 【技术分享】WebSocket漏洞与防护详解
- 详解NSURLCache缓存引发的安全漏洞
- 详解Laravel 5.8 SQL注入漏洞
- CVE-2019-11477漏洞详解详玩(删)
- WEB安全入门系列之CSRF漏洞详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。