Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/unblob/handlers/archive/dlink/shrs.py: 62%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

52 statements  

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)