Created
July 6, 2023 16:10
-
-
Save carlospolop/ef26f8eb9fafd4bc22e69e1a32b81da4 to your computer and use it in GitHub Desktop.
Print information about a macho binary using python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import plistlib | |
import struct | |
import logging | |
import lief | |
import sys | |
from typing import List | |
from macholib import MachO, mach_o | |
logger = logging.getLogger(__name__) | |
# SIGNATURE FLAGS | |
def decode_flags(flags: int) -> str: | |
flags_dict = { | |
0x00000001: 'CS_VALID', | |
0x00000002: 'CS_ADHOC', | |
0x00000004: 'CS_GET_TASK_ALLOW', | |
0x00000008: 'CS_INSTALLER', | |
0x00000010: 'CS_FORCED_LV', | |
0x00000020: 'CS_INVALID_ALLOWED', | |
0x00000100: 'CS_HARD', | |
0x00000200: 'CS_KILL', | |
0x00000400: 'CS_CHECK_EXPIRATION', | |
0x00000800: 'CS_RESTRICT', | |
0x00001000: 'CS_ENFORCEMENT', | |
0x00002000: 'CS_REQUIRE_LV', | |
0x00004000: 'CS_ENTITLEMENTS_VALIDATED', | |
0x00008000: 'CS_NVRAM_UNRESTRICTED', | |
0x00010000: 'CS_RUNTIME', | |
0x00020000: 'CS_LINKER_SIGNED', | |
0x00100000: 'CS_EXEC_SET_HARD', | |
0x00200000: 'CS_EXEC_SET_KILL', | |
0x00400000: 'CS_EXEC_SET_ENFORCEMENT', | |
0x00800000: 'CS_EXEC_INHERIT_SIP', | |
0x01000000: 'CS_KILLED', | |
0x02000000: 'CS_DYLD_PLATFORM', | |
0x04000000: 'CS_PLATFORM_BINARY', | |
0x08000000: 'CS_PLATFORM_PATH', | |
0x10000000: 'CS_DEBUGGED', | |
0x20000000: 'CS_SIGNED', | |
0x40000000: 'CS_DEV_CODE', | |
0x80000000: 'CS_DATAVAULT_CONTROLLER', | |
} | |
return ', '.join(name for bit, name in flags_dict.items() if flags & bit) | |
def find_lc_code_signature_and_header_offset(binary_path): | |
m = MachO.MachO(binary_path) | |
header = m.headers[0] | |
for header in m.headers: | |
if header.MH_MAGIC == MachO.MH_MAGIC_64 or header.MH_MAGIC == MachO.MH_CIGAM_64: | |
header_offset = header.offset | |
else: | |
return None, None | |
for cmd in header.commands: | |
if cmd[0].cmd == 0x1d: | |
offset = cmd[1].dataoff | |
size = cmd[1].datasize | |
return header_offset + offset, size | |
return None, None | |
def get_macho_formats(binary_path): | |
try: | |
macho_file = MachO.MachO(binary_path) | |
formats = set() | |
for header in macho_file.headers: | |
cpu_type = header.header.cputype | |
cpu_subtype = header.header.cpusubtype | |
formats.add(mach_o.get_cpu_subtype(cpu_type, cpu_subtype)) | |
return formats | |
except Exception as e: | |
return set() | |
def get_signature_and_entitlements(binary_path, offset, size): | |
bin_info = { | |
"version": "", | |
"size": "", | |
"flags": "", | |
"hashes": "", | |
"identifier": "", | |
"entitlements": {} | |
} | |
with open(binary_path, 'rb') as f: | |
f.seek(offset) | |
data = f.read(size) | |
multi_blob = data[:4].hex() | |
if multi_blob != "fade0cc0": | |
return None | |
count = int.from_bytes(data[8:12], byteorder='big') | |
for i in range(count): | |
start = 12 + i * 8 | |
blob_type = int.from_bytes(data[start:start+4], byteorder='big') | |
blob_offset = int.from_bytes(data[start+4:start+8], byteorder='big') | |
blob_magic = int.from_bytes(data[blob_offset:blob_offset+4], byteorder='big') | |
if blob_magic == 0xfade7171: | |
blob_length = int.from_bytes(data[blob_offset+4:blob_offset+8], byteorder='big') | |
blob_data = data[blob_offset+8:blob_offset+blob_length] | |
entitlements = plistlib.loads(blob_data) | |
bin_info['entitlements'] = entitlements | |
elif blob_magic == 0xfade0c02: | |
format_str = ">IIIIIIIIIBBBBI" | |
values = struct.unpack_from(format_str, data, blob_offset) | |
cd = { | |
'magic': values[0], | |
'length': values[1], | |
'version': values[2], | |
'flags': values[3], | |
'hashOffset': values[4], | |
'identOffset': values[5], | |
'nSpecialSlots': values[6], | |
'nCodeSlots': values[7], | |
'codeLimit': values[8], | |
'hashSize': values[9], | |
'hashType': values[10], | |
'spare1': values[11], | |
'pageSize': values[12], | |
'spare2': values[13], | |
} | |
ident_start = blob_offset + cd['identOffset'] | |
ident_end = data.index(b'\0', ident_start) | |
identifier = data[ident_start:ident_end].decode('utf-8') | |
bin_info["version"] = cd['version'] | |
bin_info["size"] = cd['length'] | |
bin_info["flags"] = f"0x{cd['flags']:08x}({decode_flags(cd['flags'])})" | |
bin_info["hashes"] = f"{cd['nCodeSlots']}+{cd['nSpecialSlots']}" | |
bin_info["identifier"] = identifier | |
return bin_info | |
def get_imports_and_symbols(binary_path): | |
bin_data = { | |
"imports": [], | |
"symbols": [], | |
"rpaths": [] | |
} | |
binary = lief.parse(binary_path) | |
for dylib in binary.libraries: | |
bin_data["imports"].append(dylib.name) | |
for command in binary.commands: | |
if isinstance(command, lief.MachO.RPathCommand): | |
bin_data["rpaths"].append(command.path) | |
for symbol in binary.symbols: | |
bin_data["symbols"].append(symbol.name) | |
return bin_data | |
def print_macho_info(macho_info): | |
for k,v in macho_info.items(): | |
if k == "entitlements": | |
print(f"{k.capitalize()}:") | |
for k2,v2 in v.items(): | |
print(f"\t{k2}: {v2}") | |
else: | |
print(f"{k.capitalize()}: {v}") | |
def machoreader(input_file: str): | |
machoreader_results = { | |
"version": "", | |
"size": "", | |
"flags": "", | |
"hashes": "", | |
"identifier": "", | |
"entitlements": {}, | |
"formats": [], | |
"imports": [], | |
"symbols": [], | |
"rpaths": [] | |
} | |
try: | |
formats = get_macho_formats(input_file) | |
machoreader_results["formats"] = formats | |
except Exception as e: | |
logger.error(f"Error in machoreader trying to get formats with file {input_file}: {e}") | |
try: | |
offset, size = find_lc_code_signature_and_header_offset(input_file) | |
if offset and size: | |
bin_info = get_signature_and_entitlements(input_file, offset, size) | |
machoreader_results["version"] = bin_info["version"] | |
machoreader_results["size"] = bin_info["size"] | |
machoreader_results["flags"] = bin_info["flags"] | |
machoreader_results["hashes"] = bin_info["hashes"] | |
machoreader_results["identifier"] = bin_info["identifier"] | |
machoreader_results["entitlements"] = bin_info["entitlements"] | |
except Exception as e: | |
logger.error(f"Error in machoreader trying to get signature and entitlements with file {input_file}: {e}") | |
try: | |
bin_data = get_imports_and_symbols(input_file) | |
machoreader_results["imports"] = bin_data["imports"] | |
machoreader_results["symbols"] = bin_data["symbols"] | |
machoreader_results["rpaths"] = bin_data["rpaths"] | |
except Exception as e: | |
logger.error(f"Error in machoreader trying to get imports and symbols with file {input_file}: {e}") | |
print_macho_info(machoreader_results) | |
if __name__ == "__main__": | |
if len(sys.argv) != 2: | |
print("Usage: python machoreader.py <macho_binary_path>") | |
sys.exit(1) | |
macho_path = sys.argv[1] | |
machoreader(macho_path) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment