文章首發(fā)于安全客:CVE-2017-6074 DCCP擁塞控制協(xié)議Double-Free提權(quán)分析
影響版本:Linux v2.6.14 - v4.9.13。 v4.9.13已修補(bǔ),v4.9.12未修補(bǔ)。 評分7.8分。 隱藏時間超過10年,從2005年10月的v2.6.14開始。
測試版本:Linux-v4.9.12 exploit及測試環(huán)境下載地址—https://github.com/bsauce/kernel-exploit-factory
編譯選項: CONFIG_IP_DCCP=y CONFIG_INET_DCCP_DIAG=y 以及與DCCP相關(guān)的選項。
在編譯時將.config中的CONFIG_E1000和CONFIG_E1000E,變更為=y。參考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.9.12.tar.xz
$ tar -xvf linux-4.9.12.tar.xz
# KASAN: 設(shè)置 make menuconfig 設(shè)置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 編譯出的bzImage目錄:/arch/x86/boot/bzImage。
漏洞描述:Linux內(nèi)核IP V6協(xié)議簇的DCCP(數(shù)據(jù)報擁塞控制協(xié)議),net/dccp/input.c中的 dccp_rcv_state_process() 函數(shù),在LISTEN狀態(tài)下錯誤處理 DCCP_PKT_REQUEST 包數(shù)據(jù)結(jié)構(gòu),用戶采用IPV6_RECVPKTINFO選項調(diào)用setsockopt()時會觸發(fā)sk_buff結(jié)構(gòu)的Double-Free。
補(bǔ)丁:patch 調(diào)用consume_skb()繼續(xù)占用skb,以避免跳到discard中kfree_skb()釋放skb。consume_skb() 表示 skb是正常釋放,kfree_skb() 表示因為某種錯誤報文被丟棄。
diff --git a/net/dccp/input.c b/net/dccp/input.c
index ba347184bda9b..8fedc2d497709 100644
--- a/net/dccp/input.c
+++ b/net/dccp/input.c
@@ -606,7 +606,8 @@ int dccp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
if (inet_csk(sk)->icsk_af_ops->conn_request(sk,
skb) < 0)
return 1;
- goto discard;
+ consume_skb(skb);
+ return 0;
}
if (dh->dccph_type == DCCP_PKT_RESET)
goto discard;
保護(hù)機(jī)制:開啟SMEP/SMAP,未開啟KASLR。
利用總結(jié):利用方式類似CVE-2016-8655。第一次觸發(fā)漏洞,堆噴偽造po->rx_ring->prb_bdqc->retire_blk_timer結(jié)構(gòu),執(zhí)行native_write_cr4(0x406e0)來關(guān)閉SMEP/SMAP;第二次觸發(fā)漏洞,堆噴偽造skb-> ... ->destructor_arg結(jié)構(gòu),執(zhí)行commit_creds(prepare_kernel_cred(0))來提權(quán)。
1. 漏洞分析
漏洞流程:
- (1)dccp_rcv_state_process() 處理請求包,如果DCCP協(xié)議棧socket狀態(tài)為
DCCP_LISTEN,且請求類型為DCCP_PKT_REQUEST,則調(diào)用 dccp_v6_conn_request() —[1]處; - (2)dccp_v6_conn_request() 中
[3]處,只要滿足條件,就將skb引用計數(shù)加1,且將skb指針保存到ireq->pktopts——[4][5];用戶可通過調(diào)用setsockopt()和IPV6_RECVPKTINFO選項來設(shè)置np->rxopt.bits.rxinfo,使之滿足條件[3]。 - (3)dccp_v6_conn_request() 返回成功,卻跳至
[2]處,該skb被__kfree_skb()強(qiáng)制釋放。之后skb再次釋放時即觸發(fā)Double-Free。
調(diào)用鏈:dccp_rcv_state_process() -> dccp_v6_conn_request()
int dccp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct dccp_hdr *dh, unsigned int len)
{
struct dccp_sock *dp = dccp_sk(sk);
struct dccp_skb_cb *dcb = DCCP_SKB_CB(skb);
const int old_state = sk->sk_state;
int queued = 0;
... ...
if (sk->sk_state == DCCP_LISTEN) {
if (dh->dccph_type == DCCP_PKT_REQUEST) {
if (inet_csk(sk)->icsk_af_ops->conn_request(sk, // [1] 實際調(diào)用 dccp_v6_conn_request() 函數(shù)
skb) < 0)
return 1;
goto discard;
}
if (dh->dccph_type == DCCP_PKT_RESET)
goto discard;
/* Caller (dccp_v4_do_rcv) will send Reset */
dcb->dccpd_reset_code = DCCP_RESET_CODE_NO_CONNECTION;
return 1;
} else if (sk->sk_state == DCCP_CLOSED) {
dcb->dccpd_reset_code = DCCP_RESET_CODE_NO_CONNECTION;
return 1;
}
... ...
if (!queued) {
discard:
__kfree_skb(skb); // [2] 錯誤釋放 skb
}
return 0;
}
// [1] dccp_v6_conn_request()
static int dccp_v6_conn_request(struct sock *sk, struct sk_buff *skb)
{
struct request_sock *req;
struct dccp_request_sock *dreq;
struct inet_request_sock *ireq;
struct ipv6_pinfo *np = inet6_sk(sk);
const __be32 service = dccp_hdr_request(skb)->dccph_req_service;
struct dccp_skb_cb *dcb = DCCP_SKB_CB(skb);
... ...
ireq = inet_rsk(req);
ireq->ir_v6_rmt_addr = ipv6_hdr(skb)->saddr;
ireq->ir_v6_loc_addr = ipv6_hdr(skb)->daddr;
ireq->ireq_family = AF_INET6;
if (ipv6_opt_accepted(sk, skb, IP6CB(skb)) ||
np->rxopt.bits.rxinfo || np->rxopt.bits.rxoinfo || // [3] 可通過 setsockopt() 和 IPV6_RECVPKTINFO 選項來設(shè)置 np->rxopt.bits.rxinfo
np->rxopt.bits.rxhlim || np->rxopt.bits.rxohlim) {
atomic_inc(&skb->users); // [4] 只要滿足其中一個條件,就會將skb的引用計數(shù)加1
ireq->pktopts = skb; // [5] 且將skb指針保存到 ireq->pktopts 中。
}
ireq->ir_iif = sk->sk_bound_dev_if;
... ...
}
2. 漏洞利用
2-1. 觸發(fā)漏洞
觸發(fā)步驟:
(1)創(chuàng)建s1 = socket(PF_INET6, SOCK_DCCP, …),并且監(jiān)聽該socket;
(2)設(shè)置該socket的屬性值
IPV6_RECVPKTINFO,使函數(shù)dccp_v6_conn_request()通過if條件[3],觸發(fā)釋放skb;(3)skb釋放后,進(jìn)行堆噴,偽造
skb-> ... ->destructor_arg->callback函數(shù),觸發(fā)二次釋放skb,執(zhí)行偽造的回調(diào)函數(shù)。
結(jié)構(gòu)鏈(偽造ubuf_info結(jié)構(gòu)):sk_buff -> skb_shared_info -> ubuf_info -> callback
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
union {
ktime_t tstamp;
struct skb_mstamp skb_mstamp;
};
};
struct rb_node rbnode; /* used in netem & tcp stack */
};
struct sock *sk;
struct net_device *dev;
... ...
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data; // <------------ (head+end) 指向 skb_shared_info 結(jié)構(gòu)
unsigned int truesize;
atomic_t users;
};
struct skb_shared_info {
unsigned char nr_frags;
__u8 tx_flags;
unsigned short gso_size;
/* Warning: this field is not always filled in (UFO)! */
unsigned short gso_segs;
unsigned short gso_type;
struct sk_buff *frag_list;
struct skb_shared_hwtstamps hwtstamps;
u32 tskey;
__be32 ip6_frag_id;
/*
* Warning : all fields before dataref are cleared in __alloc_skb()
*/
atomic_t dataref;
/* Intermediate layers must ensure that destructor_arg
* remains valid until skb destructor */
void * destructor_arg; // <------------ 指向 ubuf_info 結(jié)構(gòu)
/* must be last field, see pskb_expand_head() */
skb_frag_t frags[MAX_SKB_FRAGS];
};
struct ubuf_info {
void (*callback)(struct ubuf_info *, bool zerocopy_success); // <------------ 待偽造的回調(diào)函數(shù)
void *ctx;
unsigned long desc;
};
sk_buff二次釋放調(diào)用鏈:dccp_close() -> inet_csk_destroy_sock() -> dccp_v6_destroy_sock() -> inet6_destroy_sock() -> kfree_skb() -> __kfree_skb() -> skb_release_all() -> skb_release_data()
static struct proto dccp_v6_prot = {
.name = "DCCPv6",
.owner = THIS_MODULE,
.close = dccp_close, // close(socket) -> dccp_close() -> ... -> sk->sk_prot->destroy(sk)
... ...
.destroy = dccp_v6_destroy_sock,
... ...
};
static void skb_release_data(struct sk_buff *skb)
{
struct skb_shared_info *shinfo = skb_shinfo(skb); // skb_shared_info 在 sk_buff中線性數(shù)據(jù)區(qū)的偏移: skb->head+skb->end
int i;
if (skb->cloned &&
atomic_sub_return(skb->nohdr ? (1 << SKB_DATAREF_SHIFT) + 1 : 1,
&shinfo->dataref))
return;
for (i = 0; i < shinfo->nr_frags; i++)
__skb_frag_unref(&shinfo->frags[i]);
/*
* If skb buf is from userspace, we need to notify the caller
* the lower device DMA has done;
*/
if (shinfo->tx_flags & SKBTX_DEV_ZEROCOPY) {
struct ubuf_info *uarg;
uarg = shinfo->destructor_arg;
if (uarg->callback) // 執(zhí)行回調(diào)函數(shù)
uarg->callback(uarg, true);
}
if (shinfo->frag_list)
kfree_skb_list(shinfo->frag_list);
skb_free_head(skb);
}
#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))
static inline unsigned char *skb_end_pointer(const struct sk_buff *skb)
{
return skb->head + skb->end;
}
2-2. 關(guān)閉SMEP/SMAP
思路:參考CVE-2016-8655的利用方法,調(diào)用native_write_cr4(0x406e0)來關(guān)閉SMEP/SMAP。如果采用以上觸發(fā)方法來劫持skb-> ... ->destructor_arg->callback函數(shù),則無法傳遞參數(shù)0x406e0。所以借鑒CVE-2016-8655的利用方法,劫持回調(diào)函數(shù) packet_sock --> struct packet_ring_buffer rx_ring --> struct tpacket_kbdq_core prb_bdqc --> struct timer_list retire_blk_timer --> function
結(jié)構(gòu)鏈(偽造timer_list結(jié)構(gòu)):
struct packet_sock {
/* struct sock has to be the first member of packet_sock */
struct sock sk;
struct packet_fanout *fanout;
union tpacket_stats_u stats;
struct packet_ring_buffer rx_ring; // <--------------- rx_ring
struct packet_ring_buffer tx_ring;
... ...
};
struct packet_ring_buffer {
struct pgv *pg_vec;
... ...
unsigned int pg_vec_order;
unsigned int pg_vec_pages;
unsigned int pg_vec_len;
unsigned int __percpu *pending_refcnt;
struct tpacket_kbdq_core prb_bdqc; // <---------------- prb_bdqc
};
/* kbdq - kernel block descriptor queue */
struct tpacket_kbdq_core {
struct pgv *pkbdq;
... ...
struct sk_buff *skb; // <---------------- skb
atomic_t blk_fill_in_prog;
/* Default is set to 8ms */
#define DEFAULT_PRB_RETIRE_TOV (8)
unsigned short retire_blk_tov;
unsigned short version;
unsigned long tov_in_jiffies;
/* timer to retire an outstanding block */
struct timer_list retire_blk_timer; // <---------------- retire_blk_timer
};
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(unsigned long); // 待偽造的回調(diào)函數(shù)
unsigned long data; // 參數(shù)
u32 flags;
#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
創(chuàng)建timer調(diào)用鏈:setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void*)&tp, sizeof(tp)); —— packet_set_ring()->init_prb_bdqc()->prb_setup_retire_blk_timer()->prb_init_blk_timer()
注銷timer調(diào)用鏈:close(fd); —— packet_release() -> packet_set_ring()->prb_shutdown_retire_blk_timer() -> prb_del_retire_blk_timer() -> del_timer_sync()
2-3. 完整利用
利用步驟:
- (1)第一次觸發(fā)漏洞,偽造函數(shù)指針
po->rx_ring->prb_bdqc->retire_blk_timer->function,指向native_write_cr4()函數(shù),偽造參數(shù)po->rx_ring->prb_bdqc->retire_blk_timer->data為 0x406e0,關(guān)閉SMEP/SMAP保護(hù); - (2)第二次觸發(fā)漏洞,偽造函數(shù)指針
skb-> ... ->destructor_arg->callback,指向commit_creds(prepare_kernel_cred(0))函數(shù),提權(quán); - (3)如果能讀取特權(quán)文件,表示提權(quán)成功,fork子進(jìn)程彈shell,避免直接彈shell時釋放sk_buff導(dǎo)致崩潰。
堆噴射:注意,關(guān)閉SMEP/SMAP時噴射覆蓋的是packet_sock對象,大小為0x580;提權(quán)時噴射覆蓋的是sk_buff指向的數(shù)據(jù)區(qū)和skb_shared_info結(jié)構(gòu)所在的堆塊,大小為0x800。這兩個對象都位于0x800大小的堆塊中,所以exp中發(fā)送的占位數(shù)據(jù)大小是1536,也就是0x600,對齊后大小為0x800。關(guān)于sk_buff對象的知識可以參考第3節(jié),了解sk_buff結(jié)構(gòu)和skb_shared_info結(jié)構(gòu)的空間排布關(guān)系。
修正偏移:
# 1. timer offset ---> 偏移為 0x2e8+0x30+104
gef? p/x &(*(struct packet_sock*)0)->rx_ring
$3 = 0x2e8 =744
gef? p/x &(*(struct packet_ring_buffer*)0)->prb_bdqc
$4 = 0x30
gef? p/x &(*(struct tpacket_kbdq_core*)0)->retire_blk_timer
$5 = 0x68 =104
# 2. skb_shared_info offset ---> 偏移為 0x6c0
/exp $ cat /tmp/kallsyms | grep skb_release_data
ffffffff81783260 t skb_release_data
gef? x /30i 0xffffffff81783260
0xffffffff81783260 <skb_release_data>: nop DWORD PTR [rax+rax*1+0x0]
0xffffffff81783265 <skb_release_data+5>: push rbp
0xffffffff81783266 <skb_release_data+6>: mov rbp,rsp
0xffffffff81783269 <skb_release_data+9>: push r14
0xffffffff8178326b <skb_release_data+11>: push r13
0xffffffff8178326d <skb_release_data+13>: push r12
0xffffffff8178326f <skb_release_data+15>: push rbx
=> 0xffffffff81783270 <skb_release_data+16>: movzx eax,BYTE PTR [rdi+0x8e]
0xffffffff81783277 <skb_release_data+23>: mov r14d,DWORD PTR [rdi+0xcc]
0xffffffff8178327e <skb_release_data+30>: add r14,QWORD PTR [rdi+0xd0]
0xffffffff81783285 <skb_release_data+37>: test al,0x1
0xffffffff81783287 <skb_release_data+39>: je 0xffffffff817832af <skb_release_data+79>
gef? p skb
$1 = (struct sk_buff *) 0xffff88007fa5f200
gef? p *(struct sk_buff *) 0xffff88007fa5f200
tail = 0x4ac,
end = 0x6c0,
head = 0xffff88007a890800 "",
data = 0xffff88007a890c78
提權(quán)成功:

