深入解析Pwn2Own 2021:佳能ImageCLASS MF644Cdw打印机漏洞利用全记录

本文详细记录了研究团队如何逆向分析佳能MF644Cdw打印机固件,发现并利用CVE-2022-24674漏洞的过程。从固件解密到最终实现远程代码执行,展示了完整的漏洞利用链和技术细节。

Pwn2Own 2021 佳能 ImageCLASS MF644Cdw 漏洞分析报告

引言

Pwn2Own Austin 2021于2021年8月宣布,并引入了新的类别,包括打印机。基于我们之前对打印机的经验,我们决定针对三个型号中的一个进行研究。其中,佳能ImageCLASS MF644Cdw似乎是最有趣的目标:之前的研究有限(主要针对Pixma喷墨打印机)。基于这一点,我们在甚至还没有购买打印机之前就开始分析固件。

我们的团队由3名成员组成:

  • Nicolas Devillers (@nikaiw)
  • Jean-Romain Garnier (@JRomainG)
  • Raphaël Rigo (@trou)

注意:本分析报告基于打印机固件版本10.02,这是Pwn2Own时最新的可用版本。

目录

  • 固件提取与分析
  • 硬件初步检查
  • 漏洞利用设置
  • 漏洞分析
  • 漏洞利用
  • 有效载荷
  • 补丁分析
  • 结论

固件提取与分析

下载固件

佳能网站有个有趣的特点:如果没有匹配特定型号的序列号,就无法下载该型号的固件。正如你可能猜到的,当你想下载一个你不拥有的型号的固件时,这特别烦人。我们想到了两个选项:

  1. 在评论或列表中找到该型号的图片
  2. 在Shodan上找到相同型号的序列号

幸运的是,MFC644cdw被PCmag详细评论过,其中一张图片包含了用于评论的打印机的序列号。这使我们能够从佳能美国网站下载固件。当时该网站上可用的版本是06.03。

预测固件URL

作为旁注,一旦获得序列号,我们可以下载多个版本的固件,适用于不同的操作系统。例如,macOS的版本06.03具有以下文件名:mac-mf644-a-fw-v0603-64.dmg,相关的下载链接是https://pdisp01.c-wss.com/gdl/WWUFORedirectSerialTarget.do?id=OTUwMzkyMzJk&cmp=ABR&lang=EN。正如URL所暗示的,此页面要求提供序列号,如果序列号有效,则将您重定向到实际的固件。在这种情况下:https://gdlp01.c-wss.com/gds/5/0400006275/01/mac-mf644-a-fw-v0603-64.dmg

当然,第一个URL中的base64编码的id很有趣:解码后,你得到(字面字符串)95039232d,这又是40000627501的十六进制表示,这是实际固件URL的一部分!

更多的例子让我们理解到URL中带有单个数字的部分(在我们的例子中是/5/)只是URL路径下一部分的最后一位数字(在这个例子中是/0400006275/)。我们假设这可能用于负载平衡或其他类似的原因。利用这些知识,我们能够下载许多不同型号的各种固件映像。我们还发现,美国或欧洲的佳能页面不如日本页面更新,日本页面在撰写时已有版本09.01。

然而,它们都落后于现实:最新的固件版本是10.02,这实际上是通过打印机的固件更新机制检索的。https://gdlp01.c-wss.com/rmds/oi/fwupdate/mf640c_740c_lbp620c_660c/contents.xml给了我们实际的更新版本。

固件类型

