Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/unblob/handlers/archive/qnap/qnap_networking.py: 57%

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

23 statements  

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)