【kernel exploit】CVE-2017-6074 DCCP拥塞控制协议Double-Free提权分析

2021/09/17 Kernel-exploit 共 18003 字,约 52 分钟

【kernel exploit】CVE-2017-6074 DCCP拥塞控制协议Double-Free提权分析

文章首发于安全客: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_E1000CONFIG_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,以避免跳到discardkfree_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

提权成功

succeed


3. sk_buff 扩展学习

目的:了解sk_buff结构和skb_shared_info结构的空间排布关系。

3-1. sk_buff 结构

sk_buff结构体:sk_buff结构体关联多个其他结构体,第一是线性数据区,由sk_buff->headsk_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->endskb->head 所指向的地址都是不变的。这块数据区是用来存放应用层发下来的数据和各层的协议信息。但在计算数据长度或者操作协议信息时,一般都要和实际的数据存放指针为准。实际数据指针为data和tail,data指向实际数据开始的地方,tail指向实际数据结束的地方。

sk_buff结构体中的指针和数据区关系:

1

包构造与数据区变化

  • (1)sk_buff结构数据区刚被申请好,此时head指针、data指针、tail指针都是指向同一个地方。记住前面讲过的:head指针和end指针指向的位置一直都不变,而对于数据的变化和协议信息的添加都是通过data指针和tail指针的改变来表现的。
  • (2)开始准备存储应用层下发过来的数据,通过调用函数 skb_reserve() 来使data指针和tail指针同时向下移动,空出一部分空间来为后期添加协议信息。
  • (3)开始存储数据了,通过调用函数 skb_put() 来使tail指针向下移动空出空间来添加数据,此时 skb->dataskb->tail 之间存放的都是数据信息,无协议信息。
  • (4)这时就开始调用函数 skb_push() 来使data指针向上移动,空出空间来添加各层协议信息。直到最后到达二层,添加完帧头然后就开始发包了。

2

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];	// 这是个比较重要的数组,到讲分片结构数据区时会细讲
};

3

分片结构的非线性数据区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。分别如下图所示:

4

5

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_cacheskbuff_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

利用漏洞CVE-2017-6074获取root权限

【漏洞预警】雪藏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内核竞争条件漏洞调试分析

What I Learnt From the CVE-2016-8655 Exploit

sk_buff 整理笔记(一、数据结构)

sk_buff整理笔记(二、操作函数)

sk_buff整理笔记(三、内存申请和释放)

sk_buff整理笔记(四、克隆与复制)

sk_buff整理笔记(五、队列管理函数)

文档信息

Search

    Table of Contents