关于固件"类型"的一个小说明。更新XML对每种内容类型有3个不同的条目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<contents-information>
  <content kind="bootable" value="1" deliveryCount="1" version="1003" base_url="http://pdisp01.c-wss.com/gdl/WWUFORedirectSerialTarget.do" >
    <query arg="id" value="OTUwMzZkMDQ5" />
    <query arg="cmp" value="Z03" />
    <query arg="lang" value="JA" />
  </content>
  <content kind="bootable" value="2" deliveryCount="1" version="1003" base_url="http://pdisp01.c-wss.com/gdl/WWUFORedirectSerialTarget.do" >
    <query arg="id" value="OTUwMzZkMGFk" />
    <query arg="cmp" value="Z03" />
    <query arg="lang" value="JA" />
  </content>
  <content kind="bootable" value="3" deliveryCount="1" version="1003" base_url="http://pdisp01.c-wss.com/gdl/WWUFORedirectSerialTarget.do" >
    <query arg="id" value="OTUwMzZkMTEx" />
    <query arg="cmp" value="Z03" />
    <query arg="lang" value="JA" />
  </content>

对应:

  • gdl_MF640C_740C_LBP620C_660C_Series_MainController_TYPEA_V10.02.bin
  • gdl_MF640C_740C_LBP620C_660C_Series_MainController_TYPEB_V10.02.bin
  • gdl_MF640C_740C_LBP620C_660C_Series_MainController_TYPEC_V10.02.bin

每种类型对应XML URL中列出的一个型号:

  • MF640C => TYPEA
  • MF740C => TYPEB
  • LBP620C => TYPEC

解密:黑盒尝试

基本固件提取

Windows更新如win-mf644-a-fw-v0603.exe是Zip SFX文件,包含实际的更新程序:mf644c_v0603_typea_w.exe。这是在Hiew中看到的PE文件末尾:

1
2
004767F0:  58 50 41 44-44 49 4E 47-50 41 44 44-49 4E 47 58  XPADDINGPADDINGX
00072C00:  4E 43 46 57-00 00 00 00-3D 31 5D 08-20 00 00 00  NCFW    =1]

正如你所看到的(地址从RVA变为物理偏移),固件更新似乎存储在PE的末尾作为覆盖,并方便地以NCFW魔术头开始。macOS固件更新可以用7z提取,并包含一个大文件:mf644c_v0603_typea_m64.app/Contents/Resources/.USTBINDDATA,除了PE签名和一些偏移外,几乎与Windows覆盖相同。

在查看了一堆固件后,很明显更新的页脚包含有关固件更新各个部分的信息,包括一个很好的USTINFO.TXT文件,描述了目标型号等。NCFW魔术也在由UST页脚描述的最大"文件"中多次出现。经过一些试验和错误,其格式被理解,并允许我们将固件分割成其基本组件。

所有这些信息都被编译到unpack_fw.py脚本中。

弱加密,但有多弱?

主要的固件文件Bootable.bin.sig被加密,但它似乎是用一个非常简单的算法加密的,我们可以通过查看模式来确定:

1
2
3
4
00000040  20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F  !"#$%&'()*+,-./
00000050  30 31 32 33 34 35 36 37 38 39 3A 3B 39 FC E8 7A 0123456789:;9..z
00000060  34 35 4F 50 44 45 46 37 48 49 CA 4B 4D 4E 4F 50 45OPDEF7HI.KMNOP
00000070  51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 QRSTUVWXYZ[\]^_`

通常假设在明文固件中有大块的00或FF,这使我们能够对潜在的加密算法有不同的假设。递增的数字很可能意味着某种字节计数器。然后我们尝试将其与一些基本操作结合并尝试解密:

  • 与字节计数器异或 => 失败
  • 与计数器和反馈异或 => 失败

尝试使用已知明文(其中明文不是00或FF)在这个阶段是不可能的,因为我们还没有解密的固件映像。团队中有一个逆向工程师,明显的下一步是尝试找到实现解密的代码:

  • 更新工具不解密固件,而是按原样发送 => 失败
  • 检查以前型号的固件,尝试找到支持加密"NCFW"更新的未加密代码:失败

然而,我们找到了具有类似结构的未加密固件文件,这给了我们一些已知明文,但没有给出任何关于解决方案的真正线索。

硬件:初步检查

主板和串口

一旦我们收到打印机,我们当然开始拆卸它以寻找有趣的硬件特性和帮助我们获取固件访问权限的方法。

查看硬件,我们考虑了这些不同的方法来获取更多信息:

  • 主板上存在SPI,读取它
  • 主板上存在未焊接的eMMC,读取它
  • 找一个旧型号,带有未加密的固件和更简单的闪存来拆卸,读取,获利。幸运的是,我们不必在这个方向上走得更远。
  • 一些打印机已知有一个用于调试的串口,提供一个小型shell。找一个并用它来运行调试命令以获取明文/内存转储(注意:当然我们后来找到了串口)

服务模式

所有企业打印机都有一个服务模式,供技术人员诊断潜在问题。YouTube是关于如何进入它的良好信息来源。在这个型号上,操作有点奇怪,因为必须按下"不可见"的按钮。一旦进入服务模式,调试日志可以转储到USB闪存驱动器上,这会创建几个文件:

  • SUBLOG.TXT
  • SUBLOG.BIN显然是SUBLOG.TXT,用与加密固件相同模式的算法加密。

解密固件

程序合成方法

在这一点上,这是我们的思路:

  • 加密算法似乎"微不足道"(很多模式,逐字节)
  • SUBLOG.TXT给了我们很多明文
  • 我们太懒了,不想通过黑盒/推理找到它

由于程序合成在过去几年发展得相当快,我们决定尝试让一个工具为我们合成解密算法。我们当然使用了来自SUBLOG.TXT的已知明文,这可以用作约束。Rosette似乎易于使用且非常适合,所以我们选择了它。我们开始遵循一个很好的教程,该教程在整数上工作,但在尝试直接将其转换为位向量时给我们带来了一些麻烦。

然而,我们很快意识到我们不必合成一个程序(对于所有输入),而是实际上解一个方程,其中未知数是满足使用已知明文/密文对构建的所有约束的程序。“Essential” guide to Rosette在一个例子中为我们介绍了这一点。所以我们开始定义"程序"语法和crypt函数,它使用语法定义一个程序,有两个操作数,最多3层深:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(define int8? (bitvector 8))
(define (int8 i)
  (bv i int8?))

(define-grammar (fast-int8 x y)  ; Grammar of int32 expressions over two inputs:
  [expr
   (choose x y (?? int8?)        ; <expr> := x | y | <32-bit integer constant> |
           ((bop) (expr) (expr))  ;           (<bop> <expr> <expr>) |
           ((uop) (expr)))]       ;           (<uop> <expr>)
  [bop
   (choose bvadd bvsub bvand      ; <bop>  := bvadd  | bvsub | bvand |
           bvor bvxor bvshl       ;           bvor   | bvxor | bvshl |
           bvlshr bvashr)]        ;           bvlshr | bvashr
  [uop
   (choose bvneg bvnot)])         ; <uop>  := bvneg | bvnot

(define (crypt x i)
  (fast-int8 x i #:depth 3))

一旦完成,我们可以基于已知的明文/加密对及其位置(字节计数器i)定义约束。然后我们向Rosette询问满足约束的crypt程序的实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(define sol (solve
  (assert
; removing constraints speed things up
    (&& (bveq (crypt (int8 #x62) (int8 0)) (int8 #x3d))
; [...]        
        (bveq (crypt (int8 #x69) (int8 7)) (int8 #x3d))
        (bveq (crypt (int8 #x06) (int8 #x16)) (int8 #x20))
        (bveq (crypt (int8 #x5e) (int8 #x17)) (int8 #x73))
        (bveq (crypt (int8 #x5e) (int8 #x18)) (int8 #x75))
        (bveq (crypt (int8 #xe8) (int8 #x19)) (int8 #x62))
; [...]        
        (bveq (crypt (int8 #xc3) (int8 #xe0)) (int8 #x3a))
        (bveq (crypt (int8 #xef) (int8 #xff)) (int8 #x20))
        )
    )
  ))

(print-forms sol)

运行racket rosette.rkt并等待几分钟后,我们得到以下输出:

1
2
3
4
5
(list 'define '(crypt x i)
 (list
  'bvor
  (list 'bvlshr '(bvsub i x) (list 'bvadd (bv #x87 8) (bv #x80 8)))
  '(bvsub (bvadd i i) (bvadd x x))))

这是一个有效的解密程序!但它有点不整洁。所以让我们把它转换成C,并进行简单的简化:

1
2
3
4
uint8_t crypt(uint8_t i, uint8_t x) {
    uint8_t t = i-x;
    return (((2*t)&0xFF)|((t>>((0x87+0x80)&0xFF))&0xFF))&0xFF;
}

并使用gcc -m32 -O2通过https://godbolt.org编译它以获得优化版本:

1
2
3
4
mov     al, byte ptr [esp+4]
sub     al, byte ptr [esp+8]
rol     al
ret

所以我们的加密算法是一个简单的ror(x-i, 1)

漏洞利用设置

在我们解密了固件并注意到串口之后,我们决定建立一个环境,以便于我们利用漏洞。

我们在与打印机相同的网络上设置了一个Raspberry Pi,该打印机也连接到打印机的串口。通过这种方式,我们可以远程利用漏洞,同时通过串口提供的许多功能控制打印机的状态。

串口:dry shell

串口让我们访问了上述的dry shell,它在我们的利用尝试期间提供了难以置信的帮助来理解/控制打印机状态和调试它。

在提供的许多强大功能中,以下是最有用的:

  • 执行完整内存转储的能力:一种检索更新固件未加密的简单快速方法。
  • 执行基本文件系统操作的能力。
  • 列出运行任务及其关联内存段的能力。
  • 启动FTP守护进程的能力,这将在以后派上用场。
  • 检查特定地址内存内容的能力。

这个功能在利用尝试期间被大量使用以了解发生了什么。一个烦人的事情是存在一个看门狗,如果HTTP守护进程崩溃,它会重新启动整个打印机。我们必须在任何利用尝试后快速运行此命令。

漏洞分析

攻击面

Pwn2Own规则规定,如果有身份验证,应该绕过它。因此,获胜的最简单方法是在非身份验证功能中找到漏洞。这包括明显的事情,如:

  • 打印功能和协议
  • 各种网页
  • HTTP服务器
  • SNMP服务器

我们首先枚举了Web服务器处理的"常规"网页(通过检查代码中注册的页面),包括奇怪的/elf/子页面。然后我们意识到固件中还有其他可用的URL,这些URL显然不是由通常的代码处理的:/privet/,用于基于云的打印。

漏洞函数

逆向工程固件相当直接,即使二进制文件很大。CPU是标准的ARMv7。通过逆向处理程序,我们很快找到了以下函数。注意,所有名称都是手动添加的,要么取自调试日志字符串,要么在逆向之后:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall ntpv_isXPrivetTokenValid(char *token)
{
  int tklen; // r0
  char *colon; // r1
  char *v4; // r1
  int timestamp; // r4
  int v7; // r2
  int v8; // r3
  int lvl; // r1
  int time_delta; // r0
  const char *msg; // r2
  char buffer[256]; // [sp+4h] [bp-174h] BYREF
  char str_to_hash[28]; // [sp+104h] [bp-74h] BYREF
  char sha1_res[24]; // [sp+120h] [bp-58h] BYREF
  int sha1_from_token[6]; // [sp+138h] [bp-40h] BYREF
  char last_part[12]; // [sp+150h] [bp-28h] BYREF
  int now; // [sp+15Ch] [bp-1Ch] BYREF
  int sha1len; // [sp+164h] [bp-14h] BYREF

  bzero(buffer, 0x100u);
  bzero(
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计