From dd49f449c3a538241ab291f1bda14654bfdbb46e Mon Sep 17 00:00:00 2001 From: Karmaz95 Date: Thu, 18 Jul 2024 20:25:41 +0200 Subject: [PATCH] Preparing CrimsonUroboros for SnakeVIII - Sandbox article --- VIII. Sandbox/python/CrimsonUroboros.py | 293 +++++++++++++++++++++--- 1 file changed, 267 insertions(+), 26 deletions(-) diff --git a/VIII. Sandbox/python/CrimsonUroboros.py b/VIII. Sandbox/python/CrimsonUroboros.py index 526c624..7da61ad 100755 --- a/VIII. Sandbox/python/CrimsonUroboros.py +++ b/VIII. Sandbox/python/CrimsonUroboros.py @@ -17,29 +17,140 @@ import threading import time import xattr +### --- APP BUNDLE EXTENSION --- ### +class BundleProcessor: + def __init__(self, bundle_path): + '''This class contains part of the code related to the App Bundle Extension. + It extends the Snake instance abilities beyond only Mach-O analysis. + When -b/--bundle flag is used, it will analyze the App Bundle when an instance of this (BundleProcessor) class is created. + Then, it can be communicated from Snake object with the new methods dependent on the files in App Bundle.''' + self.bundle_path = os.path.abspath(bundle_path) + self.info_plist_path = os.path.join(self.bundle_path, 'Contents/Info.plist') + self.info_plist_exists = self.hasInfoPlist() + self.frameworks_path = os.path.join(self.bundle_path, 'Contents/Frameworks') + self.frameworks_exists = self.hasFrameworks() + self.plugins_path = os.path.join(self.bundle_path, 'Contents/PlugIns') + self.plugins_exists = self.hasPlugIns() + + def hasInfoPlist(self): + ''' Return True if Info.plist exists in the bundle. ''' + if os.path.exists(self.info_plist_path): + return True + return False + + def getBundleInfoCFBundleExecutableValue(self): + ''' Return the CFBundleExecutable value from the Info.plist file if it exists. Otherwise, return None. ''' + if self.info_plist_exists: + with open(self.info_plist_path, 'rb') as f: + plist_data = plistlib.load(f) + return plist_data.get('CFBundleExecutable', None) + return None + + def getBundleStructure(self): + ''' Return the structure of the bundle in tree format, including hidden files and with permissions. ''' + return os.popen(f"tree -ACp '{self.bundle_path}'").read() + + def getBundleInfo(self): + ''' Return the info of the bundle in a more readable JSON format. ''' + if self.info_plist_exists: + with open(self.info_plist_path, 'rb') as f: + plist_data = plistlib.load(f) + return json.dumps(plist_data, indent=4) + return None + + def checkInfoPropertyListSyntaxErrors(self, info_plist_path): + ''' Check the named property list file for syntax errors using /usr/bin/plutil .''' + plutil_command = ["/usr/bin/plutil", info_plist_path] + plutil_result = subprocess.run(plutil_command, capture_output=True) + if plutil_result.returncode == 0: + return True + else: + return False + + def checkBundleInfoSyntax(self): + if self.info_plist_exists: + if self.checkInfoPropertyListSyntaxErrors(self.info_plist_path): + return True + return False + + def hasFrameworks(self): + ''' Return True if Frameworks directory exists in the bundle and contains at least one framework. ''' + if os.path.exists(self.frameworks_path): + if os.listdir(self.frameworks_path): + return True + return False + + def getFrameworks(self): + ''' Return list of frameworks in the Frameworks directory. ''' + if self.frameworks_exists: + return os.listdir(self.frameworks_path) + else: + return None + + def hasPlugIns(self): + ''' Return True if PlugIns directory exists in the bundle and contains at least one plug-in. ''' + if os.path.exists(self.plugins_path): + if os.listdir(self.plugins_path): + return True + return False + + def getPlugIns(self): + ''' Return list of plug-ins in the PlugIns directory. ''' + if self.plugins_exists: + return os.listdir(self.plugins_path) + else: + return None + ### --- I. MACH-O --- ### class MachOProcessor: - def __init__(self, file_path): + def __init__(self, file_path, bundle_processor): '''This class contains part of the code from the main() for the SnakeI: Mach-O part.''' self.file_path = os.path.abspath(file_path) + self.macho_and_fat_magic_numbers = { + 0xfeedface, # 32-bit Mach-O + 0xfeedfacf, # 64-bit Mach-O + 0xcefaedfe, # 32-bit Mach-O, byte-swapped + 0xcffaedfe, # 64-bit Mach-O, byte-swapped + 0xcafebabe, # Fat binary + 0xbebafeca # Fat binary, byte-swapped + } + self.bundle_processor = bundle_processor + + def isFileMachO(self, filepath): + '''Check if file is Mach-O. ''' + try: + with open(filepath, 'rb') as f: + magic = f.read(4) + if len(magic) < 4: + return False + magic_number = int.from_bytes(magic, byteorder='big') + return magic_number in self.macho_and_fat_magic_numbers + except Exception: + return False def parseFatBinary(self): - '''Return Fat Binary object.''' - return lief.MachO.parse(self.file_path) + '''Return Fat Binary object if file exists.''' + if os.path.exists(self.file_path): + if self.isFileMachO(self.file_path): + return lief.MachO.parse(self.file_path) + else: + return None def process(self, args): '''Executes the code for the SnakeI: Mach-O.''' - if not os.path.exists(self.file_path): # Check if file_path specified in the --path argument exists. + if not os.path.exists(self.file_path) and self.bundle_processor is None: # Check if file_path specified in the --path argument exists and if bundle is not specified. print(f'The file {self.file_path} does not exist.') exit() global binaries # It must be global, becuase after the MachOProcessor object is destructed, the snake_instance would point to invalid memory ("binary" is dependant on "binaries"). global snake_instance # Must be global for further processors classes. + global bundle_processor # Same situation as with binaries object. + bundle_processor = self.bundle_processor # Transfer the bundle_processor to the global scope binaries = self.parseFatBinary() - if binaries is None: - exit() # Exit if the file is not valid macho + if binaries is None and self.bundle_processor is None: + exit() # Exit if the file is not valid macho and bundle is not specified snake_instance = SnakeVII(binaries, self.file_path) # Initialize the latest Snake class @@ -160,20 +271,39 @@ class MachOProcessor: if args.constructors: # Print constructors snake_instance.printConstructors() + if args.dump_section: # Dump section to a stdout + snake_instance.dumpSectionToStdout(args.dump_section) + + if args.bundle_structure: # Print bundle structure + snake_instance.printBundleStructure() + + if args.bundle_info: # Print bundle info (XML -> JSON) + snake_instance.printBundleInfo() + + if args.bundle_info_syntax_check: # Check if bundle info syntax is valid + snake_instance.printBundleInfoSyntax() + + if args.bundle_frameworks: # Print bundle frameworks + snake_instance.printBundleFrameworks() + + if args.bundle_plugins: # Print bundle plugins + snake_instance.printBundlePlugIns() + class SnakeI: def __init__(self, binaries, file_path): ''' When initiated, the program parses a Universal binary (binaries parameter) and extracts the ARM64 Mach-O. If the file is not in a universal format but is a valid ARM64 Mach-O, it is taken as a binary parameter during initialization. ''' - self.binary = self.parseFatBinary(binaries) + if binaries is not None: # Exception for bundles where binaries are not valid Mach-O + self.binary = self.parseFatBinary(binaries) + self.segments_count, self.file_start, self.file_size, self.file_end = self.getSegmentsInfo() + self.load_commands = self.getLoadCommands() + self.endianess = self.getEndianess() + self.format_specifier = 'I' # For struct.pack + self.reversed_format_specifier = '>I' if self.getEndianess() == 'little' else 'I' # For struct.pack - self.reversed_format_specifier = '>I' if self.getEndianess() == 'little' else '