文章首發(fā)于安全客:CVE-2017-6074 DCCP擁塞控制協(xié)議Double-Free提權(quán)分析
影響版本:Linux v2.6.14 - v4.9.13。 v4.9.13已修補,v4.9.12未修補少态。 評分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助泽。
補丁: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;
保護機制:開啟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()
強制釋放。之后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釋放后,進行堆噴雁比,偽造
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保護慌核; - (2)第二次觸發(fā)漏洞,偽造函數(shù)指針
skb-> ... ->destructor_arg->callback
申尼,指向commit_creds(prepare_kernel_cred(0))
函數(shù)垮卓,提權(quán); - (3)如果能讀取特權(quán)文件师幕,表示提權(quán)成功粟按,fork子進程彈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 擴展學(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é)議信息婆排。直到最后到達二層声旺,添加完幀頭然后就開始發(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ū)時會細講
};
分片結(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()
:向后擴大數(shù)據(jù)區(qū)空間态蒂,headroom空間不變杭措,tailroom空間減少,skb->data指針不變钾恢,skb->tail指針下移手素; - (2)
skb_push()
:向前擴大數(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)存時温峭,則要仔細銷毀過程了,因為從這個緩存池中申請的內(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