3. sk_buff 擴(kuò)展學(xué)習(xí)
目的:了解sk_buff結(jié)構(gòu)和skb_shared_info結(jié)構(gòu)的空間排布關(guān)系。
3-1. sk_buff 結(jié)構(gòu)
sk_buff結(jié)構(gòu)體:sk_buff結(jié)構(gòu)體關(guān)聯(lián)多個其他結(jié)構(gòu)體,第一是線性數(shù)據(jù)區(qū),由sk_buff->head和sk_buff->end指向的數(shù)據(jù)塊,用來存儲sk_buff結(jié)構(gòu)的數(shù)據(jù)也即是存儲數(shù)據(jù)包的內(nèi)容和各層協(xié)議頭。第二是分片結(jié)構(gòu),也即skb_shared_info結(jié)構(gòu),跟在線性數(shù)據(jù)區(qū)后面,即是end指針的下一個字節(jié)開始就是分片結(jié)構(gòu),用來表示IP分片的一個結(jié)構(gòu)體。因此,skb_shared_info分片結(jié)構(gòu)和sk_buff的線性數(shù)據(jù)區(qū)內(nèi)存分配及銷毀時都是一起的。第三個是分片結(jié)構(gòu)指向的非線性數(shù)據(jù)區(qū),即是IP分片內(nèi)容。
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next; // sk_buff結(jié)構(gòu)體是雙鏈表, 指向下一個sk_buff結(jié)構(gòu)體
struct sk_buff *prev; // 指向前一個sk_buff結(jié)構(gòu)體
union {
ktime_t tstamp; // 時間戳,表示這個skb的接收到的時間,一般是在包從驅(qū)動中往二層發(fā)送的接口函數(shù)中設(shè)置
struct skb_mstamp skb_mstamp;
};
};
struct rb_node rbnode; /* used in netem & tcp stack */
};
struct sock *sk; // 指向擁有此緩沖的套接字sock結(jié)構(gòu)體,即:宿主傳輸控制模塊
struct net_device *dev; // 表示一個網(wǎng)絡(luò)設(shè)備,當(dāng)skb為輸出/輸入時,dev表示要輸出/輸入到的設(shè)備
char cb[48] __aligned(8);
unsigned long _skb_refdst;
void (*destructor)(struct sk_buff *skb); // 這是析構(gòu)函數(shù),后期在skb內(nèi)存銷毀時會用到
unsigned int len, // 表示數(shù)據(jù)區(qū)的總長度: (tail - data)與分片結(jié)構(gòu)體數(shù)據(jù)區(qū)的長度之和。注意是數(shù)據(jù)的有效長度
data_len; // 只表示分片結(jié)構(gòu)體數(shù)據(jù)區(qū)的長度(skb_shared_info->page指向的數(shù)據(jù)長度),所以len = (tail - data) + data_len
__u16 mac_len,
hdr_len;
... ...
__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;
__be16 protocol; // 這是包的協(xié)議類型,標(biāo)識是IP包還是ARP包或者其他數(shù)據(jù)包。
__u16 transport_header; // 指向四層幀頭結(jié)構(gòu)體指針
__u16 network_header; // 指向三層IP頭結(jié)構(gòu)體指針
__u16 mac_header; // 指向二層mac頭的頭
/* private: */
__u32 headers_end[0];
/* public: */
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail; // 指向線性數(shù)據(jù)區(qū)中實際數(shù)據(jù)結(jié)束的位置
sk_buff_data_t end; // 指向線性數(shù)據(jù)區(qū)中結(jié)束的位置(非實際數(shù)據(jù)區(qū)域結(jié)束位置)
unsigned char *head, // 指向線性數(shù)據(jù)區(qū)中開始的位置(非實際數(shù)據(jù)區(qū)域開始位置)
*data; // 指向數(shù)據(jù)區(qū)中實際數(shù)據(jù)開始的位置
unsigned int truesize; // 表示緩沖區(qū)總長度,包括sk_buff自身長度+線性數(shù)據(jù)區(qū)+分片結(jié)構(gòu)體的數(shù)據(jù)區(qū)長度, truesize = len + sizeof(sk_buff) = (data - tail) + data_len + sizeof(sk_buff)
atomic_t users; // 引用計數(shù),表明了有多少實體引用了這個skb。其作用就是在銷毀skb結(jié)構(gòu)體時,先查看下users是否為零,若不為零,則調(diào)用函數(shù)遞減下引用計數(shù)users即可;當(dāng)某一次銷毀時,users為零才真正釋放內(nèi)存空間。有兩個操作函數(shù):atomic_inc()引用計數(shù)增加1;atomic_dec()引用計數(shù)減去1;
};
3-2. sk_buff 線性數(shù)據(jù)區(qū)
sk_buff線性數(shù)據(jù)區(qū):數(shù)據(jù)區(qū)的大小是:(skb->end - skb->head);對于每個數(shù)據(jù)包來說這個大小都是固定的,而且在傳輸過程中 skb->end 和 skb->head 所指向的地址都是不變的。這塊數(shù)據(jù)區(qū)是用來存放應(yīng)用層發(fā)下來的數(shù)據(jù)和各層的協(xié)議信息。但在計算數(shù)據(jù)長度或者操作協(xié)議信息時,一般都要和實際的數(shù)據(jù)存放指針為準(zhǔn)。實際數(shù)據(jù)指針為data和tail,data指向?qū)嶋H數(shù)據(jù)開始的地方,tail指向?qū)嶋H數(shù)據(jù)結(jié)束的地方。
sk_buff結(jié)構(gòu)體中的指針和數(shù)據(jù)區(qū)關(guān)系:

