补丁分析

补丁链接 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)

这个函数传入的是一个二级指针,并没有影响到原始值。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