From 6c1c61806cd3214492f5ed4ff6c3189303bf29da Mon Sep 17 00:00:00 2001 From: MaxToffy Date: Mon, 21 Oct 2024 09:49:08 +0200 Subject: [PATCH 1/3] Added support for "export" hive format + compute bootkey from class names --- examples/registry-read.py | 5 +- examples/secretsdump.py | 54 +++++-- impacket/examples/secretsdump.py | 12 +- impacket/winregistry.py | 237 ++++++++++++++++++++++++++++++- 4 files changed, 290 insertions(+), 18 deletions(-) diff --git a/examples/registry-read.py b/examples/registry-read.py index 4b5656199d..11f506f3ee 100755 --- a/examples/registry-read.py +++ b/examples/registry-read.py @@ -148,13 +148,16 @@ def main(): walk_parser = subparsers.add_parser('walk', help='walks the registry from the name node down') walk_parser.add_argument('-name', action='store', required=True, help='registry class name to start walking down from') + # Hive format + parser.add_argument('-format', action='store', choices=['save', 'export'], default='save',help="Hive format, either 'save' or 'export' (default: 'save')") + if len(sys.argv)==1: parser.print_help() sys.exit(1) options = parser.parse_args() - reg = winregistry.Registry(options.hive) + reg = winregistry.Registry(options.hive,hiveFormat = options.format) if options.action.upper() == 'ENUM_KEY': print("[%s]" % options.name) diff --git a/examples/secretsdump.py b/examples/secretsdump.py index 1ab5616ed1..4e5333e1c4 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -95,8 +95,14 @@ def __init__(self, remoteName, username='', password='', domain='', options=None self.__rodc = options.rodcNo self.__systemHive = options.system self.__bootkey = options.bootkey + self.__jdClass = options.lsa_jd + self.__skew1Class = options.lsa_skew1 + self.__gbgClass = options.lsa_gbg + self.__dataClass = options.lsa_data self.__securityHive = options.security + self.__securityHiveExport = options.security_export self.__samHive = options.sam + self.__samHiveExport = options.sam_export self.__ntdsFile = options.ntds self.__skipSam = options.skip_sam self.__skipSecurity = options.skip_security @@ -170,6 +176,19 @@ def ldapConnect(self): self.__aesKey, kdcHost=self.__kdcHost) else: raise + + def getBootKey(self, jdClass, skew1Class, gbgClass, dataClass): + import binascii + bootKey = b'' + tmpKey = jdClass.encode('utf-8') + skew1Class.encode('utf-8') + gbgClass.encode('utf-8') + dataClass.encode('utf-8') + transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7] + tmpKey = binascii.unhexlify(tmpKey) + for i in range(len(tmpKey)): + bootKey += tmpKey[transforms[i]:transforms[i] + 1] + + logging.info('Target system bootKey: 0x%s' % binascii.hexlify(bootKey).decode('utf-8')) + + return bootKey def dump(self): try: @@ -217,10 +236,11 @@ def dump(self): if self.__ntdsFile is not None: # Let's grab target's configuration about LM Hashes storage self.__noLMHash = localOperations.checkNoLMHashPolicy() - else: + elif self.__bootkey: import binascii bootKey = binascii.unhexlify(self.__bootkey) - + elif self.__jdClass and self.__skew1Class and self.__gbgClass and self.__dataClass: + bootKey = self.getBootKey(self.__jdClass, self.__skew1Class, self.__gbgClass, self.__dataClass) else: self.__isRemote = True bootKey = None @@ -274,10 +294,14 @@ def dump(self): try: if self.__isRemote is True: SAMFileName = self.__remoteOps.saveSAM() - else: + elif self.__samHive: SAMFileName = self.__samHive + samFormat = "save" + elif self.__samHiveExport: + SAMFileName = self.__samHiveExport + samFormat = "export" - self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote = self.__isRemote) + self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote = self.__isRemote, format = samFormat) self.__SAMHashes.dump() if self.__outputFileName is not None: self.__SAMHashes.export(self.__outputFileName) @@ -288,11 +312,15 @@ def dump(self): try: if self.__isRemote is True: SECURITYFileName = self.__remoteOps.saveSECURITY() - else: + elif self.__securityHive: SECURITYFileName = self.__securityHive + securityFormat = "save" + elif self.__securityHiveExport: + SECURITYFileName = self.__securityHiveExport + securityFormat = "export" self.__LSASecrets = LSASecrets(SECURITYFileName, bootKey, self.__remoteOps, - isRemote=self.__isRemote, history=self.__history) + isRemote=self.__isRemote, history=self.__history, format = securityFormat) self.__LSASecrets.dumpCachedHashes() if self.__outputFileName is not None: self.__LSASecrets.exportCached(self.__outputFileName) @@ -400,8 +428,14 @@ def cleanup(self): parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') parser.add_argument('-system', action='store', help='SYSTEM hive to parse') parser.add_argument('-bootkey', action='store', help='bootkey for SYSTEM hive') - parser.add_argument('-security', action='store', help='SECURITY hive to parse') - parser.add_argument('-sam', action='store', help='SAM hive to parse') + parser.add_argument('-lsa-jd', action='store', help='Class name of HKLM\SYSTEM\CurrentControlSet\Control\Lsa\JD to compute the bootkey') + parser.add_argument('-lsa-skew1', action='store', help='Class name of HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Skew1 to compute the bootkey') + parser.add_argument('-lsa-gbg', action='store', help='Class name of HKLM\SYSTEM\CurrentControlSet\Control\Lsa\GBG to compute the bootkey') + parser.add_argument('-lsa-data', action='store', help='Class name of HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Data to compute the bootkey') + parser.add_argument('-security', action='store', help='SECURITY hive to parse in "save" format') + parser.add_argument('-security-export', action='store', help='SECURITY hive to parse in "export" format') + parser.add_argument('-sam', action='store', help='SAM hive to parse in "save" format') + parser.add_argument('-sam-export', action='store', help='SAM hive to parse in "export" format') parser.add_argument('-ntds', action='store', help='NTDS.DIT file to parse') parser.add_argument('-resumefile', action='store', help='resume file name to resume NTDS.DIT session dump (only ' 'available to DRSUAPI approach). This file will also be used to keep updating the session\'s ' @@ -506,8 +540,8 @@ def cleanup(self): sys.exit(1) if remoteName.upper() == 'LOCAL' and username == '': - if options.system is None and options.bootkey is None: - logging.error('Either the SYSTEM hive or bootkey is required for local parsing, check help') + if options.system is None and options.bootkey is None and (options.lsa_jd is None or options.lsa_skew1 is None or options.lsa_gbg is None or options.lsa_data is None): + logging.error('Either the SYSTEM hive, bootkey or the LSA\'s class names is required for local parsing, check help') sys.exit(1) else: diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index 1d97516c3e..832986af96 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -1293,10 +1293,10 @@ def decryptAES(key, value, iv=b'\x00'*16): class OfflineRegistry: - def __init__(self, hiveFile = None, isRemote = False): + def __init__(self, hiveFile = None, isRemote = False, format = "save"): self.__hiveFile = hiveFile if self.__hiveFile is not None: - self.__registryHive = winregistry.Registry(self.__hiveFile, isRemote) + self.__registryHive = winregistry.Registry(self.__hiveFile, isRemote, format) def enumKey(self, searchKey): parentKey = self.__registryHive.findKey(searchKey) @@ -1340,8 +1340,8 @@ def finish(self): self.__registryHive.close() class SAMHashes(OfflineRegistry): - def __init__(self, samFile, bootKey, isRemote = False, perSecretCallback = lambda secret: _print_helper(secret)): - OfflineRegistry.__init__(self, samFile, isRemote) + def __init__(self, samFile, bootKey, isRemote = False, format = "save", perSecretCallback = lambda secret: _print_helper(secret)): + OfflineRegistry.__init__(self, samFile, isRemote, format) self.__samFile = samFile self.__hashedBootKey = b'' self.__bootKey = bootKey @@ -1488,9 +1488,9 @@ class SECRET_TYPE: LSA_RAW = 2 LSA_KERBEROS = 3 - def __init__(self, securityFile, bootKey, remoteOps=None, isRemote=False, history=False, + def __init__(self, securityFile, bootKey, remoteOps=None, isRemote=False, history=False, format = "save", perSecretCallback=lambda secretType, secret: _print_helper(secret)): - OfflineRegistry.__init__(self, securityFile, isRemote) + OfflineRegistry.__init__(self, securityFile, isRemote, format) self.__hashedBootKey = b'' self.__bootKey = bootKey self.__LSAKey = b'' diff --git a/impacket/winregistry.py b/impacket/winregistry.py index 6089f2a913..2f703f08cd 100644 --- a/impacket/winregistry.py +++ b/impacket/winregistry.py @@ -25,6 +25,8 @@ from __future__ import division from __future__ import print_function import sys +import re +from binascii import unhexlify from struct import unpack import ntpath from six import b @@ -160,7 +162,7 @@ class REG_HASH(Structure): b'sk': REG_SK, } -class Registry: +class saveRegistryParser: def __init__(self, hive, isRemote = False): self.__hive = hive if isRemote is True: @@ -493,3 +495,236 @@ def getClass(self, className): if key['OffsetClassName'] > 0: value = self.__getBlock(key['OffsetClassName']) return value['Data'] + + +class RegistryNode: + def __init__(self, keyName, nodeName, data = None): + self.keyName = keyName + self.nodeName = nodeName + self.data = data + self.childKeys = {} + + def addChildNode(self, childKey): + self.childKeys = self.childKeys | childKey + + +class exportRegistryParser: + def __init__(self, hive): + self.indent = '' + self.__hive = hive + self.fd = open(hive, encoding='utf-16-le') + self.__buildRegistryTree() + + def close(self): + if hasattr(self, 'fd'): + self.fd.close() + + def __del__(self): + self.close() + + def __parseValue(self,ValueType,ValueData): + ValueType = ValueType.replace('"','') + if not ValueData : + return REG_SZ,ValueType + elif ValueType == 'hex(0)': + return REG_NONE, ValueData[0] + elif ValueType == 'hex(2)': + return REG_EXPAND_SZ, ValueData[0] + elif ValueType == 'hex': + return REG_BINARY, ValueData[0] + elif ValueType == 'dword': + return REG_DWORD, ValueData[0] + elif ValueType == 'hex(7)': + return REG_MULTISZ, ValueData[0] + elif ValueType == 'hex(b)': + return REG_QWORD, ValueData[0] + else: + return int(ValueType.replace('hex(','0x').replace(')',''),16), ValueData[0] + + def __keyToNodePath(self, key): + return key.replace(f'{self.registryTree.keyName}\\','').strip('\\').split('\\') + + def __findNode(self, nodePath): + node = self.registryTree + try: + if nodePath != ['']: + for tempNode in nodePath: + node = node.childKeys[tempNode] + return node + except: + return None + + def __extractData(self, regkey_values): + if not regkey_values: + return { 'default' : [REG_SZ, '']} + else: + data = {} + for line in regkey_values.split('\n'): + ValueName, ValueType, *ValueData = line.split(':') + if ValueName == '@': + ValueName = 'default' + else: + ValueName = ValueName.replace('"','') + ValueType, ValueData = self.__parseValue(ValueType,ValueData) + data = data | {ValueName : [ValueType, ValueData]} + return data + + def __buildChildNode(self, keyName, regkey_values): + nodeName = ''.join(keyName.split('\\')[-1:]) + data = self.__extractData(regkey_values) + node = { nodeName : RegistryNode(keyName, nodeName, data)} + + return node + + def __buildRegistryTree(self): + pattern = re.compile(r'^\[(.*?)\]([\S\s]*?)\n\n',re.MULTILINE) + file = self.fd.read() + rootKey = True + for match in pattern.findall(file): + keyName = match[0] + regkey_values = match[1].strip('\n').replace(',','').replace(' ','').replace('\\\n','').replace('=',':') + + if rootKey is True: + data = self.__extractData(regkey_values) + nodeName = ''.join(keyName.split('\\')[-1:]) + self.registryTree = RegistryNode(keyName, nodeName, data) + rootKey = False + else: + parentPath = self.__keyToNodePath(keyName)[:-1] + node = self.__buildChildNode(keyName, regkey_values) + parentNode = self.__findNode(parentPath) + parentNode.addChildNode(node) + + def __walkSubNodes(self, node): + print("%s%s" % (self.indent, node.nodeName )) + self.indent += ' ' + if node.childKeys == {}: + self.indent = self.indent[:-2] + return + + for subNode in list(node.childKeys.values()): + self.__walkSubNodes(subNode) + + self.indent = self.indent[:-2] + + def walk(self, parentKey): + path = self.__keyToNodePath(parentKey) + node = self.__findNode(path) + + if node is None: + return + + for subNode in list(node.childKeys.values()): + self.__walkSubNodes(subNode) + + def printValue(self, valueType, valueData): + if valueType in [REG_SZ, REG_EXPAND_SZ, REG_MULTISZ] : + if valueData == b'' or valueData == b'\x00\x00': + print('NULL') + else: + print("%s" % (valueData.decode('utf-16le'))) + elif valueType == REG_BINARY: + print('') + hexdump(valueData, self.indent) + elif valueType == REG_DWORD: + if valueData == b'': + print(0) + else: + print(int.from_bytes(valueData)) + elif valueType == REG_QWORD: + print("%d" % (unpack(' 1: + print('') + hexdump(valueData, self.indent) + else: + print(" NULL") + except: + print(" NULL") + else: + print("Unknown Type 0x%x!" % valueType) + hexdump(valueData) + + def findKey(self, key): + if key == '\\': + return '\\' + else: + return '\\'.join(self.__keyToNodePath(key)) + + def enumKey(self, key): + path = self.__keyToNodePath(key) + node = self.__findNode(path) + return list(node.childKeys.keys()) + + def enumValues(self,key): + path = self.__keyToNodePath(key) + node = self.__findNode(path) + values = list(node.data.keys()) + return [s.encode('utf-8') for s in values] + + def getValue(self, keyValue, valueName=None): + """ returns a tuple with (ValueType, ValueData) for the requested keyValue + valueName is the name of the value (which can contain '\\') + if valueName is not given, keyValue must be a string containing the full path to the value + if valueName is given, keyValue should be the string containing the path to the key containing valueName + """ + path = self.__keyToNodePath(keyValue) + if valueName is None: + keyPath = path[:-1] + regValue = ''.join(path[-1:]) + else: + keyPath = path + regValue = valueName + + try: + node = self.__findNode(keyPath) + ValueType, ValueData = node.data[regValue] + if ValueType in [REG_SZ]: + return ValueType, ValueData.encode("utf-16-le") + else: + return ValueType, unhexlify(ValueData) + except: + return None + + def getClass(self, className): + # Export format does not contain class name + return None + + +class Registry: + def __init__(self, hive, isRemote = False, hiveFormat = 'save'): + self.indent = '' + self.__hiveFormat = hiveFormat + if self.__hiveFormat == 'save': + self.__registryParser = saveRegistryParser(hive, isRemote) + elif self.__hiveFormat == 'export': + self.__registryParser = exportRegistryParser(hive) + + def close(self): + if hasattr(self, 'fd'): + self.fd.close() + + def __del__(self): + self.close() + + def walk(self, parentKey): + return self.__registryParser.walk(parentKey) + + def findKey(self, key): + return self.__registryParser.findKey(key) + + def printValue(self, valueType, valueData): + return self.__registryParser.printValue(valueType, valueData) + + def enumKey(self, parentKey): + return self.__registryParser.enumKey(parentKey) + + def enumValues(self,key): + return self.__registryParser.enumValues(key) + + def getValue(self, keyValue, valueName=None): + return self.__registryParser.getValue(keyValue, valueName) + + def getClass(self, className): + return self.__registryParser.getClass(className) \ No newline at end of file From a16f4b8055c290300781c0eebb6809b690eb89d9 Mon Sep 17 00:00:00 2001 From: MaxToffy Date: Mon, 21 Oct 2024 18:13:39 +0200 Subject: [PATCH 2/3] Fixing bug when key does not exist --- impacket/winregistry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/impacket/winregistry.py b/impacket/winregistry.py index 2f703f08cd..a9f63b451e 100644 --- a/impacket/winregistry.py +++ b/impacket/winregistry.py @@ -660,8 +660,11 @@ def enumKey(self, key): def enumValues(self,key): path = self.__keyToNodePath(key) node = self.__findNode(path) - values = list(node.data.keys()) - return [s.encode('utf-8') for s in values] + if not node: + return None + else: + values = list(node.data.keys()) + return [s.encode('utf-8') for s in values] def getValue(self, keyValue, valueName=None): """ returns a tuple with (ValueType, ValueData) for the requested keyValue From b55f9a7cb8a1b256bab2bc199facbfb752eec079 Mon Sep 17 00:00:00 2001 From: MaxToffy Date: Wed, 30 Oct 2024 14:31:32 +0100 Subject: [PATCH 3/3] Better data extraction to avoid special characters errors --- impacket/winregistry.py | 53 +++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/impacket/winregistry.py b/impacket/winregistry.py index a9f63b451e..7b1750af9b 100644 --- a/impacket/winregistry.py +++ b/impacket/winregistry.py @@ -522,24 +522,21 @@ def close(self): def __del__(self): self.close() - def __parseValue(self,ValueType,ValueData): - ValueType = ValueType.replace('"','') - if not ValueData : - return REG_SZ,ValueType - elif ValueType == 'hex(0)': - return REG_NONE, ValueData[0] + def __parseType(self, ValueType): + if ValueType == 'hex(0)': + return REG_NONE elif ValueType == 'hex(2)': - return REG_EXPAND_SZ, ValueData[0] + return REG_EXPAND_SZ elif ValueType == 'hex': - return REG_BINARY, ValueData[0] + return REG_BINARY elif ValueType == 'dword': - return REG_DWORD, ValueData[0] + return REG_DWORD elif ValueType == 'hex(7)': - return REG_MULTISZ, ValueData[0] + return REG_MULTISZ elif ValueType == 'hex(b)': - return REG_QWORD, ValueData[0] + return REG_QWORD else: - return int(ValueType.replace('hex(','0x').replace(')',''),16), ValueData[0] + return int(ValueType.replace('hex(','0x').replace(')',''),16) def __keyToNodePath(self, key): return key.replace(f'{self.registryTree.keyName}\\','').strip('\\').split('\\') @@ -559,13 +556,29 @@ def __extractData(self, regkey_values): return { 'default' : [REG_SZ, '']} else: data = {} - for line in regkey_values.split('\n'): - ValueName, ValueType, *ValueData = line.split(':') - if ValueName == '@': - ValueName = 'default' + pattern_regsz = re.compile(r'^(?:"(.*)"|(@))="(.*)"$') + pattern_other = re.compile(r'^(?:"(.*)"|(@))=(.*):([\S\s]*)$') + + pattern_split_values = re.compile(r'^([\S\s]*?)$(?