近年来,打印机已成为企业内网中不可或缺的设备之一,其功能也随着科技的发展日益增多,除了一般的打印或传真功能外,也开始支持AirPrint等云打印服务,使其更易于使用。从移动设备直接打印现已成为物联网(IoT)时代的基本要求。鉴于其便捷性,人们也常用它来打印公司内部的一些敏感文件,这使得保证打印机的安全变得更加重要。

2021年,研究人员在佳能(Canon)和惠普(HP)打印机中发现了Pre-auth RCE漏洞(CVE-2022-24673和CVE-2022-3942),另外还在Lexmark中发现了漏洞CVE-2021-44734,并在Pwn2Own Austin 2021中顺利取得了Canon ImageCLASS MF644Cdw、HP Color LaserJet Pro MFP M283fdw及 Lexmark MC3224i的控制权。这篇研究将详述Canon和HP漏洞的细节及其利用方式。

早期在使用打印机时,通常需要使用IEEE1284或USB打印机电缆将打印机连接到计算机,并且在使用时会额外装上厂商提供的打印机驱动程序。而如今,市场上的大多数打印机都不需要USB或传统的电缆,只要将打印机通过网线连接到内网,就可以立即找到并使用打印机。

目前市面上的打印机新增了各种各样的服务,如FTP、AirPrint、Bonjour等,目的无非是使打印变得更加便捷。

动机:为什么要研究打印机?

红队

在做红队评估时,我们发现打印机普遍出现在公司内网,但几乎总是会有不止一台打印机常常被忽视且缺乏更新。这也是红队隐藏动作的绝佳目标,因为它通常很难被发现。值得一提的是,比较大型的企业也很有可能将其连接到AD,使其成为恶意行为者猎取机密信息的入口点。

Pwn2Own Austin 2021

另一个原因是打印机在2021年首次成为Pwn2Own Mobile的主要目标之一。我们也准备再次挑战Pwn2Own舞台,便决定一探究竟。

起初,我们以为这一过程非常简单,与大多数物联网设备一样,能轻易找到其中的命令注入漏洞,殊不知有许多打印机使用RTOS而非一般的Linux系统,这促使我们决心挑战它。

本文将重点介绍较为精彩的佳能和惠普部分,Lexmark有机会再谈。

分析

刚开始研究的时候,我们参考了很多文章,都是需要拆解硬件进行分析,才能获得调试控制台,再用内存转储方法获取原始固件。但最终,我们选择了另一种方式,没有拆除任何一台打印机。

佳能-固件提取

初始分析版本为v6.03,我们一开始使用binwalk去解析它,但固件是经过模糊处理的,我们无法直接分析。

【经过模糊处理的佳能ImageCLASS MF644Cdw固件】

我们还尝试了Synacktiv的《TREASURE CHEST PARTY QUEST: FROM DOOM TO EXPLOIT》及Contextis research的《Hacking Canon Pixma printer - DOOM Encryption》等文章。但这次是一个完全不同的系列,我们无法使用相同的方法来消除固件的混淆。

所以我们开始分析混淆过的固件格式和内容。

我们大致上可以从被混淆的固件中看到,每个混淆过的固件的开头都会是Magic NCFW,并带有该固件的大小,而其他部分都是被混淆的数据。

所以我们开始猜想,也许这台打印机的旧固件没有被混淆,直到某个特定的版本才开始混淆。如果我们能得到中间版本,也许就有机会得到去混淆的方法。而这个Magic也可以让我们区分它是否被混淆了。

以下这个网址是通过官网或更新包获取的固件下载URL。

https://pdisp01.c-wss.com/gdl/WWUFORedirectTarget.do?id=MDQwMDAwNDc1MjA1&cmp=Z01&lang=EN

经过分析,它可以分为三个部分。

我们可以大致推断出下载URL的规则。借此方法,我们便能下载所有版本的固件。我们当时下载的版本包括:

  • V2.01
  • V4.02
  • V6.03
  • V9.03
  • V10.02

而V10.02是几周会才会发布的版本,可以先从这里优先下载。在下载了所有版本之后,我们发现这个系列的固件都是经过混淆的,并且无法从以前版本获得去混淆的方法。

但我们可以尝试下载佳能的其他系列固件,看看是否有类似的混淆算法。下载完所有固件后,文件总大小为130GB。我们可以通过查找NCFW和servicemode.html找到未混淆的固件。

最后,我们找到了四个满足条件的固件,并选择WG7000系列打印机进行分析,并找到了疑似去混淆的函数。

幸运的是,通过重写这个函数,可以解出明文的MF644Cdw固件。

在提取固件之后,我们需要映像基址(image base address),以便IDA能够有效地识别和引用字符串。首先,我们通过常用的分析工具rbasefind找到基址(image base)。

我们找到的第一个base是0x400b0000。但在用IDA反编译后,大部分函数与调试消息字符串不对应。

如上图所示,loc_4489AC08应该指向函数名的字符串,但是这个地址不是一个常规字符串。相反地,它被识别为代码段,其内容也不是字符串。这表明该位置不是实际地址,而是有轻微的偏移,但它并没有对反编译函数造成太大问题。

如何解决这个问题?我们首先找到一个已知函数名的函数和属于它的函数名字符串进行调整。找到偏移量后,我们将image base调整到正确的地址即可。最终找到的image base是0x40affde0。调整后,可以看到原来的函数名可以正确识别。

接下来就可以正常分析固件,而经过初步分析可得知,佳能ImageCLASS MF644Cdw架构如下:

OS - DryOSV2
Customized RTOS by Canon
ARMv7 32bit little-endian
Linked with application code into single image
Kernel
Service

惠普-固件提取

惠普的固件相对容易获得。我们可以使用binwalk -Z来获取正确的固件。这一过程大约花了3-4天左右的时间。而其他寻找image base address 等步骤,则与佳能相同,此处不再赘叙。经过初步分析,我们发现HP Color LaserJet Pro MFP M283fdw的架构如下:

OS
RTOS - Modify from ThreadX/Green Hills
ARM11 Mixed-endian
Code - little-endian
Data - Big-endian

攻击面

目前市场上的大多数打印机都默认启用了许多服务。

服务

端口

描述

RUI

TCP 80/443

Web接口

PDL

TCP 9100

页面描述语言

PJL

TCP 9100

打印机作业语言

IPP

TCP 631

Internet打印协议

LPD

TCP 515

行式打印机(LPD)协议

SNMP

UDP 161

简单网络管理协议(SNMP)

SLP

TCP 427

服务定位协议

mDNS

UDP5353

多播DNS(mDNS)

LLMNR

UDP 5355

链路本地组播名称解析(LLMNR)

一般来说,为了方便管理,通常都会打开RUI(web界面),再者9100端口也是打印机常用的接口,主要用于传输打印数据。

其他选项因供应商而异,但通常都有列出的选项,并且大多数都是默认启用的。在评估了整个架构之后,我们将重点关注服务发现和DNS系列服务。因为我们的长期经验经常观察到,制造商实现的此类协议往往容易出现漏洞。我们分析的主要服务是SLP、mDNS和LLMNR。

接下来,我们就以Pwn2Own Austin 2021作为案例研究,看看这些协议通常存在哪些问题。

在Pwn2Own大会上入侵打印机

佳能-服务定位协议

SLP是一种服务发现协议,它允许计算机和其他设备在局域网中找到服务。在过去,EXSI的SLP中存在许多漏洞。而佳能的SLP服务主要由自己实现。在研究SLP的细节之前,我们需要讨论一下SLP数据包的结构。

【SLP包结构图】

这里我们只需要注意function-id。该字段会决定请求类型和有效负载部分的格式。而佳能只会实现服务请求和属性请求两种。

在属性请求(Attribute Request, AttrRqst)中,用户可以根据服务和范围获取属性列表。我们可以指定要查找的范围,例如Canon打印机。

示例:

而属性请求结构大致如下所示:

它主要包括长度(length)和值(value)。解析这种格式需要特别注意,因为这里很容易出问题。事实上,佳能在解析这个结构时就出了问题。

漏洞

在解析作用域列表时,它将转义字符转换为ASCII。例如,\41将被转换为a。但是这个简单的转换有什么问题呢?让我们看一下伪代码(Pseudo code)。

int parse_scope_list(...){
    char destbuf[36];
    unsigned int max = 34;
    parse_escape_char(...,destbuf,max);
}

如上面的代码所示,在parse_scope_list中,它将固定大小的缓冲区destbuf和最大大小34传递给parse_escape_char。这里没什么问题,让我们看一下parse_escape_char。