包構(gòu)造與數(shù)據(jù)區(qū)變化:
- (1)sk_buff結(jié)構(gòu)數(shù)據(jù)區(qū)剛被申請好,此時head指針、data指針、tail指針都是指向同一個地方。記住前面講過的:head指針和end指針指向的位置一直都不變,而對于數(shù)據(jù)的變化和協(xié)議信息的添加都是通過data指針和tail指針的改變來表現(xiàn)的。
- (2)開始準(zhǔn)備存儲應(yīng)用層下發(fā)過來的數(shù)據(jù),通過調(diào)用函數(shù)
skb_reserve()來使data指針和tail指針同時向下移動,空出一部分空間來為后期添加協(xié)議信息。 - (3)開始存儲數(shù)據(jù)了,通過調(diào)用函數(shù)
skb_put()來使tail指針向下移動空出空間來添加數(shù)據(jù),此時skb->data和skb->tail之間存放的都是數(shù)據(jù)信息,無協(xié)議信息。 - (4)這時就開始調(diào)用函數(shù)
skb_push()來使data指針向上移動,空出空間來添加各層協(xié)議信息。直到最后到達(dá)二層,添加完幀頭然后就開始發(fā)包了。

3-3. sk_buff 非線性數(shù)據(jù)區(qū)
skb_shared_info 分片結(jié)構(gòu)體: 這個分片結(jié)構(gòu)體和sk_buff結(jié)構(gòu)的線性數(shù)據(jù)區(qū)是一體的,sk_buff->end指針的下個字節(jié)就是分片結(jié)構(gòu)的開始位置,所以在各種操作時都把他們兩個結(jié)構(gòu)看做是一個來操作。比如:為sk_buff結(jié)構(gòu)的數(shù)據(jù)區(qū)申請和釋放空間時,分片結(jié)構(gòu)也會跟著該數(shù)據(jù)區(qū)一起分配和釋放。而克隆時,sk_buff 的數(shù)據(jù)區(qū)和分片結(jié)構(gòu)都由分片結(jié)構(gòu)中的 dataref 成員字段來標(biāo)識是否被引用。關(guān)系如下圖所示:
struct skb_shared_info {
unsigned char nr_frags; // 表示有多少個分片結(jié)構(gòu)
__u8 tx_flags;
unsigned short gso_size;
unsigned short gso_segs;
unsigned short gso_type; // 分片的類型
struct sk_buff *frag_list; // 這也是一種類型的分配數(shù)據(jù)
struct skb_shared_hwtstamps hwtstamps;
u32 tskey;
__be32 ip6_frag_id;
atomic_t dataref; // 用于數(shù)據(jù)區(qū)的引用計數(shù),克隆一個skb結(jié)構(gòu)體時,會增加一個引用計數(shù)
void * destructor_arg;
/* must be last field, see pskb_expand_head() */
skb_frag_t frags[MAX_SKB_FRAGS]; // 這是個比較重要的數(shù)組,到講分片結(jié)構(gòu)數(shù)據(jù)區(qū)時會細(xì)講
};

