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.
- 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
|