diff --git a/X. NU/python/CrimsonUroboros.py b/X. NU/python/CrimsonUroboros.py index 098ed42..cb322c6 100755 --- a/X. NU/python/CrimsonUroboros.py +++ b/X. NU/python/CrimsonUroboros.py @@ -2225,30 +2225,6 @@ class AMFIProcessor: pass def process(self, args): - if args.dump_prelink_info is not None: # nargs="?", const='PRELINK_info.txt' # Dump '__PRELINK_INFO,__info' to a given file (default: 'PRELINK_info.txt') - snake_instance.dumpPrelink_info(args.dump_prelink_info) - - if args.dump_prelink_text is not None: # Dump '__PRELINK_TEXT,__text' to a given file (default: 'PRELINK_text.txt') - snake_instance.dumpPrelink_text(args.dump_prelink_text) - - if args.dump_prelink_kext is not None: # Dump prelinked KEXT from decompressed Kernel Cache to a file named: prelinked_{kext_name}.bin - snake_instance.dumpKernelExtensionFromPRELINK_TEXT(args.dump_prelink_kext) - - if args.kext_prelinkinfo: # Print _Prelink properties from PRELINK_INFO,__info for a give kext - snake_instance.printParsedPRELINK_INFO_plist(args.kext_prelinkinfo) - - if args.kmod_info: # Print parsed kmod_info for the given kext - snake_instance.printParsedkmod_info(args.kmod_info) - - if args.kext_entry: # Print kext entrypoint - snake_instance.printKextEntryPoint(args.kext_entry) - - if args.kext_exit: # Print kext exitpoint - snake_instance.printKextExitPoint(args.kext_exit) - - if args.mig: # Search for MIG subsystem and prints message handlers - snake_instance.printMIG() - if args.has_suid: # Print file SUID status snake_instance.printHasSetUID() @@ -2284,258 +2260,6 @@ class SnakeVI(SnakeV): 'applemobilefileintegrity.kext' : 'applemobilefileintegrity', } - def loadPRELINK_INFOFromFile(self, prelink_info_filename): # Not used yet. - ''' - Read PRELINK_INFO,__info section from file (with alignment). - The last line in the dumped section plist is broken, because of alignment. - This function remove it so the plistlib.loads work. - It returns loaded PLIST {prelink_info_plist}. - ''' - prelink_info_plist_bytes = self.readBytesFromFile(prelink_info_filename) - prelink_as_bytes_without_last_line = self.removeNullBytesAlignment(prelink_info_plist_bytes) - prelink_info_plist = plistlib.loads(prelink_as_bytes_without_last_line) - return prelink_info_plist - - def calcTwoComplement64(self, value): - ''' Convert negative int to hex representation. ''' - return hex((value + (1 << 64)) % (1 << 64)) - - def removeNullBytesAlignment(self, string_as_bytes): - ''' - The last line in the PLISTs and other files dumped from memory will almost always be aligned with 0x00 bytes. - This function: - Detects lines in a given bytes {string_as_bytes}. - Removes the last line. - Returns a new {string_as_bytes}. - ''' - decoded_string = string_as_bytes.decode('utf-8') - decoded_string_without_last_line = decoded_string[:decoded_string.rfind('\n')] - string_as_bytes_without_last_line = decoded_string_without_last_line.encode() - return string_as_bytes_without_last_line - - def dumpPrelink_info(self, filename): - ''' Dump '__PRELINK_INFO,__info' to a given file (default: 'PRELINK_info.txt') ''' - segment_name = '__PRELINK_INFO' - section_name = '__info' - if self.dumpSection(segment_name, section_name, filename): - print("SUCCESS: __PRELINK_INFO,__info dump") - - def dumpPrelink_text(self, filename): - ''' Dump '__PRELINK_TEXT,__text' to a given file (default: 'PRELINK_text.txt') ''' - segment_name = '__PRELINK_TEXT' - section_name = '__text' - if self.dumpSection(segment_name, section_name, filename): - print("SUCCESS: __PRELINK_TEXT,__text dump") - - def extractPRELINK_INFO_plist(self): - ''' Extract '__PRELINK_INFO,__info' and return it. ''' - segment_name = '__PRELINK_INFO' - section_name = '__info' - extracted_bytes = self.extractSection(segment_name, section_name) - return extracted_bytes - - def parsePRELINK_INFO_plist(self, kext_name): - ''' Extract PLIST properties values from '__PRELINK_INFO,__info' section for the given {kext_name}: - _PrelinkBundlePath - _PrelinkExecutableLoadAddr - _PrelinkExecutableRelativePath - _PrelinkExecutableSize - _PrelinkExecutableSourceAddr - _PrelinkKmodInfo - ''' - #prelink_info_plist = self.loadPRELINK_INFO(prelink_info_filename) # For loading PRELINK_INFO from file - prelink_as_bytes = self.extractPRELINK_INFO_plist() - prelink_as_bytes_without_last_line = self.removeNullBytesAlignment(prelink_as_bytes) - prelink_info_plist = plistlib.loads(prelink_as_bytes_without_last_line) - - kext_name = kext_name.lower() - if kext_name in self.kext_map: - kext_name = self.kext_map[kext_name] - - # Iterate over the parsed dictionary - for item in prelink_info_plist['_PrelinkInfoDictionary']: - PrelinkExecutableRelativePath = item.get('_PrelinkExecutableRelativePath', '').lower() - - # Check if the '_PrelinkExecutableRelativePath' contains {kext_name} in its path - if kext_name in PrelinkExecutableRelativePath: - # Extract the desired keys and their corresponding values - bundle_path = item.get('_PrelinkBundlePath') - - executable_load_addr = str(item.get('_PrelinkExecutableLoadAddr')).lower() - if executable_load_addr.startswith("0x"): - executable_load_addr = int(executable_load_addr, 16) - elif executable_load_addr.startswith("-"): - executable_load_addr = self.calcTwoComplement64(int(executable_load_addr)) - - executable_relative_path = item.get('_PrelinkExecutableRelativePath') - - executable_size = str(item.get('_PrelinkExecutableSize')).lower() - if executable_size.startswith("0x"): - executable_size = int(executable_size, 16) - elif executable_size.startswith("-"): - executable_size = self.calcTwoComplement64(int(executable_size)) - - source_addr = str(item.get('_PrelinkExecutableSourceAddr')).lower() - if source_addr.startswith("0x"): - source_addr = int(source_addr, 16) - elif source_addr.startswith("-"): - source_addr = self.calcTwoComplement64(int(source_addr)) - - kmod_info = str(item.get('_PrelinkKmodInfo')).lower() - if kmod_info.startswith("0x"): - kmod_info = int(kmod_info, 16) - elif kmod_info.startswith("-"): - kmod_info = self.calcTwoComplement64(int(kmod_info)) - - return bundle_path, executable_load_addr, executable_relative_path, executable_size, source_addr, kmod_info - - def printParsedPRELINK_INFO_plist(self, kext_name): - ''' Print extracted properties for PRELINK_INFO Plist for a given kext. ''' - bundle_path, executable_load_addr, executable_relative_path, executable_size, source_addr, kmod_info = self.parsePRELINK_INFO_plist(kext_name) - print(f'_PrelinkBundlePath: {bundle_path}') - print(f'_PrelinkExecutableLoadAddr: {executable_load_addr}') - print(f'_PrelinkExecutableRelativePath: {executable_relative_path}') - print(f'_PrelinkExecutableSize: {hex(int(executable_size))}') - print(f'_PrelinkExecutableSourceAddr: {source_addr}') - print(f'_PrelinkKmodInfo: {kmod_info}') - - def dumpKernelExtensionFromPRELINK_TEXT(self, kext_name): - ''' Dump prelinked KEXT {kext_name} from decompressed Kernel Cache PRELINK_TEXT segment -p {file_path} to a file named: prelinked_{kext_name}.bin ''' - segment_section = '__PRELINK_TEXT,__text' - - if not self.hasSection(segment_section): # If segment does not exist - break - print(f'Specified binary file does not have {segment_section} - the extension was not dumped.') - return False - - _, kext_load_addr, _, kext_size, source_addr, _ = self.parsePRELINK_INFO_plist(kext_name) - kext_load_addr = int(kext_load_addr, 16) - kext_size = int(kext_size, 16) - output_path = f'prelinked_{kext_name}.bin' - - kext_offset = self.calcRealAddressFromVM(kext_load_addr) - self.dumpData(kext_offset, kext_size, output_path) - - def parsekmod_info(self, kext_name): - ''' Parse kmod_info structure for the given {kext_name} from Kernel Cache ''' - _, _, _, _, _, kmod_info_vm_addr = self.parsePRELINK_INFO_plist(kext_name) - kmod_info_in_file = self.calcRealAddressFromVM(kmod_info_vm_addr) - kmod_info_size = ctypes.sizeof(AppleStructuresManager.kmod_info) - extracted_kmod_info_bytes = self.extractBytesAtOffset(kmod_info_in_file, kmod_info_size) - # debug + - #Utils.printQuadWordsLittleEndian64(extracted_kmod_info_bytes) - # debug - - kmod_info_as_dict = AppleStructuresManager.kmod_info.parse(extracted_kmod_info_bytes) - return kmod_info_as_dict - - def printParsedkmod_info(self, kext_name): - ''' Printing function for --kmod_info ''' - kmod_info_as_dict = self.parsekmod_info(kext_name) - for k, v in kmod_info_as_dict.items(): - print(f'{k.ljust(16)}: {v}') - - def calcKextEntryPoint(self, kext_name): - ''' Calculate the __start for the given {kext_name} Kernel Extension ''' - kmod_info_as_dict = self.parsekmod_info(kext_name) - start = int(kmod_info_as_dict['start'], 16) & 0xFFFFFFFF - - kernelcache_text_segment = self.getSegment('__TEXT') - kernelcache_text_segment_base = kernelcache_text_segment.virtual_address - - return start + kernelcache_text_segment_base - - def printKextEntryPoint(self, kext_name): - ''' Printing function for --kext_entry flag. ''' - kext_entrypoint = hex(self.calcKextEntryPoint(kext_name)) - print(f'{kext_name} entrypoint: {kext_entrypoint}') - - def calcKextExitPoint(self, kext_name): - ''' Calculate the __stop for the given {kext_name} Kernel Extension ''' - kmod_info_as_dict = self.parsekmod_info(kext_name) - stop = int(kmod_info_as_dict['stop'], 16) & 0xFFFFFFFF - - kernelcache_text_segment = self.getSegment('__TEXT') - kernelcache_text_segment_base = kernelcache_text_segment.virtual_address - - return stop + kernelcache_text_segment_base - - def printKextExitPoint(self, kext_name): - ''' Printing function for --kext_exit flag. ''' - kext_exitpoint = hex(self.calcKextEntryPoint(kext_name)) - print(f'{kext_name} exitpoint: {kext_exitpoint}') - - def parseMIG(self): - ''' Search for MIG subsystem messages. I was using this Hopper script as an inspiration: https://github.com/knightsc/hopper/blob/master/scripts/MIG%20Detect.py - - Returns a dictionary like: {'_MIG_subsystem_1000': {'_MIG_msg_1000': routine_for_msg}} - ''' - va_start = self.getVirtualMemoryStartingAddress() - mig_subsystem_size = ctypes.sizeof(AppleStructuresManager.mig_subsystem) - routine_descriptor_size = ctypes.sizeof(AppleStructuresManager.routine_descriptor) - mig_subsystems = {} - - # The MIG should be in __DATA,__const | __DATA_CONST,__const | __CONST,__constdata, but it is not always the case. - # Great example is decompressed kernelcache, there are no __const section. Conclusion, would be to iterate over each segment, but there is a problem with alignment. - for section in self.binary.sections: - #if ('const' in section.name and 'DATA' in section.segment.name): - section_bytes = section.content.tobytes() - section_size = section.size - alignment = pow(2,section.alignment) - - # Loop through section bytes using alignment to speed up - current_offset = 0 - while current_offset < section_size: - chunk = section_bytes[current_offset:current_offset+mig_subsystem_size] - mig_subsystem_dict = AppleStructuresManager.mig_subsystem.parse(chunk) - number_of_msgs = mig_subsystem_dict['end'] - mig_subsystem_dict['start'] - - # Check for possible mig_subsystem structure: - if (number_of_msgs > 0 and - number_of_msgs < 1024 and - mig_subsystem_dict['server'] != 0 and - mig_subsystem_dict['start'] > 0 and - mig_subsystem_dict['end'] > 0 and - mig_subsystem_dict['reserved'] == 0 and - mig_subsystem_dict['routine_0'] == 0): - ''' - # print(f'{hex(mig_subsystem_dict["server"])} {hex(mig_subsystem_dict["start"])}') - # At this stage I get 0x8028000000007e74 instead of 0x100007e74 and I do not know why. The same goes for every impl_routine later too... - # I can manually repair it by: & 0xFFFFFFFF | __TEXT - # It is temp fix, there must be a "proper way" - todo - ''' - mig_subsystem_dict['server'] = mig_subsystem_dict['server'] & 0xFFFFFFFF | va_start # Fix according to the above comment - mig_subsystem_number = mig_subsystem_dict['start'] - subsystem_name = "MIG_subsystem_{0}".format(mig_subsystem_number) - mig_subsystems[subsystem_name] = {} - current_offset += mig_subsystem_size - - # If mig_subsystem structure was found, iterate over all routines - msg = 0 - while msg < number_of_msgs: - routine_name = "MIG_msg_{0}".format(mig_subsystem_number+msg) - chunk = section_bytes[current_offset:current_offset+routine_descriptor_size] - routine_descriptor_dict = AppleStructuresManager.routine_descriptor.parse(chunk) - if routine_descriptor_dict['impl_routine'] != 0: - routine_descriptor_dict['impl_routine'] = routine_descriptor_dict['impl_routine'] & 0xFFFFFFFF | va_start # Fix like subsystem - mig_subsystems[subsystem_name].update({routine_name: routine_descriptor_dict}) - current_offset += routine_descriptor_size - msg += 1 - - continue # To find more subsystems we continue the parent while without adding below alignment, because we added routine_descriptor_size - - current_offset += alignment - - return(mig_subsystems) - - def printMIG(self): - ''' Iterates over each subsystem and its associated messages, printing them in the nice format. ''' - mig_subsystems = self.parseMIG() - - for subsystem, messages in mig_subsystems.items(): - print(subsystem + ":") - - for message, details in messages.items(): - print(f"- {message}: {hex(details['impl_routine'])}") - def hasSetUID(self): """ Check if a file has the SUID (Set User ID) bit set. @@ -2889,6 +2613,9 @@ class SandboxProcessor: if args.extract_sandbox_operations: # Extract sandbox operations from the kernelcache.decompressed file snake_instance.printSandboxOperations() + if args.extract_sandbox_platform_profile: # Extract sandbox platform profile from the sandbox.kext file + snake_instance.extractSandboxPlatformProfile() + class SnakeVIII(SnakeVII): def __init__(self, binaries, file_path): super().__init__(binaries, file_path) @@ -3118,6 +2845,15 @@ class SnakeVIII(SnakeVII): for operation in operations: print(operation) + def extractSandboxPlatformProfile(self): + ''' Extract the sandbox platform profile from the sandbox.kext. ''' + start_address = self.binary.get_symbol("_platform_profile_data").value + end_address = self.binary.get_symbol("_collection_data").value + + data = self.binary.get_content_from_virtual_address(start_address, end_address - start_address) + + sys.stdout.buffer.write(data) + ### ---- IX. TCC --- ### class TCCProcessor: def __init__(self): @@ -3416,12 +3152,33 @@ class XNUProcessor: def process(self, args): - if args.xnu: - snake_instance.printXNU() - if args.parse_mpo: snake_instance.printMPO(args.parse_mpo) + if args.dump_prelink_info is not None: # nargs="?", const='PRELINK_info.txt' # Dump '__PRELINK_INFO,__info' to a given file (default: 'PRELINK_info.txt') + snake_instance.dumpPrelink_info(args.dump_prelink_info) + + if args.dump_prelink_text is not None: # Dump '__PRELINK_TEXT,__text' to a given file (default: 'PRELINK_text.txt') + snake_instance.dumpPrelink_text(args.dump_prelink_text) + + if args.dump_prelink_kext is not None: # Dump prelinked KEXT from decompressed Kernel Cache to a file named: prelinked_{kext_name}.bin + snake_instance.dumpKernelExtensionFromPRELINK_TEXT(args.dump_prelink_kext) + + if args.kext_prelinkinfo: # Print _Prelink properties from PRELINK_INFO,__info for a give kext + snake_instance.printParsedPRELINK_INFO_plist(args.kext_prelinkinfo) + + if args.kmod_info: # Print parsed kmod_info for the given kext + snake_instance.printParsedkmod_info(args.kmod_info) + + if args.kext_entry: # Print kext entrypoint + snake_instance.printKextEntryPoint(args.kext_entry) + + if args.kext_exit: # Print kext exitpoint + snake_instance.printKextExitPoint(args.kext_exit) + + if args.mig: # Search for MIG subsystem and prints message handlers + snake_instance.printMIG() + class SnakeX(SnakeIX): def __init__(self, binaries, file_path): super().__init__(binaries, file_path) @@ -3481,8 +3238,257 @@ class SnakeX(SnakeIX): else: print("No MPOs defined in the given address.") - def printXNU(self): - print("XNU related functions are not implemented yet.") + def loadPRELINK_INFOFromFile(self, prelink_info_filename): # Not used yet. + ''' + Read PRELINK_INFO,__info section from file (with alignment). + The last line in the dumped section plist is broken, because of alignment. + This function remove it so the plistlib.loads work. + It returns loaded PLIST {prelink_info_plist}. + ''' + prelink_info_plist_bytes = self.readBytesFromFile(prelink_info_filename) + prelink_as_bytes_without_last_line = self.removeNullBytesAlignment(prelink_info_plist_bytes) + prelink_info_plist = plistlib.loads(prelink_as_bytes_without_last_line) + return prelink_info_plist + + def calcTwoComplement64(self, value): + ''' Convert negative int to hex representation. ''' + return hex((value + (1 << 64)) % (1 << 64)) + + def removeNullBytesAlignment(self, string_as_bytes): + ''' + The last line in the PLISTs and other files dumped from memory will almost always be aligned with 0x00 bytes. + This function: + Detects lines in a given bytes {string_as_bytes}. + Removes the last line. + Returns a new {string_as_bytes}. + ''' + decoded_string = string_as_bytes.decode('utf-8') + decoded_string_without_last_line = decoded_string[:decoded_string.rfind('\n')] + string_as_bytes_without_last_line = decoded_string_without_last_line.encode() + return string_as_bytes_without_last_line + + def dumpPrelink_info(self, filename): + ''' Dump '__PRELINK_INFO,__info' to a given file (default: 'PRELINK_info.txt') ''' + segment_name = '__PRELINK_INFO' + section_name = '__info' + if self.dumpSection(segment_name, section_name, filename): + print("SUCCESS: __PRELINK_INFO,__info dump") + + def dumpPrelink_text(self, filename): + ''' Dump '__PRELINK_TEXT,__text' to a given file (default: 'PRELINK_text.txt') ''' + segment_name = '__PRELINK_TEXT' + section_name = '__text' + if self.dumpSection(segment_name, section_name, filename): + print("SUCCESS: __PRELINK_TEXT,__text dump") + + def extractPRELINK_INFO_plist(self): + ''' Extract '__PRELINK_INFO,__info' and return it. ''' + segment_name = '__PRELINK_INFO' + section_name = '__info' + extracted_bytes = self.extractSection(segment_name, section_name) + return extracted_bytes + + def parsePRELINK_INFO_plist(self, kext_name): + ''' Extract PLIST properties values from '__PRELINK_INFO,__info' section for the given {kext_name}: + _PrelinkBundlePath + _PrelinkExecutableLoadAddr + _PrelinkExecutableRelativePath + _PrelinkExecutableSize + _PrelinkExecutableSourceAddr + _PrelinkKmodInfo + ''' + #prelink_info_plist = self.loadPRELINK_INFO(prelink_info_filename) # For loading PRELINK_INFO from file + prelink_as_bytes = self.extractPRELINK_INFO_plist() + prelink_as_bytes_without_last_line = self.removeNullBytesAlignment(prelink_as_bytes) + prelink_info_plist = plistlib.loads(prelink_as_bytes_without_last_line) + + kext_name = kext_name.lower() + if kext_name in self.kext_map: + kext_name = self.kext_map[kext_name] + + # Iterate over the parsed dictionary + for item in prelink_info_plist['_PrelinkInfoDictionary']: + PrelinkExecutableRelativePath = item.get('_PrelinkExecutableRelativePath', '').lower() + + # Check if the '_PrelinkExecutableRelativePath' contains {kext_name} in its path + if kext_name in PrelinkExecutableRelativePath: + # Extract the desired keys and their corresponding values + bundle_path = item.get('_PrelinkBundlePath') + + executable_load_addr = str(item.get('_PrelinkExecutableLoadAddr')).lower() + if executable_load_addr.startswith("0x"): + executable_load_addr = int(executable_load_addr, 16) + elif executable_load_addr.startswith("-"): + executable_load_addr = self.calcTwoComplement64(int(executable_load_addr)) + + executable_relative_path = item.get('_PrelinkExecutableRelativePath') + + executable_size = str(item.get('_PrelinkExecutableSize')).lower() + if executable_size.startswith("0x"): + executable_size = int(executable_size, 16) + elif executable_size.startswith("-"): + executable_size = self.calcTwoComplement64(int(executable_size)) + + source_addr = str(item.get('_PrelinkExecutableSourceAddr')).lower() + if source_addr.startswith("0x"): + source_addr = int(source_addr, 16) + elif source_addr.startswith("-"): + source_addr = self.calcTwoComplement64(int(source_addr)) + + kmod_info = str(item.get('_PrelinkKmodInfo')).lower() + if kmod_info.startswith("0x"): + kmod_info = int(kmod_info, 16) + elif kmod_info.startswith("-"): + kmod_info = self.calcTwoComplement64(int(kmod_info)) + + return bundle_path, executable_load_addr, executable_relative_path, executable_size, source_addr, kmod_info + + def printParsedPRELINK_INFO_plist(self, kext_name): + ''' Print extracted properties for PRELINK_INFO Plist for a given kext. ''' + bundle_path, executable_load_addr, executable_relative_path, executable_size, source_addr, kmod_info = self.parsePRELINK_INFO_plist(kext_name) + print(f'_PrelinkBundlePath: {bundle_path}') + print(f'_PrelinkExecutableLoadAddr: {executable_load_addr}') + print(f'_PrelinkExecutableRelativePath: {executable_relative_path}') + print(f'_PrelinkExecutableSize: {hex(int(executable_size))}') + print(f'_PrelinkExecutableSourceAddr: {source_addr}') + print(f'_PrelinkKmodInfo: {kmod_info}') + + def dumpKernelExtensionFromPRELINK_TEXT(self, kext_name): + ''' Dump prelinked KEXT {kext_name} from decompressed Kernel Cache PRELINK_TEXT segment -p {file_path} to a file named: prelinked_{kext_name}.bin ''' + segment_section = '__PRELINK_TEXT,__text' + + if not self.hasSection(segment_section): # If segment does not exist - break + print(f'Specified binary file does not have {segment_section} - the extension was not dumped.') + return False + + _, kext_load_addr, _, kext_size, source_addr, _ = self.parsePRELINK_INFO_plist(kext_name) + kext_load_addr = int(kext_load_addr, 16) + kext_size = int(kext_size, 16) + output_path = f'prelinked_{kext_name}.bin' + + kext_offset = self.calcRealAddressFromVM(kext_load_addr) + self.dumpData(kext_offset, kext_size, output_path) + + def parsekmod_info(self, kext_name): + ''' Parse kmod_info structure for the given {kext_name} from Kernel Cache ''' + _, _, _, _, _, kmod_info_vm_addr = self.parsePRELINK_INFO_plist(kext_name) + kmod_info_in_file = self.calcRealAddressFromVM(kmod_info_vm_addr) + kmod_info_size = ctypes.sizeof(AppleStructuresManager.kmod_info) + extracted_kmod_info_bytes = self.extractBytesAtOffset(kmod_info_in_file, kmod_info_size) + # debug + + #Utils.printQuadWordsLittleEndian64(extracted_kmod_info_bytes) + # debug - + kmod_info_as_dict = AppleStructuresManager.kmod_info.parse(extracted_kmod_info_bytes) + return kmod_info_as_dict + + def printParsedkmod_info(self, kext_name): + ''' Printing function for --kmod_info ''' + kmod_info_as_dict = self.parsekmod_info(kext_name) + for k, v in kmod_info_as_dict.items(): + print(f'{k.ljust(16)}: {v}') + + def calcKextEntryPoint(self, kext_name): + ''' Calculate the __start for the given {kext_name} Kernel Extension ''' + kmod_info_as_dict = self.parsekmod_info(kext_name) + start = int(kmod_info_as_dict['start'], 16) & 0xFFFFFFFF + + kernelcache_text_segment = self.getSegment('__TEXT') + kernelcache_text_segment_base = kernelcache_text_segment.virtual_address + + return start + kernelcache_text_segment_base + + def printKextEntryPoint(self, kext_name): + ''' Printing function for --kext_entry flag. ''' + kext_entrypoint = hex(self.calcKextEntryPoint(kext_name)) + print(f'{kext_name} entrypoint: {kext_entrypoint}') + + def calcKextExitPoint(self, kext_name): + ''' Calculate the __stop for the given {kext_name} Kernel Extension ''' + kmod_info_as_dict = self.parsekmod_info(kext_name) + stop = int(kmod_info_as_dict['stop'], 16) & 0xFFFFFFFF + + kernelcache_text_segment = self.getSegment('__TEXT') + kernelcache_text_segment_base = kernelcache_text_segment.virtual_address + + return stop + kernelcache_text_segment_base + + def printKextExitPoint(self, kext_name): + ''' Printing function for --kext_exit flag. ''' + kext_exitpoint = hex(self.calcKextEntryPoint(kext_name)) + print(f'{kext_name} exitpoint: {kext_exitpoint}') + + def parseMIG(self): + ''' Search for MIG subsystem messages. I was using this Hopper script as an inspiration: https://github.com/knightsc/hopper/blob/master/scripts/MIG%20Detect.py + + Returns a dictionary like: {'_MIG_subsystem_1000': {'_MIG_msg_1000': routine_for_msg}} + ''' + va_start = self.getVirtualMemoryStartingAddress() + mig_subsystem_size = ctypes.sizeof(AppleStructuresManager.mig_subsystem) + routine_descriptor_size = ctypes.sizeof(AppleStructuresManager.routine_descriptor) + mig_subsystems = {} + + # The MIG should be in __DATA,__const | __DATA_CONST,__const | __CONST,__constdata, but it is not always the case. + # Great example is decompressed kernelcache, there are no __const section. Conclusion, would be to iterate over each segment, but there is a problem with alignment. + for section in self.binary.sections: + #if ('const' in section.name and 'DATA' in section.segment.name): + section_bytes = section.content.tobytes() + section_size = section.size + alignment = pow(2,section.alignment) + + # Loop through section bytes using alignment to speed up + current_offset = 0 + while current_offset < section_size: + chunk = section_bytes[current_offset:current_offset+mig_subsystem_size] + mig_subsystem_dict = AppleStructuresManager.mig_subsystem.parse(chunk) + number_of_msgs = mig_subsystem_dict['end'] - mig_subsystem_dict['start'] + + # Check for possible mig_subsystem structure: + if (number_of_msgs > 0 and + number_of_msgs < 1024 and + mig_subsystem_dict['server'] != 0 and + mig_subsystem_dict['start'] > 0 and + mig_subsystem_dict['end'] > 0 and + mig_subsystem_dict['reserved'] == 0 and + mig_subsystem_dict['routine_0'] == 0): + ''' + # print(f'{hex(mig_subsystem_dict["server"])} {hex(mig_subsystem_dict["start"])}') + # At this stage I get 0x8028000000007e74 instead of 0x100007e74 and I do not know why. The same goes for every impl_routine later too... + # I can manually repair it by: & 0xFFFFFFFF | __TEXT + # It is temp fix, there must be a "proper way" - todo + ''' + mig_subsystem_dict['server'] = mig_subsystem_dict['server'] & 0xFFFFFFFF | va_start # Fix according to the above comment + mig_subsystem_number = mig_subsystem_dict['start'] + subsystem_name = "MIG_subsystem_{0}".format(mig_subsystem_number) + mig_subsystems[subsystem_name] = {} + current_offset += mig_subsystem_size + + # If mig_subsystem structure was found, iterate over all routines + msg = 0 + while msg < number_of_msgs: + routine_name = "MIG_msg_{0}".format(mig_subsystem_number+msg) + chunk = section_bytes[current_offset:current_offset+routine_descriptor_size] + routine_descriptor_dict = AppleStructuresManager.routine_descriptor.parse(chunk) + if routine_descriptor_dict['impl_routine'] != 0: + routine_descriptor_dict['impl_routine'] = routine_descriptor_dict['impl_routine'] & 0xFFFFFFFF | va_start # Fix like subsystem + mig_subsystems[subsystem_name].update({routine_name: routine_descriptor_dict}) + current_offset += routine_descriptor_size + msg += 1 + + continue # To find more subsystems we continue the parent while without adding below alignment, because we added routine_descriptor_size + + current_offset += alignment + + return(mig_subsystems) + + def printMIG(self): + ''' Iterates over each subsystem and its associated messages, printing them in the nice format. ''' + mig_subsystems = self.parseMIG() + + for subsystem, messages in mig_subsystems.items(): + print(subsystem + ":") + + for message, details in messages.items(): + print(f"- {message}: {hex(details['impl_routine'])}") ### --- ARGUMENT PARSER --- ### class ArgumentParser: @@ -3607,14 +3613,6 @@ class ArgumentParser: def addAMFIArgs(self): amfi_group = self.parser.add_argument_group('AMFI ARGS') - amfi_group.add_argument('--dump_prelink_info', metavar='(optional) out_name', nargs="?", const='PRELINK_info.txt', help='Dump "__PRELINK_INFO,__info" to a given file (default: "PRELINK_info.txt")') - amfi_group.add_argument('--dump_prelink_text', metavar='(optional) out_name', nargs="?", const='PRELINK_text.txt', help='Dump "__PRELINK_TEXT,__text" to a given file (default: "PRELINK_text.txt")') - amfi_group.add_argument('--dump_prelink_kext', metavar='kext_name', nargs="?", help='Dump prelinked KEXT {kext_name} from decompressed Kernel Cache PRELINK_TEXT segment to a file named: prelinked_{kext_name}.bin') - amfi_group.add_argument('--kext_prelinkinfo', metavar='kext_name', nargs="?", help='Print _Prelink properties from PRELINK_INFO,__info for a give {kext_name}') - amfi_group.add_argument('--kmod_info', metavar='kext_name', help="Parse kmod_info structure for the given {kext_name} from Kernel Cache") - amfi_group.add_argument('--kext_entry', metavar='kext_name', help="Calculate the virtual memory address of the __start (entrypoint) for the given {kext_name} Kernel Extension") - amfi_group.add_argument('--kext_exit', metavar='kext_name', help="Calculate the virtual memory address of the __stop (exitpoint) for the given {kext_name} Kernel Extension") - amfi_group.add_argument('--mig', action='store_true', help="Search for MIG subsystem and prints message handlers") amfi_group.add_argument('--has_suid', action='store_true', help="Check if the file has SetUID bit set") amfi_group.add_argument('--has_sgid', action='store_true', help="Check if the file has SetGID bit set") amfi_group.add_argument('--has_sticky', action='store_true', help="Check if the file has sticky bit set") @@ -3648,6 +3646,7 @@ class ArgumentParser: sandbox_group.add_argument('--sandbox_profile_data', action='store_true', help="Print raw bytes ofthe sandbox profile data from the sandbox container metadata") sandbox_group.add_argument('--dump_kext', help="Dump the kernel extension binary from the kernelcache.decompressed file", metavar='kext_name') sandbox_group.add_argument('--extract_sandbox_operations', action='store_true', help="Extract sandbox operations from the Sandbox.kext file") + sandbox_group.add_argument('--extract_sandbox_platform_profile', action='store_true', help="Extract sandbox platform profile from the Sandbox.kext file") def addTCCArgs(self): tcc_group = self.parser.add_argument_group('TCC ARGS') @@ -3670,8 +3669,15 @@ class ArgumentParser: def addXNUArgs(self): xnu_group = self.parser.add_argument_group('XNU ARGS') - xnu_group.add_argument('--xnu', action='store_true', help="Print XNU related information") xnu_group.add_argument('--parse_mpo', metavar='mpo_addr', help="Parse mac_policy_ops at given address from Kernel Cache and print pointers in use (not zeroed)") + xnu_group.add_argument('--dump_prelink_info', metavar='(optional) out_name', nargs="?", const='PRELINK_info.txt', help='Dump "__PRELINK_INFO,__info" to a given file (default: "PRELINK_info.txt")') + xnu_group.add_argument('--dump_prelink_text', metavar='(optional) out_name', nargs="?", const='PRELINK_text.txt', help='Dump "__PRELINK_TEXT,__text" to a given file (default: "PRELINK_text.txt")') + xnu_group.add_argument('--dump_prelink_kext', metavar='kext_name', nargs="?", help='Dump prelinked KEXT {kext_name} from decompressed Kernel Cache PRELINK_TEXT segment to a file named: prelinked_{kext_name}.bin') + xnu_group.add_argument('--kext_prelinkinfo', metavar='kext_name', nargs="?", help='Print _Prelink properties from PRELINK_INFO,__info for a give {kext_name}') + xnu_group.add_argument('--kmod_info', metavar='kext_name', help="Parse kmod_info structure for the given {kext_name} from Kernel Cache") + xnu_group.add_argument('--kext_entry', metavar='kext_name', help="Calculate the virtual memory address of the __start (entrypoint) for the given {kext_name} Kernel Extension") + xnu_group.add_argument('--kext_exit', metavar='kext_name', help="Calculate the virtual memory address of the __stop (exitpoint) for the given {kext_name} Kernel Extension") + xnu_group.add_argument('--mig', action='store_true', help="Search for MIG subsystem and prints message handlers") def parseArgs(self): args = self.parser.parse_args() diff --git a/tests/test_CrimsonUroboros.py b/tests/test_CrimsonUroboros.py index 436124c..ca8709b 100644 --- a/tests/test_CrimsonUroboros.py +++ b/tests/test_CrimsonUroboros.py @@ -30,7 +30,7 @@ We do it for each TestSnake class. } ''' -snake_class = SnakeIX +snake_class = SnakeX class Compiler: """ @@ -1929,166 +1929,6 @@ class TestSnakeVI(): os.system("rm -rf kernelcache") assert not os.path.exists("kernelcache") - def test_dump_prelink_info(self): - '''Test the --dump_prelink_info flag of SnakeVI.''' - args_list = ['-p', self.kernelcache_path, '--dump_prelink_info'] - args = argumentWrapper(args_list) - snake_hatchery = SnakeHatchery(args, snake_class) - snake_hatchery.hatch() - - def code_block(): - macho_processor = MachOProcessor() - macho_processor.process(args) - amfi_processor = AMFIProcessor() - amfi_processor.process(args) - - executeCodeBlock(code_block) - - assert os.path.exists('PRELINK_info.txt') - os.remove('PRELINK_info.txt') - - def test_dump_prelink_text(self): - '''Test the --dump_prelink_text flag of SnakeVI.''' - args_list = ['-p', self.kernelcache_path, '--dump_prelink_text'] - args = argumentWrapper(args_list) - snake_hatchery = SnakeHatchery(args, snake_class) - snake_hatchery.hatch() - - def code_block(): - macho_processor = MachOProcessor() - macho_processor.process(args) - amfi_processor = AMFIProcessor() - amfi_processor.process(args) - - executeCodeBlock(code_block) - - assert os.path.exists('PRELINK_text.txt') - os.remove('PRELINK_text.txt') - - def test_dump_prelink_kext(self): - '''Test the --dump_prelink_kext flag of SnakeVI.''' - args_list = ['-p', self.kernelcache_path, '--dump_prelink_kext', 'amfi'] - args = argumentWrapper(args_list) - snake_hatchery = SnakeHatchery(args, snake_class) - snake_hatchery.hatch() - - def code_block(): - macho_processor = MachOProcessor() - macho_processor.process(args) - amfi_processor = AMFIProcessor() - amfi_processor.process(args) - - executeCodeBlock(code_block) - - assert os.path.exists('prelinked_amfi.bin') - os.remove('prelinked_amfi.bin') - - def test_kext_prelinkinfo(self): - '''Test the --kext_prelinkinfo flag of SnakeVI.''' - args_list = ['-p', self.kernelcache_path, '--kext_prelinkinfo', 'amfi'] - args = argumentWrapper(args_list) - snake_hatchery = SnakeHatchery(args, snake_class) - snake_hatchery.hatch() - - def code_block(): - macho_processor = MachOProcessor() - macho_processor.process(args) - amfi_processor = AMFIProcessor() - amfi_processor.process(args) - - uroboros_output = executeCodeBlock(code_block) - expected_output = '_PrelinkBundlePath: /System/Library/Extensions/AppleMobileFileIntegrity.kext' - - assert expected_output in uroboros_output - - def test_kmod_info(self): - '''Test the --kmod_info flag of SnakeVI.''' - args_list = ['-p', self.kernelcache_path, '--kmod_info', 'amfi'] - args = argumentWrapper(args_list) - snake_hatchery = SnakeHatchery(args, snake_class) - snake_hatchery.hatch() - - def code_block(): - macho_processor = MachOProcessor() - macho_processor.process(args) - amfi_processor = AMFIProcessor() - amfi_processor.process(args) - - uroboros_output = executeCodeBlock(code_block) - expected_output = 'name : com.apple.driver.AppleMobileFileIntegrity' - - assert expected_output in uroboros_output - - def test_kext_entry(self): - '''Test the --kext_entry flag of SnakeVI.''' - args_list = ['-p', self.kernelcache_path, '--kext_entry', 'amfi'] - args = argumentWrapper(args_list) - snake_hatchery = SnakeHatchery(args, snake_class) - snake_hatchery.hatch() - - def code_block(): - macho_processor = MachOProcessor() - macho_processor.process(args) - amfi_processor = AMFIProcessor() - amfi_processor.process(args) - - uroboros_output = executeCodeBlock(code_block) - expected_output = 'amfi entrypoint:' - - assert expected_output in uroboros_output - - def test_kext_exit(self): - '''Test the --kext_exit flag of SnakeVI.''' - args_list = ['-p', self.kernelcache_path, '--kext_exit', 'amfi'] - args = argumentWrapper(args_list) - snake_hatchery = SnakeHatchery(args, snake_class) - snake_hatchery.hatch() - - def code_block(): - macho_processor = MachOProcessor() - macho_processor.process(args) - amfi_processor = AMFIProcessor() - amfi_processor.process(args) - - uroboros_output = executeCodeBlock(code_block) - expected_output = 'amfi exitpoint:' - - assert expected_output in uroboros_output - - def test_mig(self): - '''Test the --mig flag of SnakeVI.''' - args_list = ['-p', '/usr/libexec/amfid', '--mig'] - args = argumentWrapper(args_list) - snake_hatchery = SnakeHatchery(args, snake_class) - snake_hatchery.hatch() - - def code_block(): - macho_processor = MachOProcessor() - macho_processor.process(args) - amfi_processor = AMFIProcessor() - amfi_processor.process(args) - - uroboros_output = executeCodeBlock(code_block) - expected_output_1 = 'MIG_subsystem_1000:' - expected_output_2 = 'MIG_msg_1000' - expected_output_3 = 'MIG_msg_1001' - expected_output_4 = 'MIG_msg_1002' - expected_output_5 = 'MIG_msg_1003' - expected_output_6 = 'MIG_msg_1004' - expected_output_7 = 'MIG_msg_1005' - expected_output_8 = 'MIG_msg_1006' - expected_output_9 = 'MIG_msg_1007' - - assert expected_output_1 in uroboros_output - assert expected_output_2 in uroboros_output - assert expected_output_3 in uroboros_output - assert expected_output_4 in uroboros_output - assert expected_output_5 in uroboros_output - assert expected_output_6 in uroboros_output - assert expected_output_7 in uroboros_output - assert expected_output_8 in uroboros_output - assert expected_output_9 in uroboros_output - def test_has_suid(self): '''Test the --has_suid flag of SnakeVI.''' args_list = ['-p', 'hello_6_s', '--has_suid'] @@ -2584,6 +2424,19 @@ class TestSnakeVIII(): assert expected_output in uroboros_output os.remove("sandbox") + def test_extract_sandbox_platform_profile(self): + '''Test the --extract_sandbox_platform_profile flag of SnakeVIII.''' + a = run_and_get_stdout(f'python3 CrimsonUroboros.py -p {self.kernelcache_path} --dump_kext sandbox') + assert os.path.exists("sandbox") + + uroboros_output = run_and_get_stdout('python3 CrimsonUroboros.py -p sandbox --extract_sandbox_platform_profile > platform_profile.bin') + expected_output = 'object has no attribute ' + + with open("platform_profile.bin", 'rb') as f: + assert expected_output.encode() not in f.read() + os.remove("sandbox") + os.remove("platform_profile.bin") + class TestSnakeIX: '''Testing IX. TCC Permissions''' @@ -2607,7 +2460,7 @@ class TestSnakeIX: tcc_processor.process(args) uroboros_output = executeCodeBlock(code_block) - assert 'Error accessing /var/db/locationd/clients.plist' in uroboros_output + assert '' in uroboros_output def test_tcc_fda(self): '''Test the --tcc_fda flag for Full Disk Access permission''' @@ -2812,3 +2665,178 @@ class TestSnakeIX: uroboros_output = executeCodeBlock(code_block) assert 'iCloud Access: False' in uroboros_output +class TestSnakeX(): + '''Testing X. XNU''' + @classmethod + def setup_class(cls): + # Decompress KernelCache + result = decompressKernelcache() + assert result == 0 + assert os.path.exists("kernelcache") + cls.kernelcache_path = run_and_get_stdout('ls kernelcache/System/Volumes/Preboot/*/boot/*/System/Library/Caches/com.apple.kernelcaches/kernelcache.decompressed') + + @classmethod + def teardown_class(cls): + # Purge kernelcache directory + os.system("rm -rf kernelcache") + assert not os.path.exists("kernelcache") + + def test_dump_prelink_info(self): + '''Test the --dump_prelink_info flag of SnakeX.''' + args_list = ['-p', self.kernelcache_path, '--dump_prelink_info'] + args = argumentWrapper(args_list) + snake_hatchery = SnakeHatchery(args, snake_class) + snake_hatchery.hatch() + + def code_block(): + macho_processor = MachOProcessor() + macho_processor.process(args) + xnu_processor = XNUProcessor() + xnu_processor.process(args) + + executeCodeBlock(code_block) + + assert os.path.exists('PRELINK_info.txt') + os.remove('PRELINK_info.txt') + + def test_dump_prelink_text(self): + '''Test the --dump_prelink_text flag of SnakeX.''' + args_list = ['-p', self.kernelcache_path, '--dump_prelink_text'] + args = argumentWrapper(args_list) + snake_hatchery = SnakeHatchery(args, snake_class) + snake_hatchery.hatch() + + def code_block(): + macho_processor = MachOProcessor() + macho_processor.process(args) + xnu_processor = XNUProcessor() + xnu_processor.process(args) + + executeCodeBlock(code_block) + + assert os.path.exists('PRELINK_text.txt') + os.remove('PRELINK_text.txt') + + def test_dump_prelink_kext(self): + '''Test the --dump_prelink_kext flag of SnakeX.''' + args_list = ['-p', self.kernelcache_path, '--dump_prelink_kext', 'amfi'] + args = argumentWrapper(args_list) + snake_hatchery = SnakeHatchery(args, snake_class) + snake_hatchery.hatch() + + def code_block(): + macho_processor = MachOProcessor() + macho_processor.process(args) + xnu_processor = XNUProcessor() + xnu_processor.process(args) + + executeCodeBlock(code_block) + + assert os.path.exists('prelinked_amfi.bin') + os.remove('prelinked_amfi.bin') + + def test_kext_prelinkinfo(self): + '''Test the --kext_prelinkinfo flag of SnakeX.''' + args_list = ['-p', self.kernelcache_path, '--kext_prelinkinfo', 'amfi'] + args = argumentWrapper(args_list) + snake_hatchery = SnakeHatchery(args, snake_class) + snake_hatchery.hatch() + + def code_block(): + macho_processor = MachOProcessor() + macho_processor.process(args) + xnu_processor = XNUProcessor() + xnu_processor.process(args) + + uroboros_output = executeCodeBlock(code_block) + expected_output = '_PrelinkBundlePath: /System/Library/Extensions/AppleMobileFileIntegrity.kext' + + assert expected_output in uroboros_output + + def test_kmod_info(self): + '''Test the --kmod_info flag of SnakeX.''' + args_list = ['-p', self.kernelcache_path, '--kmod_info', 'amfi'] + args = argumentWrapper(args_list) + snake_hatchery = SnakeHatchery(args, snake_class) + snake_hatchery.hatch() + + def code_block(): + macho_processor = MachOProcessor() + macho_processor.process(args) + xnu_processor = XNUProcessor() + xnu_processor.process(args) + + uroboros_output = executeCodeBlock(code_block) + expected_output = 'name : com.apple.driver.AppleMobileFileIntegrity' + + assert expected_output in uroboros_output + + def test_kext_entry(self): + '''Test the --kext_entry flag of SnakeX.''' + args_list = ['-p', self.kernelcache_path, '--kext_entry', 'amfi'] + args = argumentWrapper(args_list) + snake_hatchery = SnakeHatchery(args, snake_class) + snake_hatchery.hatch() + + def code_block(): + macho_processor = MachOProcessor() + macho_processor.process(args) + xnu_processor = XNUProcessor() + xnu_processor.process(args) + + uroboros_output = executeCodeBlock(code_block) + expected_output = 'amfi entrypoint:' + + assert expected_output in uroboros_output + + def test_kext_exit(self): + '''Test the --kext_exit flag of SnakeX.''' + args_list = ['-p', self.kernelcache_path, '--kext_exit', 'amfi'] + args = argumentWrapper(args_list) + snake_hatchery = SnakeHatchery(args, snake_class) + snake_hatchery.hatch() + + def code_block(): + macho_processor = MachOProcessor() + macho_processor.process(args) + xnu_processor = XNUProcessor() + xnu_processor.process(args) + + uroboros_output = executeCodeBlock(code_block) + expected_output = 'amfi exitpoint:' + + assert expected_output in uroboros_output + + def test_mig(self): + '''Test the --mig flag of SnakeX.''' + args_list = ['-p', '/usr/libexec/amfid', '--mig'] + args = argumentWrapper(args_list) + snake_hatchery = SnakeHatchery(args, snake_class) + snake_hatchery.hatch() + + def code_block(): + macho_processor = MachOProcessor() + macho_processor.process(args) + xnu_processor = XNUProcessor() + xnu_processor.process(args) + + uroboros_output = executeCodeBlock(code_block) + expected_output_1 = 'MIG_subsystem_1000:' + expected_output_2 = 'MIG_msg_1000' + expected_output_3 = 'MIG_msg_1001' + expected_output_4 = 'MIG_msg_1002' + expected_output_5 = 'MIG_msg_1003' + expected_output_6 = 'MIG_msg_1004' + expected_output_7 = 'MIG_msg_1005' + expected_output_8 = 'MIG_msg_1006' + expected_output_9 = 'MIG_msg_1007' + + assert expected_output_1 in uroboros_output + assert expected_output_2 in uroboros_output + assert expected_output_3 in uroboros_output + assert expected_output_4 in uroboros_output + assert expected_output_5 in uroboros_output + assert expected_output_6 in uroboros_output + assert expected_output_7 in uroboros_output + assert expected_output_8 in uroboros_output + assert expected_output_9 in uroboros_output