Skip to content

Instantly share code, notes, and snippets.

@FrankSpierings
Last active April 25, 2021 13:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FrankSpierings/9ea1b9b2c7547ba34d8e9e2717e7f3d6 to your computer and use it in GitHub Desktop.
Save FrankSpierings/9ea1b9b2c7547ba34d8e9e2717e7f3d6 to your computer and use it in GitHub Desktop.
Decrypt Protect file using PVK Domain key
# Referenced sources:
# - Mimikatz
# - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/5cf2e6b9-3195-4f85-bc18-05b50e6d4e11
# - https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/ns-wincrypt-publickeystruc
from io import BytesIO
import struct
import math
import codecs
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
USHORT = '<H'
SHORT = '<h'
UINT = '<I'
INT = '<i'
DWORD = '<I'
DWORD64 = '<Q'
BOOL = '?'
CHAR = 'c'
UCHAR = 'B'
class Structure:
_fields_ = [
# (struct.format, name, [lambda])
]
def __init__(self, buf: BytesIO):
self._read_fields(buf, self._fields_)
def _read_fields(self, buf: BytesIO, fields: tuple):
for field in fields:
typedef = field[0]
typesize = struct.calcsize(typedef)
value = buf.read(typesize)
value = struct.unpack(typedef, value)[0]
if len(field) > 2:
# Execute lambda function
value = field[2](value)
setattr(self, field[1], value)
def __repr__(self) -> str:
output = []
for attr in dir(self):
if not callable(getattr(self, attr)) and not attr.startswith('_'):
value = getattr(self, attr)
if isinstance(value, int):
output += ['{0}=0x{1:x}'.format(attr, value)]
elif isinstance(value, bytes):
output += ['{0}=0x{1}'.format(attr, codecs.encode(value, 'hex').decode())]
else:
output += ['{0}={1}'.format(attr, value)]
return ' '.join(output)
class PVK_FILE():
class PVK_FILE_HEADER(Structure):
_fields_ = [
(DWORD, 'magic'),
(DWORD, 'version'),
(DWORD, 'keyspec'),
(DWORD, 'encrypt_type'),
(UINT, 'encrypt_data'),
(UINT, 'pvk'),
]
class RSA_PRIV_KEY_BLOB(Structure):
_fields_ = [
(UCHAR, 'type'),
(UCHAR, 'version'),
(SHORT, 'reserved'),
(DWORD, 'keyalg'),
(DWORD, 'magic'),
(DWORD, 'bitlen'),
(DWORD, 'e'),
]
def __init__(self, buf: BytesIO):
super().__init__(buf)
def largeint(v):
return int.from_bytes(v, byteorder='little', signed=False)
BL8 = '{0}s'.format(math.ceil(self.bitlen / 8))
BL16 = '{0}s'.format(math.ceil(self.bitlen / 16))
dynamic_fields = [
(BL8, 'n', largeint),
(BL16, 'p', largeint),
(BL16, 'q', largeint),
(BL16, 'dp', largeint),
(BL16, 'dq', largeint),
(BL16, 'iq', largeint),
(BL8, 'd', largeint),
]
self._read_fields(buf, dynamic_fields)
def __init__(self, buf: BytesIO):
self.header = self.PVK_FILE_HEADER(buf)
self.rsa_blob = self.RSA_PRIV_KEY_BLOB(buf)
self.rsa = RSA.construct(
(self.rsa_blob.n, self.rsa_blob.e, self.rsa_blob.d))
class MASTERKEYS_FILE():
class MASTERKEYS_FILE_HEADER(Structure):
_fields_ = [
(DWORD, 'version'),
(DWORD, 'unknown0'),
(DWORD, 'unknown1'),
('72s', 'guid', lambda v: codecs.decode(v, 'utf-16-le')),
(DWORD, 'unknown2'),
(DWORD, 'unknown3'),
(DWORD, 'flags'),
(DWORD64, 'masterkeylen'),
(DWORD64, 'backupkeylen'),
(DWORD64, 'credhistlen'),
(DWORD64, 'domainkeylen'),
]
class DPAPI_MASTERKEY(Structure):
_fields_ = [
(DWORD, 'version'),
('16s', 'salt'),
(DWORD, 'rounds'),
(UINT, 'hash'),
(UINT, 'crypt'),
]
def __init__(self, buf: BytesIO, size: int):
start = buf.tell()
super().__init__(buf)
length = size - (buf.tell() - start)
self.key = buf.read(length)
class DPAPI_MASTERKEY_DOMAINKEY(Structure):
_GUID = '{0}s'.format(4 + 2 + 2 + 8)
def bytes_to_guid(b: bytes) -> str:
return '{0:08x}-{1:04x}-{2:04x}-{3}-{4}'.format(
int.from_bytes(b[0:4], byteorder='little', signed=False),
int.from_bytes(b[4:6], byteorder='little', signed=False),
int.from_bytes(b[6:8], byteorder='little', signed=False),
''.join(['{0:02x}'.format(i) for i in b[8:10]]),
''.join(['{0:02x}'.format(i) for i in b[10:16]]))
_fields_ = [
(DWORD, 'version'),
(DWORD, 'secretlen'),
(DWORD, 'accesschecklen'),
(_GUID, 'guid', bytes_to_guid),
]
def __init__(self, buf: BytesIO, size: int):
super().__init__(buf)
self.secret = buf.read(self.secretlen)
self.accesscheck = buf.read(self.accesschecklen)
def __init__(self, buf: BytesIO):
self.header = self.MASTERKEYS_FILE_HEADER(buf)
self.masterkey = None
self.backupkey = None
self.credhist = None
self.domainkey = None
if self.header.masterkeylen > 0:
self.masterkey = self.DPAPI_MASTERKEY(
buf, self.header.masterkeylen)
if self.header.backupkeylen > 0:
self.backupkey = self.DPAPI_MASTERKEY(
buf, self.header.backupkeylen)
if self.header.credhistlen > 0:
# TODO: Not implemented
buf.read(self.header.credhistlen)
if self.header.domainkeylen > 0:
self.domainkey = self.DPAPI_MASTERKEY_DOMAINKEY(
buf, self.header.domainkeylen)
class DPAPI_DOMAIN_RSA_MASTER_KEY(Structure):
_fields_ = [
(DWORD, 'masterkeylen'),
(DWORD, 'suppkeylen'),
]
def __init__(self, buf: BytesIO):
super().__init__(buf)
self.masterkey = buf.read(self.masterkeylen)
self.suppkey = buf.read(self.suppkeylen)
def dpapi_masterkey(f_pvk: BytesIO, f_protect: BytesIO):
pvk = PVK_FILE(f_pvk)
mks = MASTERKEYS_FILE(f_protect)
cipher = PKCS1_v1_5.new(pvk.rsa)
dec = cipher.decrypt(mks.domainkey.secret[::-1], None)
return DPAPI_DOMAIN_RSA_MASTER_KEY(BytesIO(dec))
PVK_PATH = ''
PROTECT_PATH = ''
dpapi_masterkey(open(PVK_PATH, 'rb'), open(PROTECT_PATH, 'rb'))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment