1import hashlib
2import io
3from pathlib import Path
4
5from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
6from structlog import get_logger
7
8from unblob.file_utils import File, InvalidInputFormat
9from unblob.models import (
10 Endian,
11 Extractor,
12 HandlerDoc,
13 HandlerType,
14 HexString,
15 Reference,
16 StructHandler,
17 StructParser,
18 ValidChunk,
19)
20
21logger = get_logger()
22
23C_DEFINITIONS = r"""
24
25 typedef struct dlink_shrs_header {
26 char magic[4]; /* SHRS */
27 uint32 file_size; /* Length of decrypted firmware in bytes */
28 uint32 file_size_no_padding; /* Length of decrypted firmware - padding */
29 char iv[16]; /* Length of AES 128 cbc IV */
30 char decrypted_key_digest[64]; /* SHA512 64 byte message digest of decrypted firmware + key */
31 char decrypted_digest[64]; /* SHA512 64 byte message digest of decrypted firmware */
32 char encrypted_digest[64]; /* SHA512 64 byte message digest of encrypted firmware */
33 char unused[512]; /* 512 unused NULL bytes (0xdc to 0x2dc) */
34 char signature_1[512]; /* 512 byte Signature 1 */
35 char signature_2[512]; /* 512 byte Signature 2 */
36 } dlink_header_t;
37"""
38
39KEY = bytes.fromhex("c05fbf1936c99429ce2a0781f08d6ad8")
40sha512 = hashlib.sha512()
41
42
43class SHRSExtractor(Extractor):
44 def __init__(self):
45 self._struct_parser = StructParser(C_DEFINITIONS)
46
47 def extract(self, inpath: Path, outdir: Path):
48 outpath = outdir.joinpath(f"{inpath.name}.decrypted")
49 with File.from_path(inpath) as file:
50 header = self._struct_parser.parse(
51 "dlink_header_t", file, endian=Endian.BIG
52 )
53 cipher = Cipher(algorithms.AES(KEY), modes.CBC(header.iv))
54 decryptor = cipher.decryptor()
55 outfile = outpath.open("wb")
56 ciphertext = file.read(1024)
57 while ciphertext and len(ciphertext) % 16 == 0:
58 plaintext = decryptor.update(ciphertext)
59 outfile.write(plaintext)
60 ciphertext = file.read(1024)
61 outfile.write(decryptor.finalize())
62 outfile.close()
63
64
65class SHRSHandler(StructHandler):
66 NAME = "shrs"
67
68 PATTERNS = [HexString("53 48 52 53")]
69
70 C_DEFINITIONS = C_DEFINITIONS
71 HEADER_STRUCT = "dlink_header_t"
72 EXTRACTOR = SHRSExtractor()
73
74 DOC = HandlerDoc(
75 name="D-Link SHRS",
76 description="SHRS is a D-Link firmware format with a custom header containing metadata, SHA-512 digests, and AES-CBC encryption parameters. The firmware data is encrypted using a fixed key and IV stored in the header.",
77 handler_type=HandlerType.ARCHIVE,
78 vendor="D-Link",
79 references=[
80 Reference(
81 title="Breaking the D-Link DIR3060 Firmware Encryption - Recon - Part 1",
82 url="https://0x00sec.org/t/breaking-the-d-link-dir3060-firmware-encryption-recon-part-1/21943", # Replace with actual reference if available
83 )
84 ],
85 limitations=[],
86 )
87
88 def is_valid_header(self, header, file: File) -> bool:
89 if header.file_size < len(header):
90 return False
91 # we're exactly past the header, we compute the digest
92 digest = hashlib.sha512(file.read(header.file_size_no_padding)).digest()
93 # we seek back to where we were
94 file.seek(-header.file_size_no_padding, io.SEEK_CUR)
95 return digest == header.encrypted_digest
96
97 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
98 header = self.parse_header(file, endian=Endian.BIG)
99
100 if not self.is_valid_header(header, file):
101 raise InvalidInputFormat("Invalid SHRS header.")
102
103 cipher = Cipher(algorithms.AES(KEY), modes.CBC(header.iv))
104 decryptor = cipher.decryptor()
105 read_bytes = 0
106
107 while read_bytes < header.file_size:
108 read_bytes += len(decryptor.update(file.read(1024)))
109 end_offset = file.tell()
110
111 return ValidChunk(start_offset=start_offset, end_offset=end_offset)