int ip_append_data(struct sock *sk, struct flowi4 *fl4, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, struct ipcm_cookie *ipc, struct rtable **rtp, unsigned int flags){ struct inet_sock *inet = inet_sk(sk); int err; if (flags&MSG_PROBE) return 0; if (skb_queue_empty(&sk->sk_write_queue)) { err = ip_setup_cork(sk, &inet->cork.base, ipc, rtp); if (err) return err; } else { transhdrlen = 0; } return __ip_append_data(sk, fl4, &sk->sk_write_queue, &inet->cork.base, sk_page_frag(sk), getfrag, from, length, transhdrlen, flags);}getfrag(void *from, char *to, int offset,int len, int odd, struct sk_buff *skb) 函數主要是做了將數據賦值到 skb中。而一個skb就是一個sk_buff 結構體的指針。struct sk_buff - socket buffer 就是一個sock的緩衝區。參數 int transhdrlen 是一個代表著傳輸層header的長度,同時也是標誌是否為第一個fragment的標誌。參數 unsigned int flags 則是一個標誌。在本函數中主要用到了 MSG_PROBE(進行MTU路徑探測,而不真正進行數據發送)、MSG_MORE(代表後續還有數據被發送)在 ip_append_data 中首先判斷flags是否開啟了 MSG_PROBE 選項,如果開啟了,那麼就直接返回0。接下來判斷sk_buff隊列是否為空,如果是空的話通過 ip_setup_cork 初始化 cork 變量。如果不空那麼設置 transhdrlen = 0 說明不是第一個fragment。接下來調用:__ip_append_data 也是主要的處理流程。__ip_append_datastatic int __ip_append_data(struct sock *sk, struct flowi4 *fl4, struct sk_buff_head *queue, struct inet_cork *cork, struct page_frag *pfrag, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, ){ struct inet_sock *inet = inet_sk(sk); struct sk_buff *skb; struct ip_options *opt = cork->opt; int hh_len; int exthdrlen; int mtu; int copy; int err; int offset = 0; unsigned int maxfraglen, fragheaderlen, maxnonfragsize; int csummode = CHECKSUM_NONE; struct rtable *rt = (struct rtable *)cork->dst; u32 tskey = 0; skb = skb_peek_tail(queue); exthdrlen = !skb ? rt->dst.header_len : 0; mtu = cork->fragsize; if (cork->tx_flags & SKBTX_ANY_SW_TSTAMP && sk->sk_tsflags & SOF_TIMESTAMPING_OPT_ID) tskey = sk->sk_tskey++; hh_len = LL_RESERVED_SPACE(rt->dst.dev); fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0); maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen; maxnonfragsize = ip_sk_ignore_df(sk) ? 0xFFFF : mtu; if (cork->length + length > maxnonfragsize - fragheaderlen) { ip_local_error(sk, EMSGSIZE, fl4->daddr, inet->inet_dport, mtu - (opt ? opt->optlen : 0)); return -EMSGSIZE; } if (transhdrlen && length + fragheaderlen <= mtu && rt->dst.dev->features & NETIF_F_V4_CSUM && !(flags & MSG_MORE) && !exthdrlen) csummode = CHECKSUM_PARTIAL; cork->length += length; if (((length > mtu) || (skb && skb_is_gso(skb))) && (sk->sk_protocol == IPPROTO_UDP) && (rt->dst.dev->features & NETIF_F_UFO) && !rt->dst.header_len && (sk->sk_type == SOCK_DGRAM)) { err = ip_ufo_append_data(sk, queue, getfrag, from, length, hh_len, fragheaderlen, transhdrlen, maxfraglen, flags); if (err) goto error; return 0; } if (!skb) goto alloc_new_skb;copy 代表的是最後一個skb的剩餘空間。
skb->len 表示此 SKB 管理的 Data Buffer 中數據的總長度copy < 0:即mtu < skb->len 溢出了。有些數據需要從當前的IP分片中移動到新的片段中copy<0if (copy <= 0) { char *data; unsigned int datalen; unsigned int fraglen; unsigned int fraggap; unsigned int alloclen; struct sk_buff *skb_prev;alloc_new_skb: skb_prev = skb; if (skb_prev) fraggap = skb_prev->len - maxfraglen; else fraggap = 0; datalen = length + fraggap; if (datalen > mtu - fragheaderlen) datalen = maxfraglen - fragheaderlen; fraglen = datalen + fragheaderlen; if ((flags & MSG_MORE) && !(rt->dst.dev->features&NETIF_F_SG)) alloclen = mtu; else alloclen = fraglen; alloclen += exthdrlen; if (datalen == length + fraggap) alloclen += rt->dst.trailer_len;當我們初步確定了需要分配的新的skb的大小 alloclen 後:if (transhdrlen) { skb = sock_alloc_send_skb(sk, alloclen + hh_len + 15, (flags & MSG_DONTWAIT), &err); } else { skb = NULL; if (atomic_read(&sk->sk_wmem_alloc) <= 2 * sk->sk_sndbuf) skb = sock_wmalloc(sk, alloclen + hh_len + 15, 1, sk->sk_allocation); if (unlikely(!skb)) err = -ENOBUFS; }if (!skb) goto error;skb->ip_summed = csummode; skb->csum = 0; skb_reserve(skb, hh_len);skb_shinfo(skb)->tx_flags = cork->tx_flags; cork->tx_flags = 0; skb_shinfo(skb)->tskey = tskey; tskey = 0;data = skb_put(skb, fraglen + exthdrlen); skb_set_network_header(skb, exthdrlen); skb->transport_header = (skb->network_header + fragheaderlen); data += fragheaderlen + exthdrlen; if (fraggap) { skb->csum = skb_copy_and_csum_bits( skb_prev, maxfraglen, data + transhdrlen, fraggap, 0); skb_prev->csum = csum_sub(skb_prev->csum, skb->csum); data += fraggap; pskb_trim_unique(skb_prev, maxfraglen); } copy = datalen - transhdrlen - fraggap; if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) { err = -EFAULT; kfree_skb(skb); goto error; } offset += copy; length -= datalen - fraggap; transhdrlen = 0; exthdrlen = 0; csummode = CHECKSUM_NONE; __skb_queue_tail(queue, skb); continue; }(2)分配的新skb->len剛好是確切的大小(小於mtu) 在這裡我們只討論最複雜的這種情況(並且也是跟我們漏洞最相關的這種情況),其餘的更加細節的可以看:https://blog.csdn.net/minghe_uestc/article/details/7836920?utm_source=blogxgwz2
poc/exp
poc/exp分析#define SHINFO_OFFSET 3164 int size = SHINFO_OFFSET + sizeof(struct skb_shared_info);int rv = send(s, buffer, size, MSG_MORE);int val = 1;rv = setsockopt(s, SOL_SOCKET, SO_NO_CHECK, &val, sizeof(val));send(s, buffer, 1, 0); close(s);AF_INET 代表TCP/IP協議族,在socket編程中只能是AF_INET。s_addr 代表ip地址,INADDR_LOOPBACK代表綁定地址LOOPBAC, 往往是127.0.0.1, 只能收到127.0.0.1上面的連接請求。htons將其轉換成一個轉換成網絡數據格式的數字。當我們建立好socket並初始化之後,第一次send,帶上標記為MSG_MORE告訴系統我們接下來還有數據要發送。此時走UFO路徑如果要發送的是UDP數據包,且系統支持UFO,並且需要分片(length > mtu),那麼 send() 最終會進入:ip_ufo_append_datastatic inline int ip_ufo_append_data(struct sock *sk, struct sk_buff_head *queue, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int hh_len, int fragheaderlen, int transhdrlen, int maxfraglen, unsigned int flags){ struct sk_buff *skb; int err; skb = skb_peek_tail(queue); if (!skb) { skb = sock_alloc_send_skb(sk, hh_len + fragheaderlen + transhdrlen + 20, (flags & MSG_DONTWAIT), &err); if (!skb) return err; skb_reserve(skb, hh_len); skb_put(skb, fragheaderlen + transhdrlen); skb_reset_network_header(skb); skb->transport_header = skb->network_header + fragheaderlen; skb->csum = 0; __skb_queue_tail(queue, skb); } else if (skb_is_gso(skb)) { goto append; } skb->ip_summed = CHECKSUM_PARTIAL; skb_shinfo(skb)->gso_size = maxfraglen - fragheaderlen; skb_shinfo(skb)->gso_type = SKB_GSO_UDP; append: return skb_append_datato_frags(sk, skb, getfrag, from, (length - transhdrlen));}調用 sock_alloc_send_skb 分配一個新的skb,然後把數據放到新的skb的非線性區域中。(skb_share_info)struct skb_shared_info { unsigned char nr_frags; __u8 tx_flags; unsigned short gso_size; unsigned short gso_segs; unsigned short gso_type; struct sk_buff *frag_list; struct skb_shared_hwtstamps hwtstamps; u32 tskey; __be32 ip6_frag_id; atomic_t dataref; void * destructor_arg; skb_frag_t frags[MAX_SKB_FRAGS];};注意在本條UFO路徑中我們skb中數據的大小是大於mtu的!通過 skb_shinfo(SKB) 宏也可以看出來skb_shared_info與skb之間的關係。在第二次send之前,我們調用 setsockopt 來設置了 SO_NO_CHECK 標誌,即不校驗checksum。(內核是通過SO_NO_CHECK的標誌來判斷用UFO機制還是non-UFO機制,這一點在剛剛的源碼中並不明顯。請直接看下面漏洞補丁那裡的patch)#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))造成接下來 第二次send的時候越過UFO路徑而執行non-UFO的代碼。在non-UFO中,此時計算的 copy = mtu - skb_len 小於0,此時的skb是直接從skb隊尾取出來的,也就是第一次send時new alloc出來的len > mtu的skb。由於copy < 0,那麼在 non-UFO 路徑上觸發了重新分配skb的操作。copy = mtu - skb->len; if(copy<0){char *data;unsigned int datalen;unsigned int fraglen;unsigned int fraggap;unsigned int alloclen; struct sk_buff *skb_prev = skb; fraggap = skb_prev->len - maxfraglen; datalen = length + fraggap; skb_copy_and_csum_bits(skb_prev, maxfraglen,data + transhdrlen, fraggap, 0); }其中的 skb_copy_and_csum_bits 將舊的 skb_prev 中的數據(UFO路徑中的skb)複製到新分配的sk_buff中(即skb_shared_info->frags[]中的page_frag),從而造成溢出。而對於 skb_shared_info 存在一個成員 void * destructor_arg 他是skb釋放時的在 kfree_skb 中底層對於其產生的一個析構函數的調用。這裡很有意思的一個處理,直接將一個void 賦給一個 ubuf_info 類型。struct ubuf_info { void (*callback)(struct ubuf_info *, bool zerocopy_success); void *ctx; unsigned long desc;};當完成了skb DMA時,通過他調用回調函數做析構,釋放緩衝區。並且此時skb引用計數為0。而ctx負責跟蹤設備上下文。desc負責跟蹤用戶空間的緩衝區索引。zerocopy_success代表是否發生 零拷貝.destructor_arg 就可以實現程序流劫持了。具體的打法有很多:1. 如果不開smep/kaslr,那麼直接ret2usr即可。2. 如果開了smep,可以做內核rop先劫持cr4來關閉smep。然後jmp到payload3. 如果開了kaslr。我看了一下網上最通用的exp,用了一種非常有意思的方式來bypass KASLR。通過使用 klogctl 讀取內核日誌,然後在內核日誌中查找 'Freeing unused' 這個字符串。然後找到與其同一行的ffffff開頭的數字,最後 & 0xffffffffff000000ul 拿到一個地址,由於偏移不變,那麼接下來就有了其他gadgets/函數在內核中的準確地址了。exp地址:https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-1000112/poc.c
patch:https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=85f1bd9a7b5a79d5baa8bf44af19658f7bf77bfaWhen iteratively building a UDP datagram with MSG_MORE and that
datagram exceeds MTU, consistently choose UFO or fragmentation.Once skb_is_gso, always apply ufo. Conversely, once a datagram is
split across multiple skbs, do not consider ufo.Sendpage already maintains the first invariant, only add the second.
IPv6 does not have a sendpage implementation to modify.A gso skb must have a partial checksum, do not follow sk_no_check_tx
in udp_send_skb.大概就是說,之前的話主要是由於 SO_NO_CHECK 可以控制UFO路徑切換造成問題。但是現在的話一旦我們有了gso(Generic Segmentation Offload一種UFO分片優化,發生在數據送到網卡之前),那麼就會調用ufo,而不是產生路徑切換的隱患。同時作者也說了如果數據報被分片到多個skb中,那麼不要使用ufo了。參考UFO (UDP Fragmentation Offload)Packet fragmentation and segmentation offload in UDP and VXLAN Linux網絡協議棧--ip_append_data函數分析關於網絡編程中MTU、TCP、UDP優化配置的一些總結CVE-2017-1000112-UFO 學習總結看雪ID:ScUpax0s
https://bbs.pediy.com/user-home-876323.htm
*本文由看雪論壇 ScUpax0s 原創,轉載請註明來自看雪社區。