补丁链接 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包结构:由一个公共头,以及一个或几个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
结构体表示
该结构体中,几个重要的成员
peer : 结构体表示关联的对等端点(远程端点)
需要完整的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,内核崩溃了,但是崩溃信息并不像想象的那样,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)
这个函数传入的是一个二级指针,并没有影响到原始值。