dissect.target 3.17.dev15__py3-none-any.whl → 3.17.dev18__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -258,3 +258,4 @@ register("vdi", "VdiContainer")
258
258
  register("hdd", "HddContainer")
259
259
  register("hds", "HdsContainer")
260
260
  register("split", "SplitContainer")
261
+ register("fortifw", "FortiFirmwareContainer")
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import gzip
4
+ import io
5
+ import logging
6
+ import zlib
7
+ from itertools import cycle, islice
8
+ from pathlib import Path
9
+ from typing import BinaryIO
10
+
11
+ from dissect.util.stream import AlignedStream, RangeStream, RelativeStream
12
+
13
+ from dissect.target.container import Container
14
+ from dissect.target.tools.utils import catch_sigpipe
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ def find_xor_key(fh: BinaryIO) -> bytes:
20
+ """Find the XOR key for the firmware file by using known plaintext of zeros.
21
+
22
+ File-like object ``fh`` must be seeked to the correct offset where it should decode to all zeroes (0x00).
23
+
24
+ Arguments:
25
+ fh: File-like object to read from.
26
+
27
+ Returns:
28
+ bytes: XOR key, note that the XOR key is not validated and may be incorrect.
29
+ """
30
+ key = bytearray()
31
+
32
+ pos = fh.tell()
33
+ buf = fh.read(32)
34
+ fh.seek(pos)
35
+
36
+ if pos % 512 == 0:
37
+ xor_char = 0xFF
38
+ else:
39
+ fh.seek(pos - 1)
40
+ xor_char = ord(fh.read(1))
41
+
42
+ for i, k_char in enumerate(buf):
43
+ idx = (i + pos) & 0x1F
44
+ key.append((xor_char ^ k_char ^ idx) & 0xFF)
45
+ xor_char = k_char
46
+
47
+ # align xor key
48
+ koffset = 32 - (pos & 0x1F)
49
+ key = islice(cycle(key), koffset, koffset + 32)
50
+ return bytes(key)
51
+
52
+
53
+ class FortiFirmwareFile(AlignedStream):
54
+ """Fortinet firmware file, handles transparant decompression and deobfuscation of the firmware file."""
55
+
56
+ def __init__(self, fh: BinaryIO):
57
+ self.fh = fh
58
+ self.trailer_offset = None
59
+ self.trailer_data = None
60
+ self.xor_key = None
61
+ self.is_gzipped = False
62
+
63
+ size = None
64
+
65
+ # Check if the file is gzipped
66
+ self.fh.seek(0)
67
+ header = self.fh.read(4)
68
+ if header.startswith(b"\x1f\x8b"):
69
+ self.is_gzipped = True
70
+
71
+ # Find the extra metadata behind the gzip compressed data
72
+ # as a bonus we can also calculate the size of the firmware here
73
+ dec = zlib.decompressobj(wbits=16 + zlib.MAX_WBITS)
74
+ self.fh.seek(0)
75
+ size = 0
76
+ while True:
77
+ data = self.fh.read(io.DEFAULT_BUFFER_SIZE)
78
+ if not data:
79
+ break
80
+ d = dec.decompress(dec.unconsumed_tail + data)
81
+ size += len(d)
82
+
83
+ # Ignore the trailer data of the gzip file if we have any
84
+ if dec.unused_data:
85
+ self.trailer_offset = self.fh.seek(-len(dec.unused_data), io.SEEK_END)
86
+ self.trailer_data = self.fh.read()
87
+ log.info("Found trailer offset: %d, data: %r", self.trailer_offset, self.trailer_data)
88
+ self.fh = RangeStream(self.fh, 0, self.trailer_offset)
89
+
90
+ self.fh.seek(0)
91
+ self.fh = gzip.GzipFile(fileobj=self.fh)
92
+
93
+ # Find the xor key based on known offsets where the firmware should decode to zero bytes
94
+ for zero_offset in (0x30, 0x40, 0x400):
95
+ self.fh.seek(zero_offset)
96
+ xor_key = find_xor_key(self.fh)
97
+ if xor_key.isalnum():
98
+ self.xor_key = xor_key
99
+ log.info("Found key %r @ offset %s", self.xor_key, zero_offset)
100
+ break
101
+ else:
102
+ log.info("No xor key found")
103
+
104
+ # Determine the size of the firmware file if we didn't calculate it yet
105
+ if size is None:
106
+ size = self.fh.seek(0, io.SEEK_END)
107
+
108
+ log.info("firmware size: %s", size)
109
+ log.info("xor key: %r", self.xor_key)
110
+ log.info("gzipped: %s", self.is_gzipped)
111
+ self.fh.seek(0)
112
+
113
+ # Align the stream to 512 bytes which simplifies the XOR deobfuscation code
114
+ super().__init__(size=size, align=512)
115
+
116
+ def _read(self, offset: int, length: int) -> bytes:
117
+ self.fh.seek(offset)
118
+ buf = self.fh.read(length)
119
+
120
+ if not self.xor_key:
121
+ return buf
122
+
123
+ buf = bytearray(buf)
124
+ xor_char = 0xFF
125
+ for i, cur_char in enumerate(buf):
126
+ if (i + offset) % 512 == 0:
127
+ xor_char = 0xFF
128
+ idx = (i + offset) & 0x1F
129
+ buf[i] = ((self.xor_key[idx] ^ cur_char ^ xor_char) - idx) & 0xFF
130
+ xor_char = cur_char
131
+
132
+ return bytes(buf)
133
+
134
+
135
+ class FortiFirmwareContainer(Container):
136
+ __type__ = "fortifw"
137
+
138
+ def __init__(self, fh: BinaryIO | Path, *args, **kwargs):
139
+ if not hasattr(fh, "read"):
140
+ fh = fh.open("rb")
141
+
142
+ # Open the firmware file
143
+ self.ff = FortiFirmwareFile(fh)
144
+
145
+ # seek to MBR
146
+ self.fw = RelativeStream(self.ff, 0x200)
147
+ super().__init__(self.fw, self.ff.size, *args, **kwargs)
148
+
149
+ @staticmethod
150
+ def detect_fh(fh: BinaryIO, original: list | BinaryIO) -> bool:
151
+ return False
152
+
153
+ @staticmethod
154
+ def detect_path(path: Path, original: list | BinaryIO) -> bool:
155
+ # all Fortinet firmware files end with `-FORTINET.out`
156
+ return str(path).lower().endswith("-fortinet.out")
157
+
158
+ def read(self, length: int) -> bytes:
159
+ return self.fw.read(length)
160
+
161
+ def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
162
+ return self.fw.seek(offset, whence)
163
+
164
+ def tell(self) -> int:
165
+ return self.fw.tell()
166
+
167
+ def close(self) -> None:
168
+ pass
169
+
170
+
171
+ @catch_sigpipe
172
+ def main(argv: list[str] | None = None) -> None:
173
+ import argparse
174
+ import shutil
175
+ import sys
176
+
177
+ parser = argparse.ArgumentParser(description="Decompress and deobfuscate Fortinet firmware file to stdout.")
178
+ parser.add_argument("file", type=argparse.FileType("rb"), help="Fortinet firmware file")
179
+ parser.add_argument("--verbose", "-v", action="store_true", help="verbose output")
180
+ args = parser.parse_args(argv)
181
+
182
+ if args.verbose:
183
+ logging.basicConfig(level=logging.INFO)
184
+
185
+ ff = FortiFirmwareFile(args.file)
186
+ shutil.copyfileobj(ff, sys.stdout.buffer)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.17.dev15
3
+ Version: 3.17.dev18
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -1,5 +1,5 @@
1
1
  dissect/target/__init__.py,sha256=Oc7ounTgq2hE4nR6YcNabetc7SQA40ldSa35VEdZcQU,63