int __fastcall parse_escape_char(unsigned __int8 **pdata, _WORD *pdatalen, unsigned __int8 *destbuf, _WORD *max)
{
  unsigned int idx; // r7
  int v7; // r9
  int v8; // r8
  int error; // r11
  unsigned __int8 *v10; // r5
  unsigned int i; // r6
  int v12; // r1
  int v13; // r0
  unsigned int v14; // r1
  bool v15; // cc
  int v16; // r2
  bool v17; // cc
  unsigned __int8 v18; // r0
  int v19; // r0
  unsigned __int8 v20; // r0
  unsigned int v21; // r0
  unsigned int v22; // r0
  idx = 0;
  v7 = 0;
  v8 = 0;
  error = 0;
  v10 = *pdata;
  for ( i = (unsigned __int16)*pdatalen; i && !v7; i = (unsigned __int16)(i - 1) )
  {
    v12 = *v10;
    if ( v12 == ',' )
    {
      if ( i < 2 )
        return -4;
      v7 = 1;
    }
    else
    {
      if ( v12 == '\\' ) //----------------------[1]
      {
        if ( i < 3 )
          return -4;
        v13 = v10[1];
        v14 = v13 - '0';
        v15 = v13 - (unsigned int)'0' > 9;
        if ( v13 - (unsigned int)'0' > 9 )
          v15 = v13 - (unsigned int)'A' > 5;
        if ( v15 && v13 - (unsigned int)'a' > 5 )
          return -4;
        v16 = v10[2];
        v17 = v16 - (unsigned int)'0' > 9;
        if ( v16 - (unsigned int)'0' > 9 )
          v17 = v16 - (unsigned int)'A' > 5;
        if ( v17 && v16 - (unsigned int)'a' > 5 )
          return -4;
        if ( v14 <= 9 )
          v18 = 0x10 * v14;
        else
          v18 = v13 - 0x37;
        if ( v14 > 9 )
          v18 *= 0x10;
        *destbuf = v18; //-------------------[2]
        v19 = v10[2];
        v10 += 2;
        v20 = (unsigned int)(v19 - 0x30) > 9 ? (v19 - 55) & 0xF | *destbuf : *destbuf | (v19 - 0x30) & 0xF;
        *destbuf = v20;
        LOWORD(i) = i - 2;
        if ( !strchr((int)"(),\\!<=>~;*+", *destbuf) )
        {
          v21 = *destbuf;
          if ( v21 > 0x1F && v21 != 0x7F )
            return -4;
        }
        goto LABEL_40;
      }
      if ( strchr((int)"(),\\!<=>~;*+", v12) ) //-----------------------[3]
        return -4;
      v22 = *v10;
      if ( v22 <= 0x1F || v22 == 0x7F )
        return -4;
      if ( v22 != ' ' )
      {
        v8 = 0;
        goto LABEL_35;
      }
      if ( !v8 )
      {
        v8 = 1;
LABEL_35:
        if ( (unsigned __int16)*max <= idx ) //----------------------[4] 
        {
          error = 1;
          goto next_one;
        }
        if ( v8 )
          LOBYTE(v22) = 32;
        *destbuf = v22;
LABEL_40:
        ++destbuf;
        idx = (unsigned __int16)(idx + 1);
      }
    }
next_one:
    ++v10;
  }
  if ( error )
  {
    *max = 0;
    debugprintff(3645, 4, "Scope longer than buffer provided");
LABEL_48:
    *pdata = v10;
    *pdatalen = i;
    return 0;
  }
  if ( idx )
  {
    *max = idx;
    goto LABEL_48;
  }
  return -4;
}

可以看到[3]是一个不处理转义字符的case。检查长度是否超过最大值[4]。但是,如果[1]处理转义字符,则不进行长度检查,转换后的结果直接复制到目标缓冲区[2]。

一旦给定一个长转义字符串,就会导致堆栈溢出。

在找到漏洞后,第一件事就是看它本身有什么保护,来决定后续的利用计划。但经过分析,我们发现佳能打印机没有任何与内存相关的保护。

保护机制:

  • 无堆栈保护;
  • 无DEP;
  • 无ASLR;

没有堆栈保护,没有DEP和ASLR,妥妥的“黑客友好型”!就像回到90年代,一个堆栈溢出就能控制整个世界。接下来,与过去的堆栈溢出利用方法一样,我们只需要找到一个固定地址来存储shellcode,覆盖返回地址,然后跳转到shellcode。最后,我们找到了BJNP服务来存储我们的shellcode。

