Blog

Accidental Intrusion, CVE-2022-1215

by Albin Eldstål-Ahrens 2022-04-27

Introduction

Our dear colleague Benjamin is a funny character. He's the kind of person who enjoys making computer systems fall over in new and interesting ways. I guess we all are, as long as it's someone else's computer.

One monday morning, he declared to the office: "This is the third time my laptop has died during this file transfer". Investigation followed, and much amusement about Benjamin's exotic tools and the winding paths they take through the network to shovel all that data around. No obvious culprit.

The fourth time the screen went blank, a pattern was emerging. You see, Benjamin is a funny character. He's the kind of person who enjoys making computer systems fail without having to touch them. So, about a week earlier, he had the bright idea to rename his bluetooth headset %s%s%s%s%s%s%s%s in honor of the classic CWE-134, "Use of Externally-Controlled Format String". Wouldn't that be funny, if your headset was crashing people's phones on the bus? Ha.

Triage

As it happens, the crashing system was running Ubuntu, and Ubuntu runs the apport daemon. apport detects crashing applications and collects debugging and diagnostic information. Very handy.

The reason these headphones "killed" the machine is that the crashing process was the Xorg windowing server - the back-end to the entire graphical user interface. That seems pretty serious, and somewhat unexpected.

I was the one who laughed the loudest about the headphone joke coming back around, so I was given the honor of investigating the crash. Benjamin handed over a core dump, the logs from Xorg and some other bits of info collected by apport. To be able to debug this, I set up the same version of Ubuntu in a virtual machine (to give me access to the exact same binaries and libraries) and went to work.

The core dump of a crashed process contains, among other things, a stack trace. That reveals information about which function crashed and where it was called from. To investigate the contents of a core dump, load it and the offending binary in the GNU Debugger gdb:

$ gdb /usr/lib/xorg/Xorg ./coredump

warning: Could not load shared library symbols for 10 libraries, e.g. /usr/local/lib/AppProtection/libAppProtection.so.
Use the "info sharedlibrary" command to see the complete listing.
Program terminated with signal SIGABRT, Aborted.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50      ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
[Current thread is 1 (Thread 0x7fc09775c500 (LWP 283870))]
(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007fc097d51859 in __GI_abort () at abort.c:79
#2  0x0000558f266eb810 in System ()
#3  0x0000558f266f0c49 in  ()
#4  0x0000558f266f1aaa in  ()
#5  0x0000558f266e8b59 in  ()
#6  0x00007fc097f353c0 in <signal handler called> () at /lib/x86_64-linux-gnu/libpthread.so.0
#7  __strlen_avx2 () at ../sysdeps/x86_64/multiarch/strlen-avx2.S:65
#8  0x00007fc097da7d45 in __vfprintf_internal
    (s=s@entry=0x7fff57a7ec30, format=format@entry=0x7fff57a7f220 "event25 - %s%s%s%s%s%s%s%s (AVRCP): is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n", ap=ap@entry=0x7fff57a7f208, mode_flags=mode_flags@entry=2) at vfprintf-internal.c:1688
#9  0x00007fc097dbafca in __vsnprintf_internal
    (string=0x7fff57a7edc5 "event25 -  Keyboard (AVRCP): is tagged by udev as:", maxlen=<optimized out>, format=0x7fff57a7f220 "event25 - %s%s%s%s%s%s%s%s (AVRCP): is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n", args=0x7fff57a7f208, mode_flags=2) at vsnprintf.c:114
#10 0x0000558f266f06e7 in Xvscnprintf ()
#11 0x0000558f266f230b in LogVMessageVerb ()
#12 0x00007fc08884b084 in  () at /lib/x86_64-linux-gnu/libinput.so.10
#13 0x00007fc08884e9a4 in  () at /lib/x86_64-linux-gnu/libinput.so.10
#14 0x00007fc088869ad6 in  () at /lib/x86_64-linux-gnu/libinput.so.10
#15 0x00007fc088869cb9 in  () at /lib/x86_64-linux-gnu/libinput.so.10
#16 0x00007fc088869eab in libinput_path_add_device () at /lib/x86_64-linux-gnu/libinput.so.10
#17 0x00007fc0888b0b8d in  ()
#18 0x0000000000000001 in  ()
#19 0x000000000000000e in  ()
#20 0x000000000000000d in  ()
#21 0x0000558f28424360 in  ()
#22 0x0000558f285d0f40 in  ()
#23 0x0000558f265c65af in xf86SetBoolOption ()
#24 0x0000558f2816b210 in  ()
#25 0x000000000000000d in  ()
#26 0x0000558f28424360 in  ()
#27 0x0000558f285d0f40 in  ()
#28 0x0000558f265d1bbc in  ()
#29 0x0000558f265e4785 in  ()
#30 0x0000558f265e4ac8 in  ()
#31 0x0000558f266e9541 in  ()
#32 0x0000558f266e2303 in WaitForSomething ()
#33 0x0000558f26586d77 in  ()
#34 0x0000558f2658b064 in  ()
#35 0x00007fc097d530b3 in __libc_start_main (main=0x558f26574a70, argc=12, argv=0x7fff57a80b28, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fff57a80b18)

That's a good start, it tells us that the actual crash happened in __vfprintf_internal and shows us its parameters. Most of this stack trace is just filled with anonymous addresses, though. A hint is right at the top of the gdb messages:

warning: Could not load shared library symbols for 10 libraries

OK, so the debug symbols are missing for some libraries. A little bit of googling reveals that the debian-goodies package contains an absolute treasure called find-dbgsym-packages. Provide it with a core dump, and this command will

  1. Figure out which libraries were loaded at the time of the crash
  2. Find the corresponding apt packages
  3. Tell you which packages contain their debug symbols

It's just that easy! Install those packages (things like libinput10-dbgsym) and then start GDB again. Suddenly, the backtrace is more revealing:

#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007fc097d51859 in __GI_abort () at abort.c:79
#2  0x0000558f266eb810 in OsAbort () at ../../../../os/utils.c:1351
#3  0x0000558f266f0c49 in AbortServer () at ../../../../os/log.c:872
#4  0x0000558f266f1aaa in FatalError (f=f@entry=0x558f26725310 "Caught signal %d (%s). Server aborting\n") at ../../../../os/log.c:1010
#5  0x0000558f266e8b59 in OsSigHandler (unused=<optimized out>, sip=0x7fff57a7daf0, signo=11) at ../../../../os/osinit.c:156
#6  OsSigHandler (signo=11, sip=0x7fff57a7daf0, unused=<optimized out>) at ../../../../os/osinit.c:110
#7  0x00007fc097f353c0 in <signal handler called> () at /lib/x86_64-linux-gnu/libpthread.so.0
#8  __strlen_avx2 () at ../sysdeps/x86_64/multiarch/strlen-avx2.S:65
#9  0x00007fc097da7d45 in __vfprintf_internal (s=s@entry=0x7fff57a7ec30, format=format@entry=0x7fff57a7f220 "event25 - %s%s%s%s%s%s%s%s (AVRCP): is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n", ap=ap@entry=0x7fff57a7f208, mode_flags=mode_flags@entry=2)
    at vfprintf-internal.c:1688
#10 0x00007fc097dbafca in __vsnprintf_internal (string=0x7fff57a7edc5 "event25 -  Keyboard (AVRCP): is tagged by udev as:", maxlen=<optimized out>,
    maxlen@entry=1019, format=0x7fff57a7f220 "event25 - %s%s%s%s%s%s%s%s (AVRCP): is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n",
    format@entry=0x3fb <error: Cannot access memory at address 0x3fb>, args=args@entry=0x7fff57a7f208, mode_flags=mode_flags@entry=2) at vsnprintf.c:114
#11 0x00007fc097e5cfc2 in ___vsnprintf_chk (s=<optimized out>, maxlen=maxlen@entry=1019, flag=flag@entry=1, slen=slen@entry=18446744073709551615, format=format@entry=0x3fb <error: Cannot access memory at address 0x3fb>, ap=ap@entry=0x7fff57a7f208)
    at vsnprintf_chk.c:34
#12 0x0000558f266f06e7 in vsnprintf (__ap=0x7fff57a7f208, __fmt=0x3fb <error: Cannot access memory at address 0x3fb>, __n=1019, __s=<optimized out>) at /usr/include/x86_64-linux-gnu/bits/stdio2.h:80
#13 Xvscnprintf (args=0x7fff57a7f208, format=0x3fb <error: Cannot access memory at address 0x3fb>, n=1019, s=<optimized out>) at ../../../../os/xprintf.c:207
#14 Xvscnprintf (s=<optimized out>, n=1019, format=format@entry=0x7fff57a7f220 "event25 - %s%s%s%s%s%s%s%s (AVRCP): is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n", args=args@entry=0x7fff57a7f208) at ../../../../os/xprintf.c:202
#15 0x0000558f266f230b in LogVMessageVerb (type=<optimized out>, verb=3, format=0x7fff57a7f220 "event25 - %s%s%s%s%s%s%s%s (AVRCP): is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n", args=0x7fff57a7f208) at ../../../../os/log.c:720
#16 0x00007fc08884b084 in evdev_log_msg (device=device@entry=0x558f285ba530, priority=priority@entry=LIBINPUT_LOG_PRIORITY_INFO, format=format@entry=0x7fc088872d00 "is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n") at ../src/evdev.h:779
#17 0x00007fc08884e9a4 in evdev_configure_device (device=<optimized out>) at ../src/evdev.c:1774
#18 evdev_device_create (seat=seat@entry=0x558f27a8ef70, udev_device=udev_device@entry=0x558f281914b0) at ../src/evdev.c:2225
#19 0x00007fc088869ad6 in path_device_enable (input=0x558f27a8dc90, udev_device=0x558f281914b0, seat_logical_name_override=<optimized out>) at ../src/path-seat.c:191
#20 0x00007fc088869cb9 in path_create_device (libinput=0x558f27a8dc90, udev_device=0x558f281914b0, seat_name=0x0) at ../src/path-seat.c:268
#21 0x00007fc088869eab in libinput_path_add_device (libinput=0x558f27a8dc90, path=0x558f2816b210 "/dev/input/event25") at ../src/path-seat.c:399

Okay, now we're getting somewhere. Let's look at just the chain of calls leading into the crash in __vfprintf_internal:

#10 0x00007fc097dbafca in __vsnprintf_internal (... ) at vsnprintf.c:114
#11 0x00007fc097e5cfc2 in ___vsnprintf_chk (...) at vsnprintf_chk.c:34
#12 0x0000558f266f06e7 in vsnprintf (...) at /usr/include/x86_64-linux-gnu/bits/stdio2.h:80
#13 Xvscnprintf (...) at ../../../../os/xprintf.c:207
#14 Xvscnprintf (...) at ../../../../os/xprintf.c:202
#15 0x0000558f266f230b in LogVMessageVerb (... format="event25 - %s%s%s%s%s%s%s%s (AVRCP): is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n" ...) at ../../../../os/log.c:720
#16 0x00007fc08884b084 in evdev_log_msg (format="is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n") at ../src/evdev.h:779
#17 0x00007fc08884e9a4 in evdev_configure_device (device=<optimized out>) at ../src/evdev.c:1774
#18 evdev_device_create (...) at ../src/evdev.c:2225

As we can see in this cleaned up output, frame #15 has some stuff in its format string that wasn't there in frame #16. It's isn't easy to tell, but the text event25 - %s%s%s%s%s%s%s%s actually contains the name of the offensive headphones.

Looking either at the address 0x00007fc08884b084 or googling for evdev_log_msg, we find that the function evdev_log_msg is part of libinput, as is the source file ../src/evdev.h. This version of Ubuntu ships with libevdev version 1.15.5, whose source code can be found here.

The bug

Format string related crashes usually come down to the same cause: Too many %-style format tokens in the format string, and not enough parameters to the actual printf() function. For example, the following is correct and functional:

unsigned int age = 6;
const char *name = "Willy";
printf("Oh, %s, you're already %u years old?", name, age);

The printf function scans the format string and determines that the first parameter should be a string and the second parameter should be an unsigned integer.

The following example will misbehave, and quite possibly crash:

unsigned int age = 6;
const char *name = "Willy";
printf("Oh, %s, you're already %u years old?");

Again, printf will be expecting a string and an unsigned integer. It will read a string and an unsigned integer. The problem is, they weren't actually provided by the caller. No good.

So, what happened in libinput to cause this crash? Let's look at the offending function evdev_log_msg, which is in frame #16 of the stack trace above:

LIBINPUT_ATTRIBUTE_PRINTF(3, 4)
static inline void
evdev_log_msg(struct evdev_device *device,
          enum libinput_log_priority priority,
          const char *format,
          ...)
{
    va_list args;
    char buf[1024];

    if (!is_logged(evdev_libinput_context(device), priority))
        return;

    /* Anything info and above is user-visible, use the device name */
    snprintf(buf,
         sizeof(buf),
         "%-7s - %s%s%s",
         evdev_device_get_sysname(device),
         (priority > LIBINPUT_LOG_PRIORITY_DEBUG) ?  device->devname : "",
         (priority > LIBINPUT_LOG_PRIORITY_DEBUG) ?  ": " : "",
         format);

    va_start(args, format);
    log_msg_va(evdev_libinput_context(device), priority, buf, args);
    va_end(args);
}

This is a vararg function, which means that it accepts a variable number of arguments. The purpose of this function is format a message and then write it to a log somewhere - it behaves a lot like printf(). The call to log_msg_va is passing on this variable parameter list to whatever logging functionality libinput was configured to use - another printf-like function with a format string.

So what's the problem? This function is called when an evdev related thing happens, and does three things:

  1. Find the name of the evdev device
  2. Build a string buf with the device name prepended to the old format string
  3. Pass buf as a format string to log_msg_va

For example, if the device at /dev/input/event10 is named TWINKLES, format="%d" and args was a single integer 5, then the following call will eventually be made:

log_msg_va(..., priority, "event10 - TWINKLES:%d", 5);

The system's __vfprintf_internal function at the bottom of this call chain will eventually receive that format string, infer that there is a single parameter and interpret it as an integer. No problem.

But what if the device name wasn't TWINKLES? What if it was %s%s%s%s%s%s%s%s?

log_msg_va(..., priority, "event10 - %s%s%s%s%s%s%s%s:%d", 5);

__vfprintf_internal will attempt to read eight strings and an integer, followed by a big boom in the server room.

Compiler Warnings

Since non-constant format strings are a security risk, and not usually what you want to do, compilers implement warnings against this type of code. gcc, for example, has the warning format-nonliteral for this purpose. This option emits a warning when a format string is passed that isn't a string literal (i.e. a string specified with quotes).

Looking at the current version of libinput, the vulnerable function looks slightly different from the 1.15.5 we've looked at so far:

    ...
    va_start(args, format);
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored "-Wformat-nonliteral"
        log_msg_va(evdev_libinput_context(device), priority, buf, args);
    #pragma GCC diagnostic pop
        va_end(args);
}

Oh no! It appears that these warnings were disabled at some point, to clean up compilation output. As they've been enabled for a long period of time while the vulnerability was present, it is likely that this warning has not alerted the developer.

The patch

Peter Hutterer produced a patch with the input of José Expósito and the xorg-security mailing list. The patch introduces a new utility function sanitize_str which is intended to duplicate all % signs in the input. This serves as an escape sequence when interpreted by the printf family of functions.

The patch has been merged for release in the 1.20 branch, and backported to the 1.19 and 1.18 branches.

Conclusion

This vulnerability affects versions of libinput starting with 1.10, released in early 2018. It has been assigned CVE-2022-1215.

The root cause of this vulnerability is pretty easy to identify - attacker-controlled data was passed into the format string of a printf-like function. This is trivially leveraged to cause a denial of service, and can potentially be used to achieve remote code execution. As libinput is used by the Xorg server, which often runs as root, an RCE vulnerability is rather severe.

We'd like to extend our gratitude and compliments to the maintainers of libinput, who were both quick and decisive in their handling of this issue. The original bug report was assessed within 24 hours and the issue was handled with exemplary consideration given to its security implications.

Timeline

  • 2022-03-29: Reported to maintainers of libinput
  • 2022-03-30: Confirmed by vendor, patch proposed
  • 2022-04-04: CVE-2022-1215 assigned
  • 2022-04-12: Independently discovered by Lukas Lamster
  • 2022-04-19: Advisory published on xorg-announce
  • 2022-04-20: Discovered to be a duplicate of unfixed xorg/xserver#1280
  • 2022-04-20: Embargo abandoned
  • 2022-04-20: Patch for 1.20, 1.19 and 1.18 branches merged