Exploiting the HP Printer without the printer (Pwn2Own 2022)

by

Pwn2Own Team

June

2023

Introduction

Interrupt Labs exploited the HP Color LaserJet Pro M479fdw printer successfully in Pwn2Own Toronto 2022. This blog post describes the technical details of the vulnerability, and how we developed the exploit before we received the physical device.

PostScript and Compact Font Format

One of the supported printing languages on the HP Color LaserJet Pro M479fdw is PostScript. Using a feature called raw printing, it is possible to print a PostScript document by sending it to TCP port 9100 on the printer.

Fonts in the Compact Font Format (CFF) can be embedded to PostScript documents. From the CFF specification:

An INDEX is an array of variable-sized objects. It comprises a header, an offset array, and object data. The offset array specifies offsets within the object data.
[The Name INDEX] contains the PostScript language names (FontName or CIDFontName) of all the fonts in the FontSet stored in an INDEX structure.

Vulnerability

The firmware version at the time was HP_Color_LaserJet_Pro_M478_M479_series_FW_002.2230D.rfu. It contains a Linux binary plang which is responsible for parsing PostScript documents and CFF fonts embedded in them.

The Name INDEX is parsed at 0x00d7b550:


void cff_parse_name_index(void)
{
  byte bVar1;
  int *name_index_buf;
  int offset_delta;
  char *next;
  int string_data_offset;
  uint count;
  undefined *offset;
  char *dst;
  
  bVar1 = g_cff_parse_offset[2];
  g_cff_name_index_count = CONCAT11(*g_cff_parse_offset,g_cff_parse_offset[1]);
  g_cff_parse_offset = g_cff_parse_offset + 3;
  name_index_buf = (int *)ps_malloc((g_cff_name_index_count + 1) * 4);
  if (name_index_buf != (int *)0x0) {
    count = (uint)g_cff_name_index_count;
    g_cff_name_index_offsets = name_index_buf;
    cff_populate_index_offsets(count,(uint)bVar1,(byte *)name_index_buf);
    g_cff_name_index_data = g_cff_parse_offset;
    offset = g_cff_parse_offset;
    if (count != 0) {
      string_data_offset = 0;
      do {
        offset_delta = name_index_buf[1] - *name_index_buf;
        offset = offset + offset_delta;
        if (0 < offset_delta) {
          dst = g_cff_parse_offset + string_data_offset;
          do {
            next = dst + 1;
            /* Replace spaces with - */
            if (*dst == ' ') {
              *dst = '-';
            }
            dst = next;
          } while (next != g_cff_parse_offset + offset_delta + string_data_offset);
        }
        string_data_offset = string_data_offset + 1;
        name_index_buf = name_index_buf + 1;
      } while (string_data_offset < (int)count);
    }
    g_cff_parse_offset = offset;
    return;
  }
  g_cff_stop_parsing? = 4;
  g_cff_name_index_offsets = name_index_buf;
  return;
}

The length of each name in the INDEX is calculated as the difference of two offsets. There are no checks for the length of the name in the above function. The only limitation is that all space characters (0x20) are replaced with a dash (0x2d).

There are two functions where the results of the CFF parsing are used: 0x00d80f40 for CIDFonts and 0x00d828e0 for others. We chose to exploit the former as the local variables were in a much more favourable order.

In the function 0x00d80f40, there are two places where sprintf and strcat are called in a loop, and they both lead to a stack buffer overflow. The first instance is as follows:


char tmp_buf [128];
...
byte buf [608];
...

dst = (char *)((int)&DAT_0173230c + 3);
iVar18 = g_cff_name_index_data;
do {
    sprintf(tmp_buf,"%c",(uint)(byte)offset1_minus_1[iVar18]);
    iVar18 = g_cff_name_index_data;
    pcVar12 = offset1_minus_1 + g_cff_name_index_data;
    offset1_minus_1 = offset1_minus_1 + 1;
    dst = dst + 1;
    
    // Copy byte to a global buffer
    *dst = *pcVar12;
    
    // strcat call at 0x00d80ffa
    strcat((char *)buf,tmp_buf);
} while (offset1_minus_1 != offset2_minus_1);

An interesting aspect here is copying the name byte-by-byte to a global buffer (*dst = *pcVar12;). The great thing is that, unlike with the sprintf-strcat combination, the nul bytes are copied, too. We will use this in the exploit later.

The latter vulnerable loop is as follows:


char tmp_buf [128];
char another_tmp_buf [128];
byte buf [608];
int iVar18;
char fdselect_code;
byte offSize;
byte *p_this_will_overflow;

...

p_this_will_overflow = buf + 0x100;

...

start_addr = *(int *)(g_cff_name_index_offsets + offset1) + -1;
offset1 = *(int *)(g_cff_name_index_offsets + offset2) + -1;
if (start_addr < offset1) {
    do {
        /* This is where the overflow happens */
        pbVar1 = (byte *)(g_cff_name_index_data + start_addr);
        start_addr = start_addr + 1;
        sprintf(another_tmp_buf,"%c",(uint)*pbVar1);
        // strcat call at 0x00d81648
        strcat((char *)p_this_will_overflow,another_tmp_buf);
    } while (start_addr != offset1);
}

This loop is executed after the first one so this is where we should overwrite the saved instruction pointer (pc) value on the stack.

Emulated Exploit Development Environment

We started working on the exploit before we had received our printer. After experimenting with Unicorn and Qiling, the winning tooling for this particular use case turned out to be QEMU userspace emulation and GDB. ZDI has an excellent blog post on using usermode emulation to perform a cross-architectural chroot. The great thing is that even the network services are available - perfect for testing your shell.

A GDB script with the following preparatory steps was used for debugging the vulnerable function:

  1. Run the binary, break at the main function. This was a simple method to get the relevant libraries loaded
  2. Call malloc to allocate a few buffers that the PostScript/CFF code needs
  3. Set some global variables
  4. Load our CFF from a file to a buffer
  5. Hook a custom allocator to call malloc and free instead
  6. Set a few general registers
  7. Set pc register to continue execution in the CFF parser

The emulated environment matched the real device surprisingly well! So well, in fact, that once we got our hands on a physical device the exploit worked on the very first attempt!

Exploit

The good news is that ASLR is not enabled for the plang binary. The bad news is that null bytes are a bad character and there are no ROP gadget addresses without one. Fortunately, we can sneakily add a single null byte thanks to strcat terminating the string on the last iteration of the loop. This is all we need for a stack pivot.

The following registers are popped from the stack at the end of the vulnerable function:

0x00d81f46    pop.w {r4, r5, r6, r7, r8, sb, sl, fp, pc}

We can stack pivot by setting pc to the following gadget (Thumb mode):

0x0007ad12  ldmdb sl, {r4, fp, sp, pc}

We set the sl register to 0x1732324 which resides in the global buffer where our font name, including the nul bytes, was copied to in the first loop. This allows us to pivot the stack to the same global buffer.

Now that we are able to use gadgets with null bytes, we can craft a ROP chain that calls mmap to create an RWX mapping and copies our shell code to it with memcpy!

Since 0x20 is a bad char in the font name, we used a technique documented in Printing Shellz for storing an arbitrary payload in the CFF String INDEX. To get the address of the data in the String INDEX, the ROP chain dereferences a pointer at known address 0x01731364 .

The shellcode in the Pwn2Own exploit was a simple bind shell on port 4444.

The Patch

HP patched the vulnerability on 2023-06-22.

Please click on "Preferences" to confirm your cookie preferences. By default, the essential cookies are always activated. View our Cookie Policy for more information.