dissect.target 3.20.dev23__py3-none-any.whl → 3.20.dev26__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -181,6 +181,9 @@ class BtrfsFilesystemEntry(FilesystemEntry):
181
181
 
182
182
  # Add block information of the filesystem
183
183
  st_info.st_blksize = entry.btrfs.sector_size
184
- st_info.st_blocks = math.ceil(entry.size / st_info.st_blksize)
184
+
185
+ st_info.st_blocks = 0
186
+ if not self.is_dir():
187
+ st_info.st_blocks = (st_info.st_blksize // 512) * math.ceil(st_info.st_size / st_info.st_blksize)
185
188
 
186
189
  return st_info
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import math
3
4
  import stat
4
5
  from typing import BinaryIO, Iterator, Optional
5
6
 
@@ -151,12 +152,14 @@ class NtfsFilesystemEntry(FilesystemEntry):
151
152
  record = self.dereference()
152
153
 
153
154
  size = 0
155
+ real_size = 0
154
156
  if self.is_symlink():
155
157
  mode = stat.S_IFLNK
156
158
  elif self.is_file():
157
159
  mode = stat.S_IFREG
158
160
  try:
159
161
  size = record.size(self.ads)
162
+ real_size = record.size(self.ads, allocated=True)
160
163
  except NtfsFileNotFoundError as e:
161
164
  # Occurs when it cannot find the the specific ads inside its attributes
162
165
  raise FileNotFoundError from e
@@ -176,16 +179,29 @@ class NtfsFilesystemEntry(FilesystemEntry):
176
179
  0,
177
180
  size,
178
181
  stdinfo.last_access_time.timestamp(),
179
- stdinfo.last_change_time.timestamp(),
182
+ stdinfo.last_modification_time.timestamp(),
183
+ # ctime gets set to creation time for python <3.12 purposes
180
184
  stdinfo.creation_time.timestamp(),
181
185
  ]
182
186
  )
183
187
 
184
188
  # Set the nanosecond resolution separately
185
189
  st_info.st_atime_ns = stdinfo.last_access_time_ns
186
- st_info.st_mtime_ns = stdinfo.last_change_time_ns
190
+ st_info.st_mtime_ns = stdinfo.last_modification_time_ns
191
+
187
192
  st_info.st_ctime_ns = stdinfo.creation_time_ns
188
193
 
194
+ st_info.st_birthtime = stdinfo.creation_time.timestamp()
195
+ st_info.st_birthtime_ns = stdinfo.creation_time_ns
196
+
197
+ # real_size is none if the size is resident
198
+ st_info.st_blksize = record.ntfs.cluster_size
199
+ blocks = 0
200
+ if not record.resident:
201
+ blocks = math.ceil(real_size / 512)
202
+
203
+ st_info.st_blocks = blocks
204
+
189
205
  return st_info
190
206
 
191
207
  def attr(self) -> AttributeMap:
@@ -1,6 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import io
2
4
  import logging
3
5
  import uuid
6
+ from datetime import datetime
7
+ from typing import Any, Iterator
4
8
 
5
9
  from dissect.cstruct import cstruct
6
10
  from dissect.util.ts import dostimestamp
@@ -13,7 +17,9 @@ from dissect.target.helpers.descriptor_extensions import (
13
17
  UserRecordDescriptorExtension,
14
18
  )
15
19
  from dissect.target.helpers.record import create_extended_descriptor
20
+ from dissect.target.helpers.regutil import RegistryKey
16
21
  from dissect.target.plugin import Plugin, export
22
+ from dissect.target.target import Target
17
23
 
18
24
  log = logging.getLogger(__name__)
19
25
 
@@ -170,30 +176,30 @@ struct SHITEM_MTP_VOLUME_GUID {
170
176
  };
171
177
 
172
178
  struct SHITEM_MTP_VOLUME {
173
- uint16 size;
174
- uint8 type;
175
- uint8 unk0;
176
- uint16 data_size;
177
- uint32 data_signature;
178
- uint32 unk1;
179
- uint16 unk2;
180
- uint16 unk3;
181
- uint16 unk4;
182
- uint16 unk5;
183
- uint32 unk6;
184
- uint64 unk7;
185
- uint32 unk8;
186
- uint32 name_size;
187
- uint32 identifier_size;
188
- uint32 filesystem_size;
189
- uint32 num_guid;
190
- wchar name[name_size];
191
- wchar identifier[identifier_size];
192
- wchar filesystem[filesystem_size];
193
- SHITEM_MTP_VOLUME_GUID guids[num_guid];
194
- uint32 unk9;
195
- char class_identifier[16];
196
- uint32 num_properties;
179
+ uint16 size;
180
+ uint8 type;
181
+ uint8 unk0;
182
+ uint16 data_size;
183
+ uint32 data_signature;
184
+ uint32 unk1;
185
+ uint16 unk2;
186
+ uint16 unk3;
187
+ uint16 unk4;
188
+ uint16 unk5;
189
+ uint32 unk6;
190
+ uint64 unk7;
191
+ uint32 unk8;
192
+ uint32 name_size;
193
+ uint32 identifier_size;
194
+ uint32 filesystem_size;
195
+ uint32 num_guid;
196
+ wchar name[name_size];
197
+ wchar identifier[identifier_size];
198
+ wchar filesystem[filesystem_size];
199
+ SHITEM_MTP_VOLUME_GUID guids[num_guid];
200
+ uint32 unk9;
201
+ char class_identifier[16];
202
+ uint32 num_properties;
197
203
  };
198
204
 
