307 lines
12 KiB
Python
307 lines
12 KiB
Python
#!/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 <jmichel.p@gmail.com> ##
|
|
## ##
|
|
## This program is distributed under GPLv3 licence (see LICENCE.txt) ##
|
|
## ##
|
|
#############################################################################
|
|
import binascii
|
|
import struct
|
|
import hmac
|
|
import hashlib
|
|
from lib.dpapi_pick import crypto
|
|
from lib.dpapi_pick import eater
|
|
'''
|
|
crypto_CryptoAlgo={
|
|
0x6601: {"name":"DES", "keyLength":64/8, "blockLength":64/8, "IVLength":64/8, "module":"des", "keyFixup":"des_set_odd_parity"},
|
|
0x6603: {"name":"DES3", "keyLength":192/8, "blockLength":64/8, "IVLength":64/8, "module":"triple_des", "keyFixup":"des_set_odd_parity"},
|
|
0x6611: {"name":"AES", "keyLength":128/8, "blockLength":128/8, "IVLength":128/8},
|
|
0x660e: {"name":"AES-128", "keyLength":128/8, "blockLength":128/8, "IVLength":128/8},
|
|
0x660f: {"name":"AES-192", "keyLength":192/8, "blockLength":128/8, "IVLength":128/8},
|
|
0x6610: {"name":"AES-256", "keyLength":256/8, "blockLength":128/8, "IVLength":128/8},
|
|
0x8009: {"name":"HMAC", "digestLength":160/8, "blockLength":512/8},
|
|
0x8003: {"name":"md5", "digestLength":128/8, "blockLength":512/8},
|
|
0x8004: {"name":"sha1", "digestLength":160/8, "blockLength":512/8},
|
|
0x800c: {"name":"sha256", "digestLength":256/8, "blockLength":512/8},
|
|
0x800d: {"name":"sha384", "digestLength":384/8, "blockLength":1024/8},
|
|
0x800e: {"name":"sha512", "digestLength":512/8, "blockLength":1024/8}
|
|
}'''
|
|
|
|
class RPC_SID(eater.DataStruct):
|
|
"""Represents a RPC_SID structure. See MSDN for documentation"""
|
|
def __init__(self, raw=None):
|
|
self.version = None
|
|
self.idAuth = None
|
|
self.subAuth = None
|
|
eater.DataStruct.__init__(self, raw)
|
|
|
|
def parse(self, data):
|
|
#print('parse rpc')
|
|
self.version = data.eat("B")
|
|
n = data.eat("B")
|
|
self.idAuth = struct.unpack(">Q", b"\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)
|
|
|
|
def __repr__(self):
|
|
return """RPC_SID(%s):
|
|
revision = %d
|
|
identifier-authority = %r
|
|
subAuthorities = %r""" % (self, self.version, self.idAuth, self.subAuth)
|
|
|
|
|
|
class CredSystem(eater.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.machine = None
|
|
self.user = None
|
|
self.revision = None
|
|
eater.DataStruct.__init__(self, raw)
|
|
|
|
def parse(self, data):
|
|
self.revision = data.eat("L")
|
|
self.machine = data.eat("20s")
|
|
self.user = data.eat("20s")
|
|
|
|
def __repr__(self):
|
|
s = ["DPAPI_SYSTEM:"]
|
|
if self.user is not None:
|
|
s.append("\tUser Credential : %s" % self.user.encode('hex'))
|
|
if self.machine is not None:
|
|
s.append("\tMachine Credential: %s" % self.machine.encode('hex'))
|
|
return "\n".join(s)
|
|
|
|
|
|
class CredhistEntry(eater.DataStruct):
|
|
"""Represents an entry in the Credhist file"""
|
|
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
|
|
eater.DataStruct.__init__(self, raw)
|
|
|
|
def __getstate__(self):
|
|
d = dict(self.__dict__)
|
|
for k in ["cipherAlgo", "hashAlgo"]:
|
|
if k in d:
|
|
d[k] = d[k].algnum
|
|
return d
|
|
|
|
def __setstate__(self, d):
|
|
for k in ["cipherAlgo", "hashAlgo"]:
|
|
if k in d:
|
|
d[k] = crypto.CryptoAlgo(d[k])#'''crypto_CryptoAlgo[d[k]]'''
|
|
self.__dict__.update(d)
|
|
|
|
def parse(self, data):
|
|
#print('parse2')
|
|
self.revision = data.eat("L")
|
|
#print('parse3')
|
|
self.hashAlgo = crypto.CryptoAlgo(data.eat("L"))#'''crypto_CryptoAlgo[data.eat("L")]'''
|
|
self.rounds = data.eat("L")
|
|
data.eat("L")
|
|
#print('parse4')
|
|
self.cipherAlgo = crypto.CryptoAlgo(data.eat("L"))#'''crypto_CryptoAlgo'''
|
|
self.shaHashLen = data.eat("L")
|
|
self.ntHashLen = data.eat("L")
|
|
self.iv = data.eat("16s")
|
|
#print('parse5')
|
|
self.userSID = RPC_SID()
|
|
self.userSID.parse(data)
|
|
n = self.shaHashLen + self.ntHashLen
|
|
n += -n % self.cipherAlgo.blockSize#self.cipherAlgo['blockLength'] #blockSize
|
|
self.encrypted = data.eat_string(n)
|
|
#print('parse6')
|
|
self.revision2 = data.eat("L")
|
|
self.guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B")
|
|
#print(self.__repr__)
|
|
|
|
def decryptWithKey(self, enckey):
|
|
"""Decrypts this credhist entry using the given encryption key."""
|
|
#print(f'using key {enckey}')
|
|
cleartxt = crypto.dataDecrypt(self.cipherAlgo, self.hashAlgo, self.encrypted,
|
|
enckey, self.iv, self.rounds)
|
|
#print(f'cleartext {cleartxt}')
|
|
self.pwdhash = cleartxt[:self.shaHashLen]
|
|
self.ntlm = cleartxt[self.shaHashLen:self.shaHashLen + self.ntHashLen].rstrip(b"\x00")
|
|
if len(self.ntlm) != 16:
|
|
self.ntlm = None
|
|
|
|
def decryptWithHash(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.decryptWithKey() to finish the decryption.
|
|
|
|
"""
|
|
self.decryptWithKey(crypto.derivePwdHash(pwdhash, str(self.userSID)))#self.crypto ?
|
|
|
|
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 decryptWithPassword(self, password):
|
|
"""Decrypts this credhist entry with the given user's password.
|
|
Simply computes the password hash then calls self.decryptWithHash()
|
|
|
|
"""
|
|
return self.decryptWithHash(hashlib.sha1(password.encode("UTF-16LE")).digest())
|
|
|
|
'''
|
|
def jtr_shadow(self):
|
|
"""Returns a string that can be passed to John the Ripper to crack this
|
|
CREDHIST entry. Requires to use a recent jumbo version of JtR plus
|
|
the configuration snipplet in the "3rdparty" directory of DPAPIck.
|
|
|
|
Unless you know what you are doing, you shall not call this function
|
|
yourself. Instead, use the method provided by CredHistPool object.
|
|
"""
|
|
rv = []
|
|
if self.pwdhash is not None:
|
|
rv.append("%s:$dynamic_1400$%s" % (self.userSID, self.pwdhash.encode('hex')))
|
|
if self.ntlm is not None:
|
|
rv.append("%s:$NT$%s" % (self.userSID, self.ntlm.encode('hex')))
|
|
return "\n".join(rv)
|
|
'''
|
|
def __repr__(self):
|
|
s = ["CredHist entry",
|
|
"\trevision = %x\n" % self.revision,
|
|
"\thash = %r" % self.hashAlgo,
|
|
"\trounds = %i" % self.rounds,
|
|
"\tcipher = %r" % self.cipherAlgo,
|
|
"\tshaHashLen = %i" % self.shaHashLen,
|
|
"\tntHashLen = %i" % self.ntHashLen,
|
|
"\tuserSID = %s" % self.userSID,
|
|
"\tguid = %s" % self.guid,
|
|
"\tiv = %s" % binascii.hexlify(self.iv)]#.encode("hex")
|
|
if self.pwdhash is not None:
|
|
s.append("\tpwdhash = %s" % binascii.hexlify(self.pwdhash))#.encode("hex")
|
|
if self.ntlm is not None:
|
|
s.append("\tNTLM = %s" % binascii.hexlify(self.ntlm))#.encode("hex")
|
|
return "\n".join(s)
|
|
|
|
|
|
class CredHistFile(eater.DataStruct):
|
|
"""Represents a CREDHIST file.
|
|
Be aware that currently, it is not possible to check whether the decryption
|
|
succeeded or not. To circumvent that and optimize a little bit crypto
|
|
operations, once a credhist entry successfully decrypts a masterkey, the
|
|
whole CredHistFile is flagged as valid. Then, no further decryption occurs.
|
|
|
|
"""
|
|
def __init__(self, raw=None):
|
|
self.entries_list = []
|
|
self.entries = {}
|
|
self.valid = False
|
|
self.footmagic = None
|
|
self.curr_guid = None
|
|
eater.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")
|
|
#print(f'GUID:{self.curr_guid}')
|
|
|
|
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 validate(self):
|
|
"""Simply flags a file as successfully decrypted. See the class
|
|
documentation for information.
|
|
|
|
"""
|
|
self.valid = True
|
|
|
|
def decryptWithHash(self, h):
|
|
"""Try to decrypt each entry with the given hash"""
|
|
if self.valid:
|
|
return
|
|
curhash = h
|
|
for entry in self.entries_list:
|
|
try:
|
|
entry.decryptWithHash(curhash)
|
|
curhash = entry.pwdhash
|
|
except Exception as ex:
|
|
#print('erro Decrypting')
|
|
#print(ex)
|
|
continue
|
|
def decryptWithPassword(self, pwd):
|
|
"""Try to decrypt each entry with the given password.
|
|
This function simply computes the SHA-1 hash with the password, then
|
|
calls self.decryptWithHash()
|
|
|
|
"""
|
|
#print(f'password : {pwd}')
|
|
return self.decryptWithHash(hashlib.sha1(pwd.encode("UTF-16LE")).digest())
|
|
|
|
def jtr_shadow(self, validonly=False):
|
|
"""Returns a string that can be passed to John the Ripper to crack the
|
|
CREDHIST entries. Requires to use a recent jumbo version of JtR plus
|
|
the configuration snipplet in the "3rdparty" directory of DPAPIck.
|
|
|
|
If validonly is set to True, will only extract CREDHIST entries
|
|
that are known to have sucessfully decrypted a masterkey.
|
|
|
|
"""
|
|
if validonly and not self.valid:
|
|
return ""
|
|
s = []
|
|
for e in self.entries.itervalues():
|
|
s.append(e.jtr_shadow())
|
|
return "\n".join(s)
|
|
|
|
def __repr__(self):
|
|
s = ["CredHistPool: %s" % self.curr_guid]
|
|
for e in self.entries:
|
|
s.append("---")
|
|
s.append(repr(self.entries[e]))
|
|
s.append("====")
|
|
return "\n".join(s)
|
|
|
|
|
|
# vim:ts=4:expandtab:sw=4
|