dissect.target 3.17.dev34__py3-none-any.whl → 3.17.dev35__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/plugins/os/windows/credhist.py +210 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev35.dist-info}/METADATA +1 -1
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev35.dist-info}/RECORD +8 -7
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev35.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev35.dist-info}/LICENSE +0 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev35.dist-info}/WHEEL +0 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev35.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev35.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
import hashlib
|
2
|
+
import logging
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import BinaryIO, Iterator, Optional
|
6
|
+
from uuid import UUID
|
7
|
+
|
8
|
+
from dissect.cstruct import cstruct
|
9
|
+
from dissect.util.sid import read_sid
|
10
|
+
|
11
|
+
from dissect.target.exceptions import UnsupportedPluginError
|
12
|
+
from dissect.target.helpers import keychain
|
13
|
+
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
14
|
+
from dissect.target.helpers.record import create_extended_descriptor
|
15
|
+
from dissect.target.plugin import Plugin, export
|
16
|
+
from dissect.target.plugins.general.users import UserDetails
|
17
|
+
from dissect.target.plugins.os.windows.dpapi.crypto import (
|
18
|
+
CipherAlgorithm,
|
19
|
+
HashAlgorithm,
|
20
|
+
derive_password_hash,
|
21
|
+
)
|
22
|
+
from dissect.target.target import Target
|
23
|
+
|
24
|
+
log = logging.getLogger(__name__)
|
25
|
+
|
26
|
+
|
27
|
+
CredHistRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
28
|
+
"windows/credential/history",
|
29
|
+
[
|
30
|
+
("string", "guid"),
|
31
|
+
("boolean", "decrypted"),
|
32
|
+
("string", "sha1"),
|
33
|
+
("string", "nt"),
|
34
|
+
],
|
35
|
+
)
|
36
|
+
|
37
|
+
|
38
|
+
credhist_def = """
|
39
|
+
struct entry {
|
40
|
+
DWORD dwVersion;
|
41
|
+
CHAR guidLink[16];
|
42
|
+
DWORD dwNextLinkSize;
|
43
|
+
DWORD dwCredLinkType;
|
44
|
+
DWORD algHash; // ALG_ID
|
45
|
+
DWORD dwPbkdf2IterationCount;
|
46
|
+
DWORD dwSidSize;
|
47
|
+
DWORD algCrypt; // ALG_ID
|
48
|
+
DWORD dwShaHashSize;
|
49
|
+
DWORD dwNtHashSize;
|
50
|
+
CHAR pSalt[16];
|
51
|
+
CHAR pSid[dwSidSize];
|
52
|
+
CHAR encrypted[0];
|
53
|
+
};
|
54
|
+
"""
|
55
|
+
|
56
|
+
c_credhist = cstruct()
|
57
|
+
c_credhist.load(credhist_def)
|
58
|
+
|
59
|
+
|
60
|
+
@dataclass
|
61
|
+
class CredHistEntry:
|
62
|
+
version: int
|
63
|
+
guid: str
|
64
|
+
user_sid: str
|
65
|
+
sha1: Optional[bytes]
|
66
|
+
nt: Optional[bytes]
|
67
|
+
hash_alg: HashAlgorithm = field(repr=False)
|
68
|
+
cipher_alg: CipherAlgorithm = field(repr=False)
|
69
|
+
raw: c_credhist.entry = field(repr=False)
|
70
|
+
decrypted: bool = False
|
71
|
+
|
72
|
+
def decrypt(self, password_hash: bytes) -> None:
|
73
|
+
"""Decrypt this CREDHIST entry using the provided password hash. Modifies ``CredHistEntry.sha1``
|
74
|
+
and ``CredHistEntry.nt`` values.
|
75
|
+
|
76
|
+
If the decrypted ``nt`` value is 16 bytes we assume the decryption was successful.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
password_hash: Bytes of SHA1 password hash digest.
|
80
|
+
|
81
|
+
Raises:
|
82
|
+
ValueError: If the decryption seems to have failed.
|
83
|
+
"""
|
84
|
+
data = self.cipher_alg.decrypt_with_hmac(
|
85
|
+
data=self.raw.encrypted,
|
86
|
+
key=derive_password_hash(password_hash, self.user_sid),
|
87
|
+
iv=self.raw.pSalt,
|
88
|
+
hash_algorithm=self.hash_alg,
|
89
|
+
rounds=self.raw.dwPbkdf2IterationCount,
|
90
|
+
)
|
91
|
+
|
92
|
+
sha_size = self.raw.dwShaHashSize
|
93
|
+
nt_size = self.raw.dwNtHashSize
|
94
|
+
|
95
|
+
sha1 = data[:sha_size]
|
96
|
+
nt = data[sha_size : sha_size + nt_size].rstrip(b"\x00")
|
97
|
+
|
98
|
+
if len(nt) != 16:
|
99
|
+
raise ValueError("Decrypting failed, invalid password hash?")
|
100
|
+
|
101
|
+
self.decrypted = True
|
102
|
+
self.sha1 = sha1
|
103
|
+
self.nt = nt
|
104
|
+
|
105
|
+
|
106
|
+
class CredHistFile:
|
107
|
+
def __init__(self, fh: BinaryIO) -> None:
|
108
|
+
self.fh = fh
|
109
|
+
self.entries = list(self._parse())
|
110
|
+
|
111
|
+
def __repr__(self) -> str:
|
112
|
+
return f"<CredHistFile fh='{self.fh}' entries={len(self.entries)}>"
|
113
|
+
|
114
|
+
def _parse(self) -> Iterator[CredHistEntry]:
|
115
|
+
self.fh.seek(0)
|
116
|
+
try:
|
117
|
+
while True:
|
118
|
+
entry = c_credhist.entry(self.fh)
|
119
|
+
|
120
|
+
# determine size of encrypted data and add to entry
|
121
|
+
cipher_alg = CipherAlgorithm.from_id(entry.algCrypt)
|
122
|
+
enc_size = entry.dwShaHashSize + entry.dwNtHashSize
|
123
|
+
enc_size += enc_size % cipher_alg.block_length
|
124
|
+
entry.encrypted = self.fh.read(enc_size)
|
125
|
+
|
126
|
+
yield CredHistEntry(
|
127
|
+
version=entry.dwVersion,
|
128
|
+
guid=UUID(bytes_le=entry.guidLink),
|
129
|
+
user_sid=read_sid(entry.pSid),
|
130
|
+
hash_alg=HashAlgorithm.from_id(entry.algHash),
|
131
|
+
cipher_alg=cipher_alg,
|
132
|
+
sha1=None,
|
133
|
+
nt=None,
|
134
|
+
raw=entry,
|
135
|
+
)
|
136
|
+
except EOFError:
|
137
|
+
pass
|
138
|
+
|
139
|
+
def decrypt(self, password_hash: bytes) -> None:
|
140
|
+
"""Decrypt a CREDHIST chain using the provided password SHA1 hash."""
|
141
|
+
|
142
|
+
for entry in reversed(self.entries):
|
143
|
+
try:
|
144
|
+
entry.decrypt(password_hash)
|
145
|
+
except ValueError as e:
|
146
|
+
log.warning("Could not decrypt entry %s with password %s", entry.guid, password_hash.hex())
|
147
|
+
log.debug("", exc_info=e)
|
148
|
+
continue
|
149
|
+
password_hash = entry.sha1
|
150
|
+
|
151
|
+
|
152
|
+
class CredHistPlugin(Plugin):
|
153
|
+
"""Windows CREDHIST file parser.
|
154
|
+
|
155
|
+
Windows XP: ``C:\\Documents and Settings\\username\\Application Data\\Microsoft\\Protect\\CREDHIST``
|
156
|
+
Windows 7 and up: ``C:\\Users\\username\\AppData\\Roaming\\Microsoft\\Protect\\CREDHIST``
|
157
|
+
|
158
|
+
Resources:
|
159
|
+
- https://www.passcape.com/index.php?section=docsys&cmd=details&id=28#41
|
160
|
+
"""
|
161
|
+
|
162
|
+
def __init__(self, target: Target):
|
163
|
+
super().__init__(target)
|
164
|
+
self.files = list(self._find_files())
|
165
|
+
|
166
|
+
def _find_files(self) -> Iterator[tuple[UserDetails, Path]]:
|
167
|
+
hashes = set()
|
168
|
+
for user_details in self.target.user_details.all_with_home():
|
169
|
+
for path in ["AppData/Roaming/Microsoft/Protect/CREDHIST", "Application Data/Microsoft/Protect/CREDHIST"]:
|
170
|
+
credhist_path = user_details.home_path.joinpath(path)
|
171
|
+
if credhist_path.exists() and (hash := credhist_path.get().hash()) not in hashes:
|
172
|
+
hashes.add(hash)
|
173
|
+
yield user_details.user, credhist_path
|
174
|
+
|
175
|
+
def check_compatible(self) -> None:
|
176
|
+
if not self.files:
|
177
|
+
raise UnsupportedPluginError("No CREDHIST files found on target.")
|
178
|
+
|
179
|
+
@export(record=CredHistRecord)
|
180
|
+
def credhist(self) -> Iterator[CredHistRecord]:
|
181
|
+
"""Yield and decrypt all Windows CREDHIST entries on the target."""
|
182
|
+
passwords = keychain_passwords()
|
183
|
+
|
184
|
+
if not passwords:
|
185
|
+
self.target.log.warning("No passwords provided in keychain, cannot decrypt CREDHIST hashes")
|
186
|
+
|
187
|
+
for user, path in self.files:
|
188
|
+
credhist = CredHistFile(path.open("rb"))
|
189
|
+
|
190
|
+
for password in passwords:
|
191
|
+
credhist.decrypt(hashlib.sha1(password.encode("utf-16-le")).digest())
|
192
|
+
|
193
|
+
for entry in credhist.entries:
|
194
|
+
yield CredHistRecord(
|
195
|
+
guid=entry.guid,
|
196
|
+
decrypted=entry.decrypted,
|
197
|
+
sha1=entry.sha1.hex() if entry.sha1 else None,
|
198
|
+
nt=entry.nt.hex() if entry.nt else None,
|
199
|
+
_user=user,
|
200
|
+
_target=self.target,
|
201
|
+
)
|
202
|
+
|
203
|
+
|
204
|
+
def keychain_passwords() -> set:
|
205
|
+
passphrases = set()
|
206
|
+
for key in keychain.get_keys_for_provider("user") + keychain.get_keys_without_provider():
|
207
|
+
if key.key_type == keychain.KeyType.PASSPHRASE:
|
208
|
+
passphrases.add(key.value)
|
209
|
+
passphrases.add("")
|
210
|
+
return passphrases
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dissect.target
|
3
|
-
Version: 3.17.
|
3
|
+
Version: 3.17.dev35
|
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
|
@@ -260,6 +260,7 @@ dissect/target/plugins/os/windows/amcache.py,sha256=ZZNOs3bILTf0AGkDkhoatndl0j39
|
|
260
260
|
dissect/target/plugins/os/windows/catroot.py,sha256=eSfVqXvWWZpXoxKB1FT_evjXXNmlD7wHhA3lYpfQDeQ,11043
|
261
261
|
dissect/target/plugins/os/windows/cim.py,sha256=jsrpu6TZpBUh7VWI9AV2Ib5bebTwsvqOwRfa5gjJd7c,3056
|
262
262
|
dissect/target/plugins/os/windows/clfs.py,sha256=begVsZ-CY97Ksh6S1g03LjyBgu8ERY2hfNDWYPj0GXI,4872
|
263
|
+
dissect/target/plugins/os/windows/credhist.py,sha256=FX_pW-tU9esdvDTSx913kf_CpGE_1jbD6bkjDb-cxHk,7069
|
263
264
|
dissect/target/plugins/os/windows/datetime.py,sha256=tuBOkewmbCW8sFXcYp5p82oM5RCsVwmtC79BDCTLz8k,9472
|
264
265
|
dissect/target/plugins/os/windows/defender.py,sha256=Vp_IP6YKm4igR765WvXJrHQ3RMu7FJKM3VOoR8AybV8,23737
|
265
266
|
dissect/target/plugins/os/windows/env.py,sha256=-u9F9xWy6PUbQmu5Tv_MDoVmy6YB-7CbHokIK_T3S44,13891
|
@@ -339,10 +340,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
|
|
339
340
|
dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
|
340
341
|
dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
|
341
342
|
dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
|
342
|
-
dissect.target-3.17.
|
343
|
-
dissect.target-3.17.
|
344
|
-
dissect.target-3.17.
|
345
|
-
dissect.target-3.17.
|
346
|
-
dissect.target-3.17.
|
347
|
-
dissect.target-3.17.
|
348
|
-
dissect.target-3.17.
|
343
|
+
dissect.target-3.17.dev35.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
|
344
|
+
dissect.target-3.17.dev35.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
345
|
+
dissect.target-3.17.dev35.dist-info/METADATA,sha256=Dd4TxlQKuWjqnUY6nMqiEvR-YBsmXIbhGN6CLjdsGhI,11300
|
346
|
+
dissect.target-3.17.dev35.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
347
|
+
dissect.target-3.17.dev35.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
|
348
|
+
dissect.target-3.17.dev35.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
349
|
+
dissect.target-3.17.dev35.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev35.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|