dissect.target 3.18.dev15__py3-none-any.whl → 3.19__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. dissect/target/filesystem.py +44 -25
  2. dissect/target/filesystems/config.py +32 -21
  3. dissect/target/filesystems/extfs.py +4 -0
  4. dissect/target/filesystems/itunes.py +1 -1
  5. dissect/target/filesystems/tar.py +1 -1
  6. dissect/target/filesystems/zip.py +81 -46
  7. dissect/target/helpers/config.py +22 -7
  8. dissect/target/helpers/configutil.py +69 -5
  9. dissect/target/helpers/cyber.py +4 -2
  10. dissect/target/helpers/fsutil.py +32 -4
  11. dissect/target/helpers/loaderutil.py +26 -7
  12. dissect/target/helpers/network_managers.py +22 -7
  13. dissect/target/helpers/record.py +37 -0
  14. dissect/target/helpers/record_modifier.py +23 -4
  15. dissect/target/helpers/shell_application_ids.py +732 -0
  16. dissect/target/helpers/utils.py +11 -0
  17. dissect/target/loader.py +1 -0
  18. dissect/target/loaders/ab.py +285 -0
  19. dissect/target/loaders/libvirt.py +40 -0
  20. dissect/target/loaders/mqtt.py +14 -1
  21. dissect/target/loaders/tar.py +8 -4
  22. dissect/target/loaders/utm.py +3 -0
  23. dissect/target/loaders/velociraptor.py +6 -6
  24. dissect/target/plugin.py +60 -3
  25. dissect/target/plugins/apps/browser/chrome.py +1 -0
  26. dissect/target/plugins/apps/browser/chromium.py +7 -5
  27. dissect/target/plugins/apps/browser/edge.py +1 -0
  28. dissect/target/plugins/apps/browser/firefox.py +82 -36
  29. dissect/target/plugins/apps/remoteaccess/anydesk.py +70 -50
  30. dissect/target/plugins/apps/remoteaccess/remoteaccess.py +8 -8
  31. dissect/target/plugins/apps/remoteaccess/teamviewer.py +46 -31
  32. dissect/target/plugins/apps/ssh/openssh.py +1 -1
  33. dissect/target/plugins/apps/ssh/ssh.py +177 -0
  34. dissect/target/plugins/apps/texteditor/__init__.py +0 -0
  35. dissect/target/plugins/apps/texteditor/texteditor.py +13 -0
  36. dissect/target/plugins/apps/texteditor/windowsnotepad.py +340 -0
  37. dissect/target/plugins/child/qemu.py +21 -0
  38. dissect/target/plugins/filesystem/ntfs/mft.py +132 -45
  39. dissect/target/plugins/filesystem/unix/capability.py +102 -87
  40. dissect/target/plugins/filesystem/walkfs.py +32 -21
  41. dissect/target/plugins/filesystem/yara.py +144 -23
  42. dissect/target/plugins/general/network.py +82 -0
  43. dissect/target/plugins/general/users.py +14 -10
  44. dissect/target/plugins/os/unix/_os.py +19 -5
  45. dissect/target/plugins/os/unix/bsd/freebsd/_os.py +3 -5
  46. dissect/target/plugins/os/unix/esxi/_os.py +29 -23
  47. dissect/target/plugins/os/unix/etc/etc.py +5 -8
  48. dissect/target/plugins/os/unix/history.py +3 -7
  49. dissect/target/plugins/os/unix/linux/_os.py +15 -14
  50. dissect/target/plugins/os/unix/linux/android/_os.py +15 -24
  51. dissect/target/plugins/os/unix/linux/redhat/_os.py +1 -1
  52. dissect/target/plugins/os/unix/locale.py +17 -6
  53. dissect/target/plugins/os/unix/shadow.py +47 -31
  54. dissect/target/plugins/os/windows/_os.py +4 -4
  55. dissect/target/plugins/os/windows/adpolicy.py +4 -1
  56. dissect/target/plugins/os/windows/catroot.py +1 -11
  57. dissect/target/plugins/os/windows/credential/__init__.py +0 -0
  58. dissect/target/plugins/os/windows/credential/lsa.py +174 -0
  59. dissect/target/plugins/os/windows/{sam.py → credential/sam.py} +5 -2
  60. dissect/target/plugins/os/windows/defender.py +6 -3
  61. dissect/target/plugins/os/windows/dpapi/blob.py +3 -0
  62. dissect/target/plugins/os/windows/dpapi/crypto.py +61 -23
  63. dissect/target/plugins/os/windows/dpapi/dpapi.py +127 -133
  64. dissect/target/plugins/os/windows/dpapi/keyprovider/__init__.py +0 -0
  65. dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +21 -0
  66. dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +17 -0
  67. dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +20 -0
  68. dissect/target/plugins/os/windows/dpapi/keyprovider/keyprovider.py +8 -0
  69. dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +38 -0
  70. dissect/target/plugins/os/windows/dpapi/master_key.py +3 -0
  71. dissect/target/plugins/os/windows/jumplist.py +292 -0
  72. dissect/target/plugins/os/windows/lnk.py +96 -93
  73. dissect/target/plugins/os/windows/regf/shellbags.py +8 -5
  74. dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
  75. dissect/target/plugins/os/windows/regf/usb.py +179 -114
  76. dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
  77. dissect/target/plugins/os/windows/wua_history.py +1073 -0
  78. dissect/target/target.py +4 -3
  79. dissect/target/tools/fs.py +53 -15
  80. dissect/target/tools/fsutils.py +243 -0
  81. dissect/target/tools/info.py +11 -4
  82. dissect/target/tools/query.py +2 -2
  83. dissect/target/tools/shell.py +505 -333
  84. dissect/target/tools/utils.py +23 -2
  85. dissect/target/tools/yara.py +65 -0
  86. dissect/target/volumes/md.py +2 -2
  87. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
  88. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/RECORD +94 -75
  89. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
  90. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
  91. dissect/target/helpers/ssh.py +0 -177
  92. /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
  93. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
  94. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
  95. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
- from typing import Optional
3
4
 
4
5
  from dissect.target.filesystem import Filesystem
5
6
  from dissect.target.helpers.network_managers import (
@@ -8,6 +9,8 @@ from dissect.target.helpers.network_managers import (
8
9
  )
9
10
  from dissect.target.plugin import OperatingSystem, export
10
11
  from dissect.target.plugins.os.unix._os import UnixPlugin
12
+ from dissect.target.plugins.os.unix.bsd.osx._os import MacPlugin
13
+ from dissect.target.plugins.os.windows._os import WindowsPlugin
11
14
  from dissect.target.target import Target
12
15
 
13
16
  log = logging.getLogger(__name__)
@@ -20,17 +23,13 @@ class LinuxPlugin(UnixPlugin, LinuxNetworkManager):
20
23
  self.network_manager.discover()
21
24
 
22
25
  @classmethod
23
- def detect(cls, target: Target) -> Optional[Filesystem]:
26
+ def detect(cls, target: Target) -> Filesystem | None:
24
27
  for fs in target.filesystems:
25
28
  if (
26
- fs.exists("/var")
27
- and fs.exists("/etc")
28
- and fs.exists("/opt")
29
- or (fs.exists("/sys") or fs.exists("/proc"))
30
- and not fs.exists("/Library")
31
- ):
29
+ (fs.exists("/var") and fs.exists("/etc") and fs.exists("/opt"))
30
+ or (fs.exists("/sys/module") or fs.exists("/proc/sys"))
31
+ ) and not (MacPlugin.detect(target) or WindowsPlugin.detect(target)):
32
32
  return fs
33
- return None
34
33
 
35
34
  @export(property=True)
36
35
  def ips(self) -> list[str]:
@@ -41,7 +40,7 @@ class LinuxPlugin(UnixPlugin, LinuxNetworkManager):
41
40
  for ip in ip_set:
42
41
  ips.append(ip)
43
42
 
44
- for ip in parse_unix_dhcp_log_messages(self.target):
43
+ for ip in parse_unix_dhcp_log_messages(self.target, iter_all=False):
45
44
  if ip not in ips:
46
45
  ips.append(ip)
47
46
 
@@ -68,7 +67,7 @@ class LinuxPlugin(UnixPlugin, LinuxNetworkManager):
68
67
  return self.network_manager.get_config_value("netmask")
69
68
 
70
69
  @export(property=True)
71
- def version(self) -> str:
70
+ def version(self) -> str | None:
72
71
  distrib_description = self._os_release.get("DISTRIB_DESCRIPTION", "")
73
72
  name = self._os_release.get("NAME", "") or self._os_release.get("DISTRIB_ID", "")
74
73
  version = (
@@ -77,11 +76,13 @@ class LinuxPlugin(UnixPlugin, LinuxNetworkManager):
77
76
  or self._os_release.get("DISTRIB_RELEASE", "")
78
77
  )
79
78
 
79
+ if not any([name, version, distrib_description]):
80
+ return None
81
+
80
82
  if len(f"{name} {version}") > len(distrib_description):
81
- return f"{name} {version}"
83
+ distrib_description = f"{name} {version}"
82
84
 
83
- else:
84
- return distrib_description
85
+ return distrib_description or None
85
86
 
86
87
  @export(property=True)
87
88
  def os(self) -> str:
@@ -1,33 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Iterator, Optional, TextIO
3
+ from typing import Iterator, Optional
4
4
 
5
5
  from dissect.target.filesystem import Filesystem
6
- from dissect.target.helpers.record import UnixUserRecord
6
+ from dissect.target.helpers import configutil
7
+ from dissect.target.helpers.record import EmptyRecord
7
8
  from dissect.target.plugin import OperatingSystem, export
8
9
  from dissect.target.plugins.os.unix.linux._os import LinuxPlugin
9
10
  from dissect.target.target import Target
10
11
 
11
12
 
12
- class BuildProp:
13
- def __init__(self, fh: TextIO):
14
- self.props = {}
15
-
16
- for line in fh:
17
- line = line.strip()
18
-
19
- if not line or line.startswith("#"):
20
- continue
21
-
22
- k, v = line.split("=")
23
- self.props[k] = v
24
-
25
-
26
13
  class AndroidPlugin(LinuxPlugin):
27
14
  def __init__(self, target: Target):
28
15
  super().__init__(target)
29
16
  self.target = target
30
- self.props = BuildProp(self.target.fs.path("/build.prop").open("rt"))
17
+
18
+ self.props = {}
19
+ if (build_prop := self.target.fs.path("/build.prop")).exists():
20
+ self.props = configutil.parse(build_prop, separator=("=",), comment_prefixes=("#",)).parsed_data
31
21
 
32
22
  @classmethod
33
23
  def detect(cls, target: Target) -> Optional[Filesystem]:
@@ -42,8 +32,8 @@ class AndroidPlugin(LinuxPlugin):
42
32
  return cls(target)
43
33
 
44
34
  @export(property=True)
45
- def hostname(self) -> str:
46
- return self.props.props["ro.build.host"]
35
+ def hostname(self) -> Optional[str]:
36
+ return self.props.get("ro.build.host")
47
37
 
48
38
  @export(property=True)
49
39
  def ips(self) -> list[str]:
@@ -53,11 +43,11 @@ class AndroidPlugin(LinuxPlugin):
53
43
  def version(self) -> str:
54
44
  full_version = "Android"
55
45
 
56
- release_version = self.props.props.get("ro.build.version.release")
57
- if release_version := self.props.props.get("ro.build.version.release"):
46
+ release_version = self.props.get("ro.build.version.release")
47
+ if release_version := self.props.get("ro.build.version.release"):
58
48
  full_version += f" {release_version}"
59
49
 
60
- if security_patch_version := self.props.props.get("ro.build.version.security_patch"):
50
+ if security_patch_version := self.props.get("ro.build.version.security_patch"):
61
51
  full_version += f" ({security_patch_version})"
62
52
 
63
53
  return full_version
@@ -66,5 +56,6 @@ class AndroidPlugin(LinuxPlugin):
66
56
  def os(self) -> str:
67
57
  return OperatingSystem.ANDROID.value
68
58
 
69
- def users(self) -> Iterator[UnixUserRecord]:
70
- raise NotImplementedError()
59
+ @export(record=EmptyRecord)
60
+ def users(self) -> Iterator[EmptyRecord]:
61
+ yield from ()
@@ -5,7 +5,7 @@ from dissect.target.plugins.os.unix.linux._os import LinuxPlugin
5
5
  from dissect.target.target import Target
6
6
 
7
7
 
8
- class RedHat(LinuxPlugin):
8
+ class RedHatPlugin(LinuxPlugin):
9
9
  def __init__(self, target: Target):
10
10
  super().__init__(target)
11
11
 
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
4
+ from typing import Iterator
2
5
 
3
6
  from dissect.target.helpers.localeutil import normalize_language
4
7
  from dissect.target.helpers.record import TargetRecordDescriptor
@@ -30,7 +33,7 @@ class LocalePlugin(Plugin):
30
33
  pass
31
34
 
32
35
  @export(property=True)
33
- def timezone(self):
36
+ def timezone(self) -> str | None:
34
37
  """Get the timezone of the system."""
35
38
 
36
39
  # /etc/timezone should contain a simple timezone string
@@ -58,15 +61,23 @@ class LocalePlugin(Plugin):
58
61
  size = p_localtime.stat().st_size
59
62
  sha1 = p_localtime.get().sha1()
60
63
  for path in self.target.fs.path("/usr/share/zoneinfo").rglob("*"):
64
+ # Ignore posix files in zoneinfo directory (RHEL).
65
+ if path.name.startswith("posix"):
66
+ continue
67
+
61
68
  if path.is_file() and path.stat().st_size == size and path.get().sha1() == sha1:
62
69
  return timezone_from_path(path)
63
70
 
64
71
  @export(property=True)
65
- def language(self):
72
+ def language(self) -> list[str]:
66
73
  """Get the configured locale(s) of the system."""
67
- # Although this purports to be a generic function for Unix targets,
68
- # these paths are Linux specific.
69
- locale_paths = ["/etc/default/locale", "/etc/locale.conf"]
74
+
75
+ # Although this purports to be a generic function for Unix targets, these paths are Linux specific.
76
+ locale_paths = [
77
+ "/etc/default/locale",
78
+ "/etc/locale.conf",
79
+ "/etc/sysconfig/i18n",
80
+ ]
70
81
 
71
82
  found_languages = []
72
83
 
@@ -79,7 +90,7 @@ class LocalePlugin(Plugin):
79
90
  return found_languages
80
91
 
81
92
  @export(record=UnixKeyboardRecord)
82
- def keyboard(self):
93
+ def keyboard(self) -> Iterator[UnixKeyboardRecord]:
83
94
  """Get the keyboard layout(s) of the system."""
84
95
 
85
96
  paths = ["/etc/default/keyboard", "/etc/vconsole.conf"] + list(
@@ -29,39 +29,55 @@ class ShadowPlugin(Plugin):
29
29
  if not self.target.fs.path("/etc/shadow").exists():
30
30
  raise UnsupportedPluginError("No shadow file found")
31
31
 
32
+ SHADOW_FILES = ["/etc/shadow", "/etc/shadow-"]
33
+
32
34
  @export(record=UnixShadowRecord)
33
35
  def passwords(self) -> Iterator[UnixShadowRecord]:
34
- """Recover shadow records from /etc/shadow files."""
35
-
36
- if (path := self.target.fs.path("/etc/shadow")).exists():
37
- for line in path.open("rt"):
38
- line = line.strip()
39
- if line == "" or line.startswith("#"):
40
- continue
41
-
42
- shent = dict(enumerate(line.split(":")))
43
- crypt = extract_crypt_details(shent)
44
-
45
- # do not return a shadow record if we have no hash
46
- if crypt.get("hash") is None or crypt.get("hash") == "":
47
- continue
48
-
49
- yield UnixShadowRecord(
50
- name=shent.get(0),
51
- crypt=shent.get(1),
52
- algorithm=crypt.get("algo"),
53
- crypt_param=crypt.get("param"),
54
- salt=crypt.get("salt"),
55
- hash=crypt.get("hash"),
56
- last_change=shent.get(2),
57
- min_age=shent.get(3),
58
- max_age=shent.get(4),
59
- warning_period=shent.get(5),
60
- inactivity_period=shent.get(6),
61
- expiration_date=shent.get(7),
62
- unused_field=shent.get(8),
63
- _target=self.target,
64
- )
36
+ """Yield shadow records from /etc/shadow files.
37
+
38
+ Resources:
39
+ - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html#file:/etc/shadow
40
+ """
41
+
42
+ seen_hashes = set()
43
+
44
+ for shadow_file in self.SHADOW_FILES:
45
+ if (path := self.target.fs.path(shadow_file)).exists():
46
+ for line in path.open("rt"):
47
+ line = line.strip()
48
+ if line == "" or line.startswith("#"):
49
+ continue
50
+
51
+ shent = dict(enumerate(line.split(":")))
52
+ crypt = extract_crypt_details(shent)
53
+
54
+ # do not return a shadow record if we have no hash
55
+ if crypt.get("hash") is None or crypt.get("hash") == "":
56
+ continue
57
+
58
+ # prevent duplicate user hashes
59
+ current_hash = (shent.get(0), crypt.get("hash"))
60
+ if current_hash in seen_hashes:
61
+ continue
62
+
63
+ seen_hashes.add(current_hash)
64
+
65
+ yield UnixShadowRecord(
66
+ name=shent.get(0),
67
+ crypt=shent.get(1),
68
+ algorithm=crypt.get("algo"),
69
+ crypt_param=crypt.get("param"),
70
+ salt=crypt.get("salt"),
71
+ hash=crypt.get("hash"),
72
+ last_change=shent.get(2),
73
+ min_age=shent.get(3),
74
+ max_age=shent.get(4),
75
+ warning_period=shent.get(5),
76
+ inactivity_period=shent.get(6),
77
+ expiration_date=shent.get(7),
78
+ unused_field=shent.get(8),
79
+ _target=self.target,
80
+ )
65
81
 
66
82
 
67
83
  def extract_crypt_details(shent: dict) -> dict:
@@ -79,15 +79,15 @@ class WindowsPlugin(OSPlugin):
79
79
  self.target.log.debug("", exc_info=e)
80
80
 
81
81
  sysvol_drive = self.target.fs.mounts.get("sysvol")
82
- # Fallback mount the sysvol to C: if we didn't manage to mount it to any other drive letter
83
- if sysvol_drive and operator.countOf(self.target.fs.mounts.values(), sysvol_drive) == 1:
82
+ if not sysvol_drive:
83
+ self.target.log.warning("No sysvol drive found")
84
+ elif operator.countOf(self.target.fs.mounts.values(), sysvol_drive) == 1:
85
+ # Fallback mount the sysvol to C: if we didn't manage to mount it to any other drive letter
84
86
  if "c:" not in self.target.fs.mounts:
85
87
  self.target.log.debug("Unable to determine drive letter of sysvol, falling back to C:")
86
88
  self.target.fs.mount("c:", sysvol_drive)
87
89
  else:
88
90
  self.target.log.warning("Unknown drive letter for sysvol")
89
- else:
90
- self.target.log.warning("No sysvol drive found")
91
91
 
92
92
  @export(property=True)
93
93
  def hostname(self) -> Optional[str]:
@@ -69,7 +69,10 @@ class ADPolicyPlugin(Plugin):
69
69
  xml = task_file.read_text()
70
70
  tree = ElementTree.fromstring(xml)
71
71
  for task in tree.findall(".//{*}Task"):
72
- properties = task.find("Properties") or task
72
+ # https://github.com/python/cpython/issues/83122
73
+ if (properties := task.find("Properties")) is None:
74
+ properties = task
75
+
73
76
  task_data = ElementTree.tostring(task)
74
77
  yield ADPolicyRecord(
75
78
  last_modification_time=task_file_stat.st_mtime,
@@ -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,174 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from functools import cached_property
5
+ from typing import Iterator
6
+
7
+ from dissect.target.exceptions import RegistryKeyNotFoundError, UnsupportedPluginError
8
+ from dissect.target.helpers.record import TargetRecordDescriptor
9
+ from dissect.target.plugin import Plugin, export
10
+
11
+ try:
12
+ from Crypto.Cipher import AES, ARC4, DES
13
+
14
+ HAS_CRYPTO = True
15
+ except ImportError:
16
+ HAS_CRYPTO = False
17
+
18
+
19
+ LSASecretRecord = TargetRecordDescriptor(
20
+ "windows/credential/lsa",
21
+ [
22
+ ("datetime", "ts"),
23
+ ("string", "name"),
24
+ ("string", "value"),
25
+ ],
26
+ )
27
+
28
+
29
+ class LSAPlugin(Plugin):
30
+ """Windows Local Security Authority (LSA) plugin.
31
+
32
+ Resources:
33
+ - https://learn.microsoft.com/en-us/windows/win32/secauthn/lsa-authentication
34
+ - https://moyix.blogspot.com/2008/02/decrypting-lsa-secrets.html (Windows XP)
35
+ - https://github.com/fortra/impacket/blob/master/impacket/examples/secretsdump.py
36
+ """
37
+
38
+ __namespace__ = "lsa"
39
+
40
+ SECURITY_POLICY_KEY = "HKEY_LOCAL_MACHINE\\SECURITY\\Policy"
41
+ SYSTEM_KEY = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\LSA"
42
+
43
+ def check_compatible(self) -> None:
44
+ if not HAS_CRYPTO:
45
+ raise UnsupportedPluginError("Missing pycryptodome dependency")
46
+
47
+ if not self.target.has_function("registry") or not list(self.target.registry.keys(self.SYSTEM_KEY)):
48
+ raise UnsupportedPluginError("Registry key not found: %s", self.SYSTEM_KEY)
49
+
50
+ @cached_property
51
+ def syskey(self) -> bytes:
52
+ """Return byte value of Windows system SYSKEY, also called BootKey."""
53
+ lsa = self.target.registry.key(self.SYSTEM_KEY)
54
+ syskey_keys = ["JD", "Skew1", "GBG", "Data"]
55
+ # This magic value rotates the order of the data
56
+ alterator = [0x8, 0x5, 0x4, 0x2, 0xB, 0x9, 0xD, 0x3, 0x0, 0x6, 0x1, 0xC, 0xE, 0xA, 0xF, 0x7]
57
+
58
+ r = bytes.fromhex("".join([lsa.subkey(key).class_name for key in syskey_keys]))
59
+ return bytes(r[i] for i in alterator)
60
+
61
+ @cached_property
62
+ def lsakey(self) -> bytes:
63
+ """Decrypt and return the LSA key of the Windows system."""
64
+ security_pol = self.target.registry.key(self.SECURITY_POLICY_KEY)
65
+
66
+ try:
67
+ # Windows Vista or newer
68
+ enc_key = security_pol.subkey("PolEKList").value("(Default)").value
69
+ lsa_key = _decrypt_aes(enc_key, self.syskey)
70
+ return lsa_key[68:100]
71
+ except RegistryKeyNotFoundError:
72
+ pass
73
+
74
+ try:
75
+ # Windows XP
76
+ enc_key = security_pol.subkey("PolSecretEncryptionKey").value("(Default)").value
77
+ lsa_key = _decrypt_rc4(enc_key, self.syskey)
78
+ return lsa_key[16:32]
79
+ except RegistryKeyNotFoundError:
80
+ pass
81
+
82
+ raise ValueError("Unable to determine LSA policy key location in registry")
83
+
84
+ @cached_property
85
+ def _secrets(self) -> dict[str, bytes] | None:
86
+ """Return dict of Windows system decrypted LSA secrets."""
87
+ if not self.target.ntversion:
88
+ raise ValueError("Unable to determine Windows NT version")
89
+
90
+ result = {}
91
+ for subkey in self.target.registry.key(self.SECURITY_POLICY_KEY).subkey("Secrets").subkeys():
92
+ enc_data = subkey.subkey("CurrVal").value("(Default)").value
93
+
94
+ # Windows Vista or newer
95
+ if float(self.target.ntversion) >= 6.0:
96
+ secret = _decrypt_aes(enc_data, self.lsakey)
97
+
98
+ # Windows XP
99
+ else:
100
+ secret = _decrypt_des(enc_data, self.lsakey)
101
+
102
+ result[subkey.name] = secret
103
+
104
+ return result
105
+
106
+ @export(record=LSASecretRecord)
107
+ def secrets(self) -> Iterator[LSASecretRecord]:
108
+ """Yield decrypted LSA secrets from a Windows target."""
109
+ for key, value in self._secrets.items():
110
+ yield LSASecretRecord(
111
+ ts=self.target.registry.key(f"{self.SECURITY_POLICY_KEY}\\Secrets\\{key}").ts,
112
+ name=key,
113
+ value=value.hex(),
114
+ _target=self.target,
115
+ )
116
+
117
+
118
+ def _decrypt_aes(data: bytes, key: bytes) -> bytes:
119
+ ctx = hashlib.sha256()
120
+ ctx.update(key)
121
+ for _ in range(1, 1000 + 1):
122
+ ctx.update(data[28:60])
123
+
124
+ ciphertext = data[60:]
125
+ plaintext = []
126
+
127
+ for i in range(0, len(ciphertext), 16):
128
+ cipher = AES.new(ctx.digest(), AES.MODE_CBC, iv=b"\x00" * 16)
129
+ plaintext.append(cipher.decrypt(ciphertext[i : i + 16].ljust(16, b"\x00")))
130
+
131
+ return b"".join(plaintext)
132
+
133
+
134
+ def _decrypt_rc4(data: bytes, key: bytes) -> bytes:
135
+ md5 = hashlib.md5()
136
+ md5.update(key)
137
+ for _ in range(1000):
138
+ md5.update(data[60:76])
139
+ rc4_key = md5.digest()
140
+
141
+ cipher = ARC4.new(rc4_key)
142
+ return cipher.decrypt(data[12:60])
143
+
144
+
145
+ def _decrypt_des(data: bytes, key: bytes) -> bytes:
146
+ plaintext = []
147
+
148
+ enc_size = int.from_bytes(data[:4], "little")
149
+ data = data[len(data) - enc_size :]
150
+
151
+ key0 = key
152
+ for _ in range(0, len(data), 8):
153
+ ciphertext = data[:8]
154
+ block_key = _transform_key(key0[:7])
155
+
156
+ cipher = DES.new(block_key, DES.MODE_ECB)
157
+ plaintext.append(cipher.decrypt(ciphertext))
158
+
159
+ key0 = key0[7:]
160
+ data = data[8:]
161
+
162
+ if len(key0) < 7:
163
+ key0 = key[len(key0) :]
164
+
165
+ return b"".join(plaintext)
166
+
167
+
168
+ def _transform_key(key: bytes) -> bytes:
169
+ new_key = []
170
+ new_key.append(((key[0] >> 0x01) << 1) & 0xFE)
171
+ for i in range(0, 6):
172
+ new_key.append((((key[i] & ((1 << (i + 1)) - 1)) << (6 - i) | (key[i + 1] >> (i + 2))) << 1) & 0xFE)
173
+ new_key.append(((key[6] & 0x7F) << 1) & 0xFE)
174
+ return bytes(new_key)
@@ -210,7 +210,7 @@ struct SAM_HASH_AES { /* size: >=24 */
210
210
  c_sam = cstruct().load(sam_def)
211
211
 
212
212
  SamRecord = TargetRecordDescriptor(
213
- "windows/registry/sam",
213
+ "windows/credential/sam",
214
214
  [
215
215
  ("uint32", "rid"),
216
216
  ("string", "fullname"),
@@ -303,6 +303,9 @@ class SamPlugin(Plugin):
303
303
  if not HAS_CRYPTO:
304
304
  raise UnsupportedPluginError("Missing pycryptodome dependency")
305
305
 
306
+ if not self.target.has_function("lsa"):
307
+ raise UnsupportedPluginError("LSA plugin is required for SAM plugin")
308
+
306
309
  if not len(list(self.target.registry.keys(self.SAM_KEY))) > 0:
307
310
  raise UnsupportedPluginError(f"Registry key not found: {self.SAM_KEY}")
308
311
 
@@ -374,7 +377,7 @@ class SamPlugin(Plugin):
374
377
  nt (string): Parsed NT-hash.
375
378
  """
376
379
 
377
- syskey = self.target.dpapi.syskey # aka. bootkey
380
+ syskey = self.target.lsa.syskey # aka. bootkey
378
381
  samkey = self.calculate_samkey(syskey) # aka. hashed bootkey or hbootkey
379
382
 
380
383
  almpassword = b"LMPASSWORD\0"
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from typing import Any, BinaryIO, Generator, Iterable, Iterator, TextIO, Union
8
8
 
9
9
  import dissect.util.ts as ts
10
- from dissect.cstruct import Structure, cstruct
10
+ from dissect.cstruct import cstruct
11
11
  from flow.record import Record
12
12
 
13
13
  from dissect.target import plugin
@@ -357,7 +357,7 @@ class QuarantineEntry:
357
357
  resource_info = c_defender.QuarantineEntrySection2(resource_buf)
358
358
 
359
359
  # List holding all quarantine entry resources that belong to this quarantine entry.
360
- self.resources = []
360
+ self.resources: list[QuarantineEntryResource] = []
361
361
 
362
362
  for offset in resource_info.EntryOffsets:
363
363
  resource_buf.seek(offset)
@@ -393,7 +393,7 @@ class QuarantineEntryResource:
393
393
  # Move pointer
394
394
  offset += 4 + field.Size
395
395
 
396
- def _add_field(self, field: Structure):
396
+ def _add_field(self, field: c_defender.QuarantineEntryResourceField) -> None:
397
397
  if field.Identifier == FIELD_IDENTIFIER.CQuaResDataID_File:
398
398
  self.resource_id = field.Data.hex().upper()
399
399
  elif field.Identifier == FIELD_IDENTIFIER.PhysicalPath:
@@ -627,6 +627,9 @@ class MicrosoftDefenderPlugin(plugin.Plugin):
627
627
  if suffix.search(mplog_line):
628
628
  break
629
629
  match = pattern.match(block)
630
+ if not match:
631
+ return
632
+
630
633
  data = match.groupdict()
631
634
  data["_target"] = self.target
632
635
  data["source_log"] = source
@@ -90,6 +90,9 @@ class Blob:
90
90
  if self.decrypted:
91
91
  return True
92
92
 
93
+ if not master_key:
94
+ raise ValueError("No master key provided to decrypt blob with")
95
+
93
96
  for algo in [crypt_session_key_type1, crypt_session_key_type2]:
94
97
  session_key = algo(
95
98
  master_key,