1from unblob.file_utils import File
2from unblob.models import (
3 Endian,
4 HandlerDoc,
5 HandlerType,
6 HexString,
7 Reference,
8 StructHandler,
9 ValidChunk,
10)
11
12from ._qnap import C_DEFINITIONS, FOOTER_LEN, NAS_DEVICE_ID_PREFIX, QnapExtractor
13
14
15class QnapNetworkingExtractor(QnapExtractor):
16 def _get_secret(self, header) -> str:
17 return header.device_id.rstrip(b"\x00").decode("ascii")
18
19
20class QnapNetworkingHandler(StructHandler):
21 NAME = "qnap_networking"
22
23 PATTERNS = [
24 HexString("69 63 70 6e 61 73"), # "icpnas" footer signature
25 ]
26 C_DEFINITIONS = C_DEFINITIONS
27 HEADER_STRUCT = "qnap_header_t"
28 EXTRACTOR = QnapNetworkingExtractor()
29
30 DOC = HandlerDoc(
31 name="QNAP Networking",
32 description=(
33 "QNAP networking device firmware encrypted with the PC1 cipher. "
34 "The encryption key is self-describing: it is stored as the "
35 "device_id in the 74-byte 'icpnas' footer appended to the image, "
36 "unlike NAS firmware which uses a shared secret prefix."
37 ),
38 handler_type=HandlerType.ARCHIVE,
39 vendor="QNAP",
40 references=[
41 Reference(
42 title="Pwn2Own Ireland 2024: QNAP Qhora-322",
43 url="https://neodyme.io/en/blog/pwn2own-2024_qhora/",
44 ),
45 Reference(
46 title="QNAP firmware encryption/decryption (PC1)",
47 url="https://gist.github.com/galaxy4public/0420c7c9a8e3ff860c8d5dce430b2669",
48 ),
49 ],
50 limitations=[],
51 )
52
53 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
54 if start_offset != file.size() - FOOTER_LEN:
55 return None
56 header = self.parse_header(file, endian=Endian.LITTLE)
57
58 if header.encrypted_len == 0 or header.encrypted_len > start_offset:
59 return None
60
61 device_id = header.device_id.rstrip(b"\x00").decode("ascii", errors="replace")
62 if device_id.upper().startswith(NAS_DEVICE_ID_PREFIX):
63 return None
64
65 return ValidChunk(start_offset=0, end_offset=start_offset + FOOTER_LEN)