Skip to content

Instantly share code, notes, and snippets.

@carlospolop
Created July 6, 2023 16:10
Show Gist options
  • Select an option

  • Save carlospolop/ef26f8eb9fafd4bc22e69e1a32b81da4 to your computer and use it in GitHub Desktop.

Select an option

Save carlospolop/ef26f8eb9fafd4bc22e69e1a32b81da4 to your computer and use it in GitHub Desktop.

Revisions

  1. carlospolop created this gist Jul 6, 2023.
    196 changes: 196 additions & 0 deletions machoreader.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,196 @@
    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)