199
205
  struct SHITEM_USERS_PROPERTY_VIEW {
@@ -248,267 +254,6 @@ c_bag = cstruct().load(bag_def)
248
254
  DELEGATE_ITEM_IDENTIFIER = b"\x74\x1a\x59\x5e\x96\xdf\xd3\x48\x8d\x67\x17\x33\xbc\xee\x28\xba"
249
255
 
250
256
 
251
- ShellBagRecord = create_extended_descriptor([RegistryRecordDescriptorExtension, UserRecordDescriptorExtension])(
252
- "windows/shellbag",
253
- [
254
- ("path", "path"),
255
- ("datetime", "creation_time"),
256
- ("datetime", "modification_time"),
257
- ("datetime", "access_time"),
258
- ("datetime", "regf_modification_time"),
259
- ],
260
- )
261
-
262
-
263
- class ShellBagsPlugin(Plugin):
264
- """Windows Shellbags plugin.
265
-
266
- References:
267
- - https://github.com/libyal/libfwsi
268
- """
269
-
270
- KEYS = [
271
- "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell",
272
- "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\ShellNoRoam",
273
- "HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\Shell",
274
- "HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\ShellNoRoam",
275
- "HKEY_CURRENT_USER\\Software\\Classes\\Wow6432Node\\Local Settings\\Software\\Microsoft\\Windows\\Shell",
276
- "HKEY_CURRENT_USER\\Software\\Classes\\Wow6432Node\\Local Settings\\Software\\Microsoft\\Windows\\ShellNoRoam",
277
- "HKEY_CURRENT_USER\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU",
278
- ]
279
-
280
- def __init__(self, target):
281
- super().__init__(target)
282
- self.bagkeys = list(self.target.registry.keys(self.KEYS))
283
-
284
- def check_compatible(self) -> None:
285
- if not len(self.bagkeys) > 0:
286
- raise UnsupportedPluginError("No shellbags found")
287
-
288
- @export(record=ShellBagRecord)
289
- def shellbags(self):
290
- """Return Windows Shellbags.
291
-
292
- Shellbags are registry keys to improve user experience when using Windows Explorer. It stores information about
293
- for example file/folder creation time and access time.
294
-
295
- References:
296
- - https://www.hackingarticles.in/forensic-investigation-shellbags/
297
- """
298
- for regkey in self.bagkeys:
299
- try:
300
- bagsmru = regkey.subkey("BagMRU")
301
-
302
- for r in self._walk_bags(bagsmru, None):
303
- yield r
304
- except RegistryKeyNotFoundError:
305
- continue
306
- except Exception: # noqa
307
- self.target.log.exception("Exception while parsing shellbags")
308
- continue
309
-
310
- def _walk_bags(self, key, path_prefix):
311
- path_prefix = [] if path_prefix is None else [path_prefix]
312
-
313
- user = self.target.registry.get_user(key)
314
-
315
- for reg_val in key.values():
316
- name, value = reg_val.name, reg_val.value
317
- if not name.isdigit():
318
- continue
319
- path = None
320
-
321
- for item in parse_shell_item_list(value):
322
- path = "\\".join(path_prefix + [item.name])
323
- yield ShellBagRecord(
324
- path=windows_path(path),
325
- creation_time=item.creation_time,
326
- modification_time=item.modification_time,
327
- access_time=item.access_time,
328
- regf_modification_time=key.ts,
329
- _target=self.target,
330
- _user=user,
331
- _key=key,
332
- )
333
-
334
- for r in self._walk_bags(key.subkey(name), path):
335
- yield r
336
-
337
-
338
- def parse_shell_item_list(buf):
339
- offset = 0
340
- end = len(buf)
341
- list_buf = memoryview(buf)
342
-
343
- parent = None
344
- while offset < end:
345
- size = c_bag.uint16(list_buf[offset : offset + 2])
346
-
347
- if size == 0:
348
- break
349
-
350
- item_buf = list_buf[offset : offset + size]
351
-
352
- entry = None
353
- if size >= 8:
354
- signature = c_bag.uint32(item_buf[4:8])
355
- if signature == 0x39DE2184:
356
- entry = CONTROL_PANEL_CATEGORY
357
- elif signature == 0x4D677541:
358
- entry = CDBURN
359
- elif signature == 0x49534647:
360
- entry = GAME_FOLDER
361
- elif signature == 0xFFFFFF38:
362
- entry = CONTROL_PANEL_CPL_FILE
363
-
364
- if size >= 10 and not entry:
365
- signature = c_bag.uint32(item_buf[6:10])
366
- if signature == 0x07192006:
367
- entry = MTP_FILE_ENTRY
368
- elif signature == 0x10312005:
369
- entry = MTP_VOLUME
370
- elif signature in (0x10141981, 0x23A3DFD5, 0x23FEBBEE, 0x3B93AFBB, 0xBEEBEE00):
371
- entry = USERS_PROPERTY_VIEW
372
- elif signature == 0x46534643:
373
- entry = UNKNOWN_0x74
374
-
375
- if size >= 38 and not entry:
376
- if item_buf[size - 32 : size] == DELEGATE_ITEM_IDENTIFIER:
377
- entry = DELEGATE
378
-
379
- if size >= 3 and not entry:
380
- class_type = item_buf[2]
381
- mask_type = class_type & 0x70
382
-
383
- if mask_type == 0x00:
384
- if class_type == 0x00:
385
- entry = UNKNOWN0
386
- elif class_type == 0x01:
387
- entry = UNKNOWN1
388
-
389
- elif mask_type == 0x10:
390
- if class_type == 0x1F:
391
- entry = ROOT_FOLDER
392
-
393
- elif mask_type == 0x20:
394
- if class_type in (0x23, 0x25, 0x29, 0x2A, 0x2E, 0x2F):
395
- entry = VOLUME
396
-
397
- elif mask_type == 0x30:
398
- if class_type in (0x30, 0x31, 0x32, 0x35, 0x36, 0xB1):
399
- entry = FILE_ENTRY
400
-
401
- elif mask_type == 0x40:
402
- if class_type in (0x41, 0x42, 0x46, 0x47, 0x4C, 0xC3):
403
- entry = NETWORK
404
-
405
- elif mask_type == 0x50:
406
- if class_type == 0x52:
407
- entry = COMPRESSED_FOLDER
408
-
409
- elif mask_type == 0x60:
410
- if class_type == 0x61:
411
- entry = URI
412
-
413
- elif mask_type == 0x70:
414
- if class_type == 0x71:
415
- entry = CONTROL_PANEL
416
- else:
417
- if not entry:
418
- log.debug("No supported shell item found for size 0x%04x and type 0x%02x", size, class_type)
419
- entry = UNKNOWN
420
-
421
- if not entry:
422
- log.debug("No supported shell item found for size 0x%04x", size)
423
- entry = UNKNOWN
424
-
425
- entry = entry(item_buf)
426
- entry.parent = parent
427
-
428
- first_extension_block_offset = c_bag.uint16(item_buf[-2:])
429
- if 4 <= first_extension_block_offset < size - 2:
430
- extension_offset = first_extension_block_offset
431
- while extension_offset < size - 2:
432
- extension_size = c_bag.uint16(item_buf[extension_offset : extension_offset + 2])
433
-
434
- if extension_size == 0:
435
- break
436
-
437
- if extension_size > size - extension_offset:
438
- log.debug(
439
- "Extension size exceeds item size: 0x%04x > 0x%04x - 0x%04x",
440
- extension_size,
441
- size,
442
- extension_offset,
443
- )
444
- break # Extension size too large
445
-
446
- extension_buf = item_buf[extension_offset : extension_offset + extension_size]
447
- extension_signature = c_bag.uint32(extension_buf[4:8])
448
-
449
- ext = None
450
-
451
- if extension_signature >> 16 != 0xBEEF:
452
- log.debug("Got unsupported extension signature 0x%08x from item %r", extension_signature, entry)
453
- pass # Unsupported
454
-
455
- elif extension_signature == 0xBEEF0000:
456
- pass
457
-
458
- elif extension_signature == 0xBEEF0001:
459
- pass
460
-
461
- elif extension_signature == 0xBEEF0003:
462
- ext = EXTENSION_BLOCK_BEEF0004
463
-
464
- elif extension_signature == 0xBEEF0004:
465
- ext = EXTENSION_BLOCK_BEEF0004
466
-
467
- elif extension_signature == 0xBEEF0005:
468
- ext = EXTENSION_BLOCK_BEEF0005
469
-
470
- elif extension_signature == 0xBEEF0006:
471
- pass
472
-
473
- elif extension_signature == 0xBEEF000A:
474
- pass
475
-
476
- elif extension_signature == 0xBEEF0013:
477
- pass
478
-
479
- elif extension_signature == 0xBEEF0014:
480
- pass
481
-
482
- elif extension_signature == 0xBEEF0019:
483
- pass
484
-
485
- elif extension_signature == 0xBEEF0025:
486
- pass
487
-
488
- elif extension_signature == 0xBEEF0026:
489
- pass
490
-
491
- else:
492
- log.debug(
493
- "Got unsupported beef extension signature 0x%08x from item %r", extension_signature, entry
494
- )
495
- pass
496
-
497
- if ext is None:
498
- ext = EXTENSION_BLOCK
499
- log.debug("Unimplemented extension signature 0x%08x from item %r", extension_signature, entry)
500
-
501
- ext = ext(extension_buf)
502
-
503
- entry.extensions.append(ext)
504
- extension_offset += extension_size
505
-
506
- parent = entry
507
- yield entry
508
-
509
- offset += size
510
-
511
-
512
257
  class SHITEM:
513
258
  STRUCT = None
514
259
 
@@ -521,43 +266,43 @@ class SHITEM:
521
266
  self.parent = None
522
267
  self.extensions = []
523
268
 
269
+ def __repr__(self) -> str:
270
+ return f"<{self.__class__.__name__}>"
271
+
524
272
  @property
525
- def name(self):
273
+ def name(self) -> str:
526
274
  return f"<SHITEM 0x{self.size:x}>"
527
275
 
528
276
  @property
529
- def creation_time(self):
277
+ def creation_time(self) -> None:
530
278
  return None
531
279
 
532
280
  @property
533
- def modification_time(self):
281
+ def modification_time(self) -> None:
534
282
  return None
535
283
 
536
284
  @property
537
- def access_time(self):
285
+ def access_time(self) -> None:
538
286
  return None
539
287
 
540
288
  @property
541
- def file_size(self):
289
+ def file_size(self) -> None:
542
290
  return None
543
291
 
544
292
  @property
545
- def file_reference(self):
293
+ def file_reference(self) -> None:
546
294
  return None
547
295
 
548
- def extension(self, cls):
296
+ def extension(self, cls: Any) -> Any | None:
549
297
  for ext in self.extensions:
550
298
  if isinstance(ext, cls):
551
299
  return ext
552
300
  return None
553
301
 
554
- def __repr__(self):
555
- return f"<{self.__class__.__name__}>"
556
-
557
302
 
558
303
  class UNKNOWN(SHITEM):
559
304
  @property
560
- def name(self):
305
+ def name(self) -> str:
561
306
  type_number = hex(self.type) if self.type else self.type
562
307
  return f"<UNKNOWN size=0x{self.size:04x} type={type_number}>"
563
308
 
@@ -573,7 +318,7 @@ class UNKNOWN0(SHITEM):
573
318
  self.guid = uuid.UUID(bytes_le=fh.read(16))
574
319
 
575
320
  @property
576
- def name(self):
321
+ def name(self) -> str:
577
322
  if self.guid:
578
323
  GUID_name = shell_folder_ids.DESCRIPTIONS.get(str(self.guid))
579
324
  return GUID_name or f"<UNKNOWN0: {{{self.guid}}}>"
@@ -585,11 +330,11 @@ class UNKNOWN1(SHITEM):
585
330
  STRUCT = c_bag.SHITEM_UNKNOWN1
586
331
 
587
332
  @property
588
- def name(self):
333
+ def name(self) -> str:
589
334
  return f"<UNKNOWN1 0x{self.size:x}>"
590
335
 
591
336
 
592
- class ROOT_FOLDER(SHITEM): # noqa
337
+ class ROOT_FOLDER(SHITEM):
593
338
  STRUCT = c_bag.SHITEM_ROOT_FOLDER
594
339
 
595
340
  def __init__(self, fh):
@@ -601,7 +346,7 @@ class ROOT_FOLDER(SHITEM): # noqa
601
346
  self.extension = None
602
347
 
603
348
  @property
604
- def name(self):
349
+ def name(self) -> str:
605
350
  GUID_name = shell_folder_ids.DESCRIPTIONS.get(str(self.guid))
606
351
  return GUID_name or f"{{{self.item.folder_id.name}: {self.guid}}}"
607
352
 
@@ -616,12 +361,12 @@ class VOLUME(SHITEM):
616
361
  if self.type == 0x2E:
617
362
  self.identifier = uuid.UUID(bytes_le=buf[4:20].tobytes())
618
363
  else:
619
- self.volume_name = self.fh.read(20).rstrip(b"\x00").decode()
364
+ self.volume_name = self.fh.read(20).rstrip(b"\x00").decode(errors="surrogateescape")
620
365
  if self.size >= 41:
621
366
  self.identifier = uuid.UUID(bytes_le=buf[25:41].tobytes())
622
367
 
623
368
  @property
624
- def name(self):
369
+ def name(self) -> str:
625
370
  if self.volume_name:
626
371
  return self.volume_name
627
372
  if self.identifier:
@@ -630,7 +375,7 @@ class VOLUME(SHITEM):
630
375
  return f"<VOLUME 0x{self.type:02x}>"
631
376
 
632
377
 
633
- class FILE_ENTRY(SHITEM): # noqa
378
+ class FILE_ENTRY(SHITEM):
634
379
  STRUCT = c_bag.SHITEM_FILE_ENTRY
635
380
 
636
381
  def __init__(self, buf):
@@ -644,7 +389,7 @@ class FILE_ENTRY(SHITEM): # noqa
644
389
  self.primary_name = c_bag.wchar[None](self.fh)
645
390
  self.is_unicode = True
646
391
  else:
647
- self.primary_name = c_bag.char[None](self.fh).decode()
392
+ self.primary_name = c_bag.char[None](self.fh).decode(errors="surrogateescape")
648
393
  self.is_unicode = False
649
394
 
650
395
  if self.fh.tell() % 2:
@@ -659,17 +404,17 @@ class FILE_ENTRY(SHITEM): # noqa
659
404
  if self.is_unicode:
660
405
  self.secondary_name = c_bag.wchar[None](self.fh)
661
406
  else:
662
- self.secondary_name = c_bag.char[None](self.fh).decode()
407
+ self.secondary_name = c_bag.char[None](self.fh).decode(errors="surrogateescape")
663
408
 
664
409
  @property
665
- def name(self):
410
+ def name(self) -> str:
666
411
  ext = self.extension(EXTENSION_BLOCK_BEEF0004)
667
412
  if ext and ext.long_name:
668
413
  return ext.long_name
669
414
  return self.primary_name
670
415
 
671
416
  @property
672
- def modification_time(self):
417
+ def modification_time(self) -> datetime | None:
673
418
  ts = self.item.modification_time
674
419
  if ts > 0:
675
420
  return dostimestamp(ts, swap=True)
@@ -691,15 +436,15 @@ class NETWORK(SHITEM):
691
436
  self.comments = c_bag.char[None](self.fh)
692
437
 
693
438
  @property
694
- def name(self):
695
- return self.item.location.decode()
439
+ def name(self) -> str:
440
+ return self.item.location.decode(errors="surrogateescape")
696
441
 
697
442
 
698
- class COMPRESSED_FOLDER(SHITEM): # noqa
443
+ class COMPRESSED_FOLDER(SHITEM):
699
444
  STRUCT = c_bag.SHITEM_COMPRESSED_FOLDER
700
445
 
701
446
  @property
702
- def name(self):
447
+ def name(self) -> str:
703
448
  return "<COMPRESSED_FOLDER>"
704
449
 
705
450
 
@@ -714,14 +459,14 @@ class URI(SHITEM):
714
459
  if self.item.flags & 0x80:
715
460
  self.uri = c_bag.wchar[None](self.fh)
716
461
  else:
717
- self.uri = c_bag.char[None](self.fh).decode()
462
+ self.uri = c_bag.char[None](self.fh).decode(errors="surrogateescape")
718
463
 
719
464
  @property
720
- def name(self):
465
+ def name(self) -> str:
721
466
  return self.uri or "<URI>"
722
467
 
723
468
 
724
- class CONTROL_PANEL(SHITEM): # noqa
469
+ class CONTROL_PANEL(SHITEM):
725
470
  STRUCT = c_bag.SHITEM_CONTROL_PANEL
726
471
 
727
472
  def __init__(self, buf):
@@ -729,12 +474,12 @@ class CONTROL_PANEL(SHITEM): # noqa
729
474
  self.guid = uuid.UUID(bytes_le=self.item.guid)
730
475
 
731
476
  @property
732
- def name(self):
477
+ def name(self) -> str:
733
478
  GUID_name = shell_folder_ids.DESCRIPTIONS.get(str(self.guid))
734
479
  return GUID_name or f"<CONTROL_PANEL {self.guid}>"
735
480
 
736
481
 
737
- class CONTROL_PANEL_CATEGORY(SHITEM): # noqa
482
+ class CONTROL_PANEL_CATEGORY(SHITEM):
738
483
  STRUCT = c_bag.SHITEM_CONTROL_PANEL_CATEGORY
739
484
  CATEGORIES = {
740
485
  0: "All Control Panel Items",
@@ -752,7 +497,7 @@ class CONTROL_PANEL_CATEGORY(SHITEM): # noqa
752
497
  }
753
498
 
754
499
  @property
755
- def name(self):
500
+ def name(self) -> str:
756
501
  categ_str = self.CATEGORIES.get(self.item.category)
757
502
  if categ_str:
758
503
  return categ_str
@@ -763,11 +508,11 @@ class CDBURN(SHITEM):
763
508
  STRUCT = c_bag.SHITEM_CDBURN
764
509
 
765
510
  @property
766
- def name(self):
511
+ def name(self) -> str:
767
512
  return "<CDBURN>"
768
513
 
769
514
 
770
- class GAME_FOLDER(SHITEM): # noqa
515
+ class GAME_FOLDER(SHITEM):
771
516
  STRUCT = c_bag.SHITEM_GAME_FOLDER
772
517
 
773
518
  def __init__(self, buf):
@@ -775,43 +520,43 @@ class GAME_FOLDER(SHITEM): # noqa
775
520
  self.guid = uuid.UUID(bytes_le=self.item.identifier)
776
521
 
777
522
  @property
778
- def name(self):
523
+ def name(self) -> str:
779
524
  return f"<GAME_FOLDER {{{self.guid}}}>"
780
525
 
781
526
 
782
- class CONTROL_PANEL_CPL_FILE(SHITEM): # noqa
527
+ class CONTROL_PANEL_CPL_FILE(SHITEM):
783
528
  STRUCT = c_bag.SHITEM_CONTROL_PANEL_CPL_FILE
784
529
 
785
530
  @property
786
- def name(self):
531
+ def name(self) -> str:
787
532
  return f"<CONTROL_PANEL_CPL_FILE path={self.item.cpl_path} name={self.item.name} comments={self.item.comments}>"
788
533
 
789
534
 
790
- class MTP_FILE_ENTRY(SHITEM): # noqa
535
+ class MTP_FILE_ENTRY(SHITEM):
791
536
  STRUCT = c_bag.SHITEM_MTP_FILE_ENTRY
792
537
 
793
538
  @property
794
- def name(self):
539
+ def name(self) -> str:
795
540
  return "<MTP_FILE_ENTRY>"
796
541
 
797
542
  @property
798
- def creation_time(self):
543
+ def creation_time(self) -> datetime:
799
544
  return self.item.creation_time
800
545
 
801
546
  @property
802
- def modification_time(self):
547
+ def modification_time(self) -> datetime:
803
548
  return self.item.modification_time
804
549
 
805
550
 
806
- class MTP_VOLUME(SHITEM): # noqa
551
+ class MTP_VOLUME(SHITEM):
807
552
  STRUCT = c_bag.SHITEM_MTP_FILE_ENTRY
808
553
 
809
554
  @property
810
- def name(self):
555
+ def name(self) -> str:
811
556
  return "<MTP_VOLUME>"
812
557
 
813
558
 
814
- class USERS_PROPERTY_VIEW(SHITEM): # noqa
559
+ class USERS_PROPERTY_VIEW(SHITEM):
815
560
  STRUCT = c_bag.SHITEM_USERS_PROPERTY_VIEW
816
561
 
817
562
  def __init__(self, buf):
@@ -823,13 +568,13 @@ class USERS_PROPERTY_VIEW(SHITEM): # noqa
823
568
  self.guid = uuid.UUID(bytes_le=self.item.identifier)
824
569
 
825
570
  @property
826
- def name(self):
571
+ def name(self) -> str:
827
572
  # As we don't know how to handle identifier_size other than 16 bytes, we fall back to data_signature
828
573
  property_view = self.guid or self.identifier
829
574
  return f"<USERS_PROPERTY_VIEW {{{property_view}}}>"
830
575
 
831
576
 
832
- class UNKNOWN_0x74(SHITEM): # noqa
577
+ class UNKNOWN_0x74(SHITEM):
833
578
  STRUCT = c_bag.SHITEM_UNKNOWN_0x74
834
579
 
835
580
  def __init__(self, buf):
@@ -839,11 +584,11 @@ class UNKNOWN_0x74(SHITEM): # noqa
839
584
  self.subitem = c_bag.SHITEM_UNKNOWN_0x74_SUBITEM(self.fh)
840
585
 
841
586
  @property
842
- def name(self):
843
- return self.subitem.primary_name.decode() if self.subitem else "<UNKNOWN_0x74>"
587
+ def name(self) -> str:
588
+ return self.subitem.primary_name.decode(errors="surrogateescape") if self.subitem else "<UNKNOWN_0x74>"
844
589
 
845
590
  @property
846
- def modification_time(self):
591
+ def modification_time(self) -> datetime | None:
847
592
  if self.subitem.modification_time > 0:
848
593
  return dostimestamp(self.subitem.modification_time, swap=True) if self.subitem else None
849
594
  return None
@@ -858,38 +603,38 @@ class DELEGATE(SHITEM):
858
603
  self.shell_identifier = uuid.UUID(bytes_le=self.item.shell_identifier)
859
604
 
860
605
  @property
861
- def name(self):
606
+ def name(self) -> str:
862
607
  GUID_name = shell_folder_ids.DESCRIPTIONS.get(str(self.shell_identifier))
863
608
  return GUID_name if GUID_name else f"{{{self.shell_identifier}}}"
864
609
 
865
610
 
866
- class EXTENSION_BLOCK: # noqa
611
+ class EXTENSION_BLOCK:
867
612
  def __init__(self, buf):
868
613
  self.buf = buf
869
614
  self.fh = io.BytesIO(buf)
870
615
  self.header = c_bag.EXTENSION_BLOCK_HEADER(self.fh)
871
616
 
617
+ def __repr__(self) -> str:
618
+ return f"<EXTENSION_BLOCK size=0x{self.size:04x} version=0x{self.version:04x} signature=0x{self.signature:08x}>"
619
+
872
620
  @property
873
- def size(self):
621
+ def size(self) -> int:
874
622
  return self.header.size
875
623
 
876
624
  @property
877
- def data_size(self):
625
+ def data_size(self) -> int:
878
626
  return self.size - 8 # minus header
879
627
 
880
628
  @property
881
- def version(self):
629
+ def version(self) -> int:
882
630
  return self.header.version
883
631
 
884
632
  @property
885
- def signature(self):
633
+ def signature(self) -> int:
886
634
  return self.header.signature
887
635
 
888
- def __repr__(self):
889
- return f"<EXTENSION_BLOCK size=0x{self.size:04x} version=0x{self.version:04x} signature=0x{self.signature:08x}>"
890
-
891
636
 
892
- class EXTENSION_BLOCK_BEEF0004(EXTENSION_BLOCK): # noqa
637
+ class EXTENSION_BLOCK_BEEF0004(EXTENSION_BLOCK):
893
638
  def __init__(self, buf):
894
639
  super().__init__(buf)
895
640
  fh = self.fh
@@ -923,8 +668,269 @@ class EXTENSION_BLOCK_BEEF0004(EXTENSION_BLOCK): # noqa
923
668
  self.localized_name = c_bag.wchar[None](fh)
924
669
 
925
670
 
926
- class EXTENSION_BLOCK_BEEF0005(EXTENSION_BLOCK): # noqa
671
+ class EXTENSION_BLOCK_BEEF0005(EXTENSION_BLOCK):
927
672
  def __init__(self, buf):
928
673
  super().__init__(buf)
929
674
  c_bag.char[16](self.fh) # GUID?
930
675
  self.shell_items = self.fh.read(self.data_size - 18)
676
+
677
+
678
+ ShellBagRecord = create_extended_descriptor([RegistryRecordDescriptorExtension, UserRecordDescriptorExtension])(
679
+ "windows/shellbag",
680
+ [
681
+ ("datetime", "ts_mtime"),
682
+ ("datetime", "ts_atime"),
683
+ ("datetime", "ts_btime"),
684
+ ("string", "type"),
685
+ ("path", "path"),
686
+ ("datetime", "regf_mtime"),
687
+ ],
688
+ )
689
+
690
+
691
+ class ShellBagsPlugin(Plugin):
692
+ """Windows Shellbags plugin."""
693
+
694
+ KEYS = [
695
+ "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell",
696
+ "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\ShellNoRoam",
697
+ "HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\Shell",
698
+ "HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\ShellNoRoam",
699
+ "HKEY_CURRENT_USER\\Software\\Classes\\Wow6432Node\\Local Settings\\Software\\Microsoft\\Windows\\Shell",
700
+ "HKEY_CURRENT_USER\\Software\\Classes\\Wow6432Node\\Local Settings\\Software\\Microsoft\\Windows\\ShellNoRoam",
701
+ "HKEY_CURRENT_USER\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU",
702
+ ]
703
+
704
+ def __init__(self, target: Target):
705
+ super().__init__(target)
706
+ self.bagkeys: list[RegistryKey] = list(self.target.registry.keys(self.KEYS))
707
+
708
+ def check_compatible(self) -> None:
709
+ if not self.bagkeys:
710
+ raise UnsupportedPluginError("No shellbags found")
711
+
712
+ @export(record=ShellBagRecord)
713
+ def shellbags(self) -> Iterator[ShellBagRecord]:
714
+ """Yields Windows Shellbags.
715
+
716
+ Shellbags are registry keys to improve user experience when using Windows Explorer.
717
+ They contain information such as file and folder creation time and access time.
718
+
719
+ References:
720
+ - https://github.com/libyal/libfwsi
721
+ - https://www.giac.org/paper/gcfa/9576/windows-shellbag-forensics-in-depth/128522
722
+ - https://www.hackingarticles.in/forensic-investigation-shellbags/
723
+ """
724
+ for regkey in self.bagkeys:
725
+ try:
726
+ yield from self._walk_bags(regkey.subkey("BagMRU"), None)
727
+
728
+ except RegistryKeyNotFoundError:
729
+ continue
730
+
731
+ except Exception as e:
732
+ self.target.log.error("Exception while parsing shellbags")
733
+ self.target.log.debug("", exc_info=e)
734
+ continue
735
+
736
+ def _walk_bags(self, key: RegistryKey, path_prefix: str | None) -> Iterator[ShellBagRecord]:
737
+ """Recursively walk shellbags from the given RegistryKey location."""
738
+ path_prefix = [] if path_prefix is None else [path_prefix]
739
+ user = self.target.registry.get_user(key)
740
+
741
+ for reg_val in key.values():
742
+ name, value = reg_val.name, reg_val.value
743
+ if not name.isdigit():
744
+ continue
745
+ path = None
746
+
747
+ for item in parse_shell_item_list(value):
748
+ path = "\\".join(path_prefix + [item.name])
749
+ yield ShellBagRecord(
750
+ ts_mtime=item.modification_time,
751
+ ts_atime=item.access_time,
752
+ ts_btime=item.creation_time,
753
+ type=item.__class__.__name__,
754
+ path=windows_path(path),
755
+ regf_mtime=key.ts,
756
+ _key=key,
757
+ _user=user,
758
+ _target=self.target,
759
+ )
760
+
761
+ yield from self._walk_bags(key.subkey(name), path)
762
+
763
+
764
+ def parse_shell_item_list(buf: bytes) -> Iterator[SHITEM]:
765
+ """Parse a shellbag item from the given bytes."""
766
+
767
+ offset = 0
768
+ end = len(buf)
769
+ list_buf = memoryview(buf)
770
+ parent = None
771
+
772
+ while offset < end:
773
+ size = c_bag.uint16(list_buf[offset : offset + 2])
774
+
775
+ if size == 0:
776
+ break
777
+
778
+ item_buf = list_buf[offset : offset + size]
779
+
780
+ entry = None
781
+ if size >= 8:
782
+ signature = c_bag.uint32(item_buf[4:8])
783
+ if signature == 0x39DE2184:
784
+ entry = CONTROL_PANEL_CATEGORY
785
+ elif signature == 0x4D677541:
786
+ entry = CDBURN
787
+ elif signature == 0x49534647:
788
+ entry = GAME_FOLDER
789
+ elif signature == 0xFFFFFF38:
790
+ entry = CONTROL_PANEL_CPL_FILE
791
+
792
+ if size >= 10 and not entry:
793
+ signature = c_bag.uint32(item_buf[6:10])
794
+ if signature == 0x07192006:
795
+ entry = MTP_FILE_ENTRY
796
+ elif signature == 0x10312005:
797
+ entry = MTP_VOLUME
798
+ elif signature in (0x10141981, 0x23A3DFD5, 0x23FEBBEE, 0x3B93AFBB, 0xBEEBEE00):
799
+ entry = USERS_PROPERTY_VIEW
800
+ elif signature == 0x46534643:
801
+ entry = UNKNOWN_0x74
802
+
803
+ if size >= 38 and not entry:
804
+ if item_buf[size - 32 : size] == DELEGATE_ITEM_IDENTIFIER:
805
+ entry = DELEGATE
806
+
807
+ if size >= 3 and not entry:
808
+ class_type = item_buf[2]
809
+ mask_type = class_type & 0x70
810
+
811
+ if mask_type == 0x00:
812
+ if class_type == 0x00:
813
+ entry = UNKNOWN0
814
+ elif class_type == 0x01:
815
+ entry = UNKNOWN1
816
+
817
+ elif mask_type == 0x10:
818
+ if class_type == 0x1F:
819
+ entry = ROOT_FOLDER
820
+
821
+ elif mask_type == 0x20:
822
+ if class_type in (0x23, 0x25, 0x29, 0x2A, 0x2E, 0x2F):
823
+ entry = VOLUME
824
+
825
+ elif mask_type == 0x30:
826
+ if class_type in (0x30, 0x31, 0x32, 0x35, 0x36, 0xB1):
827
+ entry = FILE_ENTRY
828
+
829
+ elif mask_type == 0x40:
830
+ if class_type in (0x41, 0x42, 0x46, 0x47, 0x4C, 0xC3):
831
+ entry = NETWORK
832
+
833
+ elif mask_type == 0x50:
834
+ if class_type == 0x52:
835
+ entry = COMPRESSED_FOLDER
836
+
837
+ elif mask_type == 0x60:
838
+ if class_type == 0x61:
839
+ entry = URI
840
+
841
+ elif mask_type == 0x70:
842
+ if class_type == 0x71:
843
+ entry = CONTROL_PANEL
844
+ else:
845
+ if not entry:
846
+ log.debug("No supported shell item found for size 0x%04x and type 0x%02x", size, class_type)
847
+ entry = UNKNOWN
848
+
849
+ if not entry:
850
+ log.debug("No supported shell item found for size 0x%04x", size)
851
+ entry = UNKNOWN
852
+
853
+ entry = entry(item_buf)
854
+ entry.parent = parent
855
+
856
+ first_extension_block_offset = c_bag.uint16(item_buf[-2:])
857
+ if 4 <= first_extension_block_offset < size - 2:
858
+ extension_offset = first_extension_block_offset
859
+ while extension_offset < size - 2:
860
+ extension_size = c_bag.uint16(item_buf[extension_offset : extension_offset + 2])
861
+
862
+ if extension_size == 0:
863
+ break
864
+
865
+ if extension_size > size - extension_offset:
866
+ log.debug(
867
+ "Extension size exceeds item size: 0x%04x > 0x%04x - 0x%04x",
868
+ extension_size,
869
+ size,
870
+ extension_offset,
871
+ )
872
+ break # Extension size too large
873
+
874
+ extension_buf = item_buf[extension_offset : extension_offset + extension_size]
875
+ extension_signature = c_bag.uint32(extension_buf[4:8])
876
+
877
+ ext = None
878
+
879
+ if extension_signature >> 16 != 0xBEEF:
880
+ log.debug("Got unsupported extension signature 0x%08x from item %r", extension_signature, entry)
881
+ pass # Unsupported
882
+
883
+ elif extension_signature == 0xBEEF0000:
884
+ pass
885
+
886
+ elif extension_signature == 0xBEEF0001:
887
+ pass
888
+
889
+ elif extension_signature == 0xBEEF0003:
890
+ ext = EXTENSION_BLOCK_BEEF0004
891
+
892
+ elif extension_signature == 0xBEEF0004:
893
+ ext = EXTENSION_BLOCK_BEEF0004
894
+
895
+ elif extension_signature == 0xBEEF0005:
896
+ ext = EXTENSION_BLOCK_BEEF0005
897
+
898
+ elif extension_signature == 0xBEEF0006:
899
+ pass
900
+
901
+ elif extension_signature == 0xBEEF000A:
902
+ pass
903
+
904
+ elif extension_signature == 0xBEEF0013:
905
+ pass
906
+
907
+ elif extension_signature == 0xBEEF0014:
908
+ pass
909
+
910
+ elif extension_signature == 0xBEEF0019:
911
+ pass
912
+
913
+ elif extension_signature == 0xBEEF0025:
914
+ pass
915
+
916
+ elif extension_signature == 0xBEEF0026:
917
+ pass
918
+
919
+ else:
920
+ log.debug(
921
+ "Got unsupported beef extension signature 0x%08x from item %r", extension_signature, entry
922
+ )
923
+ pass
924
+
925
+ if ext is None:
926
+ ext = EXTENSION_BLOCK
927
+ log.debug("Unimplemented extension signature 0x%08x from item %r", extension_signature, entry)
928
+
929
+ ext = ext(extension_buf)
930
+
931
+ entry.extensions.append(ext)
932
+ extension_offset += extension_size
933
+
934
+ parent = entry
935
+ offset += size
936
+ yield entry
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.20.dev23
3
+ Version: 3.20.dev26
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
@@ -23,7 +23,7 @@ dissect/target/containers/vmdk.py,sha256=5fQGkJy4esXONXrKLbhpkQDt8Fwx19YENK2mOm7
23
23
  dissect/target/data/autocompletion/target_bash_completion.sh,sha256=wrOQ_ED-h8WFcjCmY6n4qKl84tWJv9l8ShFHDfJqJyA,3592
24
24
  dissect/target/filesystems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  dissect/target/filesystems/ad1.py,sha256=nEPzaaRsb6bL4ItFo0uLdmdLvrmK9BjqHeD3FOp3WQI,2413
26
- dissect/target/filesystems/btrfs.py,sha256=X-eMlKkx88xS2iIWRuq9yaQ82fl0DM435F1sdXkDo8g,6469
26
+ dissect/target/filesystems/btrfs.py,sha256=TotOs0-VOmgSijERZb1pOrIH_E7B1J_DRKqws8ttQTk,6569
27
27
  dissect/target/filesystems/cb.py,sha256=6LcoJiwsYu1Han31IUzVpZVDTifhTLTx_gLfNpB_p6k,5329
28
28
  dissect/target/filesystems/config.py,sha256=GQOtixIIt-Jjtpl3IVqUTujcBFfWaAZeAtvxgNUNetU,11963
29
29
  dissect/target/filesystems/cpio.py,sha256=ssVCjkAtLn2FqmNxeo6U5boyUdSYFxLWfXpytHYGPqs,641
@@ -34,7 +34,7 @@ dissect/target/filesystems/fat.py,sha256=ZSw-wS57vo5eIXJndfI1rZkGu_qh-vyioMzCZFZ
34
34
  dissect/target/filesystems/ffs.py,sha256=Wu8sS1jjmD0QXXcAaD2h_zzfvinjco8qvj0hErufZ-4,4555
35
35
  dissect/target/filesystems/itunes.py,sha256=w2lcWv6jlBPm84tsGZehxKBMXXyuW3KlmwVTF4ssQec,6395
36
36
  dissect/target/filesystems/jffs.py,sha256=Ceqa5Em2pepnXMH_XZFmSNjQyWPo1uWTthBFSMWfKRo,3926
37
- dissect/target/filesystems/ntfs.py,sha256=fGgCKjdO5GrPC21DDr0SwIxmwR7KruNIqGUzysboirA,7068
37
+ dissect/target/filesystems/ntfs.py,sha256=Losf35q9aLm-YdwVllT5so99s-GqTF1ZXMbLX0PUNC0,7624
38
38
  dissect/target/filesystems/overlay.py,sha256=d0BNZcVd3SzBcM1SZO5nX2FrEYcdtVH34BPJQ6Oh4x8,4753
39
39
  dissect/target/filesystems/smb.py,sha256=uxfcOWwEoDCw8Qpsa94T5Pn-SKd4WXs4OOrzVVI55d8,6406
40
40
  dissect/target/filesystems/squashfs.py,sha256=ehzlThXB7n96XUvQnsK5tWLsA9HIxYN-Zxl7aO9D7ts,3921
@@ -334,7 +334,7 @@ dissect/target/plugins/os/windows/regf/nethist.py,sha256=QHbG9fmZNmjSVhrgqMvMo12
334
334
  dissect/target/plugins/os/windows/regf/recentfilecache.py,sha256=goS6ajLIh6ZU-Gq4tupoxBoQCfMDp2qJgg-Nn5qFIsY,1850
335
335
  dissect/target/plugins/os/windows/regf/regf.py,sha256=D1GrljF-sV8cWIjWJ3zH7k52i1OWD8poEC_PIeZMEis,3419
336
336
  dissect/target/plugins/os/windows/regf/runkeys.py,sha256=-2HcdnVytzCt1xwgAI8rHDnwk8kwLPWURumvhrGnIHU,4278
337
- dissect/target/plugins/os/windows/regf/shellbags.py,sha256=hXAqThFkHmGPmhNRSXwMNzw25kAyIC6OOZivgpPEwTQ,25679
337
+ dissect/target/plugins/os/windows/regf/shellbags.py,sha256=-8WkdplG0FR37XgpCTd4iDdQvvrgtOk9kZY5qLsW5J8,26984
338
338
  dissect/target/plugins/os/windows/regf/shimcache.py,sha256=TY7GEFnxb8h99q12CzM0SwVlUymi4hFPae3uuM0M6kY,9998
339
339
  dissect/target/plugins/os/windows/regf/trusteddocs.py,sha256=3yvpBDM-Asg0rvGN2TwALGRm9DYogG6TxRau9D6FBbw,3700
340
340
  dissect/target/plugins/os/windows/regf/usb.py,sha256=nSAHB4Cdd0wF2C1EK_XYOfWCyqOgTZCLfDhuSmr7rdM,9709
@@ -368,10 +368,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
368
368
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
369
369
  dissect/target/volumes/md.py,sha256=7ShPtusuLGaIv27SvEETtgsuoQyAa4iAAeOR1NEaajI,1689
370
370
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
371
- dissect.target-3.20.dev23.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
372
- dissect.target-3.20.dev23.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
373
- dissect.target-3.20.dev23.dist-info/METADATA,sha256=dGuBoIpEp_hKqGr5hAlD6pnyc70LxqqzpOG40g_EeLE,12897
374
- dissect.target-3.20.dev23.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
375
- dissect.target-3.20.dev23.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
376
- dissect.target-3.20.dev23.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
377
- dissect.target-3.20.dev23.dist-info/RECORD,,
371
+ dissect.target-3.20.dev26.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
372
+ dissect.target-3.20.dev26.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
373
+ dissect.target-3.20.dev26.dist-info/METADATA,sha256=9ta9bS0OqAMhf6vc8zDqGxpXSkY-YRJMAHWRucXo3f8,12897
374
+ dissect.target-3.20.dev26.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
375
+ dissect.target-3.20.dev26.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
376
+ dissect.target-3.20.dev26.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
377
+ dissect.target-3.20.dev26.dist-info/RECORD,,