Skip to content
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

Adding support for AzureAD accounts #3

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 162 additions & 36 deletions diana-msaccountdec.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,77 @@
# limitations under the License.
""" Windows Microsoft Account DPAPI key decryption utility."""

import hashlib, optparse, os, sys
import hashlib, optparse, os, sys, hmac, struct, binascii, json
from Crypto.Cipher import AES ## pip3 install pycryptodome
import dpapick3.eater as eater
from enum import IntEnum
from typing import List

class DPAPICredKeyBlob(eater.DataStruct):
def __init__(self, raw):
eater.DataStruct.__init__(self, raw)

def parse(self, data):
self.dwBlobSize = data.eat("L")
self.dwField4 = data.eat("L")
self.dwCredKeyOffset = data.eat("L")
self.dwCredKeySize = data.eat("L")
self.Guid = "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % data.eat("L2H8B")
assert data.ofs == self.dwCredKeyOffset
self.CredKey = data.eat_string(self.dwCredKeySize)

class CacheNodeType(IntEnum):
PASSWORD = 1
UNKNOW_TWO = 2
UNKNOW_THREE = 3
UNKNOW_FOUR = 4
PIN = 5

class CacheDataNodeHeader(eater.DataStruct):
def __init__(self, raw):
eater.DataStruct.__init__(self, raw)

def parse(self, data):
self.dwNodeType = data.eat("L")
self.dwCryptoBlobSize = data.eat("L")
self.dwField8 = data.eat("L")
self.dwEncryptedPRTSize = data.eat("L")
self.dwField10 = data.eat("L")


class CacheDataNode:
def __init__(self, header : CacheDataNodeHeader):
self._header : CacheDataNodeHeader = header
self._cryptoBlob : bytes = None
self._encryptedPrtBlob : bytes = None

@property
def cryptoBlob(self):
return self._cryptoBlob

@cryptoBlob.setter
def cryptoBlob(self, value):
self._cryptoBlob = value

@property
def encryptedPRTBlob(self):
return self._encryptedPrtBlob

@encryptedPRTBlob.setter
def encryptedPRTBlob(self, value):
self._encryptedPrtBlob = value

def is_node_type_password(self) -> bool:
return self._header.dwNodeType == CacheNodeType.PASSWORD

def is_node_type_pin(self) -> bool:
return self._header.dwNodeType == CacheNodeType.PIN

def checkParams(options, args):
if not options.cachedatafile or not options.password:
sys.exit('You must provide cleartext password and cachedata file.')
if not os.path.isfile(options.cachedatafile):
sys.exit('File not found: {}.'.format(options.cachedatafile))
sys.exit(f'File not found: {options.cachedatafile}.')
return

def reverseByte(bByteInput):
Expand All @@ -37,19 +100,61 @@ def reverseByte(bByteInput):
for x in range(-1, -len(str(sHexInput)), -2): sReversed += sHexInput[x-1] + sHexInput[x]
return bytes.fromhex(sReversed)

def getEncryptedData(bLeftoverData):
## The data is divided into DataLength + Data, the DPAPI data can be the first or even the last
lstReturnData = []
while len(bLeftoverData) > 0:
iLength = int(reverseByte(bLeftoverData[:4]).hex(), 16)
bData = bLeftoverData[4:4+iLength]
if b'RSA1' in bData:
## The RSA1 public key does not follow the Length+Data system, just skip over (assuming 256 byte RSA1 modulus)
bLeftoverData = bLeftoverData[4+0x3D8:]
def parse_cache_data(file_path) -> List[CacheDataNode]:
cache_data_node_list = list()
print(f'[+] Parsing CacheData file {file_path}')
with open(file_path, "rb") as f:
file_size = f.seek(0, os.SEEK_END)
f.seek(0, os.SEEK_SET)
# First 4 byte is a version number
(version,) = struct.unpack("<I", f.read(4))
print(f"[+] CacheData file version is 0x{version:x}")
# 32 following bytes is the sha256 expected checksum
sha256_checksum = f.read(32)
# Compute checksum to check if matching
payload = f.read(file_size - f.tell())
# Read raw file
f.seek(0, os.SEEK_SET)
raw_payload = f.read(file_size)

