影响版本: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_E1000
和CONFIG_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时出现空指针引用漏洞。
补丁:patch 在 sctp_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
:与某个关联状态相关的cookiestruct 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_ops、inet_dgram_ops、inet_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_fn
:state_fn
由 sctp_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-9213、CVE-2019-8956的分析以及组合提权
Linux 内核 SCTP 协议漏洞分析与复现 (CVE-2019-8956)
https://github.com/butterflyhack/CVE-2019-8956