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