diff --git a/.gitignore b/.gitignore index 1ddd245..b777fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ config.json +IMDAppleServices # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/demo.py b/demo.py index f477d3c..0d98461 100644 --- a/demo.py +++ b/demo.py @@ -5,6 +5,18 @@ from base64 import b64decode import apns import ids +import logging +from rich.logging import RichHandler + +FORMAT = "%(message)s" +logging.basicConfig( + level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] +) + +# Set sane log levels +logging.getLogger("urllib3").setLevel(logging.WARNING) +logging.getLogger("jelly").setLevel(logging.INFO) +logging.getLogger("nac").setLevel(logging.INFO) def input_multiline(prompt): print(prompt) @@ -79,7 +91,10 @@ if CONFIG.get("id", {}).get("cert") is not None: id_keypair = ids._helpers.KeyPair(CONFIG["id"]["key"], CONFIG["id"]["cert"]) user.restore_identity(id_keypair) else: - vd = input_multiline("Enter validation data: ") + #vd = input_multiline("Enter validation data: ") + import emulated.nac + vd = emulated.nac.generate_validation_data() + vd = b64encode(vd).decode() user.register(vd) print(user.lookup(["mailto:textgpt@icloud.com"])) diff --git a/emulated/data.plist b/emulated/data.plist new file mode 100644 index 0000000..3f5f2fe --- /dev/null +++ b/emulated/data.plist @@ -0,0 +1,55 @@ + + + + + iokit + + 4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14:MLB + + QzAyNzIzMjA3QTVIV1ZRQVU= + + 4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14:ROM + + +AN3alom + + Fyp98tpgj + + U0TE/KnCc/AGEUYBuDpD8TQ= + + Gq3489ugfi + + S7OU9/LJ3K6TUgIzaNHU5kI= + + IOMACAddress + + 3KkEh8Ol + + IOPlatformSerialNumber + C02TV034HV29 + IOPlatformUUID + 0E7049C5-6CC5-57E4-9353-EF54C4332A99 + abKPld1EcMni + + uqHToZ+DNmm75/jSPMzB1ZQ= + + board-id + + TWFjLUI0ODMxQ0VCRDUyQTBDNEMA + + kbjfrfpoJU + + WnM3jhjelH3+jt4jJ2OqfiQ= + + oycqAZloTNDm + + BNdC9rh4Wxif7S9NA8W1864= + + product-name + + TWFjQm9va1BybzE0LDEA + + + root_disk_uuid + 3D6822B6-A26E-358C-BD6F-EFF407645F34 + + \ No newline at end of file diff --git a/emulated/jelly.py b/emulated/jelly.py new file mode 100644 index 0000000..5bf6668 --- /dev/null +++ b/emulated/jelly.py @@ -0,0 +1,356 @@ +from io import BytesIO +import unicorn +from . import mparser as macholibre +import logging +logger = logging.getLogger("jelly") + +STOP_ADDRESS = 0x00900000 # Used as a return address when calling functions + +ARG_REGISTERS = [ + unicorn.x86_const.UC_X86_REG_RDI, + unicorn.x86_const.UC_X86_REG_RSI, + unicorn.x86_const.UC_X86_REG_RDX, + unicorn.x86_const.UC_X86_REG_RCX, + unicorn.x86_const.UC_X86_REG_R8, + unicorn.x86_const.UC_X86_REG_R9 +] + +class VirtualInstructions: + def __init__(self, uc: unicorn.Uc): + self.uc = uc + + def push(self, value: int): + self.uc.reg_write(unicorn.x86_const.UC_X86_REG_ESP, self.uc.reg_read(unicorn.x86_const.UC_X86_REG_ESP) - 8) + self.uc.mem_write(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_ESP), value.to_bytes(8, byteorder='little')) + + def pop(self) -> int: + value = int.from_bytes(self.uc.mem_read(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_ESP), 8), byteorder='little') + self.uc.reg_write(unicorn.x86_const.UC_X86_REG_ESP, self.uc.reg_read(unicorn.x86_const.UC_X86_REG_ESP) + 8) + return value + + def _set_args(self, args: list[int]): + for i in range(len(args)): + if i < 6: + self.uc.reg_write(ARG_REGISTERS[i], args[i]) + else: + self.push(args[i]) + + + def call(self, address: int, args: list[int] = []): + logger.debug(f"Calling {hex(address)} with args {args}") + self.push(STOP_ADDRESS) + self._set_args(args) + self.uc.emu_start(address, STOP_ADDRESS) + return self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RAX) + + +class Jelly: + # Constants + UC_ARCH = unicorn.UC_ARCH_X86 + UC_MODE = unicorn.UC_MODE_64 + + BINARY_BASE = 0x0 + + HOOK_BASE = 0xD00000 + HOOK_SIZE = 0x1000 + + STACK_BASE = 0x00300000 + STACK_SIZE = 0x00100000 + + HEAP_BASE = 0x00400000 + HEAP_SIZE = 0x00100000 + + STOP_ADDRESS = 0x00900000 + + # Public variables + _hooks: dict[str, callable] = {} + """Symbol name to hook function mapping""" + + instr: VirtualInstructions = None + + uc: unicorn.Uc = None + + # Private variables + _binary: bytes = b"" + + _heap_use: int = 0 + + def __init__(self, binary: bytes): + self._binary = binary + + def setup(self, hooks: dict[str, callable] = {}): + self._hooks = hooks + self._setup_unicorn() + self.instr = VirtualInstructions(self.uc) + self._setup_hooks() + self._map_binary() + self._setup_stack() + self._setup_heap() + self._setup_stop() + + + def _setup_unicorn(self): + self.uc = unicorn.Uc(self.UC_ARCH, self.UC_MODE) + + def _setup_stack(self): + self.uc.mem_map(self.STACK_BASE, self.STACK_SIZE) + self.uc.mem_write(self.STACK_BASE, b"\x00" * self.STACK_SIZE) + + self.uc.reg_write(unicorn.x86_const.UC_X86_REG_ESP, self.STACK_BASE + self.STACK_SIZE) + self.uc.reg_write(unicorn.x86_const.UC_X86_REG_EBP, self.STACK_BASE + self.STACK_SIZE) + + def _setup_heap(self): + self.uc.mem_map(self.HEAP_BASE, self.HEAP_SIZE) + self.uc.mem_write(self.HEAP_BASE, b"\x00" * self.HEAP_SIZE) + + def debug_registers(self): + logger.debug(f""" + RAX: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RAX))} + RBX: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RBX))} + RCX: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RCX))} + RDX: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RDX))} + RSI: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RSI))} + RDI: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RDI))} + RSP: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RSP))} + RBP: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RBP))} + RIP: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RIP))} + R8: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_R8))} + R9: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_R9))} + """) + def wrap_hook(self, func: callable) -> callable: + # Get the number of arguments the function takes + arg_count = func.__code__.co_argcount + #print(f"Wrapping {arg_count} argument function {func.__name__}") + # Create a wrapper function that reads the arguments from registers and the stack + def wrapper(self: 'Jelly'): + args = [] + for i in range(1, arg_count): + if i < 6: + args.append(self.uc.reg_read(ARG_REGISTERS[i-1])) + else: + args.append(self.instr.pop()) + #print(ARG_REGISTERS[1]) + #self.debug_registers() + logger.debug(f"calling {func.__name__}") + if args != []: + logger.debug(f" with args: {args}") + ret = func(self, *args) + if ret is not None: + self.uc.reg_write(unicorn.x86_const.UC_X86_REG_RAX, ret) + return + return wrapper + + + def malloc(self, size: int) -> int: + # Very naive malloc implementation + addr = self.HEAP_BASE + self._heap_use + self._heap_use += size + return addr + + def _setup_stop(self): + self.uc.mem_map(self.STOP_ADDRESS, 0x1000) + self.uc.mem_write(self.STOP_ADDRESS, b"\xc3" * 0x1000) + + def _resolve_hook(uc: unicorn.Uc, address: int, size: int, self: 'Jelly'): + for name, addr in self._resolved_hooks.items(): + if addr == address: + logger.debug(f"{name}: ") + self._hooks[name](self) + + def _setup_hooks(self): + # Wrap all hooks + for name, func in self._hooks.items(): + self._hooks[name] = self.wrap_hook(func) + + self.uc.mem_map(self.HOOK_BASE, self.HOOK_SIZE) + # Write 'ret' instruction to all hook addresses + self.uc.mem_write(self.HOOK_BASE, b"\xc3" * self.HOOK_SIZE) + # Assign address in hook space to each hook + current_address = self.HOOK_BASE + self._resolved_hooks = {} + for hook in self._hooks: + self._resolved_hooks[hook] = current_address + current_address += 1 + # Add unicorn instruction hook to entire hook space + self.uc.hook_add(unicorn.UC_HOOK_CODE, Jelly._resolve_hook, begin=self.HOOK_BASE, end=self.HOOK_BASE + self.HOOK_SIZE, user_data=self) + + def _map_binary(self): + self.uc.mem_map(self.BINARY_BASE, round_to_page_size(len(self._binary), self.uc.ctl_get_page_size())) + self.uc.mem_write(self.BINARY_BASE, self._binary) + + # Unmap the first page so we can catch NULL derefs + self.uc.mem_unmap(0x0, self.uc.ctl_get_page_size()) + + # Parse the binary so we can process binds + p = macholibre.Parser(self._binary) + p.parse() + + for seg in p.segments: + for section in seg['sects']: + if section['type'] == 'LAZY_SYMBOL_POINTERS' or section['type'] == 'NON_LAZY_SYMBOL_POINTERS': + self._parse_lazy_binds(self.uc, section['r1'], section, self._binary[p.dysymtab['indirectsymoff']:], self._binary[p.symtab['stroff']:], self._binary[p.symtab['symoff']:]) + + self._parse_binds(self.uc, self._binary[p.dyld_info['bind_off']:p.dyld_info['bind_off']+p.dyld_info['bind_size']], p.segments) + + def _do_bind(self, mu: unicorn.Uc, type, location, name): + if type == 1: # BIND_TYPE_POINTER + if name in self._hooks: + #print(f"Hooking {name} at {hex(location)}") + mu.mem_write(location, self._resolved_hooks[name].to_bytes(8, byteorder='little')) + else: + #print(f"Unknown symbol {name}") + pass + else: + raise NotImplementedError(f"Unknown bind type {type}") + + def _parse_lazy_binds(self, mu: unicorn.Uc, indirect_offset, section, dysimtab, strtab, symtab): + logger.debug(f"Doing binds for {section['name']}") + for i in range(0, int(section['size']/8)): + # Parse into proper list? + dysym = dysimtab[(indirect_offset + i)*4:(indirect_offset + i)*4+4] + dysym = int.from_bytes(dysym, 'little') + index = dysym & 0x3fffffff + + # Proper list too? + symbol = symtab[index * 16:(index * 16) + 4] + strx = int.from_bytes(symbol, 'little') + + name = c_string(strtab, strx) # Remove _ at beginning + #print(f"Lazy bind for {hex(section['offset'] + (i * 8))} : {name}") + self._do_bind(mu, 1, section['offset'] + (i * 8), name) + + def _parse_binds(self, mu: unicorn.Uc, binds: bytes, segments): + blen = len(binds) + binds: BytesIO = BytesIO(binds) + + ordinal = 0 + symbolName = '' + type = BIND_TYPE_POINTER + addend = 0 + segIndex = 0 + segOffset = 0 + + while binds.tell() < blen: + current = binds.read(1)[0] + opcode = current & BIND_OPCODE_MASK + immediate = current & BIND_IMMEDIATE_MASK + + #print(f"{hex(offset)}: {hex(opcode)} {hex(immediate)}") + + if opcode == BIND_OPCODE_DONE: + logger.debug("BIND_OPCODE_DONE") + break + elif opcode == BIND_OPCODE_SET_DYLIB_ORDINAL_IMM: + ordinal = immediate + elif opcode == BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB: + #ordinal = uLEB128(&p); + ordinal = decodeULEB128(binds) + #raise NotImplementedError("BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB") + elif opcode == BIND_OPCODE_SET_DYLIB_SPECIAL_IMM: + if (immediate == 0): + ordinal = 0 + else: + ordinal = BIND_OPCODE_MASK | immediate + elif opcode == BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM: + # Parse string until null terminator + symbolName = '' + while True: + b = binds.read(1)[0] + if b == 0: + break + symbolName += chr(b) + #while binds[offset] != 0: + # symbolName += chr(binds[offset]) + # offset += 1 + #offset += 1 + #print(f"Symbol name: {symbolName}") + elif opcode == BIND_OPCODE_SET_TYPE_IMM: + type = immediate + elif opcode == BIND_OPCODE_SET_ADDEND_SLEB: + #addend = sLEB128(&p); + raise NotImplementedError("BIND_OPCODE_SET_ADDEND_SLEB") + elif opcode == BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB: + segIndex = immediate + segOffset = decodeULEB128(binds) + #raise NotImplementedError("BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB") + elif opcode == BIND_OPCODE_ADD_ADDR_ULEB: + segOffset += decodeULEB128(binds) + #segOffset += uLEB128(&p); + #raise NotImplementedError("BIND_OPCODE_ADD_ADDR_ULEB") + elif opcode == BIND_OPCODE_DO_BIND: + self._do_bind(mu, type, segments[segIndex]['offset'] + segOffset, symbolName) + segOffset += 8 + elif opcode == BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB: + self._do_bind(mu, type, segments[segIndex]['offset'] + segOffset, symbolName) + segOffset += decodeULEB128(binds) + 8 + #bind(type, (cast(void**) &segments[segIndex][segOffset]), symbolName, addend, generateFallback); + #segOffset += uLEB128(&p) + size_t.sizeof; + #raise NotImplementedError("BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB") + elif opcode == BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED: + #bind(type, (cast(void**) &segments[segIndex][segOffset]), symbolName, addend, generateFallback); + self._do_bind(mu, type, segments[segIndex]['offset'] + segOffset, symbolName) + segOffset += immediate * 8 + 8 + elif opcode == BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB: + count = decodeULEB128(binds) + skip = decodeULEB128(binds) + for i in range(count): + self._do_bind(mu, type, segments[segIndex]['offset'] + segOffset, symbolName) + segOffset += skip + 8 + # uint64_t count = uLEB128(&p); + # uint64_t skip = uLEB128(&p); + # for (uint64_t i = 0; i < count; i++) { + # bind(type, (cast(void**) &segments[segIndex][segOffset]), symbolName, addend, generateFallback); + # segOffset += skip + size_t.sizeof; + # } + #raise NotImplementedError("BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB") + else: + logger.error(f"Unknown bind opcode {opcode}") + +# Mach-O defines +BIND_OPCODE_DONE = 0x00 +BIND_OPCODE_SET_DYLIB_ORDINAL_IMM = 0x10 +BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB = 0x20 +BIND_OPCODE_SET_DYLIB_SPECIAL_IMM = 0x30 +BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM = 0x40 +BIND_OPCODE_SET_TYPE_IMM = 0x50 +BIND_OPCODE_SET_ADDEND_SLEB = 0x60 +BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB = 0x70 +BIND_OPCODE_ADD_ADDR_ULEB = 0x80 +BIND_OPCODE_DO_BIND = 0x90 +BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB = 0xA0 +BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED = 0xB0 +BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB = 0xC0 +BIND_OPCODE_THREADED = 0xD0 + +BIND_TYPE_POINTER = 1 + +BIND_OPCODE_MASK = 0xF0 +BIND_IMMEDIATE_MASK = 0x0F + +# Helper functions +def round_to_page_size(size: int, page_size: int) -> int: + return (size + page_size - 1) & ~(page_size - 1) + +def decodeULEB128(bytes: BytesIO) -> int: + result = 0 + shift = 0 + while True: + b = bytes.read(1)[0] + result |= (b & 0x7F) << shift + if (b & 0x80) == 0: + break + shift += 7 + return result + +def c_string(bytes, start: int = 0) -> str: + out = '' + i = start + + while True: + if i > len(bytes) or bytes[i] == 0: + break + out += chr(bytes[i]) + #print(start) + #print(chr(bytes[i])) + i += 1 + return out \ No newline at end of file diff --git a/emulated/mparser.py b/emulated/mparser.py new file mode 100644 index 0000000..8e5936b --- /dev/null +++ b/emulated/mparser.py @@ -0,0 +1,2289 @@ +""" +Hacked up version of 'macholibre', by Aaron Stephens +Licensed under Apache Version 2.0 +""" + + +import hashlib + +from collections import Counter +from datetime import datetime +from json import dump +from math import exp, log +from os import SEEK_END +from re import split +from struct import unpack +from uuid import UUID + +#from asn1crypto.cms import ContentInfo +#from asn1crypto.x509 import DirectoryString +from plistlib import loads + +#import mdictionary as mdictionary + +from io import BytesIO + +import logging +logger = logging.getLogger("jelly") + + +class Parser(): + """Main object containing all the necessary functions to parse + a mach-o binary. + """ + + def __init__(self, file): + """Initialize instance variables and flags.""" + + self.__extract_certs = False + self.__file = BytesIO(file) + self.__is_64_bit = True # default place-holder + self.__is_little_endian = True # ^^ + self.__macho = {} + self.__output = { + 'name': 'IMDAppleServices' + } + + self.file = self.__file + + def add_abnormality(self, abnormality): + """Add abnormality to output.""" + + if 'abnormalities' not in self.__output: + self.__output['abnormalities'] = [] + + self.__output['abnormalities'].append(abnormality) + + def calc_entropy(self, b): + """Calculate byte entropy for given bytes.""" + + byte_counts = Counter() + + entropy = 0 + + for i in b: + byte_counts[i] += 1 + + total = float(sum(byte_counts.values())) + + for count in byte_counts.values(): + p = float(count) / total + entropy -= p * log(p, 256) + + return entropy + + def get_string(self): + """Read a null-terminated string from macho.""" + + string = bytearray() + + c = self.__file.read(1) + + while c not in (b'\x00', ''): + string += c + c = self.__file.read(1) + + return string.decode('utf-8', errors='replace') + + def get_int(self, ignore_endian=False): + """Read a 4-byte integer from macho, account for endian-ness.""" + + integer = self.__file.read(4) + + if self.__is_little_endian and not ignore_endian: + return int.from_bytes(integer, byteorder='little') + + return int.from_bytes(integer, byteorder='big') + + def get_ll(self): + """Read an 8-byte long long from macho, account for endian-ness.""" + + longlong = self.__file.read(8) + + if self.__is_little_endian: + return int.from_bytes(longlong, byteorder='little') + + return int.from_bytes(longlong, byteorder='big') + + def make_version(self, version): + """Construct a version number from given bytes.""" + + vx = version >> 16 + vy = (version >> 8) & 0xff + vz = version & 0xff + + return '{}.{}.{}'.format(vx, vy, vz) + + def identify_file(self): + """Identify if the given file is a single Mach-O or a + Universal binary.""" + + magic = self.get_int(ignore_endian=True) + + if magic in mdictionary.machos: + return mdictionary.machos[magic] + else: + raise ValueError('Provided file has unrecognized magic: {}'.format( + magic)) + + def parse_macho_flags(self, flags): + """Parse ``flags`` into list of readable flags.""" + + output = [] + + i = 0 + + while i < 28: + if (0x1 & (flags >> i)) == 0x1: + if 2 ** i in mdictionary.flags: + output.append(mdictionary.flags[2 ** i]) + else: + self.add_abnormality('Unknown mach-o flag "{}".'.format( + 2 ** i)) + + i += 1 + + return output + + def get_segment_entropy(self, m_offset, offset, size): + """Determine byte-entropy for this segment.""" + + old = self.__file.tell() + + self.__file.seek(m_offset + offset) + #print("seeking to: " + str(m_offset + offset)) + + entropy = self.calc_entropy(self.__file.read(size)) + + self.__file.seek(old) + + return entropy + + def parse_section_attrs(self, attrs): + """Parse section attributes.""" + + output = [] + + for a in mdictionary.section_attrs: + if attrs & a == a: + output.append(mdictionary.section_attrs[a]) + + return output + + def parse_section_flags(self, output, flags): + """Parse section flags into section type and attributes.""" + + output['type'] = mdictionary.section_types[flags & 0xff] + + attrs = flags & 0xffffff00 + + output['attrs'] = self.parse_section_attrs(attrs) + + def parse_section(self): + """Parse section.""" + + name = self.__file.read(16).decode().rstrip('\u0000') + segname = self.__file.read(16).decode().rstrip('\u0000') + addr = self.get_ll() if self.__is_64_bit else self.get_int() + size = self.get_ll() if self.__is_64_bit else self.get_int() + offset = self.get_int() + align = self.get_int() + reloff = self.get_int() + nreloc = self.get_int() + flags = self.get_int() + r1 = self.get_int() + r2 = self.get_int() + r3 = self.get_int() + + #self.__file.read(12) if self.__is_64_bit else self.__file.read(8) + + output = { + 'name': name, + 'segname': segname, + 'addr': addr, + 'offset': offset, + 'align': align, + 'reloff': reloff, + 'nreloc': nreloc, + 'size': size, + 'r1': r1, + 'r2': r2, + 'r3': r3 + } + + self.parse_section_flags(output, flags) + + return output + + def parse_segment_flags(self, flags): + """Parse segment flags into readable list.""" + + output = [] + + i = 1 + + while i < 9: + if flags & i == i: + output.append(mdictionary.segment_flags[i]) + i <<= 1 + + return output + + def parse_segment(self, m_offset, m_size, cmd, cmd_size): + """Parse segment command.""" + + name = self.__file.read(16).decode().rstrip('\u0000') + vmaddr = self.get_ll() if self.__is_64_bit else self.get_int() + vmsize = self.get_ll() if self.__is_64_bit else self.get_int() + offset = self.get_ll() if self.__is_64_bit else self.get_int() + segsize = self.get_ll() if self.__is_64_bit else self.get_int() + maxprot = self.get_int() + initprot = self.get_int() + nsects = self.get_int() + flags = self.get_int() + + maxprot = mdictionary.protections[maxprot & 0b111] + initprot = mdictionary.protections[initprot & 0b111] + + entropy = self.get_segment_entropy(m_offset, offset, segsize) + + output = { + 'm_offset': m_offset, + 'cmd': cmd, + 'size': cmd_size, + 'name': name, + 'vmaddr': vmaddr, + 'vmsize': vmsize, + 'offset': offset, + 'segsize': segsize, + 'maxprot': maxprot, + 'initprot': initprot, + 'nsects': nsects, + 'entropy': entropy, + 'sects': [] + } + + sect_size = 80 if self.__is_64_bit else 68 + + for _ in range(nsects): + if self.__file.tell() + sect_size > m_offset + m_size: + self.add_abnormality('Section at offset "{}" with size "{}" ' + 'greater than mach-o size.'.format( + self.__file.tell(), sect_size)) + + break + + output['sects'].append(self.parse_section()) + + output['flags'] = self.parse_segment_flags(flags) + + return output + + def parse_symtab(self, cmd, cmd_size): + """Parse symbol table load command.""" + + symoff = self.get_int() + nsyms = self.get_int() + stroff = self.get_int() + strsize = self.get_int() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'symoff': symoff, + 'nsyms': nsyms, + 'stroff': stroff, + 'strsize': strsize + } + + return output + + def parse_symseg(self, cmd, cmd_size): + """Parse link-edit gdb symbol table info (obsolete).""" + + offset = self.get_int() + size = self.get_int() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'offset': offset, + 'size': size + } + + return output + + def parse_thread(self, cmd, cmd_size): + """Parse thread load command.""" + + state = self.get_int() + count = self.get_int() + + self.__file.read(cmd_size - 16) # skip thread_state objects. + # TODO: parse them, definitions in + + if state in mdictionary.thread_states: + state = mdictionary.thread_states[state] + else: + self.add_abnormality('Invalid THREAD STATE FLAVOR "{}" at offset ' + '"{}".'.format(state, self.__file.tell() - 8)) + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'state': state, + 'count': count + } + + return output + + def parse_fvmlib(self, cmd, cmd_size): + """Parse fvmlib load command.""" + + offset = self.__file.tell() - 8 + + self.__file.read(4) # skip name offset + + minor_version = self.get_int() + header_addr = self.get_int() + name = self.get_string() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'name': name, + 'minor_version': self.make_version(minor_version), + 'header_addr': header_addr + } + + self.__file.read(cmd_size - (self.__file.tell() - offset)) + + return output + + def parse_ident(self, cmd, cmd_size): + """Parse object identification info (obsolete).""" + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'strings': [] + } + + end = self.__file.tell() - 8 + cmd_size + + while self.__file.tell() < end: + string = self.get_string() + + if string != '': + output['strings'].append(string) + + return output + + def parse_fvmfile(self, cmd, cmd_size): + """Parse fixed VM file inclusion (internal use).""" + + name = self.get_string() + header_addr = self.get_int() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'name': name, + 'header_addr': header_addr + } + + return output + + def parse_prepage(self, cmd, cmd_size): + """Parse prepage command (internal use). Load command structure not + found. + """ + + self.__file.read(cmd_size - 8) + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size + } + + return output + + def parse_dysymtab(self, cmd, cmd_size): + """Parse dynamic link-edit symbol table info.""" + + ilocalsym = self.get_int() # index to local symbols + nlocalsym = self.get_int() # number of local symbols + iextdefsym = self.get_int() # index to externally defined sybmols + nextdefsym = self.get_int() # number of externally defined symbols + iundefsym = self.get_int() # index to undefined symbols + nundefsym = self.get_int() # number of externally defined symbols + tocoff = self.get_int() # file offset to table of contents + ntoc = self.get_int() # number of module table entries + modtaboff = self.get_int() # file offset to module table + nmodtab = self.get_int() # number of module table entries + extrefsymoff = self.get_int() # offset to referenced symbol table + nextrefsyms = self.get_int() # number of referenced symbol table entries + indirectsymoff = self.get_int() # file offset to the indirect symbol table + nindirectsyms = self.get_int() # number of indirect symbol table entries + extreloff = self.get_int() # offset to external relocation entries + nextrel = self.get_int() # number of external relocation entries + locreloff = self.get_int() # offset to local relocation entries + nlocrel = self.get_int() # number of local relocation entries + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'ilocalsym': ilocalsym, + 'nlocalsym': nlocalsym, + 'iextdefsym': iextdefsym, + 'nextdefsym': nextdefsym, + 'iundefsym': iundefsym, + 'nundefsym': nundefsym, + 'tocoff': tocoff, + 'ntoc': ntoc, + 'modtaboff': modtaboff, + 'nmodtab': nmodtab, + 'extrefsymoff': extrefsymoff, + 'nextrefsyms': nextrefsyms, + 'indirectsymoff': indirectsymoff, + 'nindirectsyms': nindirectsyms, + 'extreloff': extreloff, + 'nextrel': nextrel, + 'locreloff': locreloff, + 'nlocrel': nlocrel + } + + return output + + def parse_load_dylib(self, cmd, cmd_size): + """Parse dylib load command.""" + + offset = self.__file.tell() - 8 + + self.__file.read(4) # skip name offset + + timestamp = self.get_int() + current_version = self.get_int() + compatibility_version = self.get_int() + name = self.get_string() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'name': name, + 'timestamp': datetime.fromtimestamp(timestamp).strftime( + '%Y-%m-%d %H:%M:%S'), + 'current_version': self.make_version(current_version), + 'compatability_version': self.make_version(compatibility_version) + } + + # skip padding + self.__file.read(cmd_size - (self.__file.tell() - offset)) + + return output + + def parse_load_dylinker(self, cmd, cmd_size): + """Parse dylinker load command.""" + + offset = self.__file.tell() - 8 + + self.__file.read(4) # skip name offset + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'name': self.get_string() + } + + # skip padding + self.__file.read(cmd_size - (self.__file.tell() - offset)) + + return output + + def parse_prebound_dylib(self, cmd, cmd_size): + """Parse prebound dylib load command. An executable that is prebound to + its dynamic libraries will have one of these for each library that the + static linker used in prebinding. + """ + + name = self.get_string() + nmodules = self.get_int() + linked_modules = self.get_string() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'name': name, + 'nmodules': nmodules, + 'linked_modules': linked_modules + } + + return output + + def parse_routines(self, cmd, cmd_size): + """Parse routines load command. The routines command contains the + address of the dynamic shared library initialization routine and an + index into the module table for the module that defines the routine. + """ + + init_address = self.get_ll() if self.__is_64_bit else self.get_int() + init_module = self.get_ll() if self.__is_64_bit else self.get_int() + + self.__file.read(48) if self.__is_64_bit else self.__file.read(24) + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'init_address': init_address, + 'init_module': init_module + } + + return output + + def parse_sub_stuff(self, cmd, cmd_size): + """Parse sub_* load command.""" + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'name': self.get_string() + } + + return output + + def parse_twolevel_hints(self, cmd, cmd_size): + """Parse two-level hints load command.""" + + offset = self.get_int() + nhints = self.get_int() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'offset': offset, + 'nhints': nhints + } + + return output + + def parse_prebind_cksum(self, cmd, cmd_size): + """Parse prebind checksum load command.""" + + cksum = self.get_int() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'cksum': cksum + } + + return output + + def parse_uuid(self, cmd, cmd_size): + """Parse UUID load command.""" + + uuid = self.__file.read(16) + + if self.__is_little_endian: + uuid = unpack('<16s', uuid)[0] + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'uuid': UUID(bytes=uuid).hex + } + + return output + + def parse_linkedit_data(self, cmd, cmd_size): + """Parse link-edit data load command.""" + + dataoff = self.get_int() # file offset of data in __LINKEDIT segment + datasize = self.get_int() # file size of data in __LINKEDIT segment + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'dataoff': dataoff, + 'datasize': datasize + } + + return output + + def parse_encryption_info(self, cmd, cmd_size): + """Parse encryption info load command. Contains the file offset and size + of an encrypted segment. + """ + + cryptoff = self.get_int() + cryptsize = self.get_int() + cryptid = self.get_int() + + if cmd.endswith('64'): + self.__file.read(4) # skip padding + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'cryptoff': cryptoff, + 'cryptsize': cryptsize, + 'cryptid': cryptid + } + + return output + + def parse_dyld_info(self, cmd, cmd_size): + """Parse dyld info load command. contains the file offsets and sizes of + the new compressed form of the information dyld needs to load the + image. This information is used by dyld on Mac OS X 10.6 and later. All + information pointed to by this command is encoded using byte streams, + so no endian swapping is needed to interpret it. + """ + + rebase_off = self.get_int() # file offset to rebase info + rebase_size = self.get_int() # size of rebase info + bind_off = self.get_int() # file offset to binding info + bind_size = self.get_int() # size of binding info + weak_bind_off = self.get_int() # file offset to weak binding info + weak_bind_size = self.get_int() # size of weak binding info + lazy_bind_off = self.get_int() # file offset to lazy binding info + lazy_bind_size = self.get_int() # size of lazy binding info + export_off = self.get_int() # file offset to export info + export_size = self.get_int() # size of offset info + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'rebase_off': rebase_off, + 'rebase_size': rebase_size, + 'bind_off': bind_off, + 'bind_size': bind_size, + 'weak_bind_off': weak_bind_off, + 'weak_bind_size': weak_bind_size, + 'lazy_bind_off': lazy_bind_off, + 'lazy_bind_size': lazy_bind_size, + 'export_off': export_off, + 'export_size': export_size + } + + return output + + def parse_version_min_os(self, cmd, cmd_size): + """Parse minimum OS version load command.""" + + version = self.get_int() + sdk = self.get_int() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'version': self.make_version(version), + 'sdk': self.make_version(sdk) + } + + return output + + def parse_source_version(self, cmd, cmd_size): + """Parse source version load command.""" + + version = self.get_ll() # A.B.C.D.E packed as a24.b10.c10.d10.e10 + + mask = 0b1111111111 # 10 bit mask for B, C, D, and E + + a = version >> 40 + b = (version >> 30) & mask + c = (version >> 20) & mask + d = (version >> 10) & mask + e = version & mask + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'version': '{}.{}.{}.{}.{}'.format(a, b, c, d, e) + } + + return output + + def parse_linker_option(self, cmd, cmd_size): + """Parse linker options load command.""" + + start = self.__file.tell() - 8 + + count = self.get_int() + + linker_options = [] + + for _ in range(count): + linker_options.append(self.get_string()) + + self.__file.read(cmd_size - (self.__file.tell() - start)) + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'count': count, + 'linker_options': linker_options + } + + return output + + def parse_rpath(self, cmd, cmd_size): + """Parse rpath load command.""" + + offset = self.__file.tell() - 8 + + self.__file.read(4) # skip path offset + + path = self.get_string() + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'path': path + } + + self.__file.read(cmd_size - (self.__file.tell() - offset)) + + return output + + def parse_main(self, cmd, cmd_size): + """Parse main load command.""" + + entryoff = self.get_ll() # file (__TEXT) offset of main() + stacksize = self.get_ll() # if not zero, initialize stack size + + output = { + 'cmd': cmd, + 'cmd_size': cmd_size, + 'entryoff': entryoff, + 'stacksize': stacksize + } + + return output + + def parse_lcs(self, offset, size, nlcs, slcs): + """Determine which load commands are present and parse each one + accordingly. Return as a list. + + Load command structures found in '/usr/include/mach-o/loader.h'. + """ + + self.__macho['lcs'] = [] + self.segments = [] + + for _ in range(nlcs): + cmd = self.get_int() # Load command type + cmd_size = self.get_int() # Size of load command + + if self.__is_64_bit and cmd_size % 8 != 0: + raise ValueError('Load command size "{}" for 64-bit mach-o at ' + 'offset "{}" is not divisible by 8.'.format( + cmd_size, self.__file.tell() - 4)) + elif cmd_size % 4 != 0: + raise ValueError('Load command size "{}" for 32-bit mach-o at ' + 'offset "{}" is not divisible by 4.'.format( + cmd_size, self.__file.tell() - 4)) + + if cmd in mdictionary.loadcommands: + cmd = mdictionary.loadcommands[cmd] + else: + self.add_abnormality('Unknown load command "{}" at offset ' + '"{}".'.format( + cmd, self.__file.tell() - 8)) + + self.__file.read(cmd_size - 8) # skip load command + + if cmd == 'SEGMENT' or cmd == 'SEGMENT_64': + #self.segments.append((offset, size, cmd, cmd_size)) + #self.__macho['lcs'].append( + + parsed = self.parse_segment(offset, size, cmd, cmd_size) + self.__macho['lcs'].append(parsed) + self.segments.append(parsed) + elif cmd == 'SYMTAB': + self.symtab = self.parse_symtab(cmd, cmd_size) + self.__macho['lcs'].append(self.symtab) + elif cmd == 'SYMSEG': + self.__macho['lcs'].append(self.parse_symseg(cmd, cmd_size)) + elif cmd in ('THREAD', 'UNIXTHREAD'): + self.__macho['lcs'].append(self.parse_thread(cmd, cmd_size)) + elif cmd in ('LOADFVMLIB', 'IDFVMLIB'): + self.__macho['lcs'].append(self.parse_fvmlib(cmd, cmd_size)) + elif cmd == 'IDENT': + self.__macho['lcs'].append(self.parse_ident(cmd, cmd_size)) + elif cmd == 'FVMFILE': + self.__macho['lcs'].append(self.parse_fvmfile(cmd, cmd_size)) + elif cmd == 'PREPAGE': + self.__macho['lcs'].append(self.parse_prepage(cmd, cmd_size)) + elif cmd == 'DYSYMTAB': + self.__macho['lcs'].append(self.parse_dysymtab(cmd, cmd_size)) + elif cmd in ('LOAD_DYLIB', 'ID_DYLIB', 'LAZY_LOAD_DYLIB', + 'LOAD_WEAK_DYLIB', 'REEXPORT_DYLIB', + 'LOAD_UPWARD_DYLIB'): + self.__macho['lcs'].append( + self.parse_load_dylib(cmd, cmd_size)) + elif cmd in ('LOAD_DYLINKER', 'ID_DYLINKER', 'DYLD_ENVIRONMENT'): + self.__macho['lcs'].append( + self.parse_load_dylinker(cmd, cmd_size)) + elif cmd == 'PREBOUND_DYLIB': + self.__macho['lcs'].append( + self.parse_prebound_dylib(cmd, cmd_size)) + elif cmd in ('ROUTINES', 'ROUTINES_64'): + self.__macho['lcs'].append(self.parse_routines(cmd, cmd_size)) + elif cmd in ('SUB_FRAMEWORK', 'SUB_UMBRELLA', 'SUB_CLIENT', + 'SUB_LIBRARY'): + self.__macho['lcs'].append(self.parse_sub_stuff(cmd, cmd_size)) + elif cmd == 'TWOLEVEL_HINTS': + self.__macho['lcs'].append( + self.parse_twolevel_hints(cmd, cmd_size)) + elif cmd == 'PREBIND_CKSUM': + self.__macho['lcs'].append( + self.parse_prebind_cksum(cmd, cmd_size)) + elif cmd == 'UUID': + self.__macho['lcs'].append(self.parse_uuid(cmd, cmd_size)) + elif cmd in ('CODE_SIGNATURE', 'SEGMENT_SPLIT_INFO', + 'FUNCTION_STARTS', 'DATA_IN_CODE', + 'DYLIB_CODE_SIGN_DRS', 'LINKER_OPTIMIZATION_HINT'): + self.__macho['lcs'].append( + self.parse_linkedit_data(cmd, cmd_size)) + elif cmd in ('ENCRYPTION_INFO', 'ENCRYPTION_INFO_64'): + self.__macho['lcs'].append( + self.parse_encryption_info(cmd, cmd_size)) + elif cmd in ('DYLD_INFO', 'DYLD_INFO_ONLY'): + self.dyld_info = self.parse_dyld_info(cmd, cmd_size) + self.__macho['lcs'].append(self.dyld_info) + elif cmd in ('VERSION_MIN_MACOSX', 'VERSION_MIN_IPHONEOS', + 'VERSION_MIN_WATCHOS', 'VERSION_MIN_TVOS'): + self.__macho['lcs'].append( + self.parse_version_min_os(cmd, cmd_size)) + elif cmd == 'SOURCE_VERSION': + self.__macho['lcs'].append( + self.parse_source_version(cmd, cmd_size)) + elif cmd == 'LINKER_OPTION': + self.__macho['lcs'].append( + self.parse_linker_option(cmd, cmd_size)) + elif cmd == 'RPATH': + self.__macho['lcs'].append(self.parse_rpath(cmd, cmd_size)) + elif cmd == 'MAIN': + self.__macho['lcs'].append(self.parse_main(cmd, cmd_size)) + + def parse_syms(self, offset, size, lc_symtab): + """Parse symbol and string tables. + + Symbol table format found in: + /usr/include/mach-o/nlist.h + /usr/include/mach-o/stab.h + """ + + # Check if symbol table offset is within mach-o + if lc_symtab['symoff'] > size: + self.add_abnormality('Symbol table at offset "{}" out of ' + 'bounds.'.format( + offset + lc_symtab['symoff'])) + + return + + true_offset = offset + lc_symtab['symoff'] # beginning of symbol table + + symbol_size = 16 if self.__is_64_bit else 12 + + self.__file.seek(true_offset) + + entropy = self.calc_entropy(self.__file.read( + lc_symtab['nsyms'] * symbol_size)) + + if entropy >= 0.8: + self.add_abnormality('Symbol table with entropy of "{}" is ' + 'probably packed. Not attempting to ' + 'parse.'.format(entropy)) + + return + + if lc_symtab['symoff'] + lc_symtab['nsyms'] * symbol_size > size: + self.add_abnormality('Symbol table at offset "{}" partially out ' + 'of bounds. Attempting to parse as many ' + 'symbols as possible.'.format(true_offset)) + + self.__file.seek(true_offset) # jump to beginning of symbol table + + self.__macho['symtab'] = [] + + for _ in range(lc_symtab['nsyms']): + if self.__file.tell() + symbol_size > offset + size: + break + + n_strx = self.get_int() + n_type = int(self.__file.read(1).hex(), 16) + n_sect = int(self.__file.read(1).hex(), 16) + n_desc = int(self.__file.read(2).hex(), 16) + + n_value = self.get_ll() if self.__is_64_bit else self.get_int() + + symbol = { + 'n_strx': n_strx, + 'n_sect': n_sect, + 'n_desc': n_desc, + 'n_value': n_value + } + + if n_type >= 32: + if n_type in mdictionary.stabs: + symbol['stab'] = mdictionary.stabs[n_type] + else: + self.add_abnormality( + 'Unknown stab type "{}" at offset "{}".'.format( + n_type, self.__file.tell() - symbol_size + 4)) + else: + n_pext = n_type & 0x10 # private external symbol flag + n_ext = n_type & 0x01 # external symbol flag + n_type = n_type & 0x0e # symbol type + + if n_type in mdictionary.n_types: + n_type = mdictionary.n_types[n_type] + else: + self.add_abnormality( + 'Unknown N_TYPE "{}" at offset "{}".'.format( + n_type, self.__file.tell() - symbol_size + 4)) + + if self.__is_little_endian: + dylib = n_desc & 0x0f + ref = (n_desc >> 8) & 0xff + else: + dylib = (n_desc >> 8) & 0xff + ref = n_desc & 0x0f + + symbol['pext'] = n_pext + symbol['n_type'] = n_type + symbol['ext'] = n_ext + symbol['dylib'] = dylib + symbol['ref'] = ref + + self.__macho['symtab'].append(symbol) + + def parse_strings(self, offset, size, lc_symtab): + """Parse string table.""" + + # Check is string table offset is within mach-o + if lc_symtab['stroff'] > size: + self.add_abnormality( + 'String table at offset "{}" greater than mach-o size.'.format( + offset + lc_symtab['stroff'])) + + return + + true_offset = offset + lc_symtab['stroff'] + + self.__file.seek(true_offset) + #self.strtab = bytes(self.__file.read(lc_symtab['strsize'])) + #self.__file.seek(true_offset) + + entropy = self.calc_entropy(self.__file.read(lc_symtab['strsize'])) + + if entropy >= 0.8: + self.add_abnormality('String table with entropy of "{}" is ' + 'probably packed. Not attempting to ' + 'parse.'.format(entropy)) + + return + + if true_offset + lc_symtab['strsize'] > offset + size: + self.add_abnormality('String Table at offset "{}" partially out ' + 'of bounds. Attempting to parse as many ' + 'strings as possible.'.format(true_offset)) + + self.__macho['strtab'] = [] + + self.__file.seek(true_offset) + + while self.__file.tell() < true_offset + lc_symtab['strsize']: + try: + string = self.get_string() + + if string != '': + self.__macho['strtab'].append(string) + except: + break + + def parse_imports(self, offset, size, lc_symtab, lc_dysymtab=None, + lc_dylibs=None): + """Parse undefined external symbols (imports) out of the symbol and + string tables. + """ + + self.__macho['imports'] = [] + + true_offset = offset + lc_symtab['stroff'] + + undef_syms = None + + if lc_dysymtab is not None: # Use symtab layout info from DYSYMTAB + i_undef = lc_dysymtab['nlocalsym'] + lc_dysymtab['nextdefsym'] - 1 + j_undef = i_undef + lc_dysymtab['nundefsym'] + + undef_syms = self.__macho['symtab'][i_undef:j_undef] + else: # Find undefined symbols manually by checking n_type + undef_syms = filter(lambda sym: sym['n_type'] in ('UNDF', 'PBUD'), + self.__macho['symtab']) + + for sym in undef_syms: + self.__file.seek(true_offset + sym['n_strx']) + + value = self.get_string() + + if lc_dylibs is not None: # If created with two-level namespace + dylib = sym['dylib'] + + if dylib == 0: + dylib = 'SELF_LIBRARY' + elif dylib == 254: + dylib = 'DYNAMIC_LOOKUP' + elif dylib == 255: + dylib = 'EXECUTABLE' + elif dylib > len(lc_dylibs): + dylib = f'{dylib} (OUT_OF_RANGE)' + else: + dylib = lc_dylibs[dylib - 1]['name'] + + self.__macho['imports'].append((value, dylib)) + else: + self.__macho['imports'].append(value) + + def parse_certs(self, sig_offset, index_offset): + """Parse X509 certificates out of code signature.""" + + prev = self.__file.tell() + + true_offset = sig_offset + index_offset + + self.__file.seek(true_offset) + + magic = self.get_int(ignore_endian=True) + + if magic != mdictionary.signatures['BLOBWRAPPER']: + self.add_abnormality('Bad magic "{}" for certificate blob wrapper ' + 'at offset "{}".'.format(magic, true_offset)) + + return [] + + # subtract 8 to ignore magic and size fields + size = self.get_int(ignore_endian=True) - 8 + + if size <= 0: + self.add_abnormality('Non-positive CMS size "{}" at offset ' + '"{}".'.format(size, self.__file.tell() - 4)) + + return [] + + signed_data = ContentInfo.load(self.__file.read(size))['content'] + + self.__macho['code_signature']['certs'] = [] + + for cert in signed_data['certificates']: + cert = cert.chosen + + if self.__extract_certs: + c_bytes = cert.dump() + open(hashlib.md5(c_bytes).hexdigest(), 'wb').write(c_bytes) + + subject = {} + + for rdn in cert.subject.chosen: + name = rdn[0]['type'].human_friendly + value = rdn[0]['value'] + + if name == 'Country': + subject['country'] = str(value.chosen) + elif name == 'Organization': + subject['org'] = str(value.chosen) + elif name == 'Organizational Unit': + subject['org_unit'] = str(value.chosen) + elif name == 'Common Name': + subject['common_name'] = str(value.chosen) + else: + if isinstance(value, DirectoryString): + subject[name] = str(value.chosen) + else: + subject[name] = str(value.parsed) + + issuer = {} + + for rdn in cert.issuer.chosen: + name = rdn[0]['type'].human_friendly + value = rdn[0]['value'] + + if name == 'Country': + issuer['country'] = str(value.chosen) + elif name == 'Organization': + issuer['org'] = str(value.chosen) + elif name == 'Organizational Unit': + issuer['org_unit'] = str(value.chosen) + elif name == 'Common Name': + issuer['common_name'] = str(value.chosen) + else: + if isinstance(value, DirectoryString): + issuer[name] = str(value.chosen) + else: + issuer[name] = str(value.parsed) + + certificate = { + 'subject': subject, + 'issuer': issuer, + 'serial': cert.serial_number, + 'is_ca': cert.ca + } + + self.__macho['code_signature']['certs'].append(certificate) + + self.__file.seek(prev) + + def parse_codedirectory(self, sig_offset, index_offset): + """Parse code directory from code signature.""" + + prev = self.__file.tell() + + true_offset = sig_offset + index_offset + + self.__file.seek(true_offset) + + magic = self.get_int(ignore_endian=True) + + if magic != mdictionary.signatures['CODEDIRECTORY']: + self.add_abnormality('Bad magic "{}" for code directory at offset ' + '"{}".'.format(magic, self.__file.tell() - 4)) + + return + + size = self.get_int(ignore_endian=True) + version = self.get_int(ignore_endian=True) + # TODO: not sure how to parse flags yet... + flags = self.get_int(ignore_endian=True) + hash_offset = self.get_int(ignore_endian=True) + ident_offset = self.get_int(ignore_endian=True) + n_special_slots = self.get_int(ignore_endian=True) + n_code_slots = self.get_int(ignore_endian=True) + code_limit = self.get_int(ignore_endian=True) + hash_size = int(self.__file.read(1).hex(), 16) + hash_type = mdictionary.hashes[int(self.__file.read(1).hex(), 16)] + + if version >= 0x20200: + platform = int(self.__file.read(1).hex(), 16) + else: + self.__file.read(1) # skip spare1 + + page_size = int(round(exp( + int(self.__file.read(1).hex(), 16) * log(2)))) + + self.__file.read(4) # skip spare2 + + if version >= 0x20100: + scatter_offset = self.get_int(ignore_endian=True) + if version >= 0x20200: + team_id_offset = self.get_int(ignore_endian=True) + self.__file.seek(true_offset + team_id_offset) + team_id = self.get_string() + + self.__file.seek(true_offset + ident_offset) + + identity = self.get_string() + + self.__macho['code_signature']['codedirectory'] = { + 'size': size, + 'version': version, + 'flags': flags, + 'hash_offset': hash_offset, + 'n_special_slots': n_special_slots, + 'n_code_slots': n_code_slots, + 'code_limit': code_limit, + 'hash_size': hash_size, + 'hash_type': hash_type, + 'page_size': page_size, + 'identity': identity, + 'hashes': [] + } + + if version >= 0x20100: + self.__macho['code_signature']['codedirectory']['scatter_offset'] = scatter_offset + if version >= 0x20200: + self.__macho['code_signature']['codedirectory']['platform'] = platform + self.__macho['code_signature']['codedirectory']['team_id_offset'] = team_id_offset + self.__macho['code_signature']['codedirectory']['team_id'] = team_id + + self.__file.seek( + true_offset + hash_offset - n_special_slots * hash_size) + + count = n_special_slots + n_code_slots + + for _ in range(count): + self.__macho['code_signature']['codedirectory']['hashes'].append( + self.__file.read(hash_size).hex()) + + self.__file.seek(prev) + + def get_oid(self, db, p): + """OID parser implementation from: + + http://opensource.apple.com/source/Security/Security-57337.20.44/ + OSX/libsecurity_cdsa_utilities/lib/cssmdata.cpp + """ + + q = 0 + + while True: + q = q * 128 + (db[p] & ~0x80) + + if p < len(db) and db[p] & 0x80: + p += 1 + else: + p += 1 + break + + return q, p + + def to_oid(self, length): + """Convert bytes to correct OID.""" + + if length == 0: + return '' + + data_bytes = [ + int(self.__file.read(1).hex(), 16) for i in range(length) + ] + + p = 0 + + # first byte is composite (q1, q2) + oid1, p = self.get_oid(data_bytes, p) + + q1 = min(oid1 / 40, 2) + + data = str(q1) + '.' + str(oid1 - q1 * 40) + + while p < len(data_bytes): + d, p = self.get_oid(data_bytes, p) + data += '.' + str(d) + + self.__file.read(-length & 3) + + return data + + def parse_entitlement(self, sig_offset, index_offset): + """Parse entitlement from code signature.""" + + prev = self.__file.tell() + + true_offset = sig_offset + index_offset + + self.__file.seek(true_offset) + + magic = self.get_int(ignore_endian=True) + + if magic != mdictionary.signatures['ENTITLEMENT']: + self.add_abnormality('Bad magic "{}" for entitlement at offset ' + '"{}".'.format(magic, self.__file.tell() - 4)) + + return + + # size of plist minus magic and size values + size = self.get_int(ignore_endian=True) - 8 + + try: + plist = loads(self.__file.read(size)) + except Exception as exc: + plist = {} + self.add_abnormality('Unable to parse plist at offset "{}". ' + '{}.'.format(self.__file.tell() - size, exc)) + + if 'entitlements' not in self.__macho['code_signature']: + self.__macho['code_signature']['entitlements'] = [] + + self.__macho['code_signature']['entitlements'].append({ + 'size': size, + 'plist': plist + }) + + self.__file.seek(prev) + + def parse_data(self): + """Parse data for requirement expression.""" + + length = self.get_int(ignore_endian=True) + + data = self.__file.read(length) + + self.__file.read(-length & 3) # skip padding + + return data + + def parse_match(self): + """Parse match for requirement expression.""" + + match_type = self.get_int(ignore_endian=True) + + if match_type in mdictionary.matches: + match_type = mdictionary.matches[match_type] + + if match_type == 'matchExists': + return ' /* exists */' + elif match_type == 'matchEqual': + return ' = "{}"'.format(self.parse_data().decode()) + elif match_type == 'matchContains': + return ' ~ "{}"'.format(self.parse_data().decode()) + elif match_type == 'matchBeginsWith': + return ' = "{}*"'.format(self.parse_data().decode()) + elif match_type == 'matchEndsWith': + return ' = "*{}"'.format(self.parse_data().decode()) + elif match_type == 'matchLessThan': + return ' < {}'.format(int(self.parse_data(), 16)) + elif match_type == 'matchGreaterThan': + return ' > {}'.format(int(self.parse_data(), 16)) + elif match_type == 'matchLessEqual': + return ' <= {}'.format(int(self.parse_data(), 16)) + elif match_type == 'matchGreaterEqual': + return ' >= {}'.format(int(self.parse_data(), 16)) + else: + return ' UNKNOWN MATCH TYPE "{}"'.format(match_type) + + def parse_expression(self, in_or=False): + """Parse requirement expression. Recurse if necessary""" + + # Zero out flags in high byte (TODO: Look into flags field) + operator = self.get_int(ignore_endian=True) + operator = mdictionary.operators[operator & 0xfff] + + expression = '' + + if operator == 'False': + expression += 'never' + elif operator == 'True': + expression += 'always' + elif operator == 'Ident': + expression += 'identity "{}"'.format(self.parse_data().decode()) + elif operator == 'AppleAnchor': + expression += 'anchor apple' + elif operator == 'AppleGenericAnchor': + expression += 'anchor apple generic' + elif operator == 'AnchorHash': + cert_slot = self.get_int(ignore_endian=True) + + if cert_slot in mdictionary.cert_slots: + cert_slot = mdictionary.cert_slots[cert_slot] + + expression += 'certificate {} = {}'.format( + cert_slot, self.parse_data().decode()) + elif operator == 'InfoKeyValue': + expression += 'info[{}] = "{}"'.format( + self.parse_data().decode(), self.parse_data().decode()) + elif operator == 'And': + if in_or: + expression += '({} and {})'.format( + self.parse_expression(), self.parse_expression()) + else: + expression += '{} and {}'.format( + self.parse_expression(), self.parse_expression()) + elif operator == 'Or': + if in_or: + expression += '({} or {})'.format( + self.parse_expression(in_or=True), + self.parse_expression(in_or=True)) + else: + expression += '{} or {}'.format( + self.parse_expression(in_or=True), + self.parse_expression(in_or=True)) + elif operator == 'Not': + expression += '! {}'.format(self.parse_expression()) + elif operator == 'CDHash': + expression += 'cdhash {}'.format(self.parse_data().decode()) + elif operator == 'InfoKeyField': + expression += 'info[{}]{}'.format( + self.parse_data().decode(), self.parse_match()) + elif operator == 'EntitlementField': + expression += 'entitlement[{}]{}'.format( + self.parse_data().decode(), self.parse_match()) + elif operator == 'CertField': + cert_slot = self.get_int(ignore_endian=True) + + if cert_slot in mdictionary.cert_slots: + cert_slot = mdictionary.cert_slots[cert_slot] + + expression += 'certificate {}[{}]{}'.format( + cert_slot, self.parse_data().decode(), self.parse_match()) + elif operator == 'CertGeneric': + cert_slot = self.get_int(ignore_endian=True) + + if cert_slot in mdictionary.cert_slots: + cert_slot = mdictionary.cert_slots[cert_slot] + + length = self.get_int(ignore_endian=True) + + expression += 'certificate {}[field.{}]{}'.format( + cert_slot, self.to_oid(length), self.parse_match()) + elif operator == 'CertPolicy': + cert_slot = self.get_int(ignore_endian=True) + + if cert_slot in mdictionary.cert_slots: + cert_slot = mdictionary.cert_slots[cert_slot] + + expression += 'certificate {}[policy.{}]{}'.format( + cert_slot, self.parse_data().decode(), self.parse_match()) + elif operator == 'TrustedCert': + cert_slot = self.get_int(ignore_endian=True) + + if cert_slot in mdictionary.cert_slots: + cert_slot = mdictionary.cert_slots[cert_slot] + + expression += 'certificate {} trusted'.format(cert_slot) + elif operator == 'TrustedCerts': + expression += 'anchor trusted' + elif operator == 'NamedAnchor': + expression += 'anchor apple {}'.format(self.parse_data().decode()) + elif operator == 'NamedCode': + expression += '({})'.format(self.parse_data().decode()) + elif operator == 'Platform': + platform = self.get_int(ignore_endian=True) + expression += 'platform = {}'.format(platform) + + return expression + + def parse_requirement(self, reqs_offset, req_type, req_offset): + """Parse single requirement from code signature.""" + + prev = self.__file.tell() + + true_offset = reqs_offset + req_offset + + self.__file.seek(true_offset) + + magic = self.get_int(ignore_endian=True) + + if magic != mdictionary.signatures['REQUIREMENT']: + self.add_abnormality('Bad magic "{}" for requirement at offset ' + '"{}".'.format(magic, self.__file.tell() - 4)) + + return + + self.__file.read(8) # skip size and kind fields + # (TODO: look into ``kind`` field) + + self.__macho['code_signature']['requirements'].append({ + 'req_type': req_type, + 'req_offset': req_offset, + 'expression': self.parse_expression() + }) + + self.__file.seek(prev) + + def parse_requirements(self, sig_offset, index_offset): + """Parse requirements from code signature.""" + + prev = self.__file.tell() + + true_offset = sig_offset + index_offset + + self.__file.seek(true_offset) + + magic = self.get_int(ignore_endian=True) + + if magic != mdictionary.signatures['REQUIREMENTS']: + self.add_abnormality('Bad magic "{}" for requirements at offset ' + '"{}".'.format(magic, self.__file.tell() - 4)) + + return + + self.__file.read(4) # skip size field + + count = self.get_int(ignore_endian=True) + + self.__macho['code_signature']['requirements'] = [] + + for _ in range(count): + req_type = self.get_int(ignore_endian=True) + req_type = mdictionary.requirements[req_type] + + req_offset = self.get_int(ignore_endian=True) + + self.parse_requirement(true_offset, req_type, req_offset) + + self.__file.seek(prev) + + def parse_sig(self, offset, size, lc_codesig): + """Parse code signature in its entirety.""" + + if lc_codesig['dataoff'] + lc_codesig['datasize'] > size: + self.add_abnormality('CODE_SIGNATURE at offset "{}" with size ' + '"{}" greater than mach-o size.'.format( + offset + lc_codesig['dataoff'], + lc_codesig['datasize'])) + + return + + true_offset = offset + lc_codesig['dataoff'] + + self.__file.seek(true_offset) + + magic = self.get_int(ignore_endian=True) + + if magic != mdictionary.signatures['EMBEDDED_SIGNATURE']: + self.add_abnormality('Bad magic "{}" for embedded signature at ' + 'offset "{}".'.format(magic, true_offset)) + + return + + self.__macho['code_signature'] = {} + + size = self.get_int(ignore_endian=True) + count = self.get_int(ignore_endian=True) + + for _ in range(count): + index_type = self.get_int(ignore_endian=True) + + if index_type in mdictionary.indeces: + index_type = mdictionary.indeces[index_type] + else: + self.add_abnormality('Unknown code signature index type "{}" ' + 'at offset "{}".'.format( + index_type, self.__file.tell() - 4)) + + self.__file.read(4) # skip offset + continue + + index_offset = self.get_int(ignore_endian=True) + + if index_type == 'SignatureSlot': + self.parse_certs(true_offset, index_offset) + elif index_type == 'CodeDirectorySlot': + self.parse_codedirectory(true_offset, index_offset) + elif index_type == 'EntitlementSlot': + self.parse_entitlement(true_offset, index_offset) + elif index_type == 'RequirementsSlot': + self.parse_requirements(true_offset, index_offset) + + def parse_macho(self, offset, size): + """Parse mach-o binary, possibly contained within a + universal binary. + """ + + if size is None: + self.__file.seek(0, SEEK_END) # find the end of the file + size = self.__file.tell() + + # jump to the location of this mach-o within the file + self.__file.seek(offset) + + identity = self.identify_file() + self.__is_64_bit = identity[0] + self.__is_little_endian = identity[1] + + cputype = self.get_int() # CPU type + subtype = self.get_int() # CPU sub-type + filetype = self.get_int() # Mach-o file type + nlcs = self.get_int() # Number of load commands + slcs = self.get_int() # Size of load commands + flags = self.get_int() # Mach-o flags + + if self.__is_64_bit: + self.__file.read(4) # skip padding + + if cputype in mdictionary.cputypes: + if subtype in mdictionary.cputypes[cputype]: + subtype = mdictionary.cputypes[cputype][subtype] + else: + self.add_abnormality('Unknown SUBTYPE "{}" for CPUTYPE "{}" ' + 'at offset "{}".'.format( + subtype, cputype, offset + 8)) + + cputype = mdictionary.cputypes[cputype][-2] + else: + raise ValueError('Unknown or unsupported CPUTYPE "{}" at offset ' + '"{}".'.format(cputype, offset + 4)) + + if filetype in mdictionary.filetypes: + filetype = mdictionary.filetypes[filetype] + else: + self.add_abnormality('Unknown FILETYPE "{}" at offset ' + '"{}".'.format(filetype, offset + 12)) + + flags = self.parse_macho_flags(flags) + + self.__macho['cputype'] = cputype + self.__macho['subtype'] = subtype + self.__macho['filetype'] = filetype + self.__macho['nlcs'] = nlcs + self.__macho['slcs'] = slcs + self.__macho['flags'] = flags + + # Parse load commands + self.parse_lcs(offset, size, nlcs, slcs) + + lcs = list(map(lambda x: x['cmd'], self.__macho['lcs'])) + + # Check for symbol and strings tables and parse if present + if 'SYMTAB' in lcs: + lc_symtab = self.__macho['lcs'][lcs.index('SYMTAB')] + + self.parse_syms(offset, size, lc_symtab) + self.parse_strings(offset, size, lc_symtab) + + # If symbol and strings tables were parsed, parse imports + if 'symtab' in self.__macho and 'strtab' in self.__macho: + lc_dysymtab = None + lc_dylibs = None + + # Check for presence of DYSYMTAB load command and, if present, use + # it to parse undefined external symbols (imports). Otherwise, find + # imports manually. + if 'DYSYMTAB' in lcs: + lc_dysymtab = self.__macho['lcs'][lcs.index('DYSYMTAB')] + self.dysymtab = lc_dysymtab + + # Check if the static linker used the two-level namespace feature. + # If so, pass in the list of dynamic libraries (dylibs) given in + # the 'DYLIB' load commands. + if 'TWOLEVEL' in self.__macho['flags']: + lc_dylibs = list(filter(lambda x: x['cmd'].endswith('DYLIB'), + self.__macho['lcs'])) + + self.parse_imports(offset, size, lc_symtab, + lc_dysymtab=lc_dysymtab, lc_dylibs=lc_dylibs) + + # Check for a code signature and parse if present + if 'CODE_SIGNATURE' in lcs: + lc_codesig = self.__macho['lcs'][lcs.index('CODE_SIGNATURE')] + + #self.parse_sig(offset, size, lc_codesig) + + #self.__macho['strtab'] = None + #self.__macho['symtab'] = None + self.__macho['imports'] = None + + + return self.__macho + + def parse_universal(self): + """Parses universal binary.""" + + self.__output['universal'] = { + 'machos': [] + } + + # number of mach-o's contained in this binary + n_machos = self.get_int(ignore_endian=True) + + for i in range(n_machos): + self.__file.read(8) # skip cputype and subtype fields + + offset = self.get_int(ignore_endian=True) + size = self.get_int(ignore_endian=True) + + self.__file.read(4) # skip align field + + prev = self.__file.tell() + self.parse_macho(offset, size) + self.__file.seek(prev) + + self.__output['universal']['machos'].append(self.__macho.copy()) + self.__macho.clear() + + + def u_get_offset(self, cpu_type = None, uni_index = None): + self.__file.seek(0) # return to beginning of file + + if self.__file.read(4) != b'\xca\xfe\xba\xbe': + # Throw a fit + logger.critical("Wrong magic for universal binary?") + + n_machos = self.get_int(ignore_endian=True) + + for i in range(n_machos): + self.__file.read(8) # skip cputype and subtype fields + + offset = self.get_int(ignore_endian=True) + size = self.get_int(ignore_endian=True) + + self.__file.read(4) # skip align field + + # Read the cpu type and subtype in the macho + old = self.__file.tell() + self.__file.seek(offset) + identity = self.identify_file() + self.__is_64_bit = identity[0] + self.__is_little_endian = identity[1] + + cputype = self.get_int() # CPU type + subtype = self.get_int() # CPU sub-type + + if cputype in mdictionary.cputypes: + if subtype in mdictionary.cputypes[cputype]: + subtype = mdictionary.cputypes[cputype][subtype] + else: + logger.debug("UNKNOWN CPU TYPE: " + str(cputype)) + + cputype = mdictionary.cputypes[cputype][-2] + + #print(f"CPU TYPE: {cputype} SUBTYPE: {subtype}") + + self.__file.seek(old) + if i == uni_index or cpu_type == cputype: + return offset, size + + #prev = self.__file.tell() + + #self.parse_macho(offset, size) + #self.__file.seek(prev) + + #self.__output['universal']['machos'].append(self.__macho.copy()) + #self.__macho.clear() + + + def parse_file(self): + """Determines characteristics about the entire file and begins + to parse. + """ + + contents = self.__file.read() + + self.__output['size'] = len(contents) + + self.__output['hashes'] = { + 'md5': hashlib.md5(contents).hexdigest(), + 'sha1': hashlib.sha1(contents).hexdigest(), + 'sha256': hashlib.sha256(contents).hexdigest() + } + + self.__file.seek(0) # return to beginning of file + + if self.__file.read(4) == b'\xca\xfe\xba\xbe': + self.parse_universal() + else: + self.parse_macho(0, self.__output['size']) + self.__output['macho'] = self.__macho + + def parse(self, certs: bool=False, out=None): + """Parse Mach-O file at given path, and either return a dict + or write output to provided file. + """ + + self.__extract_certs = certs + + self.parse_file() + + if out is None: + return self.__output + + dump(self.__output, out) + +class mdictionary: + cert_slots = { + -1: 'root', + 0: 'leaf' + } + + hashes = { + 0: 'No Hash', + 1: 'SHA-1', + 2: 'SHA-256' + } + + segment_flags = { + 1: 'HIGHVM', + 2: 'FVMLIB', + 4: 'NORELOC', + 8: 'PROTECTED_VERSION_1' + } + + n_types = { + 0x0: 'UNDF', + 0x2: 'ABS', + 0xe: 'SECT', + 0xc: 'PBUD', + 0xa: 'INDR' + } + + machos = { + 4277009102: (False, False), # 32 bit, big endian + 4277009103: (True, False), # 64 bit, big endian + 3472551422: (False, True), # 32 bit, little endian + 3489328638: (True, True) # 64 bit, little endian + } + + requirements = { + 1: 'HostRequirementType', + 2: 'GuestRequirementType', + 3: 'DesignatedRequirementType', + 4: 'LibraryRequirementType', + 5: 'PluginRequirementType', + } + + indeces = { + 0: 'CodeDirectorySlot', + 1: 'InfoSlot', + 2: 'RequirementsSlot', + 3: 'ResourceDirSlot', + 4: 'ApplicationSlot', + 5: 'EntitlementSlot', + 0x10000: 'SignatureSlot' + } + + matches = { + 0: 'matchExists', + 1: 'matchEqual', + 2: 'matchContains', + 3: 'matchBeginsWith', + 4: 'matchEndsWith', + 5: 'matchLessThan', + 6: 'matchGreaterThan', + 7: 'matchLessEqual', + 8: 'matchGreaterEqual' + } + + protections = { + 0b000: '---', + 0b001: 'r--', + 0b010: '-w-', + 0b011: 'rw-', + 0b100: '--x', + 0b101: 'r-x', + 0b110: '-wx', + 0b111: 'rwx' + } + + signatures = { + 'REQUIREMENT': 0xfade0c00, + 'REQUIREMENTS': 0xfade0c01, + 'CODEDIRECTORY': 0xfade0c02, + 'ENTITLEMENT': 0xfade7171, + 'BLOBWRAPPER': 0xfade0b01, + 'EMBEDDED_SIGNATURE': 0xfade0cc0, + 'DETACHED_SIGNATURE': 0xfade0cc1, + 'CODE_SIGN_DRS': 0xfade0c05 + } + + section_attrs = { + 0x80000000: 'PURE_INSTRUCTIONS', + 0x40000000: 'NO_TOC', + 0x20000000: 'STRIP_STATIC_SYMS', + 0x10000000: 'NO_DEAD_STRIP', + 0x08000000: 'LIVE_SUPPORT', + 0x04000000: 'SELF_MODIFYING_CODE', + 0x02000000: 'DEBUG', + 0x00000400: 'SOME_INSTRUCTIONS', + 0x00000200: 'EXT_RELOC', + 0x00000100: 'LOC_RELOC' + } + + filetypes = { + 1: 'OBJECT', + 2: 'EXECUTE', + 3: 'FVMLIB', + 4: 'CORE', + 5: 'PRELOAD', + 6: 'DYLIB', + 7: 'DYLINKER', + 8: 'BUNDLE', + 9: 'DYLIB_STUB', + 10: 'DSYM', + 11: 'KEXT_BUNDLE' + } + + section_types = { + 0x0: 'REGULAR', + 0x1: 'ZEROFILL', + 0x2: 'CSTRING_LITERALS', + 0x3: '4BYTE_LITERALS', + 0x4: '8BYTE_LITERALS', + 0x5: 'LITERAL_POINTERS', + 0x6: 'NON_LAZY_SYMBOL_POINTERS', + 0x7: 'LAZY_SYMBOL_POINTERS', + 0x8: 'SYMBOL_STUBS', + 0x9: 'MOD_INIT_FUNC_POINTERS', + 0xa: 'MOD_TERM_FUNC_POINTERS', + 0xb: 'COALESCED', + 0xc: 'GB_ZEROFILL', + 0xd: 'INTERPOSING', + 0xe: '16BYTE_LITERALS', + 0xf: 'DTRACE_DOF', + 0x10: 'LAZY_DYLIB_SYMBOL_POINTERS', + 0x11: 'THREAD_LOCAL_REGULAR', + 0x12: 'THREAD_LOCAL_ZEROFILL', + 0x13: 'THREAD_LOCAL_VARIABLES', + 0x14: 'THREAD_LOCAL_VARIABLE_POINTERS', + 0x15: 'THREAD_LOCAL_INIT_FUNCTION_POINTERS' + } + + operators = { + 0: 'False', + 1: 'True', + 2: 'Ident', + 3: 'AppleAnchor', + 4: 'AnchorHash', + 5: 'InfoKeyValue', + 6: 'And', + 7: 'Or', + 8: 'CDHash', + 9: 'Not', + 10: 'InfoKeyField', + 11: 'CertField', + 12: 'TrustedCert', + 13: 'TrustedCerts', + 14: 'CertGeneric', + 15: 'AppleGenericAnchor', + 16: 'EntitlementField', + 17: 'CertPolicy', + 18: 'NamedAnchor', + 19: 'NamedCode', + 20: 'Platform' + } + + thread_states = { + 1: 'x86_THREAD_STATE32', + 2: 'x86_FLOAT_STATE32', + 3: 'x86_EXCEPTION_STATE32', + 4: 'x86_THREAD_STATE64', + 5: 'x86_FLOAT_STATE64', + 6: 'x86_EXCEPTION_STATE64', + 7: 'x86_THREAD_STATE', + 8: 'x86_FLOAT_STATE', + 9: 'x86_EXCEPTION_STATE', + 10: 'x86_DEBUG_STATE32', + 11: 'x86_DEBUG_STATE64', + 12: 'x86_DEBUG_STATE', + 13: 'THREAD_STATE_NONE', + 14: 'x86_SAVED_STATE_1 (INTERNAL ONLY)', + 15: 'x86_SAVED_STATE_2 (INTERNAL ONLY)', + 16: 'x86_AVX_STATE32', + 17: 'x86_AVX_STATE64', + 18: 'x86_AVX_STATE' + } + + flags = { + 1: 'NOUNDEFS', + 2: 'INCRLINK', + 4: 'DYLDLINK', + 8: 'BINDATLOAD', + 16: 'PREBOUND', + 32: 'SPLIT_SEGS', + 64: 'LAZY_INIT', + 128: 'TWOLEVEL', + 256: 'FORCE_FLAT', + 512: 'NOMULTIDEFS', + 1024: 'NOFIXPREBINDING', + 2048: 'PREBINDABLE', + 4096: 'ALLMODSBOUND', + 8192: 'SUBSECTIONS_VIA_SYMBOLS', + 16384: 'CANONICAL', + 32768: 'WEAK_DEFINES', + 65536: 'BINDS_TO_WEAK', + 131072: 'ALLOW_STACK_EXECUTION', + 262144: 'ROOT_SAFE', + 524288: 'SETUID_SAFE', + 1048576: 'NOREEXPORTED_DYLIBS', + 2097152: 'PIE', + 4194304: 'DEAD_STRIPPABLE_DYLIB', + 8388608: 'HAS_TLV_DESCRIPTORS', + 16777216: 'NO_HEAP_EXECUTION', + 33554432: 'APP_EXTENSION_SAFE' + } + + stabs = { + 0x20: 'GSYM', + 0x22: 'FNAME', + 0x24: 'FUN', + 0x26: 'STSYM', + 0x28: 'LCSYM', + 0x2a: 'MAIN', + 0x2e: 'BNSYM', + 0x30: 'PC', + 0x32: 'AST', + 0x3a: 'MAC_UNDEF', + 0x3c: 'OPT', + 0x40: 'RSYM', + 0x44: 'SLINE', + 0x46: 'DSLINE', + 0x48: 'BSLINE', + 0x4e: 'ENSYM', + 0x60: 'SSYM', + 0x64: 'SO', + 0x66: 'OSO', + 0x80: 'LSYM', + 0x82: 'BINCL', + 0x84: 'SOL', + 0x86: 'PARAMS', + 0x88: 'VERSION', + 0x8a: 'OLEVEL', + 0xa0: 'PSYM', + 0xa2: 'EINCL', + 0xa4: 'ENTRY', + 0xc0: 'LBRAC', + 0xc2: 'EXCL', + 0xe0: 'RBRAC', + 0xe2: 'BCOMM', + 0xe4: 'ECOMM', + 0xe8: 'ECOML', + 0xfe: 'LENG' + } + + loadcommands = { + 1: 'SEGMENT', + 2: 'SYMTAB', + 3: 'SYMSEG', + 4: 'THREAD', + 5: 'UNIXTHREAD', + 6: 'LOADFVMLIB', + 7: 'IDFVMLIB', + 8: 'IDENT', + 9: 'FVMFILE', + 10: 'PREPAGE', + 11: 'DYSYMTAB', + 12: 'LOAD_DYLIB', + 13: 'ID_DYLIB', + 14: 'LOAD_DYLINKER', + 15: 'ID_DYLINKER', + 16: 'PREBOUND_DYLIB', + 17: 'ROUTINES', + 18: 'SUB_FRAMEWORK', + 19: 'SUB_UMBRELLA', + 20: 'SUB_CLIENT', + 21: 'SUB_LIBRARY', + 22: 'TWOLEVEL_HINTS', + 23: 'PREBIND_CKSUM', + 25: 'SEGMENT_64', + 26: 'ROUTINES_64', + 27: 'UUID', + 29: 'CODE_SIGNATURE', + 30: 'SEGMENT_SPLIT_INFO', + 32: 'LAZY_LOAD_DYLIB', + 33: 'ENCRYPTION_INFO', + 34: 'DYLD_INFO', + 36: 'VERSION_MIN_MACOSX', + 37: 'VERSION_MIN_IPHONEOS', + 38: 'FUNCTION_STARTS', + 39: 'DYLD_ENVIRONMENT', + 41: 'DATA_IN_CODE', + 42: 'SOURCE_VERSION', + 43: 'DYLIB_CODE_SIGN_DRS', + 44: 'ENCRYPTION_INFO_64', + 45: 'LINKER_OPTION', + 46: 'LINKER_OPTIMIZATION_HINT', + 47: 'VERSION_MIN_TVOS', + 48: 'VERSION_MIN_WATCHOS', + 49: 'NOTE', + 50: 'BUILD_VERSION', + 2147483672: 'LOAD_WEAK_DYLIB', + 2147483676: 'RPATH', + 2147483679: 'REEXPORT_DYLIB', + 2147483682: 'DYLD_INFO_ONLY', + 2147483683: 'LOAD_UPWARD_DYLIB', + 2147483688: 'MAIN', + } + + # CPU Types & Subtypes as defined in + # http://opensource.apple.com/source/cctools/cctools-822/include/mach/machine.h + cputypes = { + -1: { + -2: 'ANY', + -1: 'MULTIPLE', + 0: 'LITTLE_ENDIAN', + 1: 'BIG_ENDIAN' + }, + 1: { + -2: 'VAX', + -1: 'MULTIPLE', + 0: 'VAX_ALL', + 1: 'VAX780', + 2: 'VAX785', + 3: 'VAX750', + 4: 'VAX730', + 5: 'UVAXI', + 6: 'UVAXII', + 7: 'VAX8200', + 8: 'VAX8500', + 9: 'VAX8600', + 10: 'VAX8650', + 11: 'VAX8800', + 12: 'UVAXIII' + }, + 6: { + -2: 'MC680x0', + -1: 'MULTIPLE', + 1: 'MC680x0_ALL or MC68030', + 2: 'MC68040', + 3: 'MC68030_ONLY' + }, + 7: {-2: 'X86 (I386)', + -1: 'MULITPLE', + 0: 'INTEL_MODEL_ALL', + 3: 'X86_ALL, X86_64_ALL, I386_ALL, or 386', + 4: 'X86_ARCH1 or 486', + 5: '586 or PENT', + 8: 'X86_64_H or PENTIUM_3', + 9: 'PENTIUM_M', + 10: 'PENTIUM_4', + 11: 'ITANIUM', + 12: 'XEON', + 15: 'INTEL_FAMILY_MAX', + 22: 'PENTPRO', + 24: 'PENTIUM_3_M', + 26: 'PENTIUM_4_M', + 27: 'ITANIUM_2', + 28: 'XEON_MP', + 40: 'PENTIUM_3_XEON', + 54: 'PENTII_M3', + 86: 'PENTII_M5', + 103: 'CELERON', + 119: 'CELERON_MOBILE', + 132: '486SX' + }, + 10: { + -2: 'MC98000', + -1: 'MULTIPLE', + 0: 'MC98000_ALL', + 1: 'MC98601' + }, + 11: { + -2: 'HPPA', + -1: 'MULITPLE', + 0: 'HPPA_ALL or HPPA_7100', + 1: 'HPPA_7100LC' + }, + 12: { + -2: 'ARM', + -1: 'MULTIPLE', + 0: 'ARM_ALL', + 1: 'ARM_A500_ARCH', + 2: 'ARM_A500', + 3: 'ARM_A440', + 4: 'ARM_M4', + 5: 'ARM_V4T', + 6: 'ARM_V6', + 7: 'ARM_V5TEJ', + 8: 'ARM_XSCALE', + 9: 'ARM_V7', + 10: 'ARM_V7F', + 11: 'ARM_V7S', + 12: 'ARM_V7K', + 13: 'ARM_V8', + 14: 'ARM_V6M', + 15: 'ARM_V7M', + 16: 'ARM_V7EM' + }, + 13: { + -2: 'MC88000', + -1: 'MULTIPLE', + 0: 'MC88000_ALL', + 1: 'MMAX_JPC or MC88100', + 2: 'MC88110' + }, + 14: { + -2: 'SPARC', + -1: 'MULTIPLE', + 0: 'SPARC_ALL or SUN4_ALL', + 1: 'SUN4_260', + 2: 'SUN4_110' + }, + 15: { + -2: 'I860 (big-endian)', + -1: 'MULTIPLE', + 0: 'I860_ALL', + 1: 'I860_860' + }, + 18: { + -2: 'POWERPC', + -1: 'MULTIPLE', + 0: 'POWERPC_ALL', + 1: 'POWERPC_601', + 2: 'POWERPC_602', + 3: 'POWERPC_603', + 4: 'POWERPC_603e', + 5: 'POWERPC_603ev', + 6: 'POWERPC_604', + 7: 'POWERPC_604e', + 8: 'POWERPC_620', + 9: 'POWERPC_750', + 10: 'POWERPC_7400', + 11: 'POWERPC_7450', + 100: 'POWERPC_970' + }, + 16777223: { + -2: 'X86_64', + -1: 'MULTIPLE', + 0: 'INTEL_MODEL_ALL', + 3: 'X86_ALL, X86_64_ALL, I386_ALL, or 386', + 4: 'X86_ARCH1 or 486', + 5: '586 or PENT', + 8: 'X86_64_H or PENTIUM_3', + 9: 'PENTIUM_M', + 10: 'PENTIUM_4', + 11: 'ITANIUM', + 12: 'XEON', + 15: 'INTEL_FAMILY_MAX', + 22: 'PENTPRO', + 24: 'PENTIUM_3_M', + 26: 'PENTIUM_4_M', + 27: 'ITANIUM_2', + 28: 'XEON_MP', + 40: 'PENTIUM_3_XEON', + 54: 'PENTII_M3', + 86: 'PENTII_M5', + 103: 'CELERON', + 119: 'CELERON_MOBILE', + 132: '486SX', + 2147483648 + 0: 'INTEL_MODEL_ALL', + 2147483648 + 3: 'X86_ALL, X86_64_ALL, I386_ALL, or 386', + 2147483648 + 4: 'X86_ARCH1 or 486', + 2147483648 + 5: '586 or PENT', + 2147483648 + 8: 'X86_64_H or PENTIUM_3', + 2147483648 + 9: 'PENTIUM_M', + 2147483648 + 10: 'PENTIUM_4', + 2147483648 + 11: 'ITANIUM', + 2147483648 + 12: 'XEON', + 2147483648 + 15: 'INTEL_FAMILY_MAX', + 2147483648 + 22: 'PENTPRO', + 2147483648 + 24: 'PENTIUM_3_M', + 2147483648 + 26: 'PENTIUM_4_M', + 2147483648 + 27: 'ITANIUM_2', + 2147483648 + 28: 'XEON_MP', + 2147483648 + 40: 'PENTIUM_3_XEON', + 2147483648 + 54: 'PENTII_M3', + 2147483648 + 86: 'PENTII_M5', + 2147483648 + 103: 'CELERON', + 2147483648 + 119: 'CELERON_MOBILE', + 2147483648 + 132: '486SX' + }, + 16777228: { + -2: 'ARM64', + -1: 'MULTIPLE', + 0: 'ARM64_ALL', + 1: 'ARM64_V8', + 2147483648 + 0: 'ARM64_ALL', + 2147483648 + 1: 'ARM64_V8' + }, + 16777234: { + -2: 'POWERPC64', + -1: 'MULTIPLE', + 0: 'POWERPC_ALL', + 1: 'POWERPC_601', + 2: 'POWERPC_602', + 3: 'POWERPC_603', + 4: 'POWERPC_603e', + 5: 'POWERPC_603ev', + 6: 'POWERPC_604', + 7: 'POWERPC_604e', + 8: 'POWERPC_620', + 9: 'POWERPC_750', + 10: 'POWERPC_7400', + 11: 'POWERPC_7450', + 100: 'POWERPC_970', + 2147483648 + 0: 'POWERPC_ALL (LIB64)', + 2147483648 + 1: 'POWERPC_601 (LIB64)', + 2147483648 + 2: 'POWERPC_602 (LIB64)', + 2147483648 + 3: 'POWERPC_603 (LIB64)', + 2147483648 + 4: 'POWERPC_603e (LIB64)', + 2147483648 + 5: 'POWERPC_603ev (LIB64)', + 2147483648 + 6: 'POWERPC_604 (LIB64)', + 2147483648 + 7: 'POWERPC_604e (LIB64)', + 2147483648 + 8: 'POWERPC_620 (LIB64)', + 2147483648 + 9: 'POWERPC_750 (LIB64)', + 2147483648 + 10: 'POWERPC_7400 (LIB64)', + 2147483648 + 11: 'POWERPC_7450 (LIB64)', + 2147483648 + 100: 'POWERPC_970 (LIB64)' + } + } \ No newline at end of file diff --git a/emulated/nac.py b/emulated/nac.py new file mode 100644 index 0000000..b577997 --- /dev/null +++ b/emulated/nac.py @@ -0,0 +1,423 @@ +import hashlib +from . import mparser as macholibre +from .jelly import Jelly +import plistlib +import logging +logger = logging.getLogger("nac") + +BINARY_HASH = "e1181ccad82e6629d52c6a006645ad87ee59bd13" +BINARY_PATH = "emulated/IMDAppleServices" +BINARY_URL = "https://github.com/JJTech0130/nacserver/raw/main/IMDAppleServices" + +FAKE_DATA = plistlib.load(open("emulated/data.plist", "rb")) + +def load_binary() -> bytes: + # Open the file at BINARY_PATH, check the hash, and return the binary + # If the hash doesn't match, raise an exception + # Download the binary if it doesn't exist + import os, requests + if not os.path.exists(BINARY_PATH): + logger.info("Downloading IMDAppleServices") + resp = requests.get(BINARY_URL) + b = resp.content + # Save the binary + open(BINARY_PATH, "wb").write(b) + else: + logger.debug("Using already downloaded IMDAppleServices") + b = open(BINARY_PATH, "rb").read() + if hashlib.sha1(b).hexdigest() != BINARY_HASH: + raise Exception("Hashes don't match") + return b + + +def get_x64_slice(binary: bytes) -> bytes: + # Get the x64 slice of the binary + # If there is no x64 slice, raise an exception + p = macholibre.Parser(binary) + # Parse the binary to find the x64 slice + off, size = p.u_get_offset(cpu_type="X86_64") + return binary[off : off + size] + + +def nac_init(j: Jelly, cert: bytes): + # Allocate memory for the cert + cert_addr = j.malloc(len(cert)) + j.uc.mem_write(cert_addr, cert) + + # Allocate memory for the outputs + out_validation_ctx_addr = j.malloc(8) + out_request_bytes_addr = j.malloc(8) + out_request_len_addr = j.malloc(8) + + # Call the function + ret = j.instr.call( + 0xB1DB0, + [ + cert_addr, + len(cert), + out_validation_ctx_addr, + out_request_bytes_addr, + out_request_len_addr, + ], + ) + + #print(hex(ret)) + + if ret != 0: + n = ret & 0xffffffff + n = (n ^ 0x80000000) - 0x80000000 + raise Exception(f"Error calling nac_init: {n}") + + # Get the outputs + validation_ctx_addr = j.uc.mem_read(out_validation_ctx_addr, 8) + request_bytes_addr = j.uc.mem_read(out_request_bytes_addr, 8) + request_len = j.uc.mem_read(out_request_len_addr, 8) + + request_bytes_addr = int.from_bytes(request_bytes_addr, 'little') + request_len = int.from_bytes(request_len, 'little') + + logger.debug(f"Request @ {hex(request_bytes_addr)} : {hex(request_len)}") + + request = j.uc.mem_read(request_bytes_addr, request_len) + + validation_ctx_addr = int.from_bytes(validation_ctx_addr, 'little') + return validation_ctx_addr, request + +def nac_key_establishment(j: Jelly, validation_ctx: int, response: bytes): + response_addr = j.malloc(len(response)) + j.uc.mem_write(response_addr, response) + + ret = j.instr.call( + 0xB1DD0, + [ + validation_ctx, + response_addr, + len(response), + ], + ) + + if ret != 0: + n = ret & 0xffffffff + n = (n ^ 0x80000000) - 0x80000000 + raise Exception(f"Error calling nac_submit: {n}") + +def nac_sign(j: Jelly, validation_ctx: int): + #void *validation_ctx, void *unk_bytes, int unk_len, + # void **validation_data, int *validation_data_len + + out_validation_data_addr = j.malloc(8) + out_validation_data_len_addr = j.malloc(8) + + ret = j.instr.call( + 0xB1DF0, + [ + validation_ctx, + 0, + 0, + out_validation_data_addr, + out_validation_data_len_addr, + ], + ) + + if ret != 0: + n = ret & 0xffffffff + n = (n ^ 0x80000000) - 0x80000000 + raise Exception(f"Error calling nac_generate: {n}") + + validation_data_addr = j.uc.mem_read(out_validation_data_addr, 8) + validation_data_len = j.uc.mem_read(out_validation_data_len_addr, 8) + + validation_data_addr = int.from_bytes(validation_data_addr, 'little') + validation_data_len = int.from_bytes(validation_data_len, 'little') + + validation_data = j.uc.mem_read(validation_data_addr, validation_data_len) + + return validation_data + + +def hook_code(uc, address: int, size: int, user_data): + logger.debug(">>> Tracing instruction at 0x%x, instruction size = 0x%x" % (address, size)) + + +def malloc(j: Jelly, len: int) -> int: + # Hook malloc + # Return the address of the allocated memory + #print("malloc hook called with len = %d" % len) + return j.malloc(len) + + +def memset_chk(j: Jelly, dest: int, c: int, len: int, destlen: int): + logger.debug( + "memset_chk called with dest = 0x%x, c = 0x%x, len = 0x%x, destlen = 0x%x" + % (dest, c, len, destlen) + ) + j.uc.mem_write(dest, bytes([c]) * len) + return 0 + + +def sysctlbyname(j: Jelly): + return 0 # The output is not checked + + +def memcpy(j: Jelly, dest: int, src: int, len: int): + logger.debug("memcpy called with dest = 0x%x, src = 0x%x, len = 0x%x" % (dest, src, len)) + orig = j.uc.mem_read(src, len) + j.uc.mem_write(dest, bytes(orig)) + return 0 + +CF_OBJECTS = [] + +# struct __builtin_CFString { +# int *isa; // point to __CFConstantStringClassReference +# int flags; +# const char *str; +# long length; +# } +import struct + +def _parse_cfstr_ptr(j: Jelly, ptr: int) -> str: + size = struct.calcsize(" str: + data = j.uc.mem_read(ptr, 256) # Lazy way to do it + return data.split(b"\x00")[0].decode("utf-8") + +def IORegistryEntryCreateCFProperty(j: Jelly, entry: int, key: int, allocator: int, options: int): + key_str = _parse_cfstr_ptr(j, key) + if key_str in FAKE_DATA["iokit"]: + fake = FAKE_DATA["iokit"][key_str] + logger.debug(f"IOKit Entry: {key_str} -> {fake}") + # Return the index of the fake data in CF_OBJECTS + CF_OBJECTS.append(fake) + return len(CF_OBJECTS) # NOTE: We will have to subtract 1 from this later, can't return 0 here since that means NULL + else: + logger.debug(f"IOKit Entry: {key_str} -> None") + return 0 + +def CFGetTypeID(j: Jelly, obj: int): + obj = CF_OBJECTS[obj - 1] + if isinstance(obj, bytes): + return 1 + elif isinstance(obj, str): + return 2 + else: + raise Exception("Unknown CF object type") + +def CFDataGetLength(j: Jelly, obj: int): + obj = CF_OBJECTS[obj - 1] + if isinstance(obj, bytes): + return len(obj) + else: + raise Exception("Unknown CF object type") + +def CFDataGetBytes(j: Jelly, obj: int, range_start: int, range_end: int, buf: int): + obj = CF_OBJECTS[obj - 1] + if isinstance(obj, bytes): + data = obj[range_start:range_end] + j.uc.mem_write(buf, data) + logger.debug(f"CFDataGetBytes: {hex(range_start)}-{hex(range_end)} -> {hex(buf)}") + return len(data) + else: + raise Exception("Unknown CF object type") + +def CFDictionaryCreateMutable(j: Jelly) -> int: + CF_OBJECTS.append({}) + return len(CF_OBJECTS) + +def maybe_object_maybe_string(j: Jelly, obj: int): + # If it's already a str + if isinstance(obj, str): + return obj + elif obj > len(CF_OBJECTS): + return obj + #raise Exception(f"WTF: {hex(obj)}") + # This is probably a CFString + # return _parse_cfstr_ptr(j, obj) + else: + return CF_OBJECTS[obj - 1] + +def CFDictionaryGetValue(j: Jelly, d: int, key: int) -> int: + logger.debug(f"CFDictionaryGetValue: {d} {hex(key)}") + d = CF_OBJECTS[d - 1] + if key == 0xc3c3c3c3c3c3c3c3: + key = "DADiskDescriptionVolumeUUIDKey" # Weirdness, this is a hack + key = maybe_object_maybe_string(j, key) + if isinstance(d, dict): + if key in d: + val = d[key] + logger.debug(f"CFDictionaryGetValue: {key} -> {val}") + CF_OBJECTS.append(val) + return len(CF_OBJECTS) + else: + raise Exception("Key not found") + return 0 + else: + raise Exception("Unknown CF object type") + +def CFDictionarySetValue(j: Jelly, d: int, key: int, val: int): + d = CF_OBJECTS[d - 1] + key = maybe_object_maybe_string(j, key) + val = maybe_object_maybe_string(j, val) + if isinstance(d, dict): + d[key] = val + else: + raise Exception("Unknown CF object type") + +def DADiskCopyDescription(j: Jelly) -> int: + description = CFDictionaryCreateMutable(j) + CFDictionarySetValue(j, description, "DADiskDescriptionVolumeUUIDKey", FAKE_DATA["root_disk_uuid"]) + return description + +def CFStringCreate(j: Jelly, string: str) -> int: + CF_OBJECTS.append(string) + return len(CF_OBJECTS) + +def CFStringGetLength(j: Jelly, string: int) -> int: + string = CF_OBJECTS[string - 1] + if isinstance(string, str): + return len(string) + else: + raise Exception("Unknown CF object type") + +def CFStringGetCString(j: Jelly, string: int, buf: int, buf_len: int, encoding: int) -> int: + string = CF_OBJECTS[string - 1] + if isinstance(string, str): + data = string.encode("utf-8") + j.uc.mem_write(buf, data) + logger.debug(f"CFStringGetCString: {string} -> {hex(buf)}") + return len(data) + else: + raise Exception("Unknown CF object type") + +def IOServiceMatching(j: Jelly, name: int) -> int: + # Read the raw c string pointed to by name + name = _parse_cstr_ptr(j, name) + logger.debug(f"IOServiceMatching: {name}") + # Create a CFString from the name + name = CFStringCreate(j, name) + # Create a dictionary + d = CFDictionaryCreateMutable(j) + # Set the key "IOProviderClass" to the name + CFDictionarySetValue(j, d, "IOProviderClass", name) + # Return the dictionary + return d + +def IOServiceGetMatchingService(j: Jelly) -> int: + return 92 + +ETH_ITERATOR_HACK = False +def IOServiceGetMatchingServices(j: Jelly, port, match, existing) -> int: + global ETH_ITERATOR_HACK + ETH_ITERATOR_HACK = True + # Write 93 to existing + j.uc.mem_write(existing, bytes([93])) + return 0 + +def IOIteratorNext(j: Jelly, iterator: int) -> int: + global ETH_ITERATOR_HACK + if ETH_ITERATOR_HACK: + ETH_ITERATOR_HACK = False + return 94 + else: + return 0 + +def bzero(j: Jelly, ptr: int, len: int): + j.uc.mem_write(ptr, bytes([0]) * len) + return 0 + +def IORegistryEntryGetParentEntry(j: Jelly, entry: int, _, parent: int) -> int: + j.uc.mem_write(parent, bytes([entry + 100])) + return 0 + +import requests, plistlib +def get_cert(): + resp = requests.get("http://static.ess.apple.com/identity/validation/cert-1.0.plist") + resp = plistlib.loads(resp.content) + return resp["cert"] + +def get_session_info(req: bytes) -> bytes: + body = { + 'session-info-request': req, + } + body = plistlib.dumps(body) + resp = requests.post("https://identity.ess.apple.com/WebObjects/TDIdentityService.woa/wa/initializeValidation", data=body, verify=False) + resp = plistlib.loads(resp.content) + return resp["session-info"] + +def arc4random(j: Jelly) -> int: + import random + return random.randint(0, 0xFFFFFFFF) + #return 0 + +def load_nac() -> Jelly: + binary = load_binary() + binary = get_x64_slice(binary) + # Create a Jelly object from the binary + j = Jelly(binary) + + hooks = { + "_malloc": malloc, + "___stack_chk_guard": lambda: 0, + "___memset_chk": memset_chk, + "_sysctlbyname": lambda _: 0, + "_memcpy": memcpy, + "_kIOMasterPortDefault": lambda: 0, + "_IORegistryEntryFromPath": lambda _: 1, + "_kCFAllocatorDefault": lambda: 0, + "_IORegistryEntryCreateCFProperty": IORegistryEntryCreateCFProperty, + "_CFGetTypeID": CFGetTypeID, + "_CFStringGetTypeID": lambda _: 2, + "_CFDataGetTypeID": lambda _: 1, + "_CFDataGetLength": CFDataGetLength, + "_CFDataGetBytes": CFDataGetBytes, + "_CFRelease": lambda _: 0, + "_IOObjectRelease": lambda _: 0, + "_statfs$INODE64": lambda _: 0, + "_DASessionCreate": lambda _: 201, + "_DADiskCreateFromBSDName": lambda _: 202, + "_kDADiskDescriptionVolumeUUIDKey": lambda: 0, + "_DADiskCopyDescription": DADiskCopyDescription, + "_CFDictionaryGetValue": CFDictionaryGetValue, + "_CFUUIDCreateString": lambda _, __, uuid: uuid, + "_CFStringGetLength": CFStringGetLength, + "_CFStringGetMaximumSizeForEncoding": lambda _, length, __: length, + "_CFStringGetCString": CFStringGetCString, + "_free": lambda _: 0, + "_IOServiceMatching": IOServiceMatching, + "_IOServiceGetMatchingService": IOServiceGetMatchingService, + "_CFDictionaryCreateMutable": CFDictionaryCreateMutable, + "_kCFBooleanTrue": lambda: 0, + "_CFDictionarySetValue": CFDictionarySetValue, + "_IOServiceGetMatchingServices": IOServiceGetMatchingServices, + "_IOIteratorNext": IOIteratorNext, + "___bzero": bzero, + "_IORegistryEntryGetParentEntry": IORegistryEntryGetParentEntry, + "_arc4random": arc4random + } + j.setup(hooks) + + return j + +def generate_validation_data() -> bytes: + logger.info("Generating validation data") + j = load_nac() + logger.debug("Loaded NAC library") + val_ctx, req = nac_init(j,get_cert()) + logger.debug("Initialized NAC") + session_info = get_session_info(req) + logger.debug("Got session info") + nac_key_establishment(j, val_ctx, session_info) + logger.debug("Submitted session info") + val_data = nac_sign(j, val_ctx) + logger.info("Generated validation data") + return bytes(val_data) + +if __name__ == "__main__": + from base64 import b64encode + val_data = generate_validation_data() + logger.info(f"Validation Data: {b64encode(val_data).decode()}") + #main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2227077..3c11969 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ cryptography wheel tlslite-ng==0.8.0a43 srp -pbkdf2 \ No newline at end of file +pbkdf2 +unicorn \ No newline at end of file