cve-2019-8956 分析

2019-12-02 约 1223 字 预计阅读 6 分钟

声明:本文 【cve-2019-8956 分析】 由作者 Kaka 于 2019-12-02 09:28:33 首发 先知社区 曾经 浏览数 806 次

感谢 Kaka 的辛苦付出!

补丁分析

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

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

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now