BJNP

BJNP本身也是Canon设计的服务发现协议,过去存在许多漏洞。它会将可控的会话数据存储在全局缓冲区中。我们可以使用这个函数将我们的shellcode放在一个固定的位置,基本上也没什么严格的限制。

漏洞利用步骤

  • 使用BJNP将shellcode存储在全局缓冲区中;
  • 触发SLP中的堆栈溢出并覆盖返回地址;
  • 返回到全局缓冲区

Pwn2Own Austin 2021

一般来说,Pwn2Own组织者(ZDI)会要求参与者证明我们已经攻破了目标。这里的呈现方法取决于玩家。最初,我们想的是直接在LCD屏幕上打印logo。

然而,我们花了很多时间来弄清楚如何在屏幕上打印logo,这比发现漏洞和编写漏洞耗费的时间还要长。最后,由于时间限制,我们采用了更安全的方法,直接更改Service Mode字符串并将其打印在屏幕上。

调试

有些人可能想知道如何在这种环境中进行调试。通常有以下几种调试方法:

  • 拆卸打印机并获得调试控制台。
  • 使用旧漏洞安装自定义调试器

但是,我们已经更新到当时的最新版本。这个版本没有已知的漏洞,所以我们需要降级回版本。拆卸硬件也需要额外的时间和成本。但我们当时已经有了一个漏洞,拆除硬件或降级并不划算。最后,我们仍然使用最传统的睡眠调试(sleep debug)方法。

在ROP或执行shellcode之后,将结果打印到网页或其他可见的地方,然后调用sleep。我们可以从网页上读取结果,最后重启机器来重复这个过程。不过,实际上更好的做法还是接上调试控制台会方便一点。

接下来,让我们谈谈惠普打印机。

惠普-链路本地组播名称解析

LLMNR与mDNS非常相似。它在相同的本地链接上提供基本名称解析。但它比mDNS更直接,通常还与一些服务发现协议合作。这里简单介绍一下这个机制:

在局域网域名解析中,客户端A将首先使用组播来查找客户端C在局域网中的位置。

Client C接收到报文后,将报文返回给Client A,由Client A进行链路本地域名解析。

LLMNR主要基于DNS报文格式,其格式如下:

主要格式是标题后面跟着查询,Count表示不同类型查询的数量。

每个DNS查询由许多标签组成,每个标签将包含长度和字符串,如上图所示。还有一个消息压缩(Message Compression)机制。处理这些问题很容易出现漏洞。在BlackHat 2021的《复杂性的代价:实现相同RFC的不同漏洞》(THE COST OF COMPLEXITY:Different Vulnerabilities While Implementing the Same RFC)中,也提到了类似的问题。

漏洞

让我们来看看惠普的实操:

int llmnr_process_query(...){
    char result[292];
    consume_labels(llmnr_packet->qname,result,...);
    ...
}

这里可以看到,HP在处理LLMNR数据包时,会将一个固定大小的缓冲区传入,用来放处理后的结果,而consumer_lables则是主要用来处理DNS标签。

int __fastcall consume_labels(char *src, char *dst, llmnr_hdr *a3)
{
  int v3; // r5
  int v4; // r12
  unsigned int len; // r3
  int v6; // r4
  char v7; // r6
  bool v8; // cc
  int v9; // r0
  unsigned __int8 chr; // r6
  int result; // r0
  v3 = 0;
  v4 = 0;
  len = 0;
  v6 = 0;
  while ( 1 )
  {
    chr = src[v3]; //-------------[1]
    if ( !chr )
      break;
    if ( (int)len <= 0 )
    {
      v8 = chr <= 0xC0u;
      if ( chr == 0xC0 )
      {
        v9 = src[v3 + 1];
        v6 = 1;
        v3 = 0;
        src = (char *)a3 + v9;
      }
      else
      {
        len = src[v3++];
        v8 = v4 <= 0;
      }
      if ( !v8 )
        dst[v4++] = '.';
    }
    else
    {
      v7 = src[v3++];
      len = (char)(len - 1);
      dst[v4++] = v7; //----------[2]
    }
  }
  result = v3 + 1;
  dst[v4] = 0;
  if ( v6 )
    return 2;
  return result;
}

而在 consume_labels 中的 [1] 会得到标签长度,然后根据类型进行处理。而[2]则用于处理一般长度的情况,此处并没有对长度进行检查,就直接将标签写入dst缓冲区,导致了堆栈溢出。至此,我们原以为差不多结束了,接下来就可以用类似于佳能的方法来利用它。然而,当我们编写这个漏洞时,我们发现惠普打印机有更多的保护机制。

保护机制:

  • 无堆栈保护;
  • 有XN(DEP);
  • 有内存保护单元(MPU);
  • 无ASLR;

在这种情况下,多了XN和MPU内存保护机制,该漏洞便有了更多的限制。在没有空字节的情况下,我们只能溢出大约0x100字节,这极大地限制了我们的ROP,并使其更具挑战性。我们需要找到其他漏洞或方法来实现我们的目标。

在一段时间后,我们开始思考惠普打印机是如何实现XN(DEP)和MPU的?让我们回顾一下惠普的RTOS:

  • 所有服务代码及内核代码都在同一个二进制中。
  • 大多数的任务都运行在同一虚拟地址空间中(没有进程隔离),也几乎都运行在高权限模式下。

看完以上两点,我们想到是否可以通过了解HP RTOS中的MMU和MPU来绕过它?

接下来,我们来看一下HP RTOS MMU机制。

HP M283fdw MMU

在HP M283fdw中使用的是一阶翻译表(Translation table)来做地址翻译,每个翻译表项都表示1MB的部分,而翻译表则是固定在0x4003c000这个位置上。

每个翻译表项都会对应到一个物理地址和该节的权限。CPU就是根据这些表项决定是否可以执行或修改权限。这里涉及的权限是AP、APX和XN。我们还可以通过这个转换表项映射任何物理地址。

我们可以从上述的漏洞中注意到,如果在高权限下存在堆栈溢出,可以通过ROP修改翻译表项。但是当我们试图直接写入翻译表时,惠普打印机崩溃了。

我们检查发现,内存故障异常的主要原因是内存保护单元(MPU)保护翻译表。

接下来,我们再来看MPU的机制。

HP M283fdw MPU

MPU可以将内存划分为多个区域,并为每个区域设置单独的保护属性。它是一种与MMU完全不同的机制,经常出现在物联网设备中。HP在引导时启用MPU并定义每个区域的权限,因此我们不能操作页表。

经过长时间的逆向工程和参考ARM手册后,我们发现可以通过清除MPU_CTRL关闭MPU。我们发现该位置为0xE0400304,与ARM的规格略有不同。

漏洞利用

在了解HP的MMU和MPU机制后,我们可以很容易地使用ROP关闭MPU并成功修改翻译表项。我们可以任意修改任何服务的代码,并最终选择了Line Printer Daemon(LPD)。我们将其修改为后门,将更多的有效负载读取到指定位置,最后执行shellcode。

但有一点必须特别注意:在覆盖翻译表项和LPD代码之后,请确保Flush TLB并清掉I-cache和D-cache。否则,它很可能在旧版本中执行,导致漏洞利用失败。

Flush TLB

flush_tlb:
    mov r12, #0
    mcr p15, 0, r12, c8, c7, 0

清掉I-cache

disable_icache:
    mrc p15, 0, r1, c1, c0, 0
    bic r1, r1, #(1 << 12)
    mcr p15, 0, r1, c1, c0, 0

漏洞利用步骤

  • 触发LLMNR中的堆栈溢出并覆盖返回地址;
  • 使用有限的ROP关闭MPU;
  • 利用ROP修改翻译表项并获得读写执行权限;
  • Flush TLB;
  • 修改LPD服务的代码;
  • 清掉I-cache和D-cache;
  • 使用修改后的LPD读取我们的shellcode并执行;

Pwn2Own Austin 2021

当我们可以执行shellcode时,我们只剩下一周的时间,我们最终选择使用确切的字符串在LCD上显示Pwned by DEVCORE。

在此之后,我们还尝试将后门改为调试控制台,以方便查看内存信息,播放音乐等功能。

缓解建议

更新

首先是定期更新。上述所有提到的打印机都已有补丁,但它经常被忽视。我们经常发现打印机几年不更新,甚至直接在公司内网留下默认密码。

禁用不使用的服务

另一种缓解方法是关闭不经常使用的服务。大多数打印机默认启用了太多通常不使用的服务。我们甚至认为你可以关闭发现(discovery)服务,只打开你想使用的服务。

部署防火墙

如果能应用防火墙就更好了。大多数打印机都提供相关功能。

原文链接:

https://devco.re/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1-en/