dissect.target 3.19.dev41__py3-none-any.whl → 3.19.dev43__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.
@@ -13,6 +13,17 @@ from dissect.target.helpers import fsutil
13
13
  log = logging.getLogger(__name__)
14
14
 
15
15
 
16
+ def findall(buf: bytes, needle: bytes) -> Iterator[int]:
17
+ offset = 0
18
+ while True:
19
+ offset = buf.find(needle, offset)
20
+ if offset == -1:
21
+ break
22
+
23
+ yield offset
24
+ offset += 1
25
+
26
+
16
27
  class StrEnum(str, Enum):
17
28
  """Sortable and serializible string-based enum"""
18
29
 
@@ -5,6 +5,7 @@ from flow.record.fieldtypes import digest
5
5
 
6
6
  from dissect.target.exceptions import UnsupportedPluginError
7
7
  from dissect.target.helpers.record import TargetRecordDescriptor
8
+ from dissect.target.helpers.utils import findall
8
9
  from dissect.target.plugin import Plugin, export
9
10
 
10
11
  try:
@@ -36,17 +37,6 @@ CatrootRecord = TargetRecordDescriptor(
36
37
  )
37
38
 
38
39
 
39
- def findall(buf: bytes, needle: bytes) -> Iterator[int]:
40
- offset = 0
41
- while True:
42
- offset = buf.find(needle, offset)
43
- if offset == -1:
44
- break
45
-
46
- yield offset
47
- offset += 1
48
-
49
-
50
40
  def _get_package_name(sequence: Sequence) -> str:
51
41
  """Parse sequences within a sequence and return the 'PackageName' value if it exists."""
52
42
  for value in sequence.native.values():
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import logging
5
+ from struct import error as StructError
6
+ from typing import BinaryIO, Iterator
7
+
8
+ from dissect.cstruct import cstruct
9
+ from dissect.ole import OLE
10
+ from dissect.ole.exceptions import Error as OleError
11
+ from dissect.shellitem.lnk import Lnk
12
+
13
+ from dissect.target import Target
14
+ from dissect.target.exceptions import UnsupportedPluginError
15
+ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
16
+ from dissect.target.helpers.record import create_extended_descriptor
17
+ from dissect.target.helpers.shell_application_ids import APPLICATION_IDENTIFIERS
18
+ from dissect.target.helpers.utils import findall
19
+ from dissect.target.plugin import Plugin, export
20
+ from dissect.target.plugins.os.windows.lnk import LnkRecord, parse_lnk_file
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+ LNK_GUID = b"\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46"
25
+
26
+ JumpListRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
27
+ "windows/jumplist",
28
+ [
29
+ ("string", "type"),
30
+ ("string", "application_id"),
31
+ ("string", "application_name"),
32
+ *LnkRecord.target_fields,
33
+ ],
34
+ )
35
+
36
+
37
+ custom_destination_def = """
38
+ struct header {
39
+ int version;
40
+ int unknown1;
41
+ int unknown2;
42
+ int value_type;
43
+ }
44
+
45
+ struct header_end {
46
+ int number_of_entries;
47
+ }
48
+
49
+ struct header_end_0 {
50
+ uint16 name_length;
51
+ wchar name[name_length];
52
+ int number_of_entries;
53
+ }
54
+
55
+ struct footer {
56
+ char magic[4];
57
+ }
58
+ """
59
+
60
+ c_custom_destination = cstruct()
61
+ c_custom_destination.load(custom_destination_def)
62
+
63
+
64
+ class JumpListFile:
65
+ def __init__(self, fh: BinaryIO, file_name: str):
66
+ self.fh = fh
67
+ self.file_name = file_name
68
+
69
+ self.application_id, self.application_type = file_name.split(".")
70
+ self.application_type = self.application_type.split("-")[0]
71
+ self.application_name = APPLICATION_IDENTIFIERS.get(self.application_id)
72
+
73
+ def __iter__(self) -> Iterator[Lnk]:
74
+ raise NotImplementedError
75
+
76
+ @property
77
+ def name(self) -> str:
78
+ """Return the name of the application."""
79
+ return self.application_name
80
+
81
+ @property
82
+ def id(self) -> str:
83
+ """Return the application identifier."""
84
+ return self.application_id
85
+
86
+ @property
87
+ def type(self) -> str:
88
+ """Return the type of the Jump List file."""
89
+ return self.application_type
90
+
91
+
92
+ class AutomaticDestinationFile(JumpListFile):
93
+ """Parse Jump List AutomaticDestination file."""
94
+
95
+ def __init__(self, fh: BinaryIO, file_name: str):
96
+ super().__init__(fh, file_name)
97
+ self.ole = OLE(self.fh)
98
+
99
+ def __iter__(self) -> Iterator[Lnk]:
100
+ for dir_name in self.ole.root.listdir():
101
+ if dir_name == "DestList":
102
+ continue
103
+
104
+ dir = self.ole.get(dir_name)
105
+
106
+ for item in dir.open():
107
+ try:
108
+ yield Lnk(io.BytesIO(item))
109
+ except StructError:
110
+ continue
111
+ except Exception as e:
112
+ log.warning("Failed to parse LNK file from directory %s", dir_name)
113
+ log.debug("", exc_info=e)
114
+ continue
115
+
116
+
117
+ class CustomDestinationFile(JumpListFile):
118
+ """Parse Jump List CustomDestination file."""
119
+
120
+ MAGIC_FOOTER = 0xBABFFBAB
121
+ VERSIONS = [2]
122
+
123
+ def __init__(self, fh: BinaryIO, file_name: str):
124
+ super().__init__(fh, file_name)
125
+
126
+ self.fh.seek(-4, io.SEEK_END)
127
+ self.footer = c_custom_destination.footer(self.fh.read(4))
128
+ self.magic = int.from_bytes(self.footer.magic, "little")
129
+
130
+ self.fh.seek(0, io.SEEK_SET)
131
+ self.header = c_custom_destination.header(self.fh)
132
+ self.version = self.header.version
133
+
134
+ if self.header.value_type == 0:
135
+ self.header_end = c_custom_destination.header_end_0(self.fh)
136
+ elif self.header.value_type in [1, 2]:
137
+ self.header_end = c_custom_destination.header_end(self.fh)
138
+ else:
139
+ raise NotImplementedError(
140
+ f"The value_type ({self.header.value_type}) of the CustomDestination file is not implemented"
141
+ )
142
+
143
+ if self.version not in self.VERSIONS:
144
+ raise NotImplementedError(f"The CustomDestination file has an unsupported version: {self.version}")
145
+
146
+ if not self.MAGIC_FOOTER == self.magic:
147
+ raise ValueError(f"The CustomDestination file has an invalid magic footer: {self.magic}")
148
+
149
+ def __iter__(self) -> Iterator[Lnk]:
150
+ # Searches for all LNK GUID's because the number of entries in the header is not always correct.
151
+ buf = self.fh.read()
152
+
153
+ for offset in findall(buf, LNK_GUID):
154
+ try:
155
+ lnk = Lnk(io.BytesIO(buf[offset + len(LNK_GUID) :]))
156
+ yield lnk
157
+ except EOFError:
158
+ break
159
+ except Exception as e:
160
+ log.warning("Failed to parse LNK file from a CustomDestination file")
161
+ log.debug("", exc_info=e)
162
+ continue
163
+
164
+
165
+ class JumpListPlugin(Plugin):
166
+ """Jump List is a Windows feature introduced in Windows 7.
167
+
168
+ It stores information about recently accessed applications and files.
169
+
170
+ References:
171
+ - https://forensics.wiki/jump_lists
172
+ - https://github.com/libyal/dtformats/blob/main/documentation/Jump%20lists%20format.asciidoc
173
+ """
174
+
175
+ __namespace__ = "jumplist"
176
+
177
+ def __init__(self, target: Target):
178
+ super().__init__(target)
179
+ self.automatic_destinations = []
180
+ self.custom_destinations = []
181
+
182
+ for user_details in self.target.user_details.all_with_home():
183
+ for destination in user_details.home_path.glob(
184
+ "AppData/Roaming/Microsoft/Windows/Recent/CustomDestinations/*.customDestinations-ms"
185
+ ):
186
+ self.custom_destinations.append([destination, user_details.user])
187
+
188
+ for user_details in self.target.user_details.all_with_home():
189
+ for destination in user_details.home_path.glob(
190
+ "AppData/Roaming/Microsoft/Windows/Recent/AutomaticDestinations/*.automaticDestinations-ms"
191
+ ):
192
+ self.automatic_destinations.append([destination, user_details.user])
193
+
194
+ def check_compatible(self) -> None:
195
+ if not any([self.automatic_destinations, self.custom_destinations]):
196
+ raise UnsupportedPluginError("No Jump List files found")
197
+
198
+ @export(record=JumpListRecord)
199
+ def custom_destination(self) -> Iterator[JumpListRecord]:
200
+ """Return the content of CustomDestination Windows Jump Lists.
201
+
202
+ These are created when a user pins an application or a file in a Jump List.
203
+
204
+ Yields JumpListRecord with fields:
205
+
206
+ .. code-block:: text
207
+
208
+ type (string): Type of Jump List.
209
+ application_id (string): ID of the application.
210
+ application_name (string): Name of the application.
211
+ lnk_path (path): Path of the link (.lnk) file.
212
+ lnk_name (string): Name of the link (.lnk) file.
213
+ lnk_mtime (datetime): Modification time of the link (.lnk) file.
214
+ lnk_atime (datetime): Access time of the link (.lnk) file.
215
+ lnk_ctime (datetime): Creation time of the link (.lnk) file.
216
+ lnk_relativepath (path): Relative path of target file to the link (.lnk) file.
217
+ lnk_workdir (path): Path of the working directory the link (.lnk) file will execute from.
218
+ lnk_iconlocation (path): Path of the display icon used for the link (.lnk) file.
219
+ lnk_arguments (string): Command-line arguments passed to the target (linked) file.
220
+ local_base_path (string): Absolute path of the target (linked) file.
221
+ common_path_suffix (string): Suffix of the local_base_path.
222
+ lnk_full_path (string): Full path of the linked file. Made from local_base_path and common_path_suffix.
223
+ lnk_net_name (string): Specifies a server share path; for example, "\\\\server\\share".
224
+ lnk_device_name (string): Specifies a device; for example, the drive letter "D:"
225
+ machine_id (string): The NetBIOS name of the machine where the linked file was last known to reside.
226
+ target_mtime (datetime): Modification time of the target (linked) file.
227
+ target_atime (datetime): Access time of the target (linked) file.
228
+ target_ctime (datetime): Creation time of the target (linked) file.
229
+ """
230
+ yield from self._generate_records(self.custom_destinations, CustomDestinationFile)
231
+
232
+ @export(record=JumpListRecord)
233
+ def automatic_destination(self) -> Iterator[JumpListRecord]:
234
+ """Return the content of AutomaticDestination Windows Jump Lists.
235
+
236
+ These are created automatically when a user opens an application or file.
237
+
238
+ Yields JumpListRecord with fields:
239
+
240
+ .. code-block:: text
241
+
242
+ type (string): Type of Jump List.
243
+ application_id (string): ID of the application.
244
+ application_name (string): Name of the application.
245
+ lnk_path (path): Path of the link (.lnk) file.
246
+ lnk_name (string): Name of the link (.lnk) file.
247
+ lnk_mtime (datetime): Modification time of the link (.lnk) file.
248
+ lnk_atime (datetime): Access time of the link (.lnk) file.
249
+ lnk_ctime (datetime): Creation time of the link (.lnk) file.
250
+ lnk_relativepath (path): Relative path of target file to the link (.lnk) file.
251
+ lnk_workdir (path): Path of the working directory the link (.lnk) file will execute from.
252
+ lnk_iconlocation (path): Path of the display icon used for the link (.lnk) file.
253
+ lnk_arguments (string): Command-line arguments passed to the target (linked) file.
254
+ local_base_path (string): Absolute path of the target (linked) file.
255
+ common_path_suffix (string): Suffix of the local_base_path.
256
+ lnk_full_path (string): Full path of the linked file. Made from local_base_path and common_path_suffix.
257
+ lnk_net_name (string): Specifies a server share path; for example, "\\\\server\\share".
258
+ lnk_device_name (string): Specifies a device; for example, the drive letter "D:"
259
+ machine_id (string): The NetBIOS name of the machine where the linked file was last known to reside.
260
+ target_mtime (datetime): Modification time of the target (linked) file.
261
+ target_atime (datetime): Access time of the target (linked) file.
262
+ target_ctime (datetime): Creation time of the target (linked) file.
263
+ """
264
+ yield from self._generate_records(self.automatic_destinations, AutomaticDestinationFile)
265
+
266
+ def _generate_records(
267
+ self,
268
+ destinations: list,
269
+ destination_file: AutomaticDestinationFile | CustomDestinationFile,
270
+ ) -> Iterator[JumpListRecord]:
271
+ for destination, user in destinations:
272
+ fh = destination.open("rb")
273
+
274
+ try:
275
+ jumplist = destination_file(fh, destination.name)
276
+ except OleError:
277
+ continue
278
+ except Exception as e:
279
+ self.target.log.warning("Failed to parse Jump List file: %s", destination)
280
+ self.target.log.debug("", exc_info=e)
281
+ continue
282
+
283
+ for lnk in jumplist:
284
+ if lnk := parse_lnk_file(self.target, lnk, destination):
285
+ yield JumpListRecord(
286
+ type=jumplist.type,
287
+ application_name=jumplist.name,
288
+ application_id=jumplist.id,
289
+ **lnk._asdict(),
290
+ _user=user,
291
+ _target=self.target,
292
+ )
@@ -34,6 +34,89 @@ LnkRecord = TargetRecordDescriptor(
34
34
  )
35
35
 
36
36
 
37
+ def parse_lnk_file(target: Target, lnk_file: Lnk, lnk_path: TargetPath) -> Iterator[LnkRecord]:
38
+ # we need to get the active codepage from the system to properly decode some values
39
+ codepage = target.codepage or "ascii"
40
+
41
+ lnk_net_name = lnk_device_name = None
42
+
43
+ if lnk_file.link_header:
44
+ lnk_name = lnk_file.stringdata.name_string.string if lnk_file.flag("has_name") else None
45
+
46
+ lnk_mtime = ts.from_unix(lnk_path.stat().st_mtime)
47
+ lnk_atime = ts.from_unix(lnk_path.stat().st_atime)
48
+ lnk_ctime = ts.from_unix(lnk_path.stat().st_ctime)
49
+
50
+ lnk_relativepath = (
51
+ target.fs.path(lnk_file.stringdata.relative_path.string) if lnk_file.flag("has_relative_path") else None
52
+ )
53
+ lnk_workdir = (
54
+ target.fs.path(lnk_file.stringdata.working_dir.string) if lnk_file.flag("has_working_dir") else None
55
+ )
56
+ lnk_iconlocation = (
57
+ target.fs.path(lnk_file.stringdata.icon_location.string) if lnk_file.flag("has_icon_location") else None
58
+ )
59
+ lnk_arguments = lnk_file.stringdata.command_line_arguments.string if lnk_file.flag("has_arguments") else None
60
+ local_base_path = (
61
+ lnk_file.linkinfo.local_base_path.decode(codepage)
62
+ if lnk_file.flag("has_link_info") and lnk_file.linkinfo.flag("volumeid_and_local_basepath")
63
+ else None
64
+ )
65
+ common_path_suffix = (
66
+ lnk_file.linkinfo.common_path_suffix.decode(codepage) if lnk_file.flag("has_link_info") else None
67
+ )
68
+
69
+ if local_base_path and common_path_suffix:
70
+ lnk_full_path = target.fs.path(local_base_path + common_path_suffix)
71
+ elif local_base_path and not common_path_suffix:
72
+ lnk_full_path = target.fs.path(local_base_path)
73
+ else:
74
+ lnk_full_path = None
75
+
76
+ if lnk_file.flag("has_link_info"):
77
+ if lnk_file.linkinfo.flag("common_network_relative_link_and_pathsuffix"):
78
+ lnk_net_name = (
79
+ lnk_file.linkinfo.common_network_relative_link.net_name.decode()
80
+ if lnk_file.linkinfo.common_network_relative_link.net_name
81
+ else None
82
+ )
83
+ lnk_device_name = (
84
+ lnk_file.linkinfo.common_network_relative_link.device_name.decode()
85
+ if lnk_file.linkinfo.common_network_relative_link.device_name
86
+ else None
87
+ )
88
+ try:
89
+ machine_id = lnk_file.extradata.TRACKER_PROPS.machine_id.decode(codepage).strip("\x00")
90
+ except AttributeError:
91
+ machine_id = None
92
+
93
+ target_mtime = ts.wintimestamp(lnk_file.link_header.write_time)
94
+ target_atime = ts.wintimestamp(lnk_file.link_header.access_time)
95
+ target_ctime = ts.wintimestamp(lnk_file.link_header.creation_time)
96
+
97
+ return LnkRecord(
98
+ lnk_path=lnk_path,
99
+ lnk_name=lnk_name,
100
+ lnk_mtime=lnk_mtime,
101
+ lnk_atime=lnk_atime,
102
+ lnk_ctime=lnk_ctime,
103
+ lnk_relativepath=lnk_relativepath,
104
+ lnk_workdir=lnk_workdir,
105
+ lnk_iconlocation=lnk_iconlocation,
106
+ lnk_arguments=lnk_arguments,
107
+ local_base_path=local_base_path,
108
+ common_path_suffix=common_path_suffix,
109
+ lnk_full_path=lnk_full_path,
110
+ lnk_net_name=lnk_net_name,
111
+ lnk_device_name=lnk_device_name,
112
+ machine_id=machine_id,
113
+ target_mtime=target_mtime,
114
+ target_atime=target_atime,
115
+ target_ctime=target_ctime,
116
+ _target=target,
117
+ )
118
+
119
+
37
120
  class LnkPlugin(Plugin):
38
121
  def __init__(self, target: Target) -> None:
39
122
  super().__init__(target)
@@ -74,97 +157,9 @@ class LnkPlugin(Plugin):
74
157
  target_ctime (datetime): Creation time of the target (linked) file.
75
158
  """
76
159
 
77
- # we need to get the active codepage from the system to properly decode some values
78
- codepage = self.target.codepage or "ascii"
79
-
80
160
  for entry in self.lnk_entries(path):
81
161
  lnk_file = Lnk(entry.open())
82
- lnk_net_name = lnk_device_name = None
83
-
84
- if lnk_file.link_header:
85
- lnk_path = entry
86
- lnk_name = lnk_file.stringdata.name_string.string if lnk_file.flag("has_name") else None
87
-
88
- lnk_mtime = ts.from_unix(entry.stat().st_mtime)
89
- lnk_atime = ts.from_unix(entry.stat().st_atime)
90
- lnk_ctime = ts.from_unix(entry.stat().st_ctime)
91
-
92
- lnk_relativepath = (
93
- self.target.fs.path(lnk_file.stringdata.relative_path.string)
94
- if lnk_file.flag("has_relative_path")
95
- else None
96
- )
97
- lnk_workdir = (
98
- self.target.fs.path(lnk_file.stringdata.working_dir.string)
99
- if lnk_file.flag("has_working_dir")
100
- else None
101
- )
102
- lnk_iconlocation = (
103
- self.target.fs.path(lnk_file.stringdata.icon_location.string)
104
- if lnk_file.flag("has_icon_location")
105
- else None
106
- )
107
- lnk_arguments = (
108
- lnk_file.stringdata.command_line_arguments.string if lnk_file.flag("has_arguments") else None
109
- )
110
- local_base_path = (
111
- lnk_file.linkinfo.local_base_path.decode(codepage)
112
- if lnk_file.flag("has_link_info") and lnk_file.linkinfo.flag("volumeid_and_local_basepath")
113
- else None
114
- )
115
- common_path_suffix = (
116
- lnk_file.linkinfo.common_path_suffix.decode(codepage) if lnk_file.flag("has_link_info") else None
117
- )
118
-
119
- if local_base_path and common_path_suffix:
120
- lnk_full_path = self.target.fs.path(local_base_path + common_path_suffix)
121
- elif local_base_path and not common_path_suffix:
122
- lnk_full_path = self.target.fs.path(local_base_path)
123
- else:
124
- lnk_full_path = None
125
-
126
- if lnk_file.flag("has_link_info"):
127
- if lnk_file.linkinfo.flag("common_network_relative_link_and_pathsuffix"):
128
- lnk_net_name = (
129
- lnk_file.linkinfo.common_network_relative_link.net_name.decode()
130
- if lnk_file.linkinfo.common_network_relative_link.net_name
131
- else None
132
- )
133
- lnk_device_name = (
134
- lnk_file.linkinfo.common_network_relative_link.device_name.decode()
135
- if lnk_file.linkinfo.common_network_relative_link.device_name
136
- else None
137
- )
138
- try:
139
- machine_id = lnk_file.extradata.TRACKER_PROPS.machine_id.decode(codepage).strip("\x00")
140
- except AttributeError:
141
- machine_id = None
142
-
143
- target_mtime = ts.wintimestamp(lnk_file.link_header.write_time)
144
- target_atime = ts.wintimestamp(lnk_file.link_header.access_time)
145
- target_ctime = ts.wintimestamp(lnk_file.link_header.creation_time)
146
-
147
- yield LnkRecord(
148
- lnk_path=lnk_path,
149
- lnk_name=lnk_name,
150
- lnk_mtime=lnk_mtime,
151
- lnk_atime=lnk_atime,
152
- lnk_ctime=lnk_ctime,
153
- lnk_relativepath=lnk_relativepath,
154
- lnk_workdir=lnk_workdir,
155
- lnk_iconlocation=lnk_iconlocation,
156
- lnk_arguments=lnk_arguments,
157
- local_base_path=local_base_path,
158
- common_path_suffix=common_path_suffix,
159
- lnk_full_path=lnk_full_path,
160
- lnk_net_name=lnk_net_name,
161
- lnk_device_name=lnk_device_name,
162
- machine_id=machine_id,
163
- target_mtime=target_mtime,
164
- target_atime=target_atime,
165
- target_ctime=target_ctime,
166
- _target=self.target,
167
- )
162
+ yield parse_lnk_file(self.target, lnk_file, entry)
168
163
 
169
164
  def lnk_entries(self, path: Optional[str] = None) -> Iterator[TargetPath]:
170
165
  if path: