-
Notifications
You must be signed in to change notification settings - Fork 44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for decrypting System DPAPI secrets #305
Changes from 4 commits
25ecbf5
b20f082
8b070da
09138e9
4c26ea8
6a53acf
c0813ed
2a3b904
2db8006
8339046
66c285e
79bca33
f160324
74dd79d
e5f9124
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
from typing import Optional | ||
from uuid import UUID | ||
|
||
from dissect.cstruct import cstruct | ||
|
||
from dissect.target.plugins.os.windows.dpapi.crypto import ( | ||
CipherAlgorithm, | ||
HashAlgorithm, | ||
crypt_session_key_type1, | ||
crypt_session_key_type2, | ||
) | ||
|
||
blob_def = """ | ||
struct DPAPIBlob { | ||
DWORD dwVersion; | ||
char provider[16]; | ||
DWORD mkVersion; | ||
char guid[16]; | ||
DWORD flags; | ||
DWORD descriptionLength; | ||
char description[descriptionLength]; | ||
DWORD CipherAlgId; | ||
DWORD keyLen; | ||
DWORD saltLength; | ||
char salt[saltLength]; | ||
DWORD strongLength; | ||
char strong[strongLength]; | ||
DWORD CryptAlgId; | ||
DWORD hashLen; | ||
DWORD hmacLength; | ||
char hmac[hmacLength]; | ||
DWORD cipherTextLength; | ||
char cipherText[cipherTextLength]; | ||
DWORD signLength; | ||
char sign[signLength]; | ||
}; | ||
""" | ||
|
||
c_blob = cstruct() | ||
c_blob.load(blob_def) | ||
|
||
|
||
class Blob: | ||
"""Represents a DPAPI blob.""" | ||
|
||
def __init__(self, data: bytes): | ||
self._blob = c_blob.DPAPIBlob(data) | ||
|
||
self.version = self._blob.dwVersion | ||
self.provider = str(UUID(bytes_le=self._blob.provider)) | ||
self.mkversion = self._blob.mkVersion | ||
self.guid = str(UUID(bytes_le=self._blob.guid)) | ||
self.flags = self._blob.flags | ||
self.description = self._blob.description.decode("utf-16-le") | ||
self.cipher_algorithm = CipherAlgorithm.from_id(self._blob.CipherAlgId) | ||
self.key_len = self._blob.keyLen | ||
self.salt = self._blob.salt | ||
self.strong = self._blob.strong | ||
self.hash_algorithm = HashAlgorithm.from_id(self._blob.CryptAlgId) | ||
self.hash_len = self._blob.hashLen | ||
self.hmac = self._blob.hmac | ||
self.cipher_text = self._blob.cipherText | ||
|
||
# All the blob data between the version, provider and sign fields | ||
self.blob = data[20 : -(self._blob.signLength + 4)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm slightly worried about this . It's basically hardcoded magic values which (theoretically but super unlikely) could change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made it more dynamic, does that satisfy? I intend to add a |
||
self.sign = self._blob.sign | ||
|
||
self.clear_text = None | ||
self.decrypted = False | ||
self.sign_computed = None | ||
|
||
def decrypt( | ||
self, | ||
master_key: bytes, | ||
entropy: Optional[bytes] = None, | ||
strong_password: Optional[str] = None, | ||
smart_card_secret: Optional[bytes] = None, | ||
) -> bool: | ||
"""Try to decrypt the blob with the given master key. | ||
|
||
Args: | ||
master_key: Decrypted master key value. | ||
entropy: Optional entropy for decrypting the blob. | ||
strong_password: Optional password for decrypting the blob. | ||
smart_card_secret: MS Next Gen Crypto secret (e.g. from PIN code). | ||
|
||
Returns: | ||
True if decryption is succesful, False otherwise. | ||
""" | ||
if self.decrypted: | ||
return True | ||
|
||
for algo in [crypt_session_key_type1, crypt_session_key_type2]: | ||
session_key = algo( | ||
master_key, | ||
self.salt, | ||
self.hash_algorithm, | ||
entropy=entropy, | ||
smart_card_secret=smart_card_secret, | ||
strong_password=strong_password, | ||
) | ||
key = self.cipher_algorithm.derive_key(session_key, self.hash_algorithm) | ||
self.clear_text = self.cipher_algorithm.decrypt(self.cipher_text, key) | ||
|
||
padding = self.clear_text[-1] | ||
if padding <= self.cipher_algorithm.block_length: | ||
self.clear_text = self.clear_text[:-padding] | ||
|
||
# Check against provided HMAC | ||
self.sign_computed = algo( | ||
master_key, | ||
self.hmac, | ||
self.hash_algorithm, | ||
entropy=entropy, | ||
smart_card_secret=smart_card_secret, | ||
verify_blob=self.blob, | ||
) | ||
|
||
self.decrypted = self.sign_computed == self.sign | ||
if self.decrypted: | ||
return True | ||
|
||
self.decrypted = False | ||
return self.decrypted | ||
|
||
def __repr__(self) -> str: | ||
s = [ | ||
"DPAPI BLOB", | ||
"\n".join( | ||
( | ||
"\tversion = %(version)d", | ||
"\tprovider = %(provider)s", | ||
"\tmkey = %(guid)s", | ||
"\tflags = %(flags)#x", | ||
"\tdescr = %(description)s", | ||
"\tcipher_algo = %(cipher_algorithm)r", | ||
"\thash_algo = %(hash_algorithm)r", | ||
) | ||
) | ||
% self.__dict__, | ||
"\tsalt = %s" % self.salt.hex(), | ||
"\thmac = %s" % self.hmac.hex(), | ||
"\tcipher = %s" % self.cipher_text.hex(), | ||
"\tsign = %s" % self.sign.hex(), | ||
] | ||
if self.sign_computed is not None: | ||
s.append("\tsign_computed = %s" % self.sign_computed.hex()) | ||
if self.clear_text is not None: | ||
s.append("\tcleartext = %r" % self.clear_text) | ||
return "\n".join(s) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a specific reason to attempt to read all files, instead of just the relevant ones?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly readability and ease of expansion. Especially the Unix-y based paths would quickly get hairy and easy to forget when adding new operating system support to Dissect (e.g. BSD). It's a small amount of paths so I feel it's okay to "bruteforce" it.