cve-2019-8956 分析
补丁分析 补丁链接 https://git.kernel.org/linus/ba59fb0273076637f0add4311faa990a5eec27c0 Diffstat -rw-r--r-- net/sctp/socket.c 4 1 files changed, 2 insertions, 2 deletions diff --git a/net/sctp/socket.c
补丁分析
补丁链接 https://git.kernel.org/linus/ba59fb0273076637f0add4311faa990a5eec27c0
Diffstat
-rw-r--r-- net/sctp/socket.c 4
1 files changed, 2 insertions, 2 deletions
diff --git a/net/sctp/socket.c b/net/sctp/socket.c
index f93c3cf..65d6d04 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)
结合补丁可以看出来在sctp_sendmsg函数中,将宏list_for_each_entry替换为list_for_each_entry_safe,这两个宏均可以遍历给定的一个列表,针对这个宏的相关定义这篇文章写得很清楚,这里只简要写一下每个宏对应的功能
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
#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结构体
list_for_each_entry_safe
#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))
在内核源码的注释里也已经写了,list_for_each_entry_safe 不仅可以遍历给定类型的列表,还能防止删除对应的列表项,因为list_for_each_entry_safe每次都会提前获取next结构体指针,防止pos被删除以后,再通过pos获取可能会出发空指针解引用或其他问题。
补丁的原理应该就是这样。
sctp协议
报头
sctp包结构:由一个公共头,以及一个或几个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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Common Header |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Chunk #1 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Chunk #n |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
下面是我用wireshark抓的COOKIE_ECHO_DATA包相关信息

在公共头部除了包含源目的端口,校验和,还包含一个Verification Tag,用于确定一条sctp连接。
下面是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中一个很重要的概念,关联结构由sctp_assocition结构体表示
该结构体中,几个重要的成员
- assoc_id : 关联id(唯一)
- c : sctp_cookie 与某个关联状态相关的cookie
peer : 结构体表示关联的对等端点(远程端点)
- transport_addr_list:保存了建立关联以后的一个或多个地址
- primary_path:建立初始连接时使用的地址
- state:关联的状态
编写poc
需要完整的poc可以私信,其实很好构造,主要是在发送sctp消息的时候,将flags设置为SCTP_ABORT|SCTP_SENDALL即可。
sctp_sendmsg(server_fd,&recvbuf,sizeof(recvbuf),(struct sockaddr*)&client_addr,sizeof(client_addr),sri.sinfo_ppid,SCTP_ABORT|SCTP_SENDALL,sri.sinfo_stream,0,0
poc调试
编译并运行poc,内核崩溃了,但是崩溃信息并不像想象的那样,crash如下
[ 16.527019] general protection fault: 0000 [#1] SMP NOPTI
[ 16.527784] CPU: 1 PID: 1805 Comm: poc Not tainted 4.20.1 #5
[ 16.527784] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[ 16.527784] RIP: 0010:sctp_sendmsg_check_sflags+0x2/0xa0
[ 16.527784] Code: 6f 30 be 08 00 00 00 e8 1c fe f1 ff 48 8b 73 78 31 d2 48 89 ef 5b 5d 48 83 ee 78 e9 18 9c 00 00 5b 5d c3 0f 1f 44 00 00 55 53 <44> 8b 87 30 02 00 00 48 8b 47 20 45 85 c0 48 8b 68 30 75 09 83 b8
[ 16.527784] RSP: 0018:ffffc90000bbfc50 EFLAGS: 00010216
[ 16.527784] RAX: 0000000000000000 RBX: ffffc90000bbfdc0 RCX: 0000000000000014
[ 16.527784] RDX: ffffc90000bbfec0 RSI: 0000000000000044 RDI: dead000000000088
[ 16.527784] RBP: ffff888075098040 R08: ffff888074ad4e48 R09: ffff888074ad4e80
[ 16.527784] R10: 0000000000000000 R11: ffff888074ad4e48 R12: 0000000000000014
[ 16.527784] R13: dead000000000088 R14: ffffc90000bbfec0 R15: ffff888074c43db0
[ 16.527784] FS: 00007fee5a290700(0000) GS:ffff88807db00000(0000) knlGS:0000000000000000
[ 16.527784] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 16.527784] CR2: 00007fee5ab4a1b0 CR3: 0000000074dfa000 CR4: 00000000000006e0
[ 16.527784] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[ 16.527784] DR3: 0000000000000000 DR6: 00000000ffff4ff0 DR7: 0000000000000400
[ 16.527784] Call Trace:
[ 16.527784] sctp_sendmsg+0x51e/0x6f0
[ 16.527784] sock_sendmsg+0x31/0x40
[ 16.527784] ___sys_sendmsg+0x26a/0x2c0
[ 16.527784] ? __wake_up_common_lock+0x84/0xb0
[ 16.527784] ? n_tty_open+0x90/0x90
[ 16.527784] ? tty_write+0x1e7/0x310
[ 16.527784] ? __sys_sendmsg+0x59/0xa0
[ 16.527784] __sys_sendmsg+0x59/0xa0
[ 16.527784] do_syscall_64+0x43/0xf0
[ 16.527784] entry_SYSCALL_64_after_hwframe+0x44/0xa9
[ 16.527784] RIP: 0033:0x7fee5ae41eb0
==================================================================================
kasan:
[ 372.233643] BUG: KASAN: wild-memory-access in sctp_sendmsg_check_sflags+0x24/0x110
[ 372.233643] Read of size 8 at addr dead0000000000a8 by task poc/1813
在分析这个漏洞之前我看过网上的一篇分析文章,里面指出漏洞出发是因为将asoc置0了。如图:

如果是因为asoc被置零导致的空指针解引用,那么不应该会执行到函数sctp_sendmsg_check_sflags+0x2/0xa0,为什么这么说呢?下面是我截取的部分sctp_sendmsg的汇编。
0xffffffff81969fd7 <+23>: mov r15,QWORD PTR [rdi+0x3b8] // r15 == ep
=> 0xffffffff8196a13c <+380>: movzx eax,r13w
0xffffffff8196a140 <+384>: test r13b,0x40
0xffffffff8196a144 <+388>: mov DWORD PTR [rsp],eax
0xffffffff8196a147 <+391>: jne 0xffffffff8196a4ae <sctp_sendmsg+1262>
0xffffffff8196a4ae <+1262>: mov edx,DWORD PTR [rbp+0x398]
0xffffffff8196a4b4 <+1268>: test edx,edx
0xffffffff8196a4b6 <+1270>: jne 0xffffffff8196a14d <sctp_sendmsg+397>
0xffffffff8196a4bc <+1276>: mov rax,QWORD PTR [r15+0x78] // rax == *(ep->asocs)
0xffffffff8196a4c0 <+1280>: lea r13,[rax-0x78] // r13(&asoc) == rax-0x78
0xffffffff8196a4c4 <+1284>: cmp r15,r13 //asoc ?= ep(head)
0xffffffff8196a4c7 <+1287>: je 0xffffffff8196a65a <sctp_sendmsg+1690>
0xffffffff8196a4cd <+1293>: mov esi,DWORD PTR [rsp]
0xffffffff8196a4d0 <+1296>: mov rcx,r12
0xffffffff8196a4d3 <+1299>: mov rdx,r14
0xffffffff8196a4d6 <+1302>: mov rdi,r13
0xffffffff8196a4d9 <+1305>: call 0xffffffff81966fd0 <sctp_sendmsg_check_sflags>
0xffffffff8196a4de <+1310>: test eax,eax
0xffffffff8196a4e0 <+1312>: je 0xffffffff8196a528 <sctp_sendmsg+1384>
0xffffffff8196a4e2 <+1314>: js 0xffffffff8196a1b9 <sctp_sendmsg+505>
0xffffffff8196a528 <+1384>: mov r13,QWORD PTR [r13+0x78] // next = asoc.next
0xffffffff8196a52c <+1388>: sub r13,0x78 //asoc = next-0x78
0xffffffff8196a530 <+1392>: cmp r15,r13 //asoc ?= head
0xffffffff8196a533 <+1395>: jne 0xffffffff8196a4cd <sctp_sendmsg+1293>
0xffffffff8196a535 <+1397>: jmp 0xffffffff8196a1b9 <sctp_sendmsg+505>
上面三部分汇编的大体意思我也已经标注了,如果是因为asoc(r13)被置零,那么,地址0xffffffff8196a528处对应的r13应该为0,解引用PTR [r13+0x78]的时候势必会因为空指针解引用而出现crash,但是崩溃的时候rip并没有指向这里,而是再次进入sctp_sendmsg_check_sflags此时rdi寄存器的值是有问题的,这个值是一个非法内存,为什么会出现这样的情况?
list_for_each_entry(asoc, &ep->asocs, asocs) {
err = sctp_sendmsg_check_sflags(asoc, sflags, msg,
msg_len);
上面这段代码用for循环简写一下的话就是
for(asoc=head.asoc;asoc.asocs!=head;asoc=asoc.next){
sctp_sendmsg_check_sflags(asoc, sflags, msg,msg_len);
}
执行完一次循环以后,在执行下一次循环时,aosc被更新为asoc.next,执行sctp_sendmsg_check_sflags函数时,rdi寄存器的值是有问题的,也就是asoc有问题,因此可以考虑是不是第一次循环时asoc结构的list_head被修改了,在这个地方下一个内存断点调试。
因为问题出现在使用list_for_each_entry的时候,因此我在这个地方断下来,此时的上下文
$r13 : 0xffff8880622db410
$r14 : 0xffffc90000bbfec0 -> 0xffffc90000bbfdc0 -> 0x0100007fff930002 -> 0x0100007fff930002
$r15 : 0xffff88806c21f9d0 -> 0x0000000000000000 -> 0x0000000000000000
$eflags: [carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0010 $ss: 0x0018 $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
------------------------------------------------------------------------------------ code:x86:64 ----
0xffffffff8196a522 <sctp_sendmsg+1378> movabs eax, ds:0x6d8b4d0424448bff
0xffffffff8196a52b <sctp_sendmsg+1387> js 0xffffffff8196a576 <sctp_sendmsg+1462>
0xffffffff8196a52d <sctp_sendmsg+1389> sub ebp, 0x78
->0xffffffff8196a530 <sctp_sendmsg+1392> cmp r15, r13
0xffffffff8196a533 <sctp_sendmsg+1395> jne 0xffffffff8196a4cd <sctp_sendmsg+1293>
0xffffffff8196a535 <sctp_sendmsg+1397> jmp 0xffffffff8196a1b9 <sctp_sendmsg+505>
0xffffffff8196a53a <sctp_sendmsg+1402> test r13w, 0x204
0xffffffff8196a540 <sctp_sendmsg+1408> jne 0xffffffff8196a41d <sctp_sendmsg+1117>
0xffffffff8196a546 <sctp_sendmsg+1414> test r12, r12
------------------------------------------------------------------ source:net/sctp/socket.c+2056 ----
2051
2052 lock_sock(sk);
2053
2054 /* SCTP_SENDALL process */
2055 if ((sflags & SCTP_SENDALL) && sctp_style(sk, UDP)) {
->2056 list_for_each_entry(asoc, &ep->asocs, asocs) {
2057 err = sctp_sendmsg_check_sflags(asoc, sflags, msg,
根据分析,此时的r13跟r15分别对应asoc跟head。
在asoc偏移0x78的地方下一个内存访问端点,执行就可以了
gef> awatch *0xffff8880622db488
Hardware access (read/write) watchpoint 3: *0xffff8880622db488
然后,程序运行到了这个地方,有了一个赋值操作
-------------------------------------------------------------- source:./include/linux[...].h+127 ----
122
123 static inline void list_del(struct list_head *entry)
124 {
125 __list_del_entry(entry);
126 entry->next = LIST_POISON1;
-> 127 entry->prev = LIST_POISON2;
128 }
129
130 /**
131 * list_replace - replace old entry by new one
132 * @old : the element to be replaced
---------------------------------------------------------------------------------------- threads ----
[#0] Id 1, Name: "", stopped, reason: SIGTRAP
[#1] Id 2, Name: "", stopped, reason: SIGTRAP
------------------------------------------------------------------------------------------ trace ----
[#0] 0xffffffff81f6800e->list_del(entry=<optimized out>)
[#1] 0xffffffff81f6800e->sctp_association_free(asoc=0xffff8880622db410)
[#2] 0xffffffff81f5fc93->sctp_cmd_delete_tcb(cmds=<optimized out>, asoc=<optimized out>)
[#3] 0xffffffff81f5fc93->sctp_cmd_interpreter(state=<optimized out>, status=<optimized out>, gfp=<optimized out>, commands=<optimized out>, event_arg=<optimized out>, asoc=0xffff8880622db410, ep=<optimized out>, subtype=<optimized out>, event_type=<optimized out>)
[#4] 0xffffffff81f5fc93->sctp_side_effects(gfp=<optimized out>, commands=<optimized out>, status=<optimized out>, event_arg=<optimized out>, asoc=<optimized out>, ep=<optimized out>, state=<optimized out>, subtype=<optimized out>, event_type=<optimized out>)
[#5] 0xffffffff81f5fc93->sctp_do_sm(net=<optimized out>, event_type=<optimized out>, subtype={
chunk = SCTP_CID_INIT_ACK,
timeout = SCTP_EVENT_TIMEOUT_T1_INIT,
other = (unknown: 2),
primitive = SCTP_PRIMITIVE_ABORT
}, state=<optimized out>, ep=<optimized out>, asoc=0xffff8880622db410, event_arg=0xffff88806bf12980, gfp=0x6000c0)
结合汇编可以看出,此时的entry对应着asoc,而LIST_POISON1这个值可以通过翻源码找到,即0xdead000000000000+0x100
/*
* Architectures might want to move the poison pointer offset
* into some well-recognized area such as 0xdead000000000000,
* that is also not mappable by user-space exploits:
*/
#ifdef CONFIG_ILLEGAL_POINTER_VALUE
# define POISON_POINTER_DELTA _AC(CONFIG_ILLEGAL_POINTER_VALUE, UL)
#else
# define POISON_POINTER_DELTA 0
#endif
/*
* These are non-NULL pointers that will result in page faults
* under normal circumstances, used to verify that nobody uses
* non-initialized list entries.
*/
#define LIST_POISON1 ((void *) 0x100 + POISON_POINTER_DELTA)
#define LIST_POISON2 ((void *) 0x200 + POISON_POINTER_DELTA)
而且此时asoc结构的list_head已经被修改
gef> p (*(struct sctp_association*)0xffff8880622db410)->asocs
$1 = {
next = 0xdead000000000100,
prev = 0xffff88806c21fa48
}
然后执行到这个地方
0xffffffff8196a528 <+1384>: mov r13,QWORD PTR [r13+0x78] // next = asoc.next
0xffffffff8196a52c <+1388>: sub r13,0x78 //asoc = next-0x78
0xffffffff8196a530 <+1392>: cmp r15,r13 //asoc ?= head
0xffffffff8196a533 <+1395>: jne 0xffffffff8196a4cd <sctp_sendmsg+1293>
0xffffffff8196a535 <+1397>: jmp 0xffffffff8196a1b9 <sctp_sendmsg+505>
重新为asoc赋值。导致再次进入check_flags函数的时候,第一个参数地址无效导致crash。这样解释就可以跟crash时的上下文信息对应起来了。
还有一个问题,为什么*asoc = NULL并没有将asoc置空呢?
因为这个代码出现在sctp_side_effects函数中
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)
这个函数传入的是一个二级指针,并没有影响到原始值。