Skip to content

Commit

Permalink
winhello pin hash
Browse files Browse the repository at this point in the history
  • Loading branch information
SkelSec committed May 2, 2024
1 parent c91dcdc commit 91f9bdd
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 15 deletions.
2 changes: 1 addition & 1 deletion pypykatz/_version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

__version__ = "0.6.9"
__version__ = "0.6.10"
__banner__ = \
"""
# pypyKatz %s
Expand Down
19 changes: 19 additions & 0 deletions pypykatz/dpapi/cmdhelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pypykatz.dpapi.structures.credentialfile import CredentialFile
from pypykatz.dpapi.structures.masterkeyfile import MasterKeyFile
from pypykatz.dpapi.structures.vault import VAULT_VPOL
from pypykatz.dpapi.finders.registry import RegFinder
from winacl.dtyp.wcee.pvkfile import PVKFile


Expand Down Expand Up @@ -153,6 +154,12 @@ def add_args(self, parser, live_parser):
dpapi_cloudapkd_group.add_argument('mkf', help= 'Keyfile generated by the masterkey -o command.')
dpapi_cloudapkd_group.add_argument('keyvalue', help='KeyValue string obtained from PRT')

dpapi_winhellopin_group = dpapi_subparsers.add_parser('winhellopin', help='Get winhello hash')
dpapi_winhellopin_group.add_argument('mkd', help= 'Directory path for all machine masterkeys. Usually: C:\\System32\\Microsoft\\Protect\\S-1-5-18\\User')
dpapi_winhellopin_group.add_argument('regdir', help= 'Directory path for all registry hives. Usually: C:\\System32\\config\\. SAM, SYSTEM, SECURITY hives needed')
dpapi_winhellopin_group.add_argument('ngcdir', help= 'Directory path of NGC folder. Usually: C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Ngc\\')
dpapi_winhellopin_group.add_argument('cryptokeys', help= 'Directory path of cryptokeys folder. Usually: C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Crypto\\Keys\\')


def execute(self, args):
if len(self.keywords) > 0 and args.command in self.keywords:
Expand Down Expand Up @@ -317,6 +324,18 @@ def run(self, args):
wificonfig_enc = DPAPI.parse_wifi_config_file(args.wifixml)
wificonfig = dpapi.decrypt_wifi_config_file_inner(wificonfig_enc)
print('%s : %s' % (wificonfig['name'], wificonfig['key']))

elif args.dapi_module == 'winhellopin':
hives = RegFinder.from_dir(args.regdir)
dpapi.get_prekeys_form_registry_files(hives['SYSTEM'], hives['SECURITY'], hives['SAM'])
dpapi.decrypt_masterkey_directory(args.mkd)
results = dpapi.winhello_pin_hash_offline(args.ngcdir, args.cryptokeys)
if len(results) == 0:
print('No results!')
return

for h in results:
print(h)

elif args.dapi_module == 'describe':
def read_file_or_hex(x):
Expand Down
93 changes: 81 additions & 12 deletions pypykatz/dpapi/dpapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import sqlite3
import base64
import platform
from hashlib import sha1, pbkdf2_hmac
from hashlib import sha1, pbkdf2_hmac, sha512

import xml.etree.ElementTree as ET

Expand All @@ -25,9 +25,14 @@
from pypykatz.dpapi.structures.credentialfile import CredentialFile, CREDENTIAL_BLOB
from pypykatz.dpapi.structures.blob import DPAPI_BLOB
from pypykatz.dpapi.structures.vault import VAULT_VCRD, VAULT_VPOL, VAULT_VPOL_KEYS
from pypykatz.dpapi.finders.ngc import NGCProtectorFinder, NGCProtector
from pypykatz.dpapi.finders.cryptokeys import CryptoKeysFinder

from unicrypto.hashlib import md4 as MD4
from unicrypto.symmetric import AES, MODE_GCM, MODE_CBC
from winacl.dtyp.wcee.pvkfile import PVKFile
from winacl.dtyp.wcee.cryptoapikey import CryptoAPIKeyFile, CryptoAPIKeyProperties

from pypykatz.commons.common import UniversalEncoder, base64_decode_url


Expand Down Expand Up @@ -376,7 +381,27 @@ def decrypt_masterkey_file(self, file_path, key = None):
returns: CREDENTIAL_BLOB object
"""
with open(file_path, 'rb') as f:
return self.decrypt_masterkey_bytes(f.read(), key = key)
mks, bks = self.decrypt_masterkey_bytes(f.read(), key = key)
self.masterkeys.update(mks)
self.masterkeys.update(bks)
return mks, bks

def decrypt_masterkey_directory(self, directory, ignore_errors: bool = True):
"""
Decrypts all Masterkeyfiles in a directory
directory: path to directory
ignore_errors: if set to True, the function will not raise exceptions if a file cannot be decrypted
returns: dictionary of guid->keybytes
"""
for filename in glob.glob(os.path.join(directory, '**'), recursive = True):
if os.path.isfile(filename):
try:
self.decrypt_masterkey_file(filename)
except Exception as e:
if ignore_errors is False:
raise e
logger.debug('Failed to decrypt %s Reason: %s' % (filename, e))
return self.masterkeys

def decrypt_masterkey_bytes(self, data, key = None):
"""
Expand Down Expand Up @@ -435,8 +460,18 @@ def decrypt_credential_file(self, file_path):
"""
with open(file_path, 'rb') as f:
return self.decrypt_credential_bytes(f.read())

def get_key_for_blob(self, blob):
"""
Looks up the masterkey for a given DPAPI_BLOB object
blob: DPAPI_BLOB object
returns: bytes of the decryption key
"""
if blob.masterkey_guid not in self.masterkeys:
raise Exception('No matching masterkey was found for the blob!')
return self.masterkeys[blob.masterkey_guid]

def decrypt_credential_bytes(self, data):
def decrypt_credential_bytes(self, data, entropy = None):
"""
Decrypts CredentialFile bytes
CredentialFile holds one DPAPI blob, so the decryption is straightforward, and it also has a known structure for the cleartext.
Expand All @@ -446,11 +481,11 @@ def decrypt_credential_bytes(self, data):
returns: CREDENTIAL_BLOB object
"""
cred = CredentialFile.from_bytes(data)
dec_data = self.decrypt_blob_bytes(cred.data)
dec_data = self.decrypt_blob_bytes(cred.data, entropy = entropy)
cb = CREDENTIAL_BLOB.from_bytes(dec_data)
return cb

def decrypt_blob(self, dpapi_blob, key = None):
def decrypt_blob(self, dpapi_blob, key = None, entropy = None):
"""
Decrypts a DPAPI_BLOB object
The DPAPI blob has a GUID attributes which indicates the masterkey to be used, also it has integrity check bytes so it is possible to tell is decryption was sucsessfull.
Expand All @@ -464,9 +499,9 @@ def decrypt_blob(self, dpapi_blob, key = None):
if dpapi_blob.masterkey_guid not in self.masterkeys:
raise Exception('No matching masterkey was found for the blob!')
key = self.masterkeys[dpapi_blob.masterkey_guid]
return dpapi_blob.decrypt(key)
return dpapi_blob.decrypt(key, entropy = entropy)

def decrypt_blob_bytes(self, data, key = None):
def decrypt_blob_bytes(self, data, key = None, entropy = None):
"""
Decrypts DPAPI_BLOB bytes.
Expand All @@ -479,7 +514,7 @@ def decrypt_blob_bytes(self, data, key = None):

blob = DPAPI_BLOB.from_bytes(data)
logger.debug(str(blob))
return self.decrypt_blob(blob, key = key)
return self.decrypt_blob(blob, key = key, entropy = entropy)

def decrypt_vcrd_file(self, file_path):
"""
Expand Down Expand Up @@ -532,7 +567,7 @@ def decrypt_attr(attr, key):
res[attr].append(cleartext)
return res

def decrypt_vpol_bytes(self, data):
def decrypt_vpol_bytes(self, data, entropy = None):
"""
Decrypts the VPOL file, and returns the two keys' bytes
A VPOL file stores two encryption keys.
Expand All @@ -541,7 +576,7 @@ def decrypt_vpol_bytes(self, data):
returns touple of bytes, describing two keys
"""
vpol = VAULT_VPOL.from_bytes(data)
res = self.decrypt_blob_bytes(vpol.blobdata)
res = self.decrypt_blob_bytes(vpol.blobdata, entropy = entropy)

keys = VAULT_VPOL_KEYS.from_bytes(res)

Expand All @@ -562,8 +597,8 @@ def decrypt_vpol_file(self, file_path):
with open(file_path, 'rb') as f:
return self.decrypt_vpol_bytes(f.read())

def decrypt_securestring_bytes(self, data):
return self.decrypt_blob_bytes(data)
def decrypt_securestring_bytes(self, data, entropy = None):
return self.decrypt_blob_bytes(data, entropy = entropy)

def decrypt_securestring_hex(self, hex_str):
return self.decrypt_securestring_bytes(bytes.fromhex(hex_str))
Expand Down Expand Up @@ -815,6 +850,17 @@ def get_all_wifi_settings_offline(system_drive_letter):
@staticmethod
def get_all_wifi_settings_live():
return DPAPI.get_all_wifi_settings_offline(DPAPI.get_windows_drive_live())

@staticmethod
def strongentropy(password:str, entropy = None, dtype = 2):
"""This function generates the "extra" entropy based on the password and the provided entropy (opt)."""
res = b'' if entropy is None else entropy
if dtype == 2:
res += sha512(password.encode('utf-16-le')).digest()
else:
res += sha1(password.encode('utf-16-le')).digest()
return res


def decrypt_wifi_live(self):
# key is encrypted as system!!!
Expand Down Expand Up @@ -874,6 +920,29 @@ def decrypt_cloudapkd_prt(self, PRT):

keyvalue_dec = self.decrypt_cloudap_key(keyvalue)
return keyvalue_dec

def winhello_pin_hash_offline(self, ngc_dir, cryptokeys_dir):
"""This function presupposes that the DPAPI object already has all necessary keys loaded."""
results = []
pin_guids = []
for entry in NGCProtectorFinder.from_dir(ngc_dir):
pin_guids.append(entry.guid)

for entry in CryptoKeysFinder.from_dir(cryptokeys_dir):
if entry.description in pin_guids:
print(f'Found matching GUID: {entry.description}')
properties_raw = self.decrypt_blob_bytes(entry.fields[1], entropy=b'6jnkd5J3ZdQDtrsu\x00')
properties = CryptoAPIKeyProperties.from_bytes(properties_raw)
blob = DPAPI_BLOB.from_bytes(entry.fields[2])

salt = properties['NgcSoftwareKeyPbkdf2Salt'].value
iterations = properties['NgcSoftwareKeyPbkdf2Round'].value

entropy = b'\x78\x54\x35\x72\x5a\x57\x35\x71\x56\x56\x62\x72\x76\x70\x75\x41\x00'
hashcat_format = f'$WINHELLO$*SHA512*{iterations}*{salt.hex()}*{blob.signature.hex()}*{self.get_key_for_blob(blob).hex()}*{blob.HMAC.hex()}*{blob.to_sign.hex()}*{entropy.hex()}'

results.append(hashcat_format)
return results



Expand Down
Empty file.
50 changes: 50 additions & 0 deletions pypykatz/dpapi/finders/cryptokeys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pathlib
from typing import Iterator, List

from winacl.dtyp.wcee.cryptoapikey import CryptoAPIKeyFile, CryptoAPIKeyProperties

class CryptoKeysFinder:
def __init__(self):
self.startdir = ['ServiceProfiles','LocalService','AppData','Roaming','Microsoft','Crypto','Keys']
self.entries:List[CryptoAPIKeyFile] = []

def __iter__(self) -> Iterator[CryptoAPIKeyFile]:
return iter(self.entries)

@staticmethod
def from_windir(win_dir: str | pathlib.Path, raise_error: bool = True):
if isinstance(win_dir, str):
win_dir = pathlib.Path(win_dir).absolute()

if not win_dir.is_dir():
if raise_error:
raise ValueError(f'{win_dir} is not a directory')
return CryptoKeysFinder()

cryptokeys_dir = win_dir
for directory in CryptoKeysFinder().startdir:
cryptokeys_dir = cryptokeys_dir / directory
if not cryptokeys_dir.is_dir():
raise ValueError(f'{cryptokeys_dir} does not exist')

return CryptoKeysFinder.from_dir(cryptokeys_dir)

@staticmethod
def from_dir(cryptokeys_dir: str | pathlib.Path, raise_error: bool = True):
if isinstance(cryptokeys_dir, str):
cryptokeys_dir = pathlib.Path(cryptokeys_dir).absolute()
if not cryptokeys_dir.is_dir():
if raise_error:
raise ValueError(f'{cryptokeys_dir} is not a directory')
return CryptoKeysFinder()

finder = CryptoKeysFinder()

for filepath in cryptokeys_dir.iterdir():
if filepath.is_dir():
continue

key = CryptoAPIKeyFile.from_bytes(filepath.read_bytes())
finder.entries.append(key)

return finder
78 changes: 78 additions & 0 deletions pypykatz/dpapi/finders/ngc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import pathlib
from typing import Iterator, List

class NGCProtector:
def __init__(self):
self.sid = None
self.path = None
self.provider = None
self.guid = None


class NGCProtectorFinder:
def __init__(self):
self.startdir = ['ServiceProfiles','LocalService','AppData','Local','Microsoft','Ngc']
self.entries:List[NGCProtector] = []

def __iter__(self) -> Iterator[NGCProtector]:
return iter(self.entries)

@staticmethod
def from_windir(win_dir: str | pathlib.Path, raise_error: bool = True):
if isinstance(win_dir, str):
win_dir = pathlib.Path(win_dir).absolute()

if not win_dir.is_dir():
raise ValueError(f'{win_dir} is not a directory')

ngc_dir = win_dir
for directory in NGCProtectorFinder().startdir:
ngc_dir = ngc_dir / directory
if not ngc_dir.is_dir():
raise ValueError(f'{ngc_dir} does not exist')

return NGCProtectorFinder.from_dir(ngc_dir, raise_error=raise_error)

@staticmethod
def from_dir(ngc_dir: str | pathlib.Path, raise_error: bool = True):
if isinstance(ngc_dir, str):
ngc_dir = pathlib.Path(ngc_dir).absolute()
if not ngc_dir.is_dir():
if raise_error:
raise ValueError(f'{ngc_dir} is not a directory')
return NGCProtectorFinder()
finder = NGCProtectorFinder()

for directory in ngc_dir.iterdir():
if not directory.is_dir():
continue
if directory.name.startswith('{') and directory.name.endswith('}'):
sid_file_path = ngc_dir / directory / '1.dat'
fpd = ngc_dir / directory / 'Protectors' / '1'
if not sid_file_path.exists():
print(f'NGC missing SID file at: {sid_file_path}')
continue

if not fpd.exists():
print(f'NGC missing Protector directory at: {directory}')
continue

sid = sid_file_path.read_text('utf-16-le').strip('\x00')
pfp = fpd / '1.dat'
if not pfp.exists():
print(f'NGC missing Protector file at: {pfp}')
continue

gfp = fpd / '2.dat'
if not gfp.exists():
print(f'NGC missing GUID file at: {gfp}')
continue


protector = NGCProtector()
protector.sid = sid
protector.provider = pfp.read_text('utf-16-le').strip('\x00')
protector.guid = gfp.read_text('utf-16-le').strip('\x00')
protector.path = fpd
finder.entries.append(protector)
return finder
Loading

0 comments on commit 91f9bdd

Please sign in to comment.