1# Shared infrastructure for QNAP firmware handlers.
2import io
3from pathlib import Path
4
5from unblob.file_utils import File, FileSystem, iterate_file
6from unblob.models import (
7 Endian,
8 Extractor,
9 ExtractResult,
10 StructParser,
11)
12
13FOOTER_LEN = 74
14
15NAS_DEVICE_ID_PREFIX = "QNAPNAS"
16
17C_DEFINITIONS = """
18 typedef struct qnap_header {
19 char magic[6];
20 uint32 encrypted_len;
21 char device_id[16];
22 char file_version[16];
23 char firmware_date[16];
24 char revision[16];
25 } qnap_header_t;
26"""
27
28
29# https://gist.github.com/ulidtko/966277a465f1856109b2d2674dcee741#file-qnap-qts-fw-cryptor-py-L114
30class Cryptor:
31 def __init__(self, secret):
32 self.secret = list(bytes(secret, "ascii"))
33 self.n = len(secret) // 2
34 if self.n % 2 == 0:
35 self.secret.append(0)
36 self.precompute_k()
37 self.acc = 0
38 self.y = 0
39 self.z = 0
40
41 def scan(self, f, xs, s0):
42 s = s0
43 for x in xs:
44 w, s = f(s, x)
45 yield w
46
47 def promote(self, char):
48 return char if char < 0x80 else char - 0x101
49
50 def precompute_k(self):
51 self.k = {acc: self.table_for_acc(acc) for acc in range(256)}
52
53 def table_for_acc(self, a):
54 ks = [
55 0xFFFFFFFF
56 & (
57 (self.promote(self.secret[2 * i] ^ a) << 8)
58 + (self.secret[2 * i + 1] ^ a)
59 )
60 for i in range(self.n)
61 ]
62
63 def kstep(st, q):
64 x = st ^ q
65 y = self.lcg(x)
66 z = 0xFFFF & (0x15A * x)
67 return (z, y), y
68
69 return list(self.scan(kstep, ks, 0))
70
71 def lcg(self, x):
72 return 0xFFFF & (0x4E35 * x + 1)
73
74 def kdf(self):
75 """self.secret -> 8bit hash (+ state effects)."""
76 tt = self.k[self.acc]
77 res = 0
78 for i in range(self.n):
79 yy = self.y
80 self.y, t2 = tt[i]
81 self.z = 0xFFFF & (self.y + yy + 0x4E35 * (self.z + i))
82 res = res ^ t2 ^ self.z
83 hi, lo = res >> 8, res & 0xFF
84 return hi ^ lo
85
86 def decrypt_byte(self, v):
87 k = self.kdf()
88 r = 0xFF & (v ^ k)
89 self.acc = self.acc ^ r
90 return r
91
92 def decrypt_chunk(self, chunk):
93 return bytes(map(self.decrypt_byte, chunk))
94
95
96class QnapExtractor(Extractor):
97 def __init__(self):
98 self._struct_parser = StructParser(C_DEFINITIONS)
99
100 def _get_secret(self, header) -> str:
101 raise NotImplementedError
102
103 def extract(self, inpath: Path, outdir: Path) -> ExtractResult:
104 fs = FileSystem(outdir)
105 with (
106 File.from_path(inpath) as infile,
107 fs.open(Path(f"{inpath.name}.decrypted"), "wb+") as outfile,
108 ):
109 infile.seek(-FOOTER_LEN, io.SEEK_END)
110 header = self._struct_parser.parse("qnap_header_t", infile, Endian.LITTLE)
111 cryptor = Cryptor(self._get_secret(header))
112 for chunk in iterate_file(infile, 0, header.encrypted_len, 1024):
113 outfile.write(cryptor.decrypt_chunk(chunk))
114 for chunk in iterate_file(
115 infile,
116 header.encrypted_len,
117 infile.size() - FOOTER_LEN - header.encrypted_len,
118 1024,
119 ):
120 outfile.write(chunk)
121 return ExtractResult(reports=fs.problems)