dissect.target 3.17.dev15__py3-none-any.whl → 3.17.dev18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dissect/target/container.py +1 -0
- dissect/target/containers/fortifw.py +190 -0
- {dissect.target-3.17.dev15.dist-info → dissect.target-3.17.dev18.dist-info}/METADATA +1 -1
- {dissect.target-3.17.dev15.dist-info → dissect.target-3.17.dev18.dist-info}/RECORD +9 -8
- {dissect.target-3.17.dev15.dist-info → dissect.target-3.17.dev18.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.17.dev15.dist-info → dissect.target-3.17.dev18.dist-info}/LICENSE +0 -0
- {dissect.target-3.17.dev15.dist-info → dissect.target-3.17.dev18.dist-info}/WHEEL +0 -0
- {dissect.target-3.17.dev15.dist-info → dissect.target-3.17.dev18.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.17.dev15.dist-info → dissect.target-3.17.dev18.dist-info}/top_level.txt +0 -0
dissect/target/container.py
CHANGED
@@ -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.
|
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=
|
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.
|
339
|
-
dissect.target-3.17.
|
340
|
-
dissect.target-3.17.
|
341
|
-
dissect.target-3.17.
|
342
|
-
dissect.target-3.17.
|
343
|
-
dissect.target-3.17.
|
344
|
-
dissect.target-3.17.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.17.dev15.dist-info → dissect.target-3.17.dev18.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|