The following instructions guides a reverser into unpacking an HP firmware. Take care of choosing a complete firmware, and not a firmware incremental update.
The unpacking differs from https://vegamay.li/printer-part2, as it takes advantage of existing tools : pcldumper
and srecords
.
Prerequisites :
sudo apt-get install srecord
Create a new directory, and place your FUL firmware inside.
Use pcldumper
to decode the PCL commands :
docker run --rm -it -v $(realpath [FIRMWARE_FUL_FILE]):/firmware_ful anapsix/alpine-java bash -c '{ apk update && apk add wget && wget https://github.com/kevLmurphy/pcldumper/releases/download/1.4/pcldumper-1.4-all.jar; } &>/dev/null && java -jar pcldumper-1.4-all.jar /firmware_ful -v' > firmware.pcldumper
Then, use this script to unpack the decompressed firmware, take care of editing "# Full path to "firmware.pcldumper" without prefix !" following line :
from enum import Enum
import re
import struct
import subprocess
class SRecordType(Enum):
S0 = 2 # S0: Header, 16-bit address
S1 = 2 # S1: Data, 16-bit address
S2 = 3 # S2: Data, 24-bit address
S3 = 4 # S3: Data, 32-bit address
S4 = 0 # S4: Reserved, no address bytes
S5 = 2 # S5: Count, 16-bit count (not address)
S6 = 3 # S6: Count, 24-bit count (not address)
S7 = 4 # S7: Start Address (Termination), 32-bit address
S8 = 3 # S8: Start Address (Termination), 24-bit address
S9 = 2 # S9: Start Address (Termination), 16-bit address
def srec_crc(raw: bytes) -> int:
return 0xFF - (sum(raw) & 0xFF)
def from_twos_complement(value, bits=8):
"""
Convert a two's complement number to its original positive integer.
:param value: The two's complement number (as an integer).
:param bits: The bit-width of the number (default is 8).
:return: The original positive integer.
"""
# Check if the sign bit is set (i.e., if the number is negative)
if (value & (1 << (bits - 1))) != 0:
# Perform two's complement to find the positive representation
value = value - (1 << bits)
return abs(value)
def convert_custom_srec(raw: bytes, index):
i_1_str = f"{raw[index]:x}".zfill(2)
if not i_1_str.startswith("3"):
return None
rec_type = "S" + i_1_str[1]
try:
SRecordType[rec_type].value
except:
return None
byte_count = int.from_bytes(raw[index + 1 : index + 2], "little")
want = raw[index + 1 : index + byte_count + 2]
size = 1 + byte_count + 1
crc = want[-1]
check_crc = srec_crc(want[:-1])
if crc != check_crc:
return None
toret = rec_type + want.hex() + "\n"
return toret, size
def tiff_uncompress(data):
"""
#res = decode_tiff_packbits(b"\x08JSOFrulez\xFB!\xF81")
#assert(res == b"JSOFrulez!!!!!!111111111")
"""
decoded = bytearray()
i = 0
while i < len(data):
control_byte = data[i]
i += 1
# Literal
if control_byte < 128:
for _ in range(control_byte + 1):
decoded.append(data[i])
i += 1
# Run-length encoded
elif control_byte > 128:
# Twos complement
length = from_twos_complement(control_byte)
for _ in range(length + 1):
decoded.append(data[i])
i += 1
else:
print("NO OP DETECTED")
exit()
return decoded
def delta_row_uncompress(data: bytes, seed_row: bytes):
seed = bytearray()
seed.extend(seed_row)
seedOffset = 0
decoded = bytearray()
while True:
if len(data) == 0:
break
command = struct.unpack("B", data[:1])[0]
bytesToReplace = ((command & 0b11100000) >> 5) + 1
deltaOffset = command & 0b00011111
if deltaOffset == 31:
print("Unhandled 31 offset")
exit(-1)
deltaBytes = data[1 : 1 + bytesToReplace]
for i in range(bytesToReplace):
seed[seedOffset + deltaOffset + i] = deltaBytes[i]
seedOffset += bytesToReplace + deltaOffset
data = data[1 + bytesToReplace :]
decoded.extend(seed)
return decoded
def pcldumper_parser(filepath: str, output_filepath: str):
with open(filepath, "r") as f:
content = f.read()
decompress_func = "Unencoded"
is_transfer_raster = False
raster_data = b""
decompressed = b""
specs = {}
for line in content.splitlines():
line = line.strip()
spec_check = re.findall("[*]r(\\d+)\\w \\s*Source Raster (\\S+)", line)
if spec_check != []:
specs[spec_check[0][1]] = int(spec_check[0][0])
if is_transfer_raster and line.startswith(">>>"):
raster_data += bytes.fromhex(
re.findall(" ((?:[A-F0-9]{2} ){1,16})", line)[0]
)
elif is_transfer_raster and not line.startswith(">>>"):
is_transfer_raster = False
if decompress_func == "Unencoded":
pass
elif decompress_func == "Tagged Imaged File Format":
raster_data = tiff_uncompress(raster_data)
elif decompress_func == "Delta row compression":
raster_data = delta_row_uncompress(raster_data, seed_row)
else:
print(f"Unhandled compression method : {decompress_func}")
exit(1)
# Pad (section 6-33 of PCL doc)
if transfer_type == "Plane" and len(raster_data) != specs["Width"]:
raster_data += b"\x00" * (specs["Width"] - len(raster_data))
decompressed += raster_data
seed_row = raster_data
raster_data = b""
compression_check = re.findall("Set Compression Method [(]([^)]+)[)]", line)
if compression_check != []:
decompress_func = compression_check[0]
if not line.startswith(">>>"):
transfer_raster = re.findall(
"[*]b(\\d+)\\w \\s*Transfer Raster Data By (\\S+)", line
)
if transfer_raster == [] and "Transfer Raster Data" in line:
exit("Regex mismatch")
if transfer_raster != []:
transfer_size, transfer_type = transfer_raster[0]
is_transfer_raster = True
with open(output_filepath, "wb") as f:
f.write(decompressed)
return decompressed
def parse_srec_from_decompressed(srec_raw: bytes, output_filepath: str):
index = 0
cust_srec = ""
while index < len(srec_raw):
try:
srec, read = convert_custom_srec(srec_raw, index)
except Exception as e:
print(f"Errored out at {index} : {srec_raw[index:index+32].hex()}")
break
cust_srec += srec.upper()
index += read
with open(output_filepath, "w+") as f:
f.write(cust_srec)
# Full path to "firmware.pcldumper" without prefix !
fn = "[PATH_FIRMWARE_PCLDUMPER_WITHOUT_PREFIX]"
# Decompress the firmware
decompressed = pcldumper_parser(
f"{fn}.pcldumper",
f"{fn}.decompressed",
)
# Separate the bootloader and the binary srec records
ascii_bootloader_srec, raw_bin_srec = re.split(
b"F[A-Z0-9]+\x0aP[A-Z0-9]+\x0a", decompressed, 1
)
# Write plain SREC bootloader records to disk
# Inspect or manipulate with srec_info or srec_cat
# Strip the "SA" record at the beginning
with open(f"{fn}.bootloader_srec", "w+") as f:
f.write(ascii_bootloader_srec.decode())
# Convert custom binary srec to classic srec records
parse_srec_from_decompressed(raw_bin_srec, f"{fn}.flash_srec")
srec_info = subprocess.run(
["srec_info", f"{fn}.flash_srec"], stdout=subprocess.PIPE, text=True
)
print(srec_info.stdout)
data_start = re.findall("Data:\\s*([0-9A-Fa-f]+) - [0-9A-Fa-f]+", srec_info.stdout)[0]
# Convert raw flash srecords to binary
subprocess.run(
[
"srec_cat",
f"{fn}.flash_srec",
"-offset",
f"-0x{data_start}",
"-o",
f"{fn}.flash_with_oob",
"-binary",
]
)
# Strip OOB after each page
with open(f"{fn}.flash_with_oob", "rb") as f:
flash_with_oob = f.read()
flash = bytearray()
for i in range(0, len(flash_with_oob), 0x840):
flash.extend(flash_with_oob[i : i + 0x800])
# Save the nand flash to disk
# Beware, it needs to be window slide decompressed
# And sections needs to be load (see "Sirius Hacking")
with open(f"{fn}.flash", "wb") as f:
f.write(flash)
Then, you will need to load the flash in RAM, through a custom IDA loader for example (see https://vegamay.li/printer-part2).