HP 打印机固件解密分析

Catalpa 网络安全爱好者

本文介绍作者在研究 HP 某型号打印机时遇到的固件加密问题,并在分析固件加密方案后编写了一组工具可以用于解密此类固件。

目标简介

HP 公司在全球打印机市场占据重要份额,是”打印王国”的领头羊。近年来随着全球网络安全形势日益复杂,类似打印机等智能设备开始进入恶意攻击者的视线。同时在全球知名黑客比赛 Pwn2Own 中,HP 打印机作为比赛项目也时有登场。

在打印机市场中,利润率最高的通常并不是打印机硬件,而是各类耗材。因此市面上有很多人尝试使用第三方耗材来降低成本,而使用此类耗材就需要绕过打印机系统中的有关限制。厂商为了能够售出更多官方耗材,每次更新也会向系统中添加更多限制和检查,这是一个不断演进的过程,所以打印机厂商一般会选择应用固件加密等多种安全手段,来增加逆向破解打印机的难度。

按操作系统来说,HP 打印机主要有基于 Linux 以及 RTOS 两种,无论哪种系统,其固件加密方式应该是类似的,本次研究,我选择 HP OfficeJet Pro 8210 作为目标。OfficeJet Pro 8210 是一款发布时间较早的打印机,它使用了 Linux 作为底层操作系统,某种程度上会比 RTOS 更方便分析。

提取固件

近年来曾在 Pwn2Own 上成功破解 HP 打印机的团队如 DEVCORE、neodyme 等都发布过相关漏洞分析文章,但似乎并没有详细描述固件解密方法,其中 neodyme 团队在他们的文章中提到了部分固件加密策略,但没有发布解密工具:

1
2
3
1.A static XOR-”encrypted” key
2.The HP internal firmware project name
3.The checksum of the decompressed firmware blob

由于没有公开的固件解密方法,并且暂时也无法获取设备的 Shell 权限,我们考虑直接从设备 Flash 芯片中读取固件内容。将打印机拆机后取出主板,在主板背面可以找到一颗型号为 29f2g08abaea 的 NAND Flash 芯片,如下图所示(红色方框):

将 Flash 芯片用热风枪拆下,离线使用编程器读取内容,由于 NAND 芯片的特性,需要在本地将数据中的 ECC 等校验数据去除。

查询芯片手册发现,该芯片采用 Data area + Spare area 形式组织数据,每个 page 2112 字节,前面 2048 字节为用户数据,后面 64 字节为 ECC 数据,每个 Block 由 64 个 Page 组成,坏块标记 0x00。根据手册中的信息,可以编写脚本来处理原始数据,清除 ECC 校验内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def process_file(input_file_path, output_file_path):
chunk_size = 2112
bytes_to_write = 2048

try:
with open(input_file_path, 'rb') as input_file, open(output_file_path, 'wb') as output_file:
while True:
# Read a chunk of data
data = input_file.read(chunk_size)

# If less than 2112 bytes are read, it means end of file
if not data:
break

# Write only the first 2048 bytes to the output file
output_file.write(data[:bytes_to_write])

except IOError as e:
print(f"An error occurred: {e}")

# Example usage
input_file_path = 'READ1.BIN'
output_file_path = 'OUTPUT.BIN'
process_file(input_file_path, output_file_path)

对处理后的文件进行 binwalk,结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
133120 0x20800 PC bitmap, Windows 3.x format,, 480 x 272 x 24
777188 0xBDBE4 Zlib compressed data, best compression
777224 0xBDC08 Zlib compressed data, best compression
777260 0xBDC2C Zlib compressed data, best compression
777296 0xBDC50 Zlib compressed data, best compression
777332 0xBDC74 Zlib compressed data, best compression
777368 0xBDC98 Zlib compressed data, best compression
777400 0xBDCB8 Zlib compressed data, best compression
777452 0xBDCEC Zlib compressed data, best compression
777488 0xBDD10 Zlib compressed data, best compression
778040 0xBDF38 Zlib compressed data, best compression
778548 0xBE134 Zlib compressed data, best compression
781700 0xBED84 Zlib compressed data, best compression
781812 0xBEDF4 Zlib compressed data, best compression
782452 0xBF074 Zlib compressed data, best compression
782552 0xBF0D8 Zlib compressed data, best compression
787668 0xC04D4 Zlib compressed data, best compression
792384 0xC1740 Zlib compressed data, best compression
793424 0xC1B50 Zlib compressed data, best compression
794692 0xC2044 Zlib compressed data, best compression
796292 0xC2684 Zlib compressed data, best compression
796376 0xC26D8 Zlib compressed data, best compression
797912 0xC2CD8 Zlib compressed data, best compression
802320 0xC3E10 Zlib compressed data, best compression
802920 0xC4068 Zlib compressed data, best compression
803168 0xC4160 Zlib compressed data, best compression
803320 0xC41F8 Zlib compressed data, best compression
805900 0xC4C0C Zlib compressed data, best compression
806484 0xC4E54 Zlib compressed data, best compression
811676 0xC629C Zlib compressed data, best compression
811793 0xC6311 Zlib compressed data, best compression
812456 0xC65A8 Zlib compressed data, best compression
813092 0xC6824 Zlib compressed data, best compression
814160 0xC6C50 Zlib compressed data, best compression
817516 0xC796C Zlib compressed data, best compression
822232 0xC8BD8 Zlib compressed data, best compression
822932 0xC8E94 Zlib compressed data, best compression
823988 0xC92B4 Zlib compressed data, best compression
824100 0xC9324 Zlib compressed data, best compression
825260 0xC97AC Zlib compressed data, best compression
825772 0xC99AC Zlib compressed data, best compression
825876 0xC9A14 Zlib compressed data, best compression
826408 0xC9C28 Zlib compressed data, best compression
4030464 0x3D8000 Linux kernel ARM boot executable zImage (little-endian)
4048440 0x3DC638 gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date)
7483392 0x723000 Flattened device tree, size: 11781 bytes, version: 17
23461888 0x1660000 UBI erase count header, version: 1, EC: 0x1, VID header offset: 0x800, data offset: 0x1000
261621760 0xF980800 PC bitmap, Windows 3.x format,, 480 x 272 x 24
262772736 0xFA99800 Linux kernel ARM boot executable zImage (little-endian)
262790712 0xFA9DE38 gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date)
266225664 0xFDE4800 Flattened device tree, size: 11802 bytes, version: 17

这样就拿到了未加密的固件数据。

解密分析

从 binwalk 结果来看,该设备使用的是 ARM 架构 CPU 和 Linux 系统,同时使用 UBI 作为文件系统。将固件中 UBI 的部分提取出来,使用 UBI Reader 解压。

解压后得到一个标准的 Linux 根文件系统,下一步要确定是哪个程序负责执行固件升级操作。我们先从 HP 官方网站下载 OfficeJet Pro 8210 的升级包,尝试通过升级包中的信息来定位升级程序。

下载到的升级包为 exe 格式,可以从中解压出 .ful2 格式的更新文件,此文件基本是一个 PJL 格式的描述文件,也就是说,当在本地执行升级程序时,会将这个更新文件发送到打印机 PJL 服务中,由 PJL 执行升级动作。

从 PJL 命令中能够定位到一些有用的信息

1
2
3
4
5
@PJL COMMENT MODEL=HP OfficeJet Pro 8210
@PJL COMMENT VERSION=TESPDLPP1N001.2514A.00
@PJL COMMENT DATECODE=20250403
@PJL UPGRADE SIZE=64534679
@PJL ENTER LANGUAGE=FWUPDATE2

从上述信息可以知道,这个升级包用于 HP OfficeJet Pro 8210 设备,它的版本是 2514A,升级包大小为 64534679,PJL 内部需要调用 FWUPDATE2 命令来处理后续数据。

在解压出的文件系统中搜索 FWUPDATE2 字符串,能够定位到 plang 程序,简单逆向后发现它只是一个 PJL 解析服务,不包含和固件解密/升级有关的操作。

继续分析升级包内容,其中还包含一段 XML 格式的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version='1.0' encoding='UTF-8'?>
<manifest xsi:noNamespaceSchemaLocation='webfwupdate.xsd' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
<version>0.9</version>
<signature>
<signature_template_id>49be5195-1daa-474a-a957-1aa4ae38d9b1</signature_template_id>
<public_key_id>bdfcc564-a078-4112-a089-179e73831f27</public_key_id>
<signature_value>AgIAAAFrABQBADp+CGMl0KV41veITeohrqqhhpjUIN8AUS5suiRA4lQ8bt7g7U6gBo8B7roClXh8V3rDBfUdki+BkMXMcMVuKJLeThLxSuq09d0NrZxcbpgAe/VON/rD0JGSFGWUDHoJOqyTNipxgDzUxwnGBf/bC5IYIUsDo8Y24jZ0ItuVL3ZUBoQVUuyrskc3pMQXyWO3inuUGYezvppaiMxTwzeKujpqSMt7bp6quPXVXuapc26GXvtbZKgCQMjhJi2mImoWu5uAbb8ll/z57wWV9zmF0vhGBFts4l3dwBzEyRnjQ3NSGhJG6/cHskiUYwM77ADnIpWNKu1+9JzVfZKqw8XFFe8=</signature_value>
<digest>E3p+JOq7kzU0lzf141hIpQypfz9Dlm6RhxSEiQN9mdc=</digest>
</signature>
<signedInfo>
<update_type>optional</update_type>
<current_revision>ANY</current_revision>
<updated_revision>TESPDLPP1N001.2514A.00</updated_revision>


<LBI_blob>
<blob_path>TESPDLPP1N001.2514A.00/lbi_blob.TESPDLPP1N001.2514A.00_from_ANY</blob_path>
<size_compressed>6555032</size_compressed>
<size_uncompressed>7406784</size_uncompressed>
<blob_digest_compressed>Mh5oeVTudC4g/nbC312B8jpmvWlzLg/SxOwtfmXTN9I=</blob_digest_compressed>
<blob_digest_uncompressed>JbxisevJymAZhQ8qfCJNJ10V7jDbllnsHgaPmuRfRKo=</blob_digest_uncompressed>
</LBI_blob>
<rootfs_blob>
<blob_path>TESPDLPP1N001.2514A.00/rootfs.TESPDLPP1N001.2514A.00_from_ANY</blob_path>
<size_compressed>57977683</size_compressed>
<size_uncompressed>77463552</size_uncompressed>
<blob_digest_compressed>wlOQbDzmdmHyTKgnzvzZ9L0xr7+sWTS775Ku0fF3R8Q=</blob_digest_compressed>
<blob_digest_uncompressed>Ny6XZGGWxpzhg7gQLquITQ7TBEh/CR0FVLeCOMquNcU=</blob_digest_uncompressed>
</rootfs_blob>
</signedInfo>
</manifest>

这段数据包含升级包的版本、校验和等信息,那么系统在执行升级操作时肯定会对这段数据进行解析,所以尝试搜索其中的 xml 标签名,例如 signature_template_id,最终定位到 fwupd 程序,根据文件名也大致能确定这个程序和固件升级有关。

逆向分析此程序,搜索 xml 标签名定位到函数 A,我将之命名为 do_parse_firmware_xml,这个函数首先解析上面这段 xml 数据,可以称之为元数据,函数将解析得到的各个部分保存到 /sirius/rw/var/lib/fwupd/fwupd_manifest.sig 文件中。然后计算这段 xml 数据的 SHA256 值,和其中的 digest 进行比较,通过之后程序将 fwupd_manifest.sig 文件作为参数调用send_to_check_sig 函数,进行签名验证操作。

以上初步验证通过后,程序还会对版本做一定的检查,这可能是为了防止固件降级,但由于大部分固件这个字段都是 ANY,所以可以忽略这个检查。

在完成上面的分析之后,后续程序如何对固件进行解密就无法直接定位了,因为程序采用 C++ 编写,包含大量虚函数和间接调用,难以直接通过函数引用定位关键逻辑。这时想起 neodyme 文章中提及,固件是采用 AES 算法进行加密的,所以在程序中搜索和 AES 相关的 API 名称,找到了 gen_aes_key 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall gen_aes_key(aes_object *aes_object, char *part4, int is_any)
{
// ...

qmemcpy(part1, "@* WebFWUpdate", 14);
result = SHA256_Init(v15);
if ( result )
{
get_unknown_part(&part2);
get_unknown_part2(&part3);
SHA256_Update(v15, part1, 14);
SHA256_Update(v15, part2, *(part2 - 12));
if ( !is_any )
SHA256_Update(v15, part3, *(part3 - 12));
SHA256_Update(v15, part4, 32);
SHA256_Final(v14, v15);
AES_set_decrypt_key(v14, 128, aes_object->KEY);
//. ...
}
// ...
}

这段代码调用了 AES_set_decrypt_key 函数来设置 AES 密钥,也是 fwupd 程序中唯一一处设置密钥的位置。分析函数逻辑,AES 密钥应该是 SHA256("@* WebFWUpdate" + part2 + part3 + part4) 取结果的前 16 字节。

part2 是在 get_unknown_part 函数中赋值的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::string *__fastcall get_unknown_part(int p_part2)
{
std::string *p_part2_1; // r0

if ( check_unknown_flag(&dword_4B570) )
{
std::string::string(p_part2, unknown1);
return p_part2;
}
else
{
p_part2_1 = p_part2;
*p_part2 = &maybe_empty_string;
}
return p_part2_1;
}

实际上就是把 unknown1 赋值给 part2,通过交叉引用又找到了给 unknown1 赋值的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
stream = fopen("/rfs/.bio_pkg", "rb");
if ( stream )
{
v26 = &maybe_empty_string;
while ( fgets(data, 100, stream) )
{
if ( !strncmp(data, "BIO_FW_REV=", 11u) )
{
// ...
else
{
data[0] = tolower(*v12);
data[1] = tolower(v12[1]);
data[2] = tolower(v12[2]);
data[3] = tolower(v12[3]);
data[4] = tolower(v12[4]);
data[5] = tolower(v12[5]);
data[6] = 0;
std::string::string(&v27, data, &v26);
}
unknown1_1 = ::unknown1;
// ...

实际上是将 /.bio_pkg 文件中的 BIO_FW_REV 字段的前 6 个字节转换为小写赋值给 unknown1,进一步分析,就是将固件的开发代号赋值给 part2,以 OfficeJet Pro 8210 为例,这个固件代号是 xml 元数据中 updated_revision 参数的前六个字符即 tespdl

part3 是否要参与运算取决于 is_any 参数的值,实际上就是固件中的 current_revision,如果它是 ANY 的话就不需要考虑 part3。

part4 是外部传入的参数,由于缺少函数调用关系,无法直接确定它是什么数据。依次分析可能的函数,最终找到了 do_upgrade,它应该是负责固件解密和升级的关键函数。此函数首先生成 AES 密钥,然后进入一个循环,在循环中判断当前正在处理哪个 blob,接着通过 fork 生成子进程,等待管道信息。

在父进程中,会调用 decrypt_data 函数来对数据做 AES 解密。解密是分块进行的,每 0x10000 作为一个 block,每解密一块数据,就调用 inflate 函数对数据进行 zlib 解压。

通过分析 do_upgrade 的解密和解压逻辑,能够进一步确定升级包的文件格式:

1
2
3
4
5
6
7
8
9
10
11
12
Magic 区块: \x1b%-12345@PJL\x0a
固件基础信息区块: @PJL COMMENT xxx=xxx\x0a
固件大小: @PJL UPGRADE SIZE=xxx\x0a
Magic 区块结束: \x1b%-12345@PJL COMMENT (null)\x0a
进入固件升级命令: @PJL ENTER LANGUAGE=FWUPDATE2\x0a
Metadata 长度: 16 进制 xxx\x0a
Metadata: XML 格式的数据,包含关键信息
blob 长度: 16 进制 xxx\x0a
blob 数据: xxx
blob 长度: 16 进制 xxx\x0a
blob 数据: xxx
...

关键点在于解压数据之后,会调用 fwrite 将数据写入本地文件,然后调用 SHA256_Update 更新一个 SHA256 值。当全部数据都处理完毕,代码会将计算得到的 SHA256 值和变量 x 做比较,而这个变量 x 恰好是用来生成 AES 密钥的参数 part4,因此可以推测 part4 就是固件中对应 blob 的 blob_digest_uncompressed 值。

现在来总结一下固件解密过程,首先是 xml 元数据中,一些标签的含义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
signature_template_id: 未知
public_key_id: 用于检查固件完整性的公钥 ID
signature_value: 固件签名数据
digest: Metadata 数据的 SHA256 值

其中,digest 验证方法为:获取 signedInfo 中的每个字符串值,拼接起来计算 SHA256 值,与 signature 中的 digest 进行比较。

update_type: 固件升级类型,一般为 optional
current_revision: 使用该固件升级的版本限制,为 ANY 时表示无版本限制
updated_revision: 该固件的版本

LBI_blob、rootfs_blob: 固件的主体数据,其中 rootfs_blob 就是文件系统。一般情况下固件只包含 LBI 和 rootfs,还可以包含 patch、recovery 等 blob

blob_path: blob 解密后存放的逻辑位置
size_compressed: 压缩后的 blob 大小
size_uncompressed: 解压后的 blob 大小
blob_digest_compressed: 压缩后的 blob Checksum (SHA256)
blob_digest_uncompressed: 解压后的 blob Checksum (SHA256)

具体的固件解密方式:

  1. 通过解析 Metadata,可以将各个 blob 从固件中分割出来。
  2. 首先要计算一个 AES 密钥,计算方式为 SHA256("@* WebFWUpdate" + 固件代号 + blob_digest_uncompressed)[:16],IV 为 0x00 * 16。
  3. 使用计算得到的 AES 密钥对 blob 进行解密,解密得到的数据计算 SHA256 值应该和 blob_digest_compressed 相同。
  4. 对解密的 blob 进行 zlib inflate 解压缩,得到的数据长度应该和 size_uncompressed 相同,计算 SHA256 值应该和 blob_digest_uncompressed 相同。

综合以上思路,我编写了一个小工具可以用来解密 HP 打印机固件(可能无法覆盖全部型号),你可以在 GitHub 上获取。

参考链接

https://neodyme.io/en/blog/pwn2own-2022_printer_rce/
https://devco.re/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2/
https://vegamay.li/printer-part2
https://www.jsof-tech.com/unpacking-hp-firmware-updates-part-1/

  • Title: HP 打印机固件解密分析
  • Author: Catalpa
  • Created at : 2025-08-06 00:00:00
  • Updated at : 2025-08-06 17:34:23
  • Link: https://wzt.ac.cn/2025/08/06/hp_firmware_dec/
  • License: This work is licensed under CC BY-SA 4.0.
On this page
HP 打印机固件解密分析