【kernel exploit】CVE-2019-8956 sctp_sendmsg()空指针引用漏洞

影响版本:Linux-4.20.8以前(v4.20.8已修补) 4.19.21以前 7.8分。

测试版本:Linux-4.20.7 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项CONFIG_IP_SCTP=y CONFIG_SLAB=y

General setup —> Choose SLAB allocator (SLUB (Unqueued Allocator)) —> SLAB

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.20.7.tar.xz
$ tar -xvf linux-4.20.7.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述net/sctp/socket.c中的 sctp_sendmsg() 函数在处理SCTP_SENDALL flag时出现空指针引用漏洞。

补丁patchsctp_sendmsg()函数中,将宏 list_for_each_entry 替换为 list_for_each_entry_safe,这两个宏都可以遍历一个给定的列表(本例中是遍历 ep->asocs 链表中的 asoc 节点)。

diff --git a/net/sctp/socket.c b/net/sctp/socket.c
index f93c3cf9e5674..65d6d04546aee 100644
--- a/net/sctp/socket.c
+++ b/net/sctp/socket.c
@@ -2027,7 +2027,7 @@ static int sctp_sendmsg(struct sock *sk, struct msghdr *msg, size_t msg_len)
 	struct sctp_endpoint *ep = sctp_sk(sk)->ep;
 	struct sctp_transport *transport = NULL;
 	struct sctp_sndrcvinfo _sinfo, *sinfo;
-	struct sctp_association *asoc;
+	struct sctp_association *asoc, *tmp;
 	struct sctp_cmsgs cmsgs;
 	union sctp_addr *daddr;
 	bool new = false;
@@ -2053,7 +2053,7 @@ static int sctp_sendmsg(struct sock *sk, struct msghdr *msg, size_t msg_len)
 
 	/* SCTP_SENDALL process */
 	if ((sflags & SCTP_SENDALL) && sctp_style(sk, UDP)) {
-		list_for_each_entry(asoc, &ep->asocs, asocs) {
+		list_for_each_entry_safe(asoc, tmp, &ep->asocs, asocs) {
 			err = sctp_sendmsg_check_sflags(asoc, sflags, msg,
 							msg_len);
 			if (err == 0)

针对这个宏的定义可以参考 list_for_each_entry宏函数解析(上) 这里简要介绍每个宏的功能。

list_first_entry(ptr, type, member) // 获取list的第一个元素,调用list_entry(ptr->next, type, member)
list_entry(ptr, type, member) 		// 实际调用container_of(ptr, type, member)
container_of(ptr, type, member)  	// 根据member的偏移,求type类型结构体的首地址ptr
// 区别:list_for_each_entry_safe 不仅可以遍历给定类型的列表,还能防止删除对应的列表项,因为list_for_each_entry_safe每次都会提前获取next结构体指针(用n存放pos指向的节点的下一个节点位置,保证在遍历链表的过程中,如果出现非法地址,不会直接赋值到pos上),防止pos被删除以后,再通过pos获取可能会触发空指针解引用或其他问题。
#define list_for_each_entry(pos, head, member)              \
    for (pos = list_first_entry(head, typeof(*pos), member);    \ //获取链表第一个结构体元素
         &pos->member != (head);                    \ //当前结构体是不是最后一个
         pos = list_next_entry(pos, member))       //获取下一个pos结构体

#define list_for_each_entry_safe(pos, n, head, member)          \
    for (pos = list_first_entry(head, typeof(*pos), member),    \
        n = list_next_entry(pos, member);           \
         &pos->member != (head);                    \
         pos = n, n = list_next_entry(n, member))

保护机制:开启SMEP,关闭SMAP/kaslr。

利用总结:结合CVE-2019-9213,绕过mmap_min_addr的限制,可以mmap到低地址0xd4并伪造结构,劫持控制流。

一、漏洞分析

1.1 sctp协议

简介:流控制传输协议(Stream Control Transmission Protocol,SCTP)是一种可靠的传输协议,它在两个端点之间提供稳定、有序的数据传递服务(非常类似于 TCP),并且可以保护数据消息边界(例如 UDP)。与 TCP 和 UDP 不同,SCTP 是通过多宿主(Multi-homing)和多流(Multi-streaming)功能提供这些收益的,这两种功能均可提高可用性。

多宿主(Multi-homing)为应用程序提供了比 TCP 更高的可用性。多宿主主机就是一台具有多个网络接口的主机,因此可以通过多个 IP 地址来访问这台主机。在 TCP 中,连接(connection) 是指两个端点之间的一个通道(在这种情况下,就是两台主机的网络接口之间的一个套接字)。SCTP 引入了“联合(association)”的概念,它也是存在于两台主机之间,但可以使用每台主机上的多个接口进行协作。

// sctp包结构:由一个公共头,以及一个或几个chunk组成。公共头部包含 源端口、目的端口、Verification Tag、Checksum校验和,用于确定一条sctp连接。
		0                   1                   2                   3
        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                        Common Header                          |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                          Chunk #1                             |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                           ...                                 |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                          Chunk #n                             |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// chunk 结构
0                   1                   2                   3   
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Chunk Type  | Chunk  Flags  |        Chunk Length           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/                          Chunk Value                          /
\                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   

关联:关联结构由 sctp_assocition 结构体表示,是sctp协议通信中存储相关信息的基础结构体,如sendmsg过程中的地址、端口等信息。其中包含几个重要成员:

  • sctp_assoc_t assoc_id : 关联id(唯一)
  • struct sctp_cookie c:与某个关联状态相关的cookie
  • struct peer:该结构体表示关联的远程端
    • struct list_head transport_addr_list:保存了建立关联以后的一个或多个地址。
    • struct sctp_transport *primary_path:建立初始连接时使用的地址。
  • enum sctp_state state:关联的状态
1.2 漏洞调用链

漏洞调用链socketcall -> __sys_sendmsg() -> ___sys_sendmsg() -> sock_sendmsg() -> inet_sendmsg()inet_stream_opsinet_dgram_opsinet_sockraw_ops都将sendmsg设置为inet_sendmsg()) -> sctp_sendmsg() -> sctp_sendmsg_check_sflags() -> sctp_make_abort_user() sctp_primitive_ABORT() -> sctp_do_sm() -> sctp_side_effects() -> sctp_cmd_interpreter() -> sctp_cmd_delete_tcb() -> sctp_association_free() -> list_del()

漏洞总结:保证 sflags & SCTP_SENDALL (0x40)和 sflags & SCTP_ABORT (0x4)才能触发进入 sctp_sendmsg_check_sflags() -> sctp_make_abort_user() 触发崩溃

// (1) sctp_sendmsg() —— 发送 SCTP 数据包
static int sctp_sendmsg(struct sock *sk, struct msghdr *msg, size_t msg_len)
{
	struct sctp_endpoint *ep = sctp_sk(sk)->ep;		// sock -> sctp_sock
	struct sctp_transport *transport = NULL;
	struct sctp_sndrcvinfo _sinfo, *sinfo;
	struct sctp_association *asoc;
	struct sctp_cmsgs cmsgs;
	union sctp_addr *daddr;
	bool new = false;
	__u16 sflags;
	int err;

	/* Parse and get snd_info */
	err = sctp_sendmsg_parse(sk, &cmsgs, &_sinfo, msg, msg_len);	// [1] 从 msg 中解析出 sinfo
	if (err)
		goto out;

	sinfo  = &_sinfo;
	sflags = sinfo->sinfo_flags;									// [2] 获取 sflags

	/* Get daddr from msg */
	daddr = sctp_sendmsg_get_daddr(sk, msg, &cmsgs);
	if (IS_ERR(daddr)) {
		err = PTR_ERR(daddr);
		goto out;
	}

	lock_sock(sk);

	/* SCTP_SENDALL process */
	if ((sflags & SCTP_SENDALL) && sctp_style(sk, UDP)) {	// [3] 当标志为 SCTP_SENDALL 且 sk->type 为UDP时,就会调用 list_for_each_entry 来依次遍历 ep->asocs 链表(asocs 就是存放多个 association 连接的链表)。SCTP_SENDALL 标志代表向 asocs 链表中所有的 association 连接发送数据包,所以 asocs 链表中至少要存在一个 association 节点。
		list_for_each_entry(asoc, &ep->asocs, asocs) {
			err = sctp_sendmsg_check_sflags(asoc, sflags, msg,		// <-------------
							msg_len);
			if (err == 0)
				continue;
			if (err < 0)
				goto out_unlock;

			sctp_sendmsg_update_sinfo(asoc, sinfo, &cmsgs);

			err = sctp_sendmsg_to_asoc(asoc, msg, msg_len,
						   NULL, sinfo);
			if (err < 0)
				goto out_unlock;

			iov_iter_revert(&msg->msg_iter, err);
		}

		goto out_unlock;
	}
	... ...

out_unlock:
	release_sock(sk);
out:
	return sctp_error(sk, msg->msg_flags, err);
}

// (2) sctp_sendmsg_check_sflags()
static int sctp_sendmsg_check_sflags(struct sctp_association *asoc,
				     __u16 sflags, struct msghdr *msg,
				     size_t msg_len)
{
	struct sock *sk = asoc->base.sk;			// [1] 由于 asoc 可控,所以 struct sock sk 就可以控制。
	struct net *net = sock_net(sk);

	if (sctp_state(asoc, CLOSED) && sctp_style(sk, TCP))	// [2-1] 检查 asoc 是否处于 CLOSED 状态
		return -EPIPE;

	if ((sflags & SCTP_SENDALL) && sctp_style(sk, UDP) &&	// [2-2] 检查 asoc 是否处于监听状态
	    !sctp_state(asoc, ESTABLISHED))
		return 0;

	if (sflags & SCTP_EOF) {								// [2-3] 检查 asoc 是否 shutdown
		pr_debug("%s: shutting down association:%p\n", __func__, asoc);
		sctp_primitive_SHUTDOWN(net, asoc, NULL);

		return 0;
	}

	if (sflags & SCTP_ABORT) {					// [3] 检查 sflags 是否为 SCTP_ABORT。SCTP_ABORT 标志代表中止一个 association 连接,这是导致漏洞的关键。可参考rfc文档了解 ABORT 的用法以及 ABORT 指令的数据包格式。
		struct sctp_chunk *chunk;

		chunk = sctp_make_abort_user(asoc, msg, msg_len);	// <------------ 设置 SCTP_ABORT标志,构造 ABORT 指令的 chunk
		if (!chunk)
			return -ENOMEM;

		pr_debug("%s: aborting association:%p\n", __func__, asoc);
		sctp_primitive_ABORT(net, asoc, chunk);				// <------------ 发送中止一个 association 的 chunk

		return 0;
	}

	return 1;
}

// (3-1) sctp_make_abort_user()
struct sctp_chunk *sctp_make_abort_user(const struct sctp_association *asoc,
					struct msghdr *msg,
					size_t paylen)
{
	struct sctp_chunk *retval;
	void *payload = NULL;
	int err;

	retval = sctp_make_abort(asoc, NULL,
				 sizeof(struct sctp_errhdr) + paylen);
	if (!retval)
		goto err_chunk;

	if (paylen) {								// <-------- 我们将 paylen 设置为0,这样就不会进入循环,这样就避开了 memcpy_from_msg()。避免崩溃(paylen就是通过sendmsg发送的数据的长度)
		/* Put the msg_iov together into payload.  */
		payload = kmalloc(paylen, GFP_KERNEL);
		if (!payload)
			goto err_payload;

		err = memcpy_from_msg(payload, msg, paylen);
		if (err < 0)
			goto err_copy;
	}

	sctp_init_cause(retval, SCTP_ERROR_USER_ABORT, paylen);
	sctp_addto_chunk(retval, paylen, payload);

	if (paylen)
		kfree(payload);

	return retval;

err_copy:
	kfree(payload);
err_payload:
	sctp_chunk_free(retval);
	retval = NULL;
err_chunk:
	return retval;
}

// (3-2) sctp_primitive_ABORT() —— 实际调试才能找到位置。
int sctp_primitive_ ## name(struct net *net, struct sctp_association *asoc, \
			    void *arg) { \
	int error = 0; \
	enum sctp_event event_type; union sctp_subtype subtype; \
	enum sctp_state state; \
	struct sctp_endpoint *ep; \
	\
	event_type = SCTP_EVENT_T_PRIMITIVE; \			// 后面会用到 event_type
	subtype = SCTP_ST_PRIMITIVE(SCTP_PRIMITIVE_ ## name); \				// state / ep 都是asoc的成员变量,都可控
	state = asoc ? asoc->state : SCTP_STATE_CLOSED; \
	ep = asoc ? asoc->ep : NULL; \
	\
	error = sctp_do_sm(net, event_type, subtype, state, ep, asoc,	\	// <------------
			   arg, GFP_KERNEL); \
	return error; \
}

// (4) sctp_do_sm() —— 其参数 net、state、ep、asoc 都可控。
int sctp_do_sm(struct net *net, enum sctp_event event_type,
	       union sctp_subtype subtype, enum sctp_state state,
	       struct sctp_endpoint *ep, struct sctp_association *asoc,
	       void *event_arg, gfp_t gfp)
{
	... ...
    const struct sctp_sm_table_entry *state_fn;
	state_fn = sctp_sm_lookup_event(net, event_type, state, subtype);	// <------------

	sctp_init_cmd_seq(&commands);

	debug_pre_sfn();
	status = state_fn->fn(net, ep, asoc, subtype, event_arg, &commands);	// !!!!! [1] 发生指针调用,如果可以控制 state_fn,就可能实现任意地址调用。 通过调试发现,实际会调用 sctp_sf_do_9_1_prm_abort() 进行 ABORT操作。	主要操作是,添加一条删除asoc的commands,然后返回 SCTP_DISPOSITION_ABORT。
	debug_post_sfn();

	error = sctp_side_effects(event_type, subtype, state,					// [2] <-----------
				  ep, &asoc, event_arg, status,
				  &commands, gfp);
	debug_post_sfx();

	return error;
}
// (5) sctp_side_effects() —— 根据 status 对应的状态进行操作
static int sctp_side_effects(enum sctp_event event_type,
			     union sctp_subtype subtype,
			     enum sctp_state state,
			     struct sctp_endpoint *ep,
			     struct sctp_association **asoc,
			     void *event_arg,
			     enum sctp_disposition status,
			     struct sctp_cmd_seq *commands,
			     gfp_t gfp)
{
	int error;
	if (0 != (error = sctp_cmd_interpreter(event_type, subtype, state,
					       ep, *asoc,
					       event_arg, status,
					       commands, gfp)))
		goto bail;
    switch (status) {
    ... ...
	case SCTP_DISPOSITION_ABORT:
		/* This should now be a command. */
		*asoc = NULL;		// 启明星辰 认为是这里将asoc置空,导致最后返回到 sctp_sendmsg() 函数中,遍历后面的节点时发生零地址引用导致漏洞发生。 这个分析有问题
		break;
      
1.3 控制state_fn

控制 state_fnstate_fnsctp_sm_lookup_event() 函数返回。 sctp_do_sm() -> sctp_sm_lookup_event() -> DO_LOOKUP()

// (4-1) sctp_sm_lookup_event()
const struct sctp_sm_table_entry *sctp_sm_lookup_event(
					struct net *net,
					enum sctp_event event_type,
					enum sctp_state state,
					union sctp_subtype event_subtype)
{
	switch (event_type) {	// sctp_primitive_ABORT() 中设置了 event_type=SCTP_EVENT_T_PRIMITIVE。所以会调用 DO_LOOKUP()函数。
	case SCTP_EVENT_T_CHUNK:
		return sctp_chunk_event_lookup(net, event_subtype.chunk, state);
	case SCTP_EVENT_T_TIMEOUT:
		return DO_LOOKUP(SCTP_EVENT_TIMEOUT_MAX, timeout,
				 timeout_event_table);
	case SCTP_EVENT_T_OTHER:
		return DO_LOOKUP(SCTP_EVENT_OTHER_MAX, other,
				 other_event_table);
	case SCTP_EVENT_T_PRIMITIVE:
		return DO_LOOKUP(SCTP_EVENT_PRIMITIVE_MAX, primitive,		// <-------------
				 primitive_event_table);
	default:
		/* Yikes!  We got an illegal event type.  */
		return &bug;
	}
}
// (4-2) DO_LOOKUP() —— state_fn 就是 rtn变量,state 可控,可在此下断点查看 &_table[event_subtype._type] 的值,再根据偏移找一个索引(state控制),使得最后rtn指向用户空间,这样就能mmap下来执行shellcode。
#define DO_LOOKUP(_max, _type, _table)					\
({									\
	const struct sctp_sm_table_entry *rtn;				\
									\
	if ((event_subtype._type > (_max))) {				\
		pr_warn("table %p possible attack: event %d exceeds max %d\n", \
			_table, event_subtype._type, _max);		\
		rtn = &bug;						\
	} else								\
		rtn = &_table[event_subtype._type][(int)state];		\
									\
	rtn;								\
})

经过调试,可发现ecx是state,所以我们可以控制:

在这里插入图片描述

1.4 控制asoc

控制asoc:分析如何控制asoc,调用链——sctp_do_sm() -> sctp_side_effects() -> sctp_cmd_interpreter() -> sctp_cmd_delete_tcb() -> sctp_association_free() -> list_del()

// (8) sctp_association_free()
void sctp_association_free(struct sctp_association *asoc)
{
	struct sock *sk = asoc->base.sk;
	struct sctp_transport *transport;
	struct list_head *pos, *temp;
	int i;

	/* Only real associations count against the endpoint, so
	 * don't bother for if this is a temporary association.
	 */
	if (!list_empty(&asoc->asocs)) {
		list_del(&asoc->asocs);							// <---------- 对asoc进行了list_del操作

		/* Decrement the backlog value for a TCP-style listening
		 * socket.
		 */
		if (sctp_style(sk, TCP) && sctp_sstate(sk, LISTENING))
			sk->sk_ack_backlog--;
	}
	... ...
}
// (9) list_del()
static inline void list_del(struct list_head *entry)
{
	__list_del_entry(entry);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}
// LIST_POISON1 是 0x100, 见以下汇编代码, 遍历到下一个节点的时候会计算asoc, 0x100-0x44=0xbc
mov     eax,[edi+44h]
sub     eax,44h
mov     edi,eax
cmp     [ebp-84h],eax
jz      ...

利用思路:CVE-2019-9213可以映射0地址空间,那么我们就可以在0xbc处伪造结构,从而实现控制asoc,而上面分析的fn可控,可以劫持任意地址,这样就可以进行提权了。

1.4 调试偏移

在这里插入图片描述

mov		eax, dword ptr [eax + 0x2a0]
test 	eax, eax
jne		sctp_sendmsg_check_sflags+64 <0xc

xor 	eax, eax
cmp		edi, 3
je 		sctp_sendmsg_check_sflags+64 <0xc
    
// 填充
struct sock{
    char padding1[0x24];
    void *net;
    char padding2[0x278];
    int type;
};

struct sctp_association{
    char padding1[0x18];
    struct sock *sk;
    char padding2[0x190];
    int state;
};
// asoc的flags设置: 设置 sinfo_flags = (1 << 6) | (1 << 2);
enum sctp_sinfo_flags {
    SCTP_UNORDERED        = (1 << 0), /* Send/receive message unordered. */
    SCTP_ADDR_OVER        = (1 << 1), /* Override the primary destination. */
    SCTP_ABORT        = (1 << 2), /* Send an ABORT message to the peer. */
    SCTP_SACK_IMMEDIATELY    = (1 << 3), /* SACK should be sent without delay. */
    /* 2 bits here have been used by SCTP_PR_SCTP_MASK */
    SCTP_SENDALL        = (1 << 6),
    SCTP_PR_SCTP_ALL    = (1 << 7),
    SCTP_NOTIFICATION    = MSG_NOTIFICATION, /* Next message is not user msg but notification. */
    SCTP_EOF        = MSG_FIN,  /* Initiate graceful shutdown process. */
};

二、漏洞利用

修改偏移:需根据环境来修改 sctp_ptr->state

// offset, &_table[event_subtype._type][(int)state] = 0x3000
(0x3000 + 0x3e581ae0)/8 -2*8 = 0x7cb094c

利用思路:崩溃点是 sctp_sendmsg_check_sflags() -> sctp_make_abort_user() 。通过crash报错可知(报错是BUG: unable to handle kernel NULL pointer dereference at 000000d4),asoc指针遍历到一个非法地址0xd4。所以可以结合CVE-2019-9213 0虚拟地址映射漏洞,利用mmap映射到0xd4地址,伪造指针。接下来查看该结构体中哪些成员可控,实现任意地址读写。通过控制参数,执行 sctp_sendmsg_check_sflags() -> sctp_primitive_ABORT() -> sctp_do_sm() 中的state_fn->fn(),而 state_fn 变量由 sctp_do_sm() -> sctp_sm_lookup_event() -> DO_LOOKUP() 决定。


参考

Linux内核漏洞利用:CVE-2019-8956与CVE-2019-9213 —— 解题的思路过程值得学习

cve-2019-8956 分析

CVE-2019-9213、CVE-2019-8956的分析以及组合提权

Linux 内核 SCTP 协议漏洞分析与复现 (CVE-2019-8956)

https://github.com/butterflyhack/CVE-2019-8956

https://nvd.nist.gov/vuln/detail/CVE-2019-8956

Linux Kernel Pwn 初探

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值