2
- dissect/target/container.py,sha256=9ixufT1_0WhraqttBWwQjG80caToJqvCX8VjFk8d5F0,9307
2
+ dissect/target/container.py,sha256=0YcwcGmfJjhPXUB6DEcjWEoSuAtTDxMDpoTviMrLsxM,9353
3
3
  dissect/target/exceptions.py,sha256=VVW_Rq_vQinapz-2mbJ3UkxBEZpb2pE_7JlhMukdtrY,2877
4
4
  dissect/target/filesystem.py,sha256=VD1BA6hLqH_FPWFZ-wliEuCxnFrUK61S9VbGK7CtA5w,55597
5
5
  dissect/target/loader.py,sha256=_mrMOzKdpb7nlZJpLENOLuU4Ty92PzJem9GFDuo0PK4,7298
@@ -10,6 +10,7 @@ dissect/target/volume.py,sha256=aQZAJiny8jjwkc9UtwIRwy7nINXjCxwpO-_UDfh6-BA,1580
10
10
  dissect/target/containers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  dissect/target/containers/asdf.py,sha256=DJp0QEFwUjy2MFwKYcYqIR_BS1fQT1Yi9Kcmqt0aChM,1366
12
12
  dissect/target/containers/ewf.py,sha256=FTEPZpogDzymrbAeSnLuHNNStifLzNVhUvtbEMOyo0E,1342
13
+ dissect/target/containers/fortifw.py,sha256=2Ga89c0qPguHAPigcte8wptgF2aM9qfPTZHddkfQ8J4,5874
13
14
  dissect/target/containers/hdd.py,sha256=Y1qYpk3GePCpq2HZIyqyoGch7nzN8aeI3zWG3UGhf5o,1069
14
15
  dissect/target/containers/hds.py,sha256=xijSUSRM392Ckc9QsOsvjx7PMyeoR4qOlWGG0w4nqUU,1145
15
16
  dissect/target/containers/qcow2.py,sha256=FtXLZA-Xkegbv--dStusQntUiDqM1idSFWMtJRiL7eM,1128
@@ -335,10 +336,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
335
336
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
336
337
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
337
338
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
338
- dissect.target-3.17.dev15.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
339
- dissect.target-3.17.dev15.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
340
- dissect.target-3.17.dev15.dist-info/METADATA,sha256=Mz0QnC-O5ciS0rb8W_3OHKS9X9i4WjnzDJWZrLeePoI,11300
341
- dissect.target-3.17.dev15.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
342
- dissect.target-3.17.dev15.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
343
- dissect.target-3.17.dev15.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
344
- dissect.target-3.17.dev15.dist-info/RECORD,,
339
+ dissect.target-3.17.dev18.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
+ dissect.target-3.17.dev18.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
+ dissect.target-3.17.dev18.dist-info/METADATA,sha256=hkVxbE5ESL9e5xGrI4YY5UYYKF66WwuUIgRb8XRsaaQ,11300
342
+ dissect.target-3.17.dev18.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
+ dissect.target-3.17.dev18.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
+ dissect.target-3.17.dev18.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
+ dissect.target-3.17.dev18.dist-info/RECORD,,