文章首发于安全客:CVE-2017-6074 DCCP拥塞控制协议Double-Free提权分析
影响版本: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及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory
编译选项: CONFIG_IP_DCCP=y CONFIG_INET_DCCP_DIAG=y 以及与DCCP
相关的选项。
在编译时将.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: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:Linux内核IP V6协议簇的DCCP(数据报拥塞控制协议),net/dccp/input.c
中的 dccp_rcv_state_process() 函数,在LISTEN
状态下错误处理 DCCP_PKT_REQUEST
包数据结构,用户采用IPV6_RECVPKTINFO
选项调用setsockopt()
时会触发sk_buff
结构的Double-Free。
补丁:patch 调用consume_skb()继续占用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。
利用总结:利用方式类似CVE-2016-8655。第一次触发漏洞,堆喷伪造po->rx_ring->prb_bdqc->retire_blk_timer
结构,执行native_write_cr4(0x406e0)
来关闭SMEP/SMAP;第二次触发漏洞,堆喷伪造skb-> ... ->destructor_arg
结构,执行commit_creds(prepare_kernel_cred(0))
来提权。
1. 漏洞分析
漏洞流程:
- (1)dccp_rcv_state_process() 处理请求包,如果DCCP协议栈socket状态为
DCCP_LISTEN
,且请求类型为DCCP_PKT_REQUEST
,则调用 dccp_v6_conn_request() —[1]
处; - (2)dccp_v6_conn_request() 中
[3]
处,只要满足条件,就将skb引用计数加1,且将skb指针保存到ireq->pktopts
——[4][5]
;用户可通过调用setsockopt()
和IPV6_RECVPKTINFO
选项来设置np->rxopt.bits.rxinfo
,使之满足条件[3]
。 - (3)dccp_v6_conn_request() 返回成功,却跳至
[2]
处,该skb被__kfree_skb()
强制释放。之后skb再次释放时即触发Double-Free。
调用链: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] 实际调用 dccp_v6_conn_request() 函数
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 选项来设置 np->rxopt.bits.rxinfo
np->rxopt.bits.rxhlim || np->rxopt.bits.rxohlim) {
atomic_inc(&skb->users); // [4] 只要满足其中一个条件,就会将skb的引用计数加1
ireq->pktopts = skb; // [5] 且将skb指针保存到 ireq->pktopts 中。
}
ireq->ir_iif = sk->sk_bound_dev_if;
... ...
}
2. 漏洞利用
2-1. 触发漏洞
触发步骤:
(1)创建s1 = socket(PF_INET6, SOCK_DCCP, …),并且监听该socket;
(2)设置该socket的属性值
IPV6_RECVPKTINFO
,使函数dccp_v6_conn_request()
通过if条件[3]
,触发释放skb;(3)skb释放后,进行堆喷,伪造
skb-> ... ->destructor_arg->callback
函数,触发二次释放skb,执行伪造的回调函数。
结构链(伪造ubuf_info
结构):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 结构
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 结构
/* 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); // <------------ 待伪造的回调函数
void *ctx;
unsigned long desc;
};
sk_buff
二次释放调用链: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中线性数据区的偏移: 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) // 执行回调函数
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. 关闭SMEP/SMAP
思路:参考CVE-2016-8655的利用方法,调用native_write_cr4(0x406e0)
来关闭SMEP/SMAP。如果采用以上触发方法来劫持skb-> ... ->destructor_arg->callback
函数,则无法传递参数0x406e0
。所以借鉴CVE-2016-8655的利用方法,劫持回调函数 packet_sock --> struct packet_ring_buffer rx_ring --> struct tpacket_kbdq_core prb_bdqc --> struct timer_list retire_blk_timer --> function
结构链(伪造timer_list
结构):
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); // 待伪造的回调函数
unsigned long data; // 参数
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
};
创建timer调用链: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调用链:close(fd);
—— packet_release() -> packet_set_ring()->prb_shutdown_retire_blk_timer() -> prb_del_retire_blk_timer() -> del_timer_sync()
2-3. 完整利用
利用步骤:
- (1)第一次触发漏洞,伪造函数指针
po->rx_ring->prb_bdqc->retire_blk_timer->function
,指向native_write_cr4()
函数,伪造参数po->rx_ring->prb_bdqc->retire_blk_timer->data
为 0x406e0,关闭SMEP/SMAP保护; - (2)第二次触发漏洞,伪造函数指针
skb-> ... ->destructor_arg->callback
,指向commit_creds(prepare_kernel_cred(0))
函数,提权; - (3)如果能读取特权文件,表示提权成功,fork子进程弹shell,避免直接弹shell时释放sk_buff导致崩溃。
堆喷射:注意,关闭SMEP/SMAP时喷射覆盖的是packet_sock
对象,大小为0x580;提权时喷射覆盖的是sk_buff
指向的数据区和skb_shared_info
结构所在的堆块,大小为0x800。这两个对象都位于0x800大小的堆块中,所以exp中发送的占位数据大小是1536,也就是0x600,对齐后大小为0x800。关于sk_buff
对象的知识可以参考第3节,了解sk_buff
结构和skb_shared_info
结构的空间排布关系。
修正偏移:
# 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
提权成功:
3. sk_buff 扩展学习
目的:了解sk_buff
结构和skb_shared_info
结构的空间排布关系。
3-1. sk_buff 结构
sk_buff
结构体:sk_buff结构体关联多个其他结构体,第一是线性数据区,由sk_buff->head
和sk_buff->end
指向的数据块,用来存储sk_buff结构的数据也即是存储数据包的内容和各层协议头。第二是分片结构,也即skb_shared_info
结构,跟在线性数据区后面,即是end指针的下一个字节开始就是分片结构,用来表示IP分片的一个结构体。因此,skb_shared_info
分片结构和sk_buff的线性数据区内存分配及销毁时都是一起的。第三个是分片结构指向的非线性数据区,即是IP分片内容。
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next; // sk_buff结构体是双链表, 指向下一个sk_buff结构体
struct sk_buff *prev; // 指向前一个sk_buff结构体
union {
ktime_t tstamp; // 时间戳,表示这个skb的接收到的时间,一般是在包从驱动中往二层发送的接口函数中设置
struct skb_mstamp skb_mstamp;
};
};
struct rb_node rbnode; /* used in netem & tcp stack */
};
struct sock *sk; // 指向拥有此缓冲的套接字sock结构体,即:宿主传输控制模块
struct net_device *dev; // 表示一个网络设备,当skb为输出/输入时,dev表示要输出/输入到的设备
char cb[48] __aligned(8);
unsigned long _skb_refdst;
void (*destructor)(struct sk_buff *skb); // 这是析构函数,后期在skb内存销毁时会用到
unsigned int len, // 表示数据区的总长度: (tail - data)与分片结构体数据区的长度之和。注意是数据的有效长度
data_len; // 只表示分片结构体数据区的长度(skb_shared_info->page指向的数据长度),所以len = (tail - data) + data_len
__u16 mac_len,
hdr_len;
... ...
__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;
__be16 protocol; // 这是包的协议类型,标识是IP包还是ARP包或者其他数据包。
__u16 transport_header; // 指向四层帧头结构体指针
__u16 network_header; // 指向三层IP头结构体指针
__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; // 指向线性数据区中实际数据结束的位置
sk_buff_data_t end; // 指向线性数据区中结束的位置(非实际数据区域结束位置)
unsigned char *head, // 指向线性数据区中开始的位置(非实际数据区域开始位置)
*data; // 指向数据区中实际数据开始的位置
unsigned int truesize; // 表示缓冲区总长度,包括sk_buff自身长度+线性数据区+分片结构体的数据区长度, truesize = len + sizeof(sk_buff) = (data - tail) + data_len + sizeof(sk_buff)
atomic_t users; // 引用计数,表明了有多少实体引用了这个skb。其作用就是在销毁skb结构体时,先查看下users是否为零,若不为零,则调用函数递减下引用计数users即可;当某一次销毁时,users为零才真正释放内存空间。有两个操作函数:atomic_inc()引用计数增加1;atomic_dec()引用计数减去1;
};
3-2. sk_buff 线性数据区
sk_buff
线性数据区:数据区的大小是:(skb->end - skb->head);对于每个数据包来说这个大小都是固定的,而且在传输过程中 skb->end
和 skb->head
所指向的地址都是不变的。这块数据区是用来存放应用层发下来的数据和各层的协议信息。但在计算数据长度或者操作协议信息时,一般都要和实际的数据存放指针为准。实际数据指针为data和tail,data指向实际数据开始的地方,tail指向实际数据结束的地方。
sk_buff结构体中的指针和数据区关系:
包构造与数据区变化:
- (1)sk_buff结构数据区刚被申请好,此时head指针、data指针、tail指针都是指向同一个地方。记住前面讲过的:head指针和end指针指向的位置一直都不变,而对于数据的变化和协议信息的添加都是通过data指针和tail指针的改变来表现的。
- (2)开始准备存储应用层下发过来的数据,通过调用函数
skb_reserve()
来使data指针和tail指针同时向下移动,空出一部分空间来为后期添加协议信息。 - (3)开始存储数据了,通过调用函数
skb_put()
来使tail指针向下移动空出空间来添加数据,此时skb->data
和skb->tail
之间存放的都是数据信息,无协议信息。 - (4)这时就开始调用函数
skb_push()
来使data指针向上移动,空出空间来添加各层协议信息。直到最后到达二层,添加完帧头然后就开始发包了。
3-3. sk_buff 非线性数据区
skb_shared_info 分片结构体: 这个分片结构体和sk_buff
结构的线性数据区是一体的,sk_buff->end
指针的下个字节就是分片结构的开始位置,所以在各种操作时都把他们两个结构看做是一个来操作。比如:为sk_buff结构的数据区申请和释放空间时,分片结构也会跟着该数据区一起分配和释放。而克隆时,sk_buff 的数据区和分片结构都由分片结构中的 dataref
成员字段来标识是否被引用。关系如下图所示:
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; // 用于数据区的引用计数,克隆一个skb结构体时,会增加一个引用计数
void * destructor_arg;
/* must be last field, see pskb_expand_head() */
skb_frag_t frags[MAX_SKB_FRAGS]; // 这是个比较重要的数组,到讲分片结构数据区时会细讲
};
分片结构的非线性数据区:skb_shared_info
中有个成员字段,skb_frag_t frags[MAX_SKB_FRAGS]
,和分片结构的数据区有关。
typedef struct skb_frag_struct skb_frag_t;
struct skb_frag_struct {
struct page *page; // 指向分片数据区的指针,类似于sk_buff中的data指针
__u32 page_offset; // 偏移量,表示从page指针指向的地方,偏移page_offset
__u32 size; // 数据区的长度,即:sk_buff结构中的data_len
}
有两种数据结构来存储分片数据,一种是采用frags
数组来存储分片数据区的指针,一种是用frag_list
双链表来存储。frags
一般用在数据很多,且线性数据区放不下的情况,skb_frag_t
中是一页一页的数据;对于frag_list
,我们在分片的时候装入每个片的信息,每个片最终也被封装成一个小的skb。分别如下图所示:
3-4. sk_buff 指针操作函数
sk_buff
指针操作函数:
- (1)
skb_put()
:向后扩大数据区空间,headroom空间不变,tailroom空间减少,skb->data指针不变,skb->tail指针下移; - (2)
skb_push()
:向前扩大数据区空间,headroom空间减少,tailroom空间不变,skb->tail指针不变,skb->data指针上移; - (3)
skb_pull()
:缩小数据区空间,headroom空间增大,tailroom空间不变,skb->data指针下移,skb->tail指针不变; - (4)
skb_reserve()
:数据区不变,headroom空间增大,tailroom空间减少,skb->data和skb->tail同时下移;
head--> |----------|
| headroom |
data--> |----------|
| data |
tail--> |----------|
| tailroom |
end --> |----------|
3-5. sk_buff 分配与释放
skb分配:__alloc_skb() ,通常被三个函数所调用 alloc_skb()(常用)、alloc_skb_fclone()(分配克隆的sk_buff
结构)、dev_alloc_skb()(驱动中调用,申请时不可中断) —— 参考分配SKB。
分配SKB时,需要分配两块内存,一块是SKB描述符,一块是线性数据缓存区(包括线性数据区和skb_shared_info
结构)。
内核对于sk_buff结构的内存分配不是和一般的结构动态内存申请一样:只分配指定大小的内存空间。而是在开始的时候,在初始化函数skb_init()中就分配了两段内存(skbuff_head_cache
和skbuff_fclone_cache
)来供sk_buff后期申请时用,所以后期要为sk_buff结构动态申请内存时,都会从这两段内存中来申请(其实这不叫申请了,因为这两段内存开始就申请好了的,只是根据你要的内存大小从某个你选定的内存段中还回个指针给你罢了)。如果在这个内存段中申请失败,则再用内核中用最低层,最基本的kmalloc()来申请内存了(这才是真正的申请)。释放时也一样,并不会真正的释放,只是把数据清零,然后放回内存段中,以供下次sk_buff结构的申请。这是内核动态申请的一种策略,专门为那些经常要申请和释放的结构设计的,这种策略不仅可以提高申请和释放时的效率,而且还可以减少内存碎片的。
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结构。从高速缓存中分配,DMA有特定用途,所以排除在DMA中分配
if (!skb)
goto out;
prefetchw(skb);
size = SKB_DATA_ALIGN(size); // 数据对齐
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc); // [2] 分配线性数据缓存区:线性数据区+skb_shared_info结构。这里可以从DMA内存分配
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 分片结构初始化
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
,表明是最后一个引用该结构的,可以调用__kfree_skb()
函数直接释放。当skb释放掉后,dst_release同样会被调用以减小相关dst_entry数据结构的引用计数。如果 skb->destructor
(skb的析构函数)被初始化过,相应的函数会在此时被调用。还有分片结构体 skb_shared_info
也会相应的被释放掉,然后把所有内存空间全部返还到 skbuff_head_cache
缓存池中,这些操作都是由 kfree_skbmem()
函数来完成的。这里分片的释放涉及到了克隆问题:如果skb没有被克隆,数据区也没有其他skb引用,则直接释放即可;如果是克隆了skb结构,则当克隆数计数为1时,才能释放skb结构体;如果分片结构被克隆了,那么也要等到分片克隆计数为1时,才能释放掉分片数据结构。如果skb是从 skbuff_fclone_cache
缓存池中申请的内存时,则要仔细销毁过程了,因为从这个缓存池中申请的内存,会返还2个skb结构体和一个引用计数器。所以销毁时不仅要考虑克隆问题还要考虑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
【漏洞预警】雪藏11年:Linux kernel DCCP double-free 权限提升漏洞(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!
【技术分享】CVE-2016-8655内核竞争条件漏洞调试分析