分片結(jié)構(gòu)的非線性數(shù)據(jù)區(qū):skb_shared_info中有個成員字段,skb_frag_t frags[MAX_SKB_FRAGS],和分片結(jié)構(gòu)的數(shù)據(jù)區(qū)有關(guān)。
typedef struct skb_frag_struct skb_frag_t;
struct skb_frag_struct {
struct page *page; // 指向分片數(shù)據(jù)區(qū)的指針,類似于sk_buff中的data指針
__u32 page_offset; // 偏移量,表示從page指針指向的地方,偏移page_offset
__u32 size; // 數(shù)據(jù)區(qū)的長度,即:sk_buff結(jié)構(gòu)中的data_len
}
有兩種數(shù)據(jù)結(jié)構(gòu)來存儲分片數(shù)據(jù),一種是采用frags數(shù)組來存儲分片數(shù)據(jù)區(qū)的指針,一種是用frag_list雙鏈表來存儲。frags一般用在數(shù)據(jù)很多,且線性數(shù)據(jù)區(qū)放不下的情況,skb_frag_t中是一頁一頁的數(shù)據(jù);對于frag_list,我們在分片的時候裝入每個片的信息,每個片最終也被封裝成一個小的skb。分別如下圖所示:


3-4. sk_buff 指針操作函數(shù)
sk_buff指針操作函數(shù):
- (1)
skb_put():向后擴(kuò)大數(shù)據(jù)區(qū)空間,headroom空間不變,tailroom空間減少,skb->data指針不變,skb->tail指針下移; - (2)
skb_push():向前擴(kuò)大數(shù)據(jù)區(qū)空間,headroom空間減少,tailroom空間不變,skb->tail指針不變,skb->data指針上移; - (3)
skb_pull():縮小數(shù)據(jù)區(qū)空間,headroom空間增大,tailroom空間不變,skb->data指針下移,skb->tail指針不變; - (4)
skb_reserve():數(shù)據(jù)區(qū)不變,headroom空間增大,tailroom空間減少,skb->data和skb->tail同時下移;
head--> |----------|
| headroom |
data--> |----------|
| data |
tail--> |----------|
| tailroom |
end --> |----------|
3-5. sk_buff 分配與釋放
skb分配:__alloc_skb() ,通常被三個函數(shù)所調(diào)用 alloc_skb()(常用)、alloc_skb_fclone()(分配克隆的sk_buff結(jié)構(gòu))、dev_alloc_skb()(驅(qū)動中調(diào)用,申請時不可中斷) —— 參考分配SKB。
分配SKB時,需要分配兩塊內(nèi)存,一塊是SKB描述符,一塊是線性數(shù)據(jù)緩存區(qū)(包括線性數(shù)據(jù)區(qū)和skb_shared_info結(jié)構(gòu))。
內(nèi)核對于sk_buff結(jié)構(gòu)的內(nèi)存分配不是和一般的結(jié)構(gòu)動態(tài)內(nèi)存申請一樣:只分配指定大小的內(nèi)存空間。而是在開始的時候,在初始化函數(shù)skb_init()中就分配了兩段內(nèi)存(skbuff_head_cache和skbuff_fclone_cache)來供sk_buff后期申請時用,所以后期要為sk_buff結(jié)構(gòu)動態(tài)申請內(nèi)存時,都會從這兩段內(nèi)存中來申請(其實這不叫申請了,因為這兩段內(nèi)存開始就申請好了的,只是根據(jù)你要的內(nèi)存大小從某個你選定的內(nèi)存段中還回個指針給你罷了)。如果在這個內(nèi)存段中申請失敗,則再用內(nèi)核中用最低層,最基本的kmalloc()來申請內(nèi)存了(這才是真正的申請)。釋放時也一樣,并不會真正的釋放,只是把數(shù)據(jù)清零,然后放回內(nèi)存段中,以供下次sk_buff結(jié)構(gòu)的申請。這是內(nèi)核動態(tài)申請的一種策略,專門為那些經(jīng)常要申請和釋放的結(jié)構(gòu)設(shè)計的,這種策略不僅可以提高申請和釋放時的效率,而且還可以減少內(nèi)存碎片的。
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;
bool pfmemalloc;
cache = (flags & SKB_ALLOC_FCLONE)
? skbuff_fclone_cache : skbuff_head_cache;
if (sk_memalloc_socks() && (flags & SKB_ALLOC_RX))
gfp_mask |= __GFP_MEMALLOC;
/* Get the HEAD */
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node); // [1] 分配SKB描述符堆塊,存放sk_buff結(jié)構(gòu)。從高速緩存中分配,DMA有特定用途,所以排除在DMA中分配
if (!skb)
goto out;
prefetchw(skb);
size = SKB_DATA_ALIGN(size); // 數(shù)據(jù)對齊
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc); // [2] 分配線性數(shù)據(jù)緩存區(qū):線性數(shù)據(jù)區(qū)+skb_shared_info結(jié)構(gòu)。這里可以從DMA內(nèi)存分配
if (!data)
goto nodata;
size = SKB_WITH_OVERHEAD(ksize(data));
prefetchw(data + size);
memset(skb, 0, offsetof(struct sk_buff, tail));
/* Account for allocated memory : skb + skb->head */
skb->truesize = SKB_TRUESIZE(size); // [3] skb 初始化
skb->pfmemalloc = pfmemalloc;
atomic_set(&skb->users, 1);
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
skb->mac_header = (typeof(skb->mac_header))~0U;
skb->transport_header = (typeof(skb->transport_header))~0U;
/* make sure we initialize shinfo sequentially */
shinfo = skb_shinfo(skb); // [4] skb_shared_info 分片結(jié)構(gòu)初始化
memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
atomic_set(&shinfo->dataref, 1);
kmemcheck_annotate_variable(shinfo->destructor_arg);
out:
return skb;
nodata:
kmem_cache_free(cache, skb);
skb = NULL;
goto out;
}
EXPORT_SYMBOL(__alloc_skb);
skb釋放: kfree_skb() -> __kfree_skb() -> skb_release_all() -> skb_release_data()
如果skb->users==1,表明是最后一個引用該結(jié)構(gòu)的,可以調(diào)用__kfree_skb()函數(shù)直接釋放。當(dāng)skb釋放掉后,dst_release同樣會被調(diào)用以減小相關(guān)dst_entry數(shù)據(jù)結(jié)構(gòu)的引用計數(shù)。如果 skb->destructor(skb的析構(gòu)函數(shù))被初始化過,相應(yīng)的函數(shù)會在此時被調(diào)用。還有分片結(jié)構(gòu)體 skb_shared_info 也會相應(yīng)的被釋放掉,然后把所有內(nèi)存空間全部返還到 skbuff_head_cache 緩存池中,這些操作都是由 kfree_skbmem() 函數(shù)來完成的。這里分片的釋放涉及到了克隆問題:如果skb沒有被克隆,數(shù)據(jù)區(qū)也沒有其他skb引用,則直接釋放即可;如果是克隆了skb結(jié)構(gòu),則當(dāng)克隆數(shù)計數(shù)為1時,才能釋放skb結(jié)構(gòu)體;如果分片結(jié)構(gòu)被克隆了,那么也要等到分片克隆計數(shù)為1時,才能釋放掉分片數(shù)據(jù)結(jié)構(gòu)。如果skb是從 skbuff_fclone_cache 緩存池中申請的內(nèi)存時,則要仔細(xì)銷毀過程了,因為從這個緩存池中申請的內(nèi)存,會返還2個skb結(jié)構(gòu)體和一個引用計數(shù)器。所以銷毀時不僅要考慮克隆問題還要考慮2個skb的釋放順序。
void kfree_skb(struct sk_buff *skb)
{
if (unlikely(!skb))
return;
if (likely(atomic_read(&skb->users) == 1)) // [1] 如果 skb->users 不為1,則 skb->users 只是減1,表明減少一次引用。
smp_rmb();
else if (likely(!atomic_dec_and_test(&skb->users)))
return;
trace_kfree_skb(skb, __builtin_return_address(0));
__kfree_skb(skb);
}
EXPORT_SYMBOL(kfree_skb);
參考
https://nvd.nist.gov/vuln/detail/CVE-2017-6074
利用漏洞CVE-2017-6074獲取root權(quán)限
【漏洞預(yù)警】雪藏11年:Linux kernel DCCP double-free 權(quán)限提升漏洞(CVE-2017-6074)
https://www.openwall.com/lists/oss-security/2017/02/26/2
https://github.com/xairy/kernel-exploits/tree/master/CVE-2017-6074
ftrace: trace your kernel functions!
【技術(shù)分享】CVE-2016-8655內(nèi)核競爭條件漏洞調(diào)試分析
What I Learnt From the CVE-2016-8655 Exploit