m = hashlib.sha256()
m.update(payload)
print(f"[+] CacheData expected sha256: {str(binascii.hexlify(sha256_checksum), 'ascii')}")
print(f"[+] CacheData computed sha256: {m.hexdigest()}")
assert version == 0x02
assert sha256_checksum == m.digest()

cache_data_node_count, = struct.unpack("<I", raw_payload[0x50:0x54])
offset = 0x54

print(f"[+] Parsing Cache node headers")
for i in range (0, cache_data_node_count):
cache_data_node_header = CacheDataNodeHeader(raw_payload[offset:offset+0x14])
print(f"[+]\tFound CacheNode of type 0x{cache_data_node_header.dwNodeType:x}, CryptoBlobSize = 0x{cache_data_node_header.dwCryptoBlobSize:x}, EncryptedPRTSize = 0x{cache_data_node_header.dwEncryptedPRTSize:x}")
cache_data_node_list.append(CacheDataNode(cache_data_node_header))
offset += 0x14

print(f"[+] Parsing raw blob")
i = 0
while offset < len(raw_payload):
blob_size, = struct.unpack("<I", raw_payload[offset:offset+4])
offset += 4
if blob_size == 0:
continue
if iLength > 48: lstReturnData.append(bData)
bLeftoverData = bLeftoverData[4+iLength:]
return lstReturnData
print(f'[+]\tFound blob of size 0x{blob_size:x} (offset = 0x{offset:x}/0x{len(raw_payload):x})')
blob = raw_payload[offset:offset+blob_size]
offset += blob_size
if offset % 4 != 0:
offset += (4 - (offset % 4))
index_cache_data_node_list = i // 2
# For each cache node, there is one cryptoBlob and one encryptedPRTBlob
if i % 2 == 0:
cache_data_node_list[index_cache_data_node_list].cryptoBlob = blob
else:
cache_data_node_list[index_cache_data_node_list].encryptedPRTBlob = blob
i += 1

return cache_data_node_list

def parseDecryptedCache(bClearData, boolVerbose = True):
sPassword = None
Expand Down Expand Up @@ -84,9 +189,9 @@ def parseDecryptedCache(bClearData, boolVerbose = True):
sAccount = bAccount.decode('UTF-16LE')
else: return sPassword
print('[+] Decoded:')
print(' StableUserID : {} (There should be a SQLite archive at %localappdata%\\ConnectedDevicesPlatform\\{}\\)'.format(sStableUserId, sStableUserId))
print(' User Account : {}'.format(sAccount))
print(' XML Cipher : {}'.format(sXML))
print(f' StableUserID : {sStableUserId} (There should be a SQLite archive at %localappdata%\\ConnectedDevicesPlatform\\{sStableUserId}\\)')
print(f' User Account : {sAccount}')
print(f' XML Cipher : {sXML}')
print('')
return sPassword

Expand All @@ -103,7 +208,7 @@ def walkThroughFile(bCacheDataOrg, oCipher):
if not b'\x00\x40\x00' in bClearData: continue
sPassword = parseDecryptedCache(bClearData, True)
if sPassword:
print('[+] Success, use this as your DPAPI cleartext user password:\n {}'.format(sPassword))
print(f'[+] Success, use this as your DPAPI cleartext user password:\n {sPassword}')
break
i+=iLength
#except Exception as e: print(e)
Expand All @@ -113,10 +218,9 @@ def walkThroughFile(bCacheDataOrg, oCipher):
if __name__ == '__main__':
usage = (
'usage: %prog [options]\n\n'
'It tries to unlock (decrypt) the Microsoft Account DPAPI password.\n'
'It tries to unlock (decrypt) the Microsoft Account DPAPI password or Azure AD one.\n'
'NOTE: only works when the *cleartext* password is known\n'
'NOTE: currently does not support the AzureAD Cache\n'
r' Default Location: Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\CloudAPCache\MicrosoftAccount\<id>\Cache\CacheData')
r' Default Location: Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\CloudAPCache\[MicrosoftAccount|AzureAD]\<id>\Cache\CacheData')

parser = optparse.OptionParser(usage=usage)
parser.add_option('--cachedatafile', '-f', metavar='FILE', help=r'CloudAPCache CacheData', default=os.path.join('Windows','System32','config','systemprofile','AppData','Local','Microsoft','Windows','CloudAPCache','MicrosoftAccount','CacheData'))
Expand All @@ -129,28 +233,50 @@ def walkThroughFile(bCacheDataOrg, oCipher):

bDecryptionKey = hashlib.pbkdf2_hmac('sha256', options.password.encode('UTF-16LE'), b'', 10000)

candidates = parse_cache_data(options.cachedatafile)
file = open(options.cachedatafile,'br')
bCacheDataOrg = file.read()
file.close()

if not bCacheDataOrg[72:72+4] == b'\x02\x00\x00\x00':
exit('[-] Error: Not a valid Microsoft Live Account CacheData file?')

oCipher = AES.new(bDecryptionKey, AES.MODE_CBC, b'\x00'*16)
oCipher = AES.new(bDecryptionKey, AES.MODE_CBC, b'\x00' * 16)

## First 124 are "static length", from then on length + data
bCacheData = bCacheDataOrg[124:]
sPassword = None
lstCandidates = getEncryptedData(bCacheData)
key = None
sToExport = ''
for bEncrData in lstCandidates:
if options.export: sToExport += bEncrData[-64:].hex()+"\n"
bClearData = oCipher.decrypt(bEncrData)
for entry in candidates:
if not entry.is_node_type_password():
continue
if options.export: sToExport += entry.encryptedPRTBlob[-64:].hex() + "\n"
bClearData = oCipher.decrypt(entry.encryptedPRTBlob)
## Since we know the MS Live Emailaddress is in the decoded data, there should be a unicode '@'
if not b'\x00\x40\x00' in bClearData: continue
sPassword = parseDecryptedCache(bClearData, True)
if sPassword: print('[+] Success, use this as the DPAPI cleartext user password:\n {}'.format(sPassword))
if not sPassword: print('[-] Wrong password?')
if options.export and not sToExport == '': print('\n[+] This should be crackable with PBKDF2-SHA256+AES256 and decrypted should contain bytes "004000":\n{}'.format(sToExport))
if not b'\x00\x40\x00' in bClearData:
continue
# AzureAD
if b'Version' in bClearData:
version, flags, dword3, raw_dpapi_cred_key_size = struct.unpack("<IIII", bClearData[0:0x10])
decrypted_prt = bClearData[0x70:]
dpapi_cred_key_blob = bClearData[0x10:0x10 + raw_dpapi_cred_key_size]
dpapi_cred_key_blob_obj = DPAPICredKeyBlob(dpapi_cred_key_blob)
print(f'[+] Dumping raw DPAPI Cred key, with GUID {dpapi_cred_key_blob_obj.Guid} (0x40 bytes):')
print(dpapi_cred_key_blob_obj.CredKey)
decrypted_prt_end = decrypted_prt.rfind(b'}')
decrypted_prt = decrypted_prt[:decrypted_prt_end + 1]
key = hashlib.sha1(dpapi_cred_key_blob_obj.CredKey).digest()
j = json.loads(decrypted_prt)
sid = j['UserInfo']['PrimarySid']
encoded_sid = (sid + '\0').encode('UTF-16-LE')
key = hmac.new(key, encoded_sid, hashlib.sha1).hexdigest()
# Microsoft Account
else:
sPassword = parseDecryptedCache(bClearData, True)
if sPassword:
print(f'[+] Success, use this as the DPAPI cleartext user password:\n {sPassword}')
if key:
print(f'[+] Success, use this key to decrypt masterkeys of the user: 0x{key}')
if not sPassword and not key:
print('[-] Wrong password?')
if options.export and not sToExport == '':
print(f'\n[+] This should be crackable with PBKDF2-SHA256+AES256 and decrypted should contain bytes "004000":\n{sToExport}')

#walkThroughFile(bCacheDataOrg, oCipher)
#walkThroughFile(bCacheDataOrg, oCipher)