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
@@ -2,17 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import hmac
5
- from typing import Optional, Union
6
5
 
7
6
  try:
8
- from Crypto.Cipher import AES, ARC4
7
+ from Crypto.Cipher import AES, ARC4, DES3
9
8
 
10
9
  HAS_CRYPTO = True
11
10
  except ImportError:
12
11
  HAS_CRYPTO = False
13
12
 
14
- CIPHER_ALGORITHMS: dict[Union[int, str], CipherAlgorithm] = {}
15
- HASH_ALGORITHMS: dict[Union[int, str], HashAlgorithm] = {}
13
+ CIPHER_ALGORITHMS: dict[int | str, CipherAlgorithm] = {}
14
+ HASH_ALGORITHMS: dict[int | str, HashAlgorithm] = {}
16
15
 
17
16
 
18
17
  class CipherAlgorithm:
@@ -35,17 +34,27 @@ class CipherAlgorithm:
35
34
  return CIPHER_ALGORITHMS[name]()
36
35
 
37
36
  def derive_key(self, key: bytes, hash_algorithm: HashAlgorithm) -> bytes:
38
- """Mimics the corresponding native Microsoft function."""
37
+ """Mimics the corresponding native Microsoft function.
38
+
39
+ Resources:
40
+ - https://github.com/tijldeneut/DPAPIck3/blob/main/dpapick3/crypto.py#L185
41
+ """
42
+
39
43
  if len(key) > hash_algorithm.block_length:
40
44
  key = hashlib.new(hash_algorithm.name, key).digest()
41
45
 
42
- if len(key) >= hash_algorithm.digest_length:
46
+ if len(key) >= self.key_length:
43
47
  return key
44
48
 
45
49
  key = key.ljust(hash_algorithm.block_length, b"\x00")
46
- pad1 = bytes(c ^ 0x36 for c in key)
47
- pad2 = bytes(c ^ 0x5C for c in key)
48
- return hashlib.new(hash_algorithm.name, pad1).digest() + hashlib.new(hash_algorithm.name, pad2).digest()
50
+ pad1 = bytes(c ^ 0x36 for c in key)[: hash_algorithm.block_length]
51
+ pad2 = bytes(c ^ 0x5C for c in key)[: hash_algorithm.block_length]
52
+ key = hashlib.new(hash_algorithm.name, pad1).digest() + hashlib.new(hash_algorithm.name, pad2).digest()
53
+ key = self.fixup_key(key)
54
+ return key
55
+
56
+ def fixup_key(self, key: bytes) -> bytes:
57
+ return key
49
58
 
50
59
  def decrypt_with_hmac(
51
60
  self, data: bytes, key: bytes, iv: bytes, hash_algorithm: HashAlgorithm, rounds: int
@@ -55,7 +64,7 @@ class CipherAlgorithm:
55
64
 
56
65
  return self.decrypt(data, key, iv)
57
66
 
58
- def decrypt(self, data: bytes, key: bytes, iv: Optional[bytes] = None) -> bytes:
67
+ def decrypt(self, data: bytes, key: bytes, iv: bytes | None = None) -> bytes:
59
68
  raise NotImplementedError()
60
69
 
61
70
 
@@ -66,7 +75,7 @@ class _AES(CipherAlgorithm):
66
75
  iv_length = 128 // 8
67
76
  block_length = 128 // 8
68
77
 
69
- def decrypt(self, data: bytes, key: bytes, iv: Optional[bytes] = None) -> bytes:
78
+ def decrypt(self, data: bytes, key: bytes, iv: bytes | None = None) -> bytes:
70
79
  if not HAS_CRYPTO:
71
80
  raise RuntimeError("Missing pycryptodome dependency")
72
81
 
@@ -100,7 +109,7 @@ class _RC4(CipherAlgorithm):
100
109
  iv_length = 128 // 8
101
110
  block_length = 1 // 8
102
111
 
103
- def decrypt(self, data: bytes, key: bytes, iv: Optional[bytes] = None) -> bytes:
112
+ def decrypt(self, data: bytes, key: bytes, iv: bytes | None = None) -> bytes:
104
113
  if not HAS_CRYPTO:
105
114
  raise RuntimeError("Missing pycryptodome dependency")
106
115
 
@@ -108,6 +117,34 @@ class _RC4(CipherAlgorithm):
108
117
  return cipher.decrypt(data)
109
118
 
110
119
 
120
+ class _DES3(CipherAlgorithm):
121
+ id = 0x6603
122
+ name = "DES3"
123
+ key_length = 192 // 8
124
+ iv_length = 64 // 8
125
+ block_length = 64 // 8
126
+
127
+ def fixup_key(self, key: bytes) -> bytes:
128
+ nkey = []
129
+ for byte in key:
130
+ parity_bit = 0
131
+ for i in range(8):
132
+ parity_bit ^= (byte >> i) & 1
133
+
134
+ nkey.append(byte if parity_bit == 0 else byte | 1)
135
+ return bytes(nkey[: self.key_length])
136
+
137
+ def decrypt(self, data: bytes, key: bytes, iv: bytes | None = None) -> bytes:
138
+ if not HAS_CRYPTO:
139
+ raise RuntimeError("Missing pycryptodome dependency")
140
+
141
+ if len(key) != 24:
142
+ raise ValueError(f"Invalid DES3 CBC key length {len(key)}")
143
+
144
+ cipher = DES3.new(key, DES3.MODE_CBC, iv=iv if iv else b"\x00" * 8)
145
+ return cipher.decrypt(data)
146
+
147
+
111
148
  class HashAlgorithm:
112
149
  id: int
113
150
  name: str
@@ -123,7 +160,7 @@ class HashAlgorithm:
123
160
  return HASH_ALGORITHMS[id]()
124
161
 
125
162
  @classmethod
126
- def from_name(cls, name: str) -> Optional[HashAlgorithm]:
163
+ def from_name(cls, name: str) -> HashAlgorithm | None:
127
164
  return HASH_ALGORITHMS[name]()
128
165
 
129
166
 
@@ -148,7 +185,7 @@ class _HMAC(_SHA1):
148
185
 
149
186
 
150
187
  class _SHA256(HashAlgorithm):
151
- id = 0x8004
188
+ id = 0x800C
152
189
  name = "sha256"
153
190
  digest_length = 256 // 8
154
191
  block_length = 512 // 8
@@ -197,12 +234,12 @@ def dpapi_hmac(pwd_hash: bytes, hmac_salt: bytes, value: bytes, hash_algorithm:
197
234
 
198
235
  def crypt_session_key_type1(
199
236
  master_key: bytes,
200
- nonce: Optional[bytes],
237
+ nonce: bytes | None,
201
238
  hash_algorithm: HashAlgorithm,
202
- entropy: Optional[bytes] = None,
203
- strong_password: Optional[str] = None,
204
- smart_card_secret: Optional[bytes] = None,
205
- verify_blob: Optional[bytes] = None,
239
+ entropy: bytes | None = None,
240
+ strong_password: str | None = None,
241
+ smart_card_secret: bytes | None = None,
242
+ verify_blob: bytes | None = None,
206
243
  ) -> bytes:
207
244
  """Computes the decryption key for Type1 DPAPI blob, given the master key and optional information.
208
245
 
@@ -218,6 +255,7 @@ def crypt_session_key_type1(
218
255
  strong_password: Optional password used for decryption or the blob itself.
219
256
  smart_card_secret: Optional MS Next Gen Crypto secret (e.g. from PIN code).
220
257
  verify_blob: Optional encrypted blob used for integrity check.
258
+
221
259
  Returns:
222
260
  decryption key
223
261
  """
@@ -258,10 +296,10 @@ def crypt_session_key_type2(
258
296
  masterkey: bytes,
259
297
  nonce: bytes,
260
298
  hash_algorithm: HashAlgorithm,
261
- entropy: Optional[bytes] = None,
262
- strong_password: Optional[str] = None,
263
- smart_card_secret: Optional[bytes] = None,
264
- verify_blob: Optional[bytes] = None,
299
+ entropy: bytes | None = None,
300
+ strong_password: str | None = None,
301
+ smart_card_secret: bytes | None = None,
302
+ verify_blob: bytes | None = None,
265
303
  ) -> bytes:
266
304
  """Computes the decryption key for Type2 DPAPI blob, given the masterkey and optional information.
267
305
 
@@ -1,18 +1,11 @@
1
- import hashlib
1
+ from __future__ import annotations
2
+
2
3
  import re
3
4
  from functools import cache, cached_property
4
5
  from pathlib import Path
5
-
6
- try:
7
- from Crypto.Cipher import AES
8
-
9
- HAS_CRYPTO = True
10
- except ImportError:
11
- HAS_CRYPTO = False
12
-
6
+ from typing import Iterator
13
7
 
14
8
  from dissect.target.exceptions import UnsupportedPluginError
15
- from dissect.target.helpers import keychain
16
9
  from dissect.target.plugin import InternalPlugin
17
10
  from dissect.target.plugins.os.windows.dpapi.blob import Blob as DPAPIBlob
18
11
  from dissect.target.plugins.os.windows.dpapi.master_key import CredSystem, MasterKeyFile
@@ -20,175 +13,176 @@ from dissect.target.target import Target
20
13
 
21
14
 
22
15
  class DPAPIPlugin(InternalPlugin):
23
- __namespace__ = "dpapi"
16
+ """Windows Data Protection API (DPAPI) plugin.
24
17
 
25
- MASTER_KEY_REGEX = re.compile("^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$")
18
+ Resources:
19
+ - Reversing ``Crypt32.dll``
20
+ - https://learn.microsoft.com/en-us/windows/win32/api/dpapi/
21
+ - https://github.com/fortra/impacket/blob/master/examples/dpapi.py
22
+ - https://github.com/tijldeneut/DPAPIck3
23
+ - https://www.passcape.com/index.php?section=docsys&cmd=details&id=28
24
+ """
26
25
 
27
- SECURITY_POLICY_KEY = "HKEY_LOCAL_MACHINE\\SECURITY\\Policy"
28
- SYSTEM_KEY = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\LSA"
26
+ __namespace__ = "dpapi"
29
27
 
30
- SYSTEM_USERNAME = "System"
28
+ RE_MASTER_KEY = re.compile("^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$")
29
+ SYSTEM_SID = "S-1-5-18"
31
30
 
32
31
  def __init__(self, target: Target):
33
32
  super().__init__(target)
34
33
  self.keychain = cache(self.keychain)
35
34
 
36
35
  def check_compatible(self) -> None:
37
- if not HAS_CRYPTO:
38
- raise UnsupportedPluginError("Missing pycryptodome dependency")
39
-
40
- if not list(self.target.registry.keys(self.SYSTEM_KEY)):
41
- raise UnsupportedPluginError(f"Registry key not found: {self.SYSTEM_KEY}")
36
+ if not self.target.has_function("lsa"):
37
+ raise UnsupportedPluginError("Windows registry and LSA plugins are required for DPAPI decryption")
42
38
 
43
39
  def keychain(self) -> set:
44
- passwords = set()
45
-
46
- for key in keychain.get_keys_for_provider("user") + keychain.get_keys_without_provider():
47
- if key.key_type == keychain.KeyType.PASSPHRASE:
48
- passwords.add(key.value)
49
-
50
- # It is possible to encrypt using an empty passphrase.
51
- passwords.add("")
52
- return passwords
53
-
54
- @cached_property
55
- def syskey(self) -> bytes:
56
- lsa = self.target.registry.key(self.SYSTEM_KEY)
57
- syskey_keys = ["JD", "Skew1", "GBG", "Data"]
58
- # This magic value rotates the order of the data
59
- alterator = [0x8, 0x5, 0x4, 0x2, 0xB, 0x9, 0xD, 0x3, 0x0, 0x6, 0x1, 0xC, 0xE, 0xA, 0xF, 0x7]
60
-
61
- r = bytes.fromhex("".join([lsa.subkey(key).class_name for key in syskey_keys]))
62
- return bytes(r[i] for i in alterator)
63
-
64
- @cached_property
65
- def lsakey(self) -> bytes:
66
- policy_key = "PolEKList"
67
-
68
- encrypted_key = self.target.registry.key(self.SECURITY_POLICY_KEY).subkey(policy_key).value("(Default)").value
69
-
70
- lsa_key = _decrypt_aes(encrypted_key, self.syskey)
71
-
72
- return lsa_key[68:100]
73
-
74
- @cached_property
75
- def secrets(self) -> dict[str, bytes]:
76
- result = {}
77
-
78
- reg_secrets = self.target.registry.key(self.SECURITY_POLICY_KEY).subkey("Secrets")
79
- for subkey in reg_secrets.subkeys():
80
- enc_data = subkey.subkey("CurrVal").value("(Default)").value
81
- secret = _decrypt_aes(enc_data, self.lsakey)
82
- result[subkey.name] = secret
83
-
84
- return result
40
+ return set(self.target._dpapi_keyprovider.keys())
85
41
 
86
42
  @cached_property
87
43
  def master_keys(self) -> dict[str, dict[str, MasterKeyFile]]:
88
- # This assumes that there is no user named System.
89
- # As far as I can tell, the name "System" is saved for the actual System user
90
- # Therefore the user can't actually exist in `all_with_home`
91
- result = {self.SYSTEM_USERNAME: {}}
44
+ """Returns dict of found DPAPI master keys on the Windows target for SYSTEM and regular users."""
45
+ master_keys = {}
46
+
47
+ # Search for SYSTEM master keys
48
+ master_keys[self.SYSTEM_SID] = {}
92
49
 
93
- system_master_key_path = self.target.fs.path("sysvol/Windows/System32/Microsoft/Protect/S-1-5-18")
50
+ system_master_key_path = self.target.fs.path(f"sysvol/Windows/System32/Microsoft/Protect/{self.SYSTEM_SID}")
94
51
  system_user_master_key_path = system_master_key_path.joinpath("User")
95
52
 
96
53
  for dir in [system_master_key_path, system_user_master_key_path]:
97
- user_mks = self._load_master_keys_from_path(self.SYSTEM_USERNAME, dir)
98
- result[self.SYSTEM_USERNAME].update(user_mks)
54
+ user_mks = self._load_master_keys_from_path(self.SYSTEM_SID, dir)
55
+ master_keys[self.SYSTEM_SID].update(user_mks)
56
+
57
+ # Search for user master keys, generally located at $HOME/AppData/Roaming/Microsoft/Protect/{user_sid}/{mk_guid}
58
+ PROTECT_DIRS = [
59
+ # Windows Vista and newer
60
+ "AppData/Roaming/Microsoft/Protect",
61
+ # Windows XP
62
+ "Application Data/Microsoft/Protect",
63
+ ]
99
64
 
100
65
  for user in self.target.user_details.all_with_home():
101
- path = user.home_path.joinpath("AppData/Roaming/Microsoft/Protect").joinpath(user.user.sid)
102
- user_mks = self._load_master_keys_from_path(user.user.name, path)
103
- if user_mks:
104
- result[user.user.name] = user_mks
66
+ sid = user.user.sid
67
+ master_keys.setdefault(sid, {})
105
68
 
106
- return result
69
+ for protect_dir in PROTECT_DIRS:
70
+ path = user.home_path.joinpath(protect_dir).joinpath(sid)
71
+ if user_mks := self._load_master_keys_from_path(sid, path):
72
+ master_keys[sid] |= user_mks
107
73
 
108
- @cached_property
109
- def _users(self) -> dict[str, dict[str, str]]:
110
- return {u.name: {"sid": u.sid} for u in self.target.users()}
74
+ return master_keys
75
+
76
+ def _load_master_keys_from_path(self, sid: str, path: Path) -> Iterator[tuple[str, MasterKeyFile]]:
77
+ """Iterate over the provided ``path`` and search for master key files for the given user SID."""
111
78
 
112
- def _load_master_keys_from_path(self, username: str, path: Path) -> dict[str, MasterKeyFile]:
113
79
  if not path.exists():
114
- return {}
80
+ self.target.log.info("Unable to load master keys from path as it does not exist: %s", path)
81
+ return
115
82
 
116
- result = {}
117
83
  for file in path.iterdir():
118
- if self.MASTER_KEY_REGEX.findall(file.name):
119
- with file.open() as fh:
120
- mkf = MasterKeyFile(fh)
121
-
122
- if username == self.SYSTEM_USERNAME:
123
- dpapi_system = CredSystem(self.secrets["DPAPI_SYSTEM"][16:])
84
+ if not self.RE_MASTER_KEY.findall(file.name):
85
+ continue
86
+
87
+ with file.open() as fh:
88
+ mkf = MasterKeyFile(fh)
89
+
90
+ # Decrypt SYSTEM master key using the DPAPI_SYSTEM LSA secret.
91
+ if sid == self.SYSTEM_SID:
92
+ if "DPAPI_SYSTEM" not in self.target.lsa._secrets:
93
+ self.target.log.warning("Unable to decrypt SYSTEM master key: LSA secret missing")
94
+ continue
95
+
96
+ # Windows XP
97
+ if float(self.target.ntversion) < 6.0:
98
+ secret_offset = 8
99
+
100
+ # Windows Vista and newer
101
+ else:
102
+ secret_offset = 16
103
+
104
+ dpapi_system = CredSystem(self.target.lsa._secrets["DPAPI_SYSTEM"][secret_offset:])
105
+ mkf.decrypt_with_key(dpapi_system.machine_key)
106
+ mkf.decrypt_with_key(dpapi_system.user_key)
107
+
108
+ # Decrypting the System master key should always succeed
109
+ if not mkf.decrypted:
110
+ self.target.log.error("Failed to decrypt SYSTEM master key!")
111
+ continue
112
+
113
+ yield file.name, mkf
114
+
115
+ # Decrypt user master key
116
+ else:
117
+ # Iterate over every master key password we have from the keychain
118
+ for provider, mk_pass in self.keychain():
119
+ try:
120
+ if mkf.decrypt_with_password(sid, mk_pass):
121
+ self.target.log.info(
122
+ "Decrypted user master key with password '%s' from provider %s", mk_pass, provider
123
+ )
124
+ break
125
+ except ValueError:
126
+ pass
127
+
128
+ try:
129
+ if mkf.decrypt_with_hash(sid, bytes.fromhex(mk_pass)):
130
+ self.target.log.info(
131
+ "Decrypted SID %s master key with hash '%s' from provider %s", sid, mk_pass, provider
132
+ )
133
+ break
134
+ except ValueError:
135
+ pass
124
136
 
125
- if not mkf.decrypt_with_key(dpapi_system.machine_key):
126
- mkf.decrypt_with_key(dpapi_system.user_key)
137
+ if not mkf.decrypted:
138
+ self.target.log.warning("Could not decrypt master key '%s' for SID '%s'", file.name, sid)
127
139
 
128
- # This should not be possible, decrypting the System master key should always succeed
129
- if not mkf.decrypted:
130
- raise Exception("Failed to decrypt System master key")
140
+ yield file.name, mkf
131
141
 
132
- if user := self._users.get(username):
133
- for mk_pass in self.keychain():
134
- if mkf.decrypt_with_password(user["sid"], mk_pass):
135
- break
142
+ @cached_property
143
+ def _users(self) -> dict[str, str]:
144
+ """Cached map of username to SID."""
145
+ return {user.name: user.sid for user in self.target.users()}
136
146
 
137
- try:
138
- if mkf.decrypt_with_hash(user["sid"], bytes.fromhex(mk_pass)) is True:
139
- break
140
- except ValueError:
141
- pass
147
+ def decrypt_system_blob(self, data: bytes) -> bytes:
148
+ """Decrypt the given bytes using the SYSTEM master key."""
149
+ return self.decrypt_user_blob(data, sid=self.SYSTEM_SID)
142
150
 
143
- if not mkf.decrypted:
144
- self.target.log.warning("Could not decrypt DPAPI master key for username '%s'", username)
151
+ def decrypt_user_blob(self, data: bytes, username: str | None = None, sid: str | None = None) -> bytes:
152
+ """Decrypt the given bytes using the master key of the given SID or username."""
145
153
 
146
- result[file.name] = mkf
154
+ if not sid and not username:
155
+ raise ValueError("Either sid or username argument is required")
147
156
 
148
- return result
157
+ if not sid and username:
158
+ sid = self._users.get(username)
149
159
 
150
- def decrypt_system_blob(self, data: bytes) -> bytes:
151
- """Decrypt the given bytes using the System master key."""
152
- return self.decrypt_user_blob(data, self.SYSTEM_USERNAME)
160
+ if not sid:
161
+ raise ValueError("No SID provided or no SID found")
153
162
 
154
- def decrypt_user_blob(self, data: bytes, username: str) -> bytes:
155
- """Decrypt the given bytes using the master key of the given user."""
156
- blob = DPAPIBlob(data)
163
+ try:
164
+ blob = DPAPIBlob(data)
165
+ except EOFError as e:
166
+ raise ValueError(f"Failed to parse DPAPI blob: {e}")
157
167
 
158
- if not (mk := self.master_keys.get(username, {}).get(blob.guid)):
159
- raise ValueError(f"Blob UUID is unknown to {username} master keys")
168
+ if not (mk := self.master_keys.get(sid, {}).get(blob.guid)):
169
+ raise ValueError(f"Blob is encrypted using master key {blob.guid} that we do not have for SID {sid}")
160
170
 
161
171
  if not blob.decrypt(mk.key):
162
- raise ValueError(f"Failed to decrypt blob for user {username}")
172
+ raise ValueError(f"Failed to decrypt blob for SID {sid}")
163
173
 
164
174
  return blob.clear_text
165
175
 
166
176
  def decrypt_blob(self, data: bytes) -> bytes:
167
177
  """Attempt to decrypt the given bytes using any of the available master keys."""
168
- blob = DPAPIBlob(data)
178
+ try:
179
+ blob = DPAPIBlob(data)
180
+ except EOFError as e:
181
+ raise ValueError(f"Failed to parse DPAPI blob: {e}")
169
182
 
170
183
  for user in self.master_keys:
171
184
  for mk in self.master_keys[user].values():
172
185
  if blob.decrypt(mk.key):
173
186
  return blob.clear_text
174
187
 
175
- raise ValueError("Failed to decrypt blob")
176
-
177
-
178
- def _decrypt_aes(data: bytes, key: bytes) -> bytes:
179
- ctx = hashlib.sha256()
180
- ctx.update(key)
181
-
182
- tmp = data[28:60]
183
- for _ in range(1, 1000 + 1):
184
- ctx.update(tmp)
185
-
186
- aeskey = ctx.digest()
187
- iv = b"\x00" * 16
188
-
189
- result = []
190
- for i in range(60, len(data), 16):
191
- cipher = AES.new(aeskey, AES.MODE_CBC, iv)
192
- result.append(cipher.decrypt(data[i : i + 16].ljust(16, b"\x00")))
193
-
194
- return b"".join(result)
188
+ raise ValueError("Failed to decrypt blob using any available master key")
@@ -0,0 +1,21 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.exceptions import UnsupportedPluginError
4
+ from dissect.target.plugin import export
5
+ from dissect.target.plugins.os.windows.dpapi.keyprovider.keyprovider import (
6
+ KeyProviderPlugin,
7
+ )
8
+
9
+
10
+ class CredHistKeyProviderPlugin(KeyProviderPlugin):
11
+ __namespace__ = "_dpapi_keyprovider_credhist"
12
+
13
+ def check_compatible(self) -> None:
14
+ if not self.target.has_function("credhist"):
15
+ raise UnsupportedPluginError("CREDHIST plugin not available on target")
16
+
17
+ @export(output="yield")
18
+ def keys(self) -> Iterator[tuple[str, str]]:
19
+ for credhist in self.target.credhist():
20
+ if value := getattr(credhist, "sha1"):
21
+ yield self.__namespace__, value
@@ -0,0 +1,17 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.plugin import export
4
+ from dissect.target.plugins.os.windows.dpapi.keyprovider.keyprovider import (
5
+ KeyProviderPlugin,
6
+ )
7
+
8
+
9
+ class EmptyKeyProviderPlugin(KeyProviderPlugin):
10
+ __namespace__ = "_dpapi_keyprovider_empty"
11
+
12
+ def check_compatible(self) -> None:
13
+ return
14
+
15
+ @export(output="yield")
16
+ def keys(self) -> Iterator[tuple[str, str]]:
17
+ yield self.__namespace__, ""
@@ -0,0 +1,20 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.helpers import keychain
4
+ from dissect.target.plugin import export
5
+ from dissect.target.plugins.os.windows.dpapi.keyprovider.keyprovider import (
6
+ KeyProviderPlugin,
7
+ )
8
+
9
+
10
+ class KeychainKeyProviderPlugin(KeyProviderPlugin):
11
+ __namespace__ = "_dpapi_keyprovider_keychain"
12
+
13
+ def check_compatible(self) -> None:
14
+ return
15
+
16
+ @export(output="yield")
17
+ def keys(self) -> Iterator[tuple[str, str]]:
18
+ for key in keychain.get_keys_for_provider("user") + keychain.get_keys_without_provider():
19
+ if key.key_type == keychain.KeyType.PASSPHRASE:
20
+ yield self.__namespace__, key.value
@@ -0,0 +1,8 @@
1
+ from dissect.target.plugin import InternalNamespacePlugin
2
+
3
+
4
+ class KeyProviderPlugin(InternalNamespacePlugin):
5
+ __namespace__ = "_dpapi_keyprovider"
6
+
7
+ def check_compatible(self) -> None:
8
+ return None
@@ -0,0 +1,38 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.cstruct import cstruct
4
+
5
+ from dissect.target.exceptions import UnsupportedPluginError
6
+ from dissect.target.plugin import export
7
+ from dissect.target.plugins.os.windows.dpapi.keyprovider.keyprovider import (
8
+ KeyProviderPlugin,
9
+ )
10
+
11
+ defaultpassword_def = """
12
+ struct DefaultPassword {
13
+ DWORD length;
14
+ char flags[4*3];
15
+ WCHAR data[length/2];
16
+ char checksum_or_guid[0x10];
17
+ };
18
+ """
19
+
20
+ c_defaultpassword = cstruct().load(defaultpassword_def)
21
+
22
+
23
+ class LSADefaultPasswordKeyProviderPlugin(KeyProviderPlugin):
24
+ __namespace__ = "_dpapi_keyprovider_lsa_defaultpassword"
25
+
26
+ def check_compatible(self) -> None:
27
+ if not self.target.has_function("lsa"):
28
+ raise UnsupportedPluginError("LSA plugin not available on target")
29
+
30
+ @export(output="yield")
31
+ def keys(self) -> Iterator[tuple[str, str]]:
32
+ if default_pass := self.target.lsa._secrets.get("DefaultPassword"):
33
+ try:
34
+ value = c_defaultpassword.DefaultPassword(default_pass).data
35
+ except Exception:
36
+ self.target.log.warning("Failed to parse LSA DefaultPassword value")
37
+ return
38
+ yield self.__namespace__, value
@@ -95,6 +95,9 @@ class MasterKey:
95
95
 
96
96
  def decrypt_with_password(self, user_sid: str, pwd: str) -> bool:
97
97
  """Decrypts the master key with the given user's password and SID."""
98
+ if self.decrypted:
99
+ return True
100
+
98
101
  pwd = pwd.encode("utf-16-le")
99
102
 
100
103
  for algo in ["sha1", "md4"]: