Last active
April 25, 2021 13:44
-
-
Save FrankSpierings/9ea1b9b2c7547ba34d8e9e2717e7f3d6 to your computer and use it in GitHub Desktop.
Decrypt Protect file using PVK Domain key
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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