dissect.target 3.18.dev16__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.
- dissect/target/filesystem.py +44 -25
- dissect/target/filesystems/config.py +32 -21
- dissect/target/filesystems/extfs.py +4 -0
- dissect/target/filesystems/itunes.py +1 -1
- dissect/target/filesystems/tar.py +1 -1
- dissect/target/filesystems/zip.py +81 -46
- dissect/target/helpers/config.py +22 -7
- dissect/target/helpers/configutil.py +69 -5
- dissect/target/helpers/cyber.py +4 -2
- dissect/target/helpers/fsutil.py +32 -4
- dissect/target/helpers/loaderutil.py +26 -7
- dissect/target/helpers/network_managers.py +22 -7
- dissect/target/helpers/record.py +37 -0
- dissect/target/helpers/record_modifier.py +23 -4
- dissect/target/helpers/shell_application_ids.py +732 -0
- dissect/target/helpers/utils.py +11 -0
- dissect/target/loader.py +1 -0
- dissect/target/loaders/ab.py +285 -0
- dissect/target/loaders/libvirt.py +40 -0
- dissect/target/loaders/mqtt.py +14 -1
- dissect/target/loaders/tar.py +8 -4
- dissect/target/loaders/utm.py +3 -0
- dissect/target/loaders/velociraptor.py +6 -6
- dissect/target/plugin.py +60 -3
- dissect/target/plugins/apps/browser/chrome.py +1 -0
- dissect/target/plugins/apps/browser/chromium.py +7 -5
- dissect/target/plugins/apps/browser/edge.py +1 -0
- dissect/target/plugins/apps/browser/firefox.py +82 -36
- dissect/target/plugins/apps/remoteaccess/anydesk.py +70 -50
- dissect/target/plugins/apps/remoteaccess/remoteaccess.py +8 -8
- dissect/target/plugins/apps/remoteaccess/teamviewer.py +46 -31
- dissect/target/plugins/apps/ssh/openssh.py +1 -1
- dissect/target/plugins/apps/ssh/ssh.py +177 -0
- dissect/target/plugins/apps/texteditor/__init__.py +0 -0
- dissect/target/plugins/apps/texteditor/texteditor.py +13 -0
- dissect/target/plugins/apps/texteditor/windowsnotepad.py +340 -0
- dissect/target/plugins/child/qemu.py +21 -0
- dissect/target/plugins/filesystem/ntfs/mft.py +132 -45
- dissect/target/plugins/filesystem/unix/capability.py +102 -87
- dissect/target/plugins/filesystem/walkfs.py +32 -21
- dissect/target/plugins/filesystem/yara.py +144 -23
- dissect/target/plugins/general/network.py +82 -0
- dissect/target/plugins/general/users.py +14 -10
- dissect/target/plugins/os/unix/_os.py +19 -5
- dissect/target/plugins/os/unix/bsd/freebsd/_os.py +3 -5
- dissect/target/plugins/os/unix/esxi/_os.py +29 -23
- dissect/target/plugins/os/unix/etc/etc.py +5 -8
- dissect/target/plugins/os/unix/history.py +3 -7
- dissect/target/plugins/os/unix/linux/_os.py +15 -14
- dissect/target/plugins/os/unix/linux/android/_os.py +15 -24
- dissect/target/plugins/os/unix/linux/redhat/_os.py +1 -1
- dissect/target/plugins/os/unix/locale.py +17 -6
- dissect/target/plugins/os/unix/shadow.py +47 -31
- dissect/target/plugins/os/windows/_os.py +4 -4
- dissect/target/plugins/os/windows/adpolicy.py +4 -1
- dissect/target/plugins/os/windows/catroot.py +1 -11
- dissect/target/plugins/os/windows/credential/__init__.py +0 -0
- dissect/target/plugins/os/windows/credential/lsa.py +174 -0
- dissect/target/plugins/os/windows/{sam.py → credential/sam.py} +5 -2
- dissect/target/plugins/os/windows/defender.py +6 -3
- dissect/target/plugins/os/windows/dpapi/blob.py +3 -0
- dissect/target/plugins/os/windows/dpapi/crypto.py +61 -23
- dissect/target/plugins/os/windows/dpapi/dpapi.py +127 -133
- dissect/target/plugins/os/windows/dpapi/keyprovider/__init__.py +0 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +21 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +17 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +20 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/keyprovider.py +8 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +38 -0
- dissect/target/plugins/os/windows/dpapi/master_key.py +3 -0
- dissect/target/plugins/os/windows/jumplist.py +292 -0
- dissect/target/plugins/os/windows/lnk.py +96 -93
- dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
- dissect/target/plugins/os/windows/regf/usb.py +179 -114
- dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
- dissect/target/plugins/os/windows/wua_history.py +1073 -0
- dissect/target/target.py +4 -3
- dissect/target/tools/fs.py +53 -15
- dissect/target/tools/fsutils.py +243 -0
- dissect/target/tools/info.py +11 -4
- dissect/target/tools/query.py +2 -2
- dissect/target/tools/shell.py +505 -333
- dissect/target/tools/utils.py +23 -2
- dissect/target/tools/yara.py +65 -0
- dissect/target/volumes/md.py +2 -2
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/RECORD +93 -74
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
- dissect/target/helpers/ssh.py +0 -177
- /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
- {dissect.target-3.18.dev16.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) ->
|
26
|
+
def detect(cls, target: Target) -> Filesystem | None:
|
24
27
|
for fs in target.filesystems:
|
25
28
|
if (
|
26
|
-
fs.exists("/var")
|
27
|
-
|
28
|
-
|
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
|
-
|
83
|
+
distrib_description = f"{name} {version}"
|
82
84
|
|
83
|
-
|
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
|
3
|
+
from typing import Iterator, Optional
|
4
4
|
|
5
5
|
from dissect.target.filesystem import Filesystem
|
6
|
-
from dissect.target.helpers
|
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
|
-
|
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.
|
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.
|
57
|
-
if release_version := self.props.
|
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.
|
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
|
-
|
70
|
-
|
59
|
+
@export(record=EmptyRecord)
|
60
|
+
def users(self) -> Iterator[EmptyRecord]:
|
61
|
+
yield from ()
|
@@ -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
|
-
|
68
|
-
# these paths are Linux specific.
|
69
|
-
locale_paths = [
|
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
|
-
"""
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
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():
|
File without changes
|
@@ -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/
|
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.
|
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
|
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:
|
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
|