正文开始

关于CVE-2017-8890的原理分析已经有好几位前辈分析过了,本文也做一个简单的总结。首先会从补丁分析一下漏洞的成因,其次会从调试的角度验证之前分析的内容。

漏洞原理

通过该漏洞的patch,我们可以看到在 net/ipv4/inet_connection_sock.c中,增加了对于mc_list的置NULL操作。newsk是一个sock的结构体指针(struct sock *newsk)。 Alt text

通过查看sock结构体在kernel4.10.0中的源码,可知mc_list是ip_mc_socklist的结构体指针。所以我们可以确定ip_mc_socklist就是存在问题的结构体。 Alt text 为什么要对mc_list置NULL呢?这得看一下inet_connection_sock.c中inet_csk_clone_lock的功能。该函数的调用过程如下,引用云图信安文章中用understand生成的调用流程图: Alt text 这里对以上函数做简单分析(现在感觉用处不大),具体的过程可以看源码实现:

  • 源头是tcp_v4_rcv这个函数。tcp_v4_rcv中调用了 tcp_check_req。

  • tcp_check_req会检查包是否正确,会return一个新建立的socket。

  • tcp_check_req是调用 tcp_v4_syn_recv_sock来实现的,tcp_v4_syn_recv_sock里面会初始化和创建一个新的socket。

  • tcp_v4_syn_recv_sock 是调用tcp_create_openreq_child 来实现的。 tcp_create_openreq_child(sk, req, skb)根据监听sock和req,为新连接创建一个传输控制块,并初始化。

  • tcp_create_openreq_child是调用 inet_csk_clone_lock来实现的。该函数会创建一个socket,其实是根据父socket来创建child socket。 到这里已经定位到了inet_csk_clone_lock函数。看一下源码:
    Alt text

    这个函数在clone sk给newsk时,通过调用链:inet_csk_clone_lock -> sk_clone_lock(sk, priority)->sock_copy(newsk, sk);来完成对sk的拷贝。拷贝完成后没有对mc_list初始化,因此在newsk和sk中存在同一个指向ip_mc_socklist的结构体指针。

总结一下目前的漏洞原因:根据PATCH代码分析,在建立socket连接时,由父socket copy数据 给子 socket,mc_list这个ip_mc_socklist类型的结构体指针没有被置为NULL,因此子socket中存在对父socket中数据的引用。

POC

由于是double free的漏洞,写POC时,必然要对mc_list进行创建和二次释放。创建和释放的过程需要通过understand找关于mc_list的引用。细节还是参考云图信安的文章[1]吧。不重复赘述了。
创建过程:
Alt text 释放过程:
Alt text

POC伪代码:

sockfd = socket(AF_INET, xx, IPPROTO_TCP);
setsockopt(sockfd, SOL_IP, MCAST_JOIN_GROUP, xxxx, xxxx);
bind(sockfd, xxxx, xxxx);
listen(sockfd, xxxx)
newsockfd = accept(sockfd, xxxx, xxxx);
close(newsockfd);// first free (kfree_rcu)
sleep(5);// wait rcu free(real first free)
close(sockfd);// double free

创建一个socket并加入组播模式(MCAST_JOIN_GROUP),这时socket中存在mc_list结构体,监听该socket,并用accept等待连接。建立连接时,会创建新的子sokcet,其中依然存在父socket的mc_list结构体。关闭新的socket,这时等待RCU机制的的宽限期结束后,在rcu回调函数中触发第一次kfree。之后关闭父socket,同样的位置触发第二次free。(也就是两次free的位置都是相同的,之前被别的文章误导了)

最终POC:https://gist.github.com/thinkycx/0ff01ea54bcd5f6379aeea9483fa26e9

开始调试

环境:

  • [email protected] 4.10.0-19
  • 调试机(debugging)、被调试机(debuggee)
  • 参考:ubuntu 内核源码调试

动态调试

mc_list的创建过程

mc_list 结构定义如下,大小是0x30,其中的rcu_head的offset是0x20,func指针的offset是0x28:
Alt text

Alt text

mc_list 创建过程ip_mc_join_group() -> sock_kmalloc() -> kmalloc()

  • ip_mc_join_group
    Alt text
  • sock_kmalloc
    Alt text
  • sock_kmalloc 反汇编代码
    Alt text 在sock_kmalloc 下断点,查看rax的值也就是创建的mc_list结构体:
    gdb-peda$ x/6gx $rax
    0xffff880074a83780:	0xffff880074a83dc0	0xffff880074a83780
    0xffff880074a83790:	0xffff880037288958	0x0000000008000002
    0xffff880074a837a0:	0x0000000000000002	0x6c73797300000006
    

mc_list 的free过程

  • ip_mc_drop_socket

mc_list在这个函数里面释放。由于mc_list是一个单链表,通过next_rcu来索引下一个mc_list。因此在释放的时候,会循环遍历这个链表。此外,由于mc_list是ip_mc_socklist的结构体,引用了rcu机制(正常采用rcu机制保护的结构体中会有struct rcu_head rcu; 这个成员)因此对于该结构体写比较特殊(释放也可以理解为是写过程)。

简单来说可以理解为当有一个线程要对该成员写时,开始一个宽限期,等到所有读的线程结束后,宽限期结束,通过触发时钟中断检查是否存在回调函数,如果存在回调函数,则在回调函数中写。简单的rcu机制可以参考这篇文章:浅析linux kernel RCU机制
Alt text

  • ip_mc_leave_src (这个函数和crash有关,也贴一下)。
    Alt text

double free之:sk和newsk中共有的mc_list

根据一开始的漏洞分析,我们可以知道在inet_csk_clone_lock中sk_clone_lock完成了sock copy的过程。通过gdb调试我们可以看到sk和newsk中存在同一个指向mc_list的指针。首先我们需要知道mc_list在sk中的偏移。

这个偏移可以通过IDA查看free mc_list时的反汇编代码得到:0x310。
Alt text

找到偏移后,通过gdb调试inet_csk_clone_lock函数即可看到mc_list的创建过程,首先IDA看一下该函数地址。 这里可以考虑设置一下断点:

.text:FFFFFFFF817FB491                 call    near ptr sk_clone_lock-68B86h ; rdi sk
.text:FFFFFFFF817FB496                 test    rax, rax        ; newsk

Alt text

确定地址后,gdb查看此时newsk和sk 0x310偏移处,果然存在指向同一个mc_list的指针。(这里还不太严谨,因为patch代码是在sk_clone_lock之后置mc_list为NULL的,gdb中可以输入finish查看函数调用完了之后这个指针依然是存在的)
Alt text

定位crash点 ip_mc_leave_src

继续运行poc后,发生crash。

gdb-peda$ info registers 
rax            0x0	0x0
rbx            0x2	0x2
rcx            0x1	0x1
rdx            0x0	0x0
rsi            0xffff8800777bd2c0	0xffff8800777bd2c0
rdi            0x0	0x0
rbp            0xffffc90003ecfde0	0xffffc90003ecfde0
rsp            0xffffc90003ecfdc0	0xffffc90003ecfdc0
r8             0x0	0x0
r9             0x0	0x0
r10            0xffff880064c24db0	0xffff880064c24db0
r11            0xffff880074911110	0xffff880074911110
r12            0xffff8800777bd2c0	0xffff8800777bd2c0
r13            0xffff8800772a2138	0xffff8800772a2138
r14            0xffff8800772a2000	0xffff8800772a2000
r15            0x0	0x0
rip            0xffffffff8182f4a5	0xffffffff8182f4a5 <ip_mc_leave_src+37>
eflags         0x10202	[ IF RF ]
cs             0x10	0x10
ss             0x18	0x18
ds             0x0	0x0
es             0x0	0x0
fs             0x0	0x0
gs             0xb	0xb

gdb-peda$ x/50i ip_mc_leave_src
   0xffffffff8182f480 <ip_mc_leave_src>:	nop    DWORD PTR [rax+rax*1+0x0]
   0xffffffff8182f485 <ip_mc_leave_src+5>:	push   rbp
   0xffffffff8182f486 <ip_mc_leave_src+6>:	mov    rbp,rsp
   0xffffffff8182f489 <ip_mc_leave_src+9>:	push   r14
   0xffffffff8182f48b <ip_mc_leave_src+11>:	push   r13
   0xffffffff8182f48d <ip_mc_leave_src+13>:	push   r12
   0xffffffff8182f48f <ip_mc_leave_src+15>:	push   rbx
   0xffffffff8182f490 <ip_mc_leave_src+16>:	mov    r14,rdi
   0xffffffff8182f493 <ip_mc_leave_src+19>:	mov    rbx,QWORD PTR [rsi+0x18]
   0xffffffff8182f497 <ip_mc_leave_src+23>:	mov    r12,rsi
   0xffffffff8182f49a <ip_mc_leave_src+26>:	mov    rdi,rdx
   0xffffffff8182f49d <ip_mc_leave_src+29>:	test   rbx,rbx
   0xffffffff8182f4a0 <ip_mc_leave_src+32>:	je     0xffffffff8182f4ef <ip_mc_leave_src+111>
   0xffffffff8182f4a2 <ip_mc_leave_src+34>:	mov    edx,DWORD PTR [rsi+0x14]
=> 0xffffffff8182f4a5 <ip_mc_leave_src+37>:	mov    ecx,DWORD PTR [rbx+0x4]
   0xffffffff8182f4a8 <ip_mc_leave_src+40>:	lea    rsi,[rsi+0x8]
   0xffffffff8182f4ac <ip_mc_leave_src+44>:	lea    r8,[rbx+0x18]
   0xffffffff8182f4b0 <ip_mc_leave_src+48>:	xor    r9d,r9d
   0xffffffff8182f4b3 <ip_mc_leave_src+51>:	call   0xffffffff8182f110 <ip_mc_del_src>
   0xffffffff8182f4b8 <ip_mc_leave_src+56>:	mov    QWORD PTR [r12+0x18],0x0
   0xffffffff8182f4c1 <ip_mc_leave_src+65>:	mov    r13d,eax
   0xffffffff8182f4c4 <ip_mc_leave_src+68>:	mov    eax,DWORD PTR [rbx]
   0xffffffff8182f4c6 <ip_mc_leave_src+70>:	lea    eax,[rax*4+0x18]
   0xffffffff8182f4cd <ip_mc_leave_src+77>:	sub    DWORD PTR ds:[r14+0x138],eax
   0xffffffff8182f4d5 <ip_mc_leave_src+85>:	lea    rdi,[rbx+0x8]
   0xffffffff8182f4d9 <ip_mc_leave_src+89>:	mov    esi,0x8
   0xffffffff8182f4de <ip_mc_leave_src+94>:	call   0xffffffff810f4a40 <kfree_call_rcu>
   0xffffffff8182f4e3 <ip_mc_leave_src+99>:	pop    rbx
  • 调用栈 ip_mc_drop_socket - > ip_mc_leave_src
    gdb-peda$ bt
    #0  0xffffffff8182f4a5 in ip_mc_leave_src (sk=0xffff880077be8800, iml=0xffff88003d222540, in_dev=0xffff8800)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/net/ipv4/igmp.c:2155
    #1  0xffffffff81832f18 in ip_mc_drop_socket (sk=0xffff880077be8800)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/net/ipv4/igmp.c:2607
    #2  0xffffffff8182c2c0 in inet_release (sock=0xffff880079749180)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/net/ipv4/af_inet.c:411
    #3  0xffffffff8178b7bf in sock_release (sock=0x0 <irq_stack_union>)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/net/socket.c:599
    #4  0xffffffff8178b832 in sock_close (inode=<optimized out>, filp=<optimized out>)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/net/socket.c:1063
    #5  0xffffffff81246937 in __fput (file=0xffff88003d171000)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/fs/file_table.c:209
    #6  0xffffffff81246ade in ____fput (work=<optimized out>)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/fs/file_table.c:245
    #7  0xffffffff810a706e in task_work_run () at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/kernel/task_work.c:116
    #8  0xffffffff810032ba in tracehook_notify_resume (regs=<optimized out>)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/include/linux/tracehook.h:191
    #9  exit_to_usermode_loop (regs=0xffffc900030c7f58, cached_flags=0x2)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/arch/x86/entry/common.c:160
    #10 0xffffffff81003b29 in prepare_exit_to_usermode (regs=<optimized out>)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/arch/x86/entry/common.c:190
    #11 syscall_return_slowpath (regs=0xffffc900030c7f58)
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/arch/x86/entry/common.c:259
    #12 0xffffffff818ce948 in entry_SYSCALL_64 ()
      at /build/linux-hwe-edge-gyUj63/linux-hwe-edge-4.10.0/arch/x86/entry/entry_64.S:239
    #13 0x0000000000000000 in ?? ()
    

    最终根据oops信息发现kernel在ip_mc_leave_src中出现了空指针引用的错误(位于第二次free的调用链中)。 在第一次mc_list释放后,这块dirty内存的数据就可能会其它线程使用了。根据源码和反汇编代码分析可知,指针psf是iml->sflist处的值,即[mc_list+0x18],地址存在rbx中,rbx的值为0x2。从mc_list中取出psf后,ip_mc_leave_src中引用了psf->sl_count导致了空指针引用。 mov ecx,DWORD PTR [rbx+0x4] //arg4 psf->sl_count

  • 源码
    Alt text
  • 反汇编代码:
    gdb-peda$ x/20i ip_mc_leave_src 
     0xffffffff8182f480 <ip_mc_leave_src>:	nop    DWORD PTR [rax+rax*1+0x0]
     0xffffffff8182f485 <ip_mc_leave_src+5>:	push   rbp
     0xffffffff8182f486 <ip_mc_leave_src+6>:	mov    rbp,rsp
     0xffffffff8182f489 <ip_mc_leave_src+9>:	push   r14
     0xffffffff8182f48b <ip_mc_leave_src+11>:	push   r13
     0xffffffff8182f48d <ip_mc_leave_src+13>:	push   r12
     0xffffffff8182f48f <ip_mc_leave_src+15>:	push   rbx
     0xffffffff8182f490 <ip_mc_leave_src+16>:	mov    r14,rdi
     0xffffffff8182f493 <ip_mc_leave_src+19>:	mov    rbx,QWORD PTR [rsi+0x18] //psf
     0xffffffff8182f497 <ip_mc_leave_src+23>:	mov    r12,rsi
     0xffffffff8182f49a <ip_mc_leave_src+26>:	mov    rdi,rdx //arg1 in_dev
     0xffffffff8182f49d <ip_mc_leave_src+29>:	test   rbx,rbx //psf
       
     0xffffffff8182f4a0 <ip_mc_leave_src+32>:	je     0xffffffff8182f4ef <ip_mc_leave_src+111>
     0xffffffff8182f4a2 <ip_mc_leave_src+34>:	mov    edx,DWORD PTR [rsi+0x14] //arg3 iml->sfmode
    => 0xffffffff8182f4a5 <ip_mc_leave_src+37>:	mov    ecx,DWORD PTR [rbx+0x4] //arg4 psf->sl_count
     0xffffffff8182f4a8 <ip_mc_leave_src+40>:	lea    rsi,[rsi+0x8] //arg2 &iml->multi.imr_multiaddr.s_addr
     0xffffffff8182f4ac <ip_mc_leave_src+44>:	lea    r8,[rbx+0x18] //arg5 psf->sl_addr
     0xffffffff8182f4b0 <ip_mc_leave_src+48>:	xor    r9d,r9d  // arg6 0
     0xffffffff8182f4b3 <ip_mc_leave_src+51>:	call   0xffffffff8182f110 <ip_mc_del_src>
    

    如果psf即[mc_list+0x18]为0,那么就不会引用psf->sl_count,内核就会进入ip_mc_drop_socket的kfree_rcu()流程,触发kfree的double free。

REF

  1. CVE-2017-8890漏洞分析与利用(Root Android 7.x) http://www.freebuf.com/articles/terminal/160041.html
  2. “Phoenix Talon”in Linux Kernel —潜伏长达11年之久的内核漏洞
    • http://www.cnnetsec.com/2619.html
    • https://mp.weixin.qq.com/s/6NGH-Dk2n_BkdlJ2jSMWJQ
    • https://www.anquanke.com/post/id/86275
  3. 【威胁通告】Linux 多个内核拒绝服务漏洞 http://blog.nsfocus.net/linux-multiple-kernel-denial-service-vulnerabilities/