def test_MACHO_prebind_64(assertion): global log_history macho_64 = open(__dir__ + 'macho_64.out', 'rb').read() e = MACHO(macho_64) d = e.virt[0x100000f50:0x100000f62] assertion('structure definie\0', d.decode('latin1'), 'Extract chunk from mapped memory, in a section (64 bits)') e.virt[0x100000f50:0x100000f5c] = 'Hello World\0'.encode('latin1') d = e.pack() assertion('b29fe575093a6f68a54131e59138e1d8', hashlib.md5(d).hexdigest(), 'Writing in memory (interval) (64 bits)') e.virt[0x100000f50] = 'Hello World\0'.encode('latin1') d = e.pack() assertion('b29fe575093a6f68a54131e59138e1d8', hashlib.md5(d).hexdigest(), 'Writing in memory (address) (64 bits)') e.add( macho.Section(parent=macho.sectionHeader(parent=e.load), content='arbitrary content'.encode('latin1'))) d = e.pack() assertion('be836b2b8adcff60bcc7ca1d712a92a9', hashlib.md5(d).hexdigest(), 'Adding a section (64 bits)') e = MACHO(macho_64) e.add(type=macho.LC_SEGMENT_64, segname='__NEWTEXT', initprot=macho.VM_PROT_READ | macho.VM_PROT_EXECUTE, content='some binary data'.encode('latin1')) d = e.pack() assertion('b4ad381503c51b6dc9dc3d79fb8ca568', hashlib.md5(d).hexdigest(), 'Adding a segment (64 bits)')
def test_MACHO_ios_decibels(assertion): global log_history macho_ios = open(__dir__ + 'Decibels', 'rb').read() e = MACHO(macho_ios) assertion([ ('warn', ('Some encrypted text is not parsed with the section headers of LC_SEGMENT(__TEXT)', ), {}), ('warn', ('parse_dynamic_symbols() can only be used with x86 architectures, not %s', 12), {}), ('warn', ('Part of the file was not parsed: %d bytes', 2499), {}), ('warn', ('Some encrypted text is not parsed with the section headers of LC_SEGMENT(__TEXT)', ), {}), ('warn', ('parse_dynamic_symbols() can only be used with x86 architectures, not %s', 12), {}), ('warn', ('Part of the file was not parsed: %d bytes', 2495), {}) ], log_history, 'Parsing Decibels iOS app (logs)') log_history = [] macho_ios_hash = hashlib.md5(macho_ios).hexdigest() d = e.pack() assertion(macho_ios_hash, hashlib.md5(d).hexdigest(), 'Packing after reading iOS application Decibels') d = ('\n'.join([_ for a in e.arch for l in a.load for _ in l.otool()])).encode('latin1') assertion('0d3281e546fd6e41306dbf38e5fbd0b6', hashlib.md5(d).hexdigest(), 'Otool-like output for LC in iOS application')
def __init__( self, filename: str, cert: Certificate, privkey: PrivateKeyInfo, force: bool = False, detach_target: Optional[str] = None, hardened_runtime: bool = True, ): self.filename = filename self.content_dir = os.path.dirname( os.path.dirname(os.path.abspath(filename))) self.cert = cert self.privkey = privkey self.force = force self.detach_target = detach_target self.hardened_runtime = hardened_runtime self.hash_type = 2 self.code_signers: List[SingleCodeSigner] = [] self.files_modified: List[str] = [] with open(self.filename, "rb") as f: self.macho = MACHO(f.read(), parseSymbols=False)
def test_MACHO_lib_ATcommand(assertion): macho_lib = open(__dir__ + 'libATCommandStudioDynamic.dylib', 'rb').read() e = MACHO(macho_lib) macho_lib_hash = hashlib.md5(macho_lib).hexdigest() d = e.pack() assertion(macho_lib_hash, hashlib.md5(d).hexdigest(), 'Packing after reading libATCommandStudioDynamic') bind_s = [ _ for _ in e.sect if getattr(_, 'type', None) in ('bind_', 'weak_bind_', 'lazy_bind_', 'rebase_', 'export_') ] d = ('\n'.join([str(_) for s in bind_s for _ in s.info])).encode('latin1') assertion('8b29446352613fdb6c4a6142c7c476c3', hashlib.md5(d).hexdigest(), 'dyldinfo-like output for all binding types (libATCommand...)') bind_s = [ _ for _ in e.sect if getattr(_, 'type', None) in ('bind_', 'weak_bind_', 'lazy_bind_', 'rebase_') ] d = ('\n'.join([str(_) for s in bind_s for _ in s])).encode('latin1') assertion('66bb196759c094c0c08d8159cf61d67f', hashlib.md5(d).hexdigest(), 'dyldinfo-like output for dyld opcodes (libATCommand...)')
def test_MACHO_bin_sh(assertion): macho_bin = open(__dir__ + 'sh', 'rb').read() e = MACHO(macho_bin) macho_bin_hash = hashlib.md5(macho_bin).hexdigest() d = e.pack() assertion(macho_bin_hash, hashlib.md5(d).hexdigest(), 'Packing after reading /bin/sh')
def dump_mach_o_signature(filename): bundle, filepath = get_bundle_exec(filename) with open(filepath, "rb") as f: macho = MACHO(f.read(), parseSymbols=False) for header in get_macho_list(macho): _dump_single(filepath, header)
def test_MACHO_one_loader(assertion): global log_history f = struct.pack("<IIIIIIIIII", macho.MH_MAGIC, macho.CPU_TYPE_I386, 0, 0, 1, 12, 0, macho.LC_PREBIND_CKSUM, 12, 0) e = MACHO(f) d = e.pack() assertion(f, d, 'Parsing data, with one LC_PREBIND_CKSU loader')
def test_MACHO_empty_loader(assertion): f = struct.pack("<IIIIIIIII", macho.MH_MAGIC, macho.CPU_TYPE_I386, 0, 0, 1, 8, 0, 0, 8) e = MACHO(f) assertion(1, len(e.load), 'Parsing data, with one empty loader (lhlist length)') d = e.pack() assertion(f, d, 'Parsing data, with one empty loader (pack)')
def test_MACHO_macho64_exe(assertion): macho_64 = open(__dir__ + 'macho_64.out', 'rb').read() macho_64_hash = hashlib.md5(macho_64).hexdigest() e = MACHO(macho_64) d = e.pack() assertion(macho_64_hash, hashlib.md5(d).hexdigest(), 'Packing after reading 64-bit Mach-O executable')
def verify_mach_o_signature(filename: str): bundle, filepath = get_bundle_exec(filename) with open(filepath, "rb") as f: m = MACHO(f.read(), parseSymbols=False) # There may be multiple headers because it might be a universal binary # In that case, each architecture is essentially just another MachO binary inside of the # universal binary. So we verify the signature for each one. for header in get_macho_list(m): _verify_single(filepath, header)
def test_MACHO_additional_padding(assertion): global log_history f = struct.pack("<IIIIIIIIIII", macho.MH_MAGIC, macho.CPU_TYPE_I386, 0, 0, 1, 16, 0, macho.LC_PREBIND_CKSUM, 12, 0, 0) e = MACHO(f) assertion([('warn', ('LoadCommands have %d bytes of additional padding', 4), {})], log_history, 'Parsing invalid data, with padding after load commands (logs)') log_history = []
def test_MACHO_toolarge_cmds(assertion): global log_history f = struct.pack("<IIIIIIIII", macho.MH_MAGIC, macho.CPU_TYPE_I386, 0, 0, 1, 0xffff, 0, 0, 8) e = MACHO(f) assertion([('error', ('LoadCommands longer than file length', ), {}), ('warn', ('Part of the file was not parsed: %d bytes', 1), {})], log_history, 'Parsing a invalid output with big sizeofcmds (logs)') log_history = []
def test_MACHO_extend_segment(assertion): macho_64 = open(__dir__ + 'macho_64.out', 'rb').read() e = MACHO(macho_64) for l in e.load: if getattr(l, 'segname', None) == "__LINKEDIT": break e.load.extendSegment(l, 0x1000) d = e.pack() assertion('405962fd8a4fe751c0ea4fe1a9d02c1e', hashlib.md5(d).hexdigest(), 'Extend segment') assertion([], log_history, 'No non-regression test created unwanted log messages')
def test_MACHO_one_loader_padding(assertion): global log_history f = struct.pack("<IIIIIIIIIII", macho.MH_MAGIC, macho.CPU_TYPE_I386, 0, 0, 1, 16, 0, macho.LC_PREBIND_CKSUM, 16, 0, 0) e = MACHO(f) assertion( [('warn', ('%s has %d bytes of additional padding', 'prebind_cksum_command', 4), {})], log_history, 'Parsing invalid data, with one LC_PREBIND_CKSU loader with padding (logs)' ) log_history = []
def test_MACHO_zero_cmds(assertion): global log_history f = struct.pack("<IIIIIIIII", macho.MH_MAGIC, macho.CPU_TYPE_I386, 0, 0, 1, 0, 0, 0, 8) e = MACHO(f) assertion([('error', ('Too many load command: %d commands cannot fit in %d bytes', 1, 0), {}), ('warn', ('Part of the file was not parsed: %d bytes', 1), {})], log_history, 'Parsing a invalid output with zero sizeofcmds (logs)') log_history = []
def test_MACHO_one_loader_too_short(assertion): global log_history f = struct.pack("<IIIIIIIIIII", macho.MH_MAGIC, macho.CPU_TYPE_I386, 0, 0, 1, 8, 0, macho.LC_PREBIND_CKSUM, 8, 0, 0) e = MACHO(f) assertion( [('warn', ('%s is %d bytes too short', 'prebind_cksum_command', 4), {})], log_history, 'Parsing invalid data, with one LC_PREBIND_CKSU loader, too short (logs)' ) log_history = []
def test_MACHO_prebind_32(assertion): global log_history macho_32 = open(__dir__ + 'macho_32.out', 'rb').read() e = MACHO(macho_32) e.add(macho.LoadCommand(sex='<', wsize=32, cmd=0)) d = e.pack() assertion('6fefeaf7b4de67f8270d3425942d7a97', hashlib.md5(d).hexdigest(), 'Adding an empty command (32 bits)') f = struct.pack("<III", macho.LC_ROUTINES_64, 12, 0) l = macho.prebind_cksum_command(parent=None, sex='<', wsize=32, content=f) assertion( [('warn', ('Incoherent input cmd=%#x for %s', 26, 'prebind_cksum_command'), {})], log_history, 'Parsing incoherent load command prebind_cksum_command with LC_ROUTINES_64 tag (logs)' ) log_history = [] assertion( f, l.pack(), 'Creating a LC_PREBIND_CKSUM (with content and incoherent subclass)') f = struct.pack("<III", macho.LC_PREBIND_CKSUM, 12, 0) l = macho.prebind_cksum_command(parent=None, sex='<', wsize=32, content=f) assertion(f, l.pack(), 'Creating a LC_PREBIND_CKSUM (with content and subclass)') l = macho.LoadCommand(parent=None, sex='<', wsize=32, content=f) assertion(f, l.pack(), 'Creating a LC_PREBIND_CKSUM (from "content")') l = macho.LoadCommand(sex='<', wsize=32, cmd=macho.LC_PREBIND_CKSUM) assertion(f, l.pack(), 'Creating a LC_PREBIND_CKSUM (from "cmd")') e = MACHO(macho_32) e.add(l) d = e.pack() assertion('d7a33133a04126527eb6d270990092fa', hashlib.md5(d).hexdigest(), 'Adding a LC_PREBIND_CKSUM command') e = MACHO(macho_32) e.add(type=macho.LC_SEGMENT, segname='__NEWTEXT', initprot=macho.VM_PROT_READ | macho.VM_PROT_EXECUTE, content='some binary data'.encode('latin1')) d = e.pack() assertion('c4ad6da5422642cb15b91ccd3a09f592', hashlib.md5(d).hexdigest(), 'Adding a segment (32 bits)')
def test_MACHO_loader_lc_build_version(assertion): global log_history macho_lcbuild = open(__dir__ + 'macho_lcbuild.out', 'rb').read() macho_lcbuild_hash = hashlib.md5(macho_lcbuild).hexdigest() e = MACHO(macho_lcbuild) d = e.pack() assertion(macho_lcbuild_hash, hashlib.md5(d).hexdigest(), "Packing after reading executable with LC_BUILD_VERSION") d = ('\n'.join([_ for l in e.load for _ in l.otool()])).encode('latin1') assertion('6dd985753ccf51b0d5c7470126d43a6c', hashlib.md5(d).hexdigest(), 'Otool-like output for LC in executable with LC_BUILD_VERSION')
def get_bundle_exec(filepath: str) -> Tuple[str, str]: """ Get the path to the bundle dir (contains the Contents dir) and the executable itself. filepath may be the path to the exec, or to the bundle dir. """ filepath = os.path.abspath(filepath) if os.path.isfile(filepath): # This is a file, we should check it is a Mach-O. elfesteem can do this for us # It will raise if it is not with open(filepath, "rb") as f: macho = MACHO(f.read()) # Figure out the bundle path macos_dir = os.path.dirname(filepath) if os.path.basename(macos_dir) != "MacOS": raise Exception( "File is not in a correctly formatted Bundle. Missing MacOS dir" ) content_dir = os.path.dirname(macos_dir) if os.path.basename(content_dir) != "Contents": raise Exception( "File is not in a correctly formatted Bundle. Missing Contents dir" ) bundle_dir = os.path.dirname(content_dir) return bundle_dir, filepath elif os.path.isdir(filepath): # This is a directory. Check it is a bundle and find the binary content_dir = os.path.join(filepath, "Contents") if not os.path.isdir(content_dir): raise Exception( "Path is not a correctly formatted Bundle. Missing Contents dir" ) macos_dir = os.path.join(content_dir, "MacOS") if not os.path.isdir(macos_dir): raise Exception( "Path is not a correctly formatted Bundle. Missing MacOS dir" ) # List all file in this directory files = glob.glob(os.path.join(macos_dir, "*")) if len(files) == 0: raise Exception("No binary to sign") elif len(files) == 1: return filepath, files[0] else: raise Exception( "Multiple binaries found, unsure which to use. Please specify the path to a single binary instead" ) else: raise Exception("Path is not a bundle directory or a file")
def get_binary_info(filename): with open(filename, "rb") as f: macho = MACHO(f.read(), parseSymbols=False) if hasattr(macho, "Fhdr"): print("Universal Binary") for header in get_macho_list(macho): print(f"{_get_cpu_type_string(header.Mhdr.cputype)} Executable") try: v = _get_code_sig(header) print("Has code signature") except Exception as e: print(str(e))
def test_MACHO_unixthread_64(assertion): macho_64 = open(__dir__ + 'macho_64.out', 'rb').read() e = MACHO(macho_64) changeMainToUnixThread(e) d = e.pack() assertion('a77d64572857d5414ae414852b930370', hashlib.md5(d).hexdigest(), 'Migrating from LC_MAIN to LC_UNIXTHREAD (64 bits)') insert_start_function(e) d = e.pack() assertion( '16b63a2d3cdb3549fe9870b805eb80f5', hashlib.md5(d).hexdigest(), 'Migrating from LC_MAIN to LC_UNIXTHREAD with new segment (64 bits)')
def test_MACHO_macho32_obj(assertion): global log_history # Parsing and modifying files macho_32 = open(__dir__ + 'macho_32.o', 'rb').read() macho_32_hash = hashlib.md5(macho_32).hexdigest() e = MACHO(macho_32) d = e.pack() assertion(macho_32_hash, hashlib.md5(d).hexdigest(), 'Packing after reading 32-bit Mach-O object') assertion(e.entrypoint, -1, 'No entrypoint in a Mach-O object') assertion([('error', ('Not a unique loader with entrypoint: []', ), {})], log_history, 'No entrypoint in a Mach-O object (logs)') assertion(len(e.symbols), 3, 'Number of symbols in a Mach-O object') d = ("\n".join([_.otool() for _ in e.symbols])).encode('latin1') assertion('9543b68138927d012139e526f159846c', hashlib.md5(d).hexdigest(), 'Display symbols') assertion( e.symbols['_printf'].otool(), '_printf NO_SECT UX 0x00000000 0000', 'Find symbol by name') assertion('SymbolNotFound', e.symbols[10].__class__.__name__, 'Find symbol by invalid index') e.symbols[0].sectionindex = 5 assertion( e.symbols[0].otool(), '_a INVALID(5) SX 0x00000000 0000', 'Display symbol with invalid section') e.symbols[0].sectionindex = 0xff assertion( e.symbols[0].otool(), '_a INVALID(255) SX 0x00000000 0000', 'Display symbol with too big section index') log_history = [] e.entrypoint = 0 assertion([('error', ('Not a unique loader with entrypoint: []', ), {})], log_history, 'Cannot set entrypoint in a Mach-O object (logs)') log_history = [] for s in e.sect.sect: if not hasattr(s, 'reloclist'): continue d = s.reloclist[0].pack() assertion('a8f95e95126c45ff26d5c838300443bc', hashlib.md5(d).hexdigest(), 'Not scattered relocation in a 32-bit Mach-O object') d = s.reloclist[2].pack() assertion('4f66fe3447267f2bf90da8108ef10ba6', hashlib.md5(d).hexdigest(), 'Scattered relocation in a 32-bit Mach-O object') break
def test_MACHO_lib_tls(assertion): macho_lib = open(__dir__ + 'libcoretls.dylib', 'rb').read() e = MACHO(macho_lib) macho_lib_hash = hashlib.md5(macho_lib).hexdigest() d = e.pack() assertion(macho_lib_hash, hashlib.md5(d).hexdigest(), 'Packing after reading libcoretls') bind_s = [ _ for a in e.arch for _ in a.sect if getattr(_, 'type', None) in ('rebase_', 'export_') ] d = ('\n'.join([str(_) for s in bind_s for _ in s.info])).encode('latin1') assertion('d7983c780f70e8c81d277ee0f7f8a27d', hashlib.md5(d).hexdigest(), 'dyldinfo-like output for rebase and export (libcoretls)')
def test_MACHO_lib_dns(assertion): global log_history macho_lib = open(__dir__ + 'libdns_services.dylib', 'rb').read() e = MACHO(macho_lib) assertion(e.entrypoint, -1, 'No entrypoint in a Mach-O library') assertion([('error', ('Not a unique loader with entrypoint: []', ), {})], log_history, 'No entrypoint in a Mach-O library (logs)') log_history = [] macho_lib_hash = hashlib.md5(macho_lib).hexdigest() d = e.pack() assertion(macho_lib_hash, hashlib.md5(d).hexdigest(), 'Packing after reading DNS library') d = ('\n'.join([_ for l in e.load for _ in l.otool()])).encode('latin1') assertion('2d6194feedf82da26124d3128473a949', hashlib.md5(d).hexdigest(), 'Otool-like output including LC_SOURCE_VERSION')
def test_MACHO_minimal(assertion): global log_history # Simple tests of object creation e = MACHO(struct.pack("<I", macho.MH_MAGIC)) assertion([('warn', ( 'parse_dynamic_symbols() can only be used with x86 architectures, not %s', 0), {})], log_history, 'Parsing a minimal data, with Mach-O magic number only (logs)') log_history = [] assertion(e.entrypoint, -1, 'No entrypoint in a truncated Mach-O header') assertion([('error', ('Not a unique loader with entrypoint: []', ), {})], log_history, 'No entrypoint in a truncated Mach-O header (logs)') log_history = [] d = e.pack() assertion('37b830a1776346543c72ff53fbbe2b4a', hashlib.md5(d).hexdigest(), 'Parsing a minimal data, with Mach-O magic number only')
def test_MACHO_unixthread_32(assertion): # The function changeMainToUnixThread migrates a Mach-O binary for # recent MacOSX (using a LC_MAIN loader) to a Mac-O binary for older # versions of MacOSX (10.7 and older, using a LC_UNIXTHREAD loader). macho_32 = open(__dir__ + 'macho_32.out', 'rb').read() e = MACHO(macho_32) changeMainToUnixThread(e) d = e.pack() assertion('1aa73a50d1b941c560f08c20926f9a05', hashlib.md5(d).hexdigest(), 'Migrating from LC_MAIN to LC_UNIXTHREAD (32 bits)') insert_start_function(e) d = e.pack() assertion( '14e8007a3b5b5070c56ea2a43b6b888e', hashlib.md5(d).hexdigest(), 'Migrating from LC_MAIN to LC_UNIXTHREAD with new segment (32 bits)')
def test_MACHO_exe_SH3D(assertion): global log_history macho_app = open(__dir__ + 'SweetHome3D', 'rb').read() e = MACHO(macho_app) assertion([('warn', ( 'parse_dynamic_symbols() can only be used with x86 architectures, not %s', 18), {})], log_history, 'Parsing SweetHome3D app (logs)') log_history = [] macho_app_hash = hashlib.md5(macho_app).hexdigest() d = e.pack() assertion(macho_app_hash, hashlib.md5(d).hexdigest(), 'Packing after reading SweetHome3D app') d = ('\n'.join([_ for a in e.arch for l in a.load for _ in l.otool()])).encode('latin1') assertion('4bf0088471bd2161baf4a42dbb09dc5b', hashlib.md5(d).hexdigest(), 'Otool-like output including ppc, i386 & x86_64register state')
def test_MACHO_changeUUID(assertion): macho_64 = open(__dir__ + 'macho_64.out', 'rb').read() e = MACHO(macho_64) e.changeUUID("2A0405CF8B1F3502A605695A54C407BB") uuid_pos, = e.load.getpos(macho.LC_UUID) lh = e.load[uuid_pos] assertion((0x2A0405CF, 0x8B1F, 0x3502, 0xA605, 0x695A, 0x54C407BB), lh.uuid, 'UUID change') assertion('<LC_UUID 2A0405CF-8B1F-3502-A605-695A54C407BB>', repr(lh), 'UUID change (repr)') d = e.pack() assertion('f86802506fb24de2ac2bebd9101326e9', hashlib.md5(d).hexdigest(), 'UUID change (pack)') lh.uuid = (0, 0xAAAA, 0, 0, 0, 0x11111111) assertion((0, 0xAAAA, 0, 0, 0, 0x11111111), lh.uuid, 'set UUID') d = e.pack() assertion('c8457df239deb4c51c316bd6670a445e', hashlib.md5(d).hexdigest(), 'set UUID (pack)')
def test_MACHO_lib_print(assertion): global log_history macho_32be = open(__dir__ + 'libPrintServiceQuota.1.dylib', 'rb').read() e = MACHO(macho_32be) assertion([('warn', ( 'parse_dynamic_symbols() can only be used with x86 architectures, not %s', 18), {})], log_history, 'Parsing libPrintServiceQuota (logs)') log_history = [] macho_32be_hash = hashlib.md5(macho_32be).hexdigest() d = e.pack() assertion(macho_32be_hash, hashlib.md5(d).hexdigest(), 'Packing after reading 32-bit big-endian Mach-O shared library') d = ('\n'.join([_ for l in e.load for _ in l.otool()])).encode('latin1') assertion( 'cabaf4f4368c094bbb0c09f278510006', hashlib.md5(d).hexdigest(), 'Otool-like output for LC in 32-bit big-endian Mach-O shared library')
def test_MACHO_obj_telephony(assertion): global log_history macho_linkopt = open(__dir__ + 'TelephonyUtil.o', 'rb').read() macho_linkopt_hash = hashlib.md5(macho_linkopt).hexdigest() e = MACHO(macho_linkopt) assertion([('warn', ('Part of the file was not parsed: %d bytes', 6), {})], log_history, 'Parsing TelephonyUtil.o (logs)') log_history = [] d = e.pack() assertion( macho_linkopt_hash, hashlib.md5(d).hexdigest(), "Packing after reading object file with LC_LINKER_OPTION, 'interval' option is needed because there is some nop padding at the end of __TEXT,__text" ) d = ('\n'.join([_ for l in e.load for _ in l.otool()])).encode('latin1') assertion('984bf38084c14e435f30eebe36944b47', hashlib.md5(d).hexdigest(), 'Otool-like output for LC in object file with LC_LINKER_OPTION')