diff --git a/qiling/debugger/gdb/gdb.py b/qiling/debugger/gdb/gdb.py index f6d6498d8..658f99264 100644 --- a/qiling/debugger/gdb/gdb.py +++ b/qiling/debugger/gdb/gdb.py @@ -680,9 +680,15 @@ def handle_s(subcmd: str) -> Reply: self.gdb.resume_emu(steps=1) - # if emulation has been stopped, signal program termination - if self.ql.emu_state is QL_STATE.STOPPED: - return f'S{SIGTERM:02x}' + # NOTE: emu_state cannot be used to detect program termination here, since + # emu_start unconditionally leaves it STOPPED once it returns. instead, + # determine termination by checking whether the step ran the program all + # the way to its exit point. + effective_pc = getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) + + if effective_pc == self.gdb.exit_point: + # the program has run to completion + return f'W{self.ql.os.exit_code:02x}' # otherwise, this is just single stepping return f'S{SIGTRAP:02x}' diff --git a/qiling/debugger/gdb/xmlregs.py b/qiling/debugger/gdb/xmlregs.py index 6bc2371f4..ac7e6b421 100644 --- a/qiling/debugger/gdb/xmlregs.py +++ b/qiling/debugger/gdb/xmlregs.py @@ -93,6 +93,29 @@ def __wrapped(href: str, parse, encoding=None): # inline all xi:include elements ElementInclude.include(tree.getroot(), loader=my_loader(base_url)) + # gdb sizes a target's register set according to its osabi and fails an internal + # 'tdesc_use_registers' assertion when the target description advertises more + # registers than that osabi models. the shared target xml files are Linux-centric + # and include register banks that other osabis do not support, so drop the ones + # that are irrelevant for the current os before handing the description to gdb. + unsupported: Tuple[str, ...] = () + + # Linux-specific registers (e.g. orig_eax / orig_rax) are invalid elsewhere. + if ostype != QL_OS.LINUX: + unsupported += ('.linux',) + + # Darwin's x86 gdb only models the core and SSE register banks; advertising the + # extended banks (segments, AVX, AVX-512, MPX, PKEYS) makes gdb assert on attach. + if ostype == QL_OS.MACOS and archtype in (QL_ARCH.X86, QL_ARCH.X8664): + unsupported += ('.segments', '.avx', '.avx512', '.mpx', '.pkeys') + + if unsupported: + root = tree.getroot() + + for feature in root.findall('feature'): + if feature.get('name', '').endswith(unsupported): + root.remove(feature) + # patch xml osabi element with the appropriate abi tag osabi = tree.find('osabi') diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index a7e7f87bc..0a1c4e587 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -20,7 +20,7 @@ from qiling.os.macos.task import MachoTask from qiling.os.macos.kernel_func import FileSystem, map_commpage from qiling.os.macos.mach_port import MachPort, MachPortManager -from qiling.os.macos.subsystems import MachHostServer, MachTaskServer +from qiling.os.macos.subsystems import MachHostServer, MachTaskServer, MachThreadServer from qiling.os.macos.utils import env_dict_to_array, page_align_end from qiling.os.macos.thread import QlMachoThreadManagement, QlMachoThread @@ -29,8 +29,10 @@ def load_commpage(ql): if ql.arch.type == QL_ARCH.X8664: COMM_PAGE_START_ADDRESS = X8664_COMM_PAGE_START_ADDRESS - else: + elif ql.arch.type == QL_ARCH.ARM64: COMM_PAGE_START_ADDRESS = ARM64_COMM_PAGE_START_ADDRESS + else: + raise NotImplementedError ql.mem.write(COMM_PAGE_START_ADDRESS + COMM_PAGE_SIGNATURE, b'\x00') ql.mem.write(COMM_PAGE_START_ADDRESS + COMM_PAGE_CPU_CAPABILITIES64, b'\x00\x00\x00\x00') @@ -89,12 +91,23 @@ def __init__(self, ql, dyld_path=None): self.kext_name = None def run(self): - self.profile = self.ql.profile - stack_address = int(self.profile.get("OS64", "stack_address"), 16) - stack_size = int(self.profile.get("OS64", "stack_size"), 16) - vmmap_trap_address = int(self.profile.get("OS64", "vmmap_trap_address"), 16) - self.heap_address = int(self.profile.get("OS64", "heap_address"), 16) - self.heap_size = int(self.profile.get("OS64", "heap_size"), 16) + self.profile = self.ql.profile + + if self.ql.arch.type == QL_ARCH.X86: + stack_address = int(self.profile.get("OS32", "stack_address"), 16) + stack_size = int(self.profile.get("OS32", "stack_size"), 16) + vmmap_trap_address = int(self.profile.get("OS32", "vmmap_trap_address"), 16) + heap_address = int(self.profile.get("OS32", "heap_address"), 16) + heap_size = int(self.profile.get("OS32", "heap_size"), 16) + else: + stack_address = int(self.profile.get("OS64", "stack_address"), 16) + stack_size = int(self.profile.get("OS64", "stack_size"), 16) + vmmap_trap_address = int(self.profile.get("OS64", "vmmap_trap_address"), 16) + heap_address = int(self.profile.get("OS64", "heap_address"), 16) + heap_size = int(self.profile.get("OS64", "heap_size"), 16) + + self.heap_address = heap_address + self.heap_size = heap_size self.stack_address = stack_address self.stack_size = stack_size @@ -113,9 +126,10 @@ def run(self): self.ql.os.macho_port_manager = MachPortManager(self.ql, self.ql.os.macho_mach_port) self.ql.os.macho_host_server = MachHostServer(self.ql) self.ql.os.macho_task_server = MachTaskServer(self.ql) - + self.ql.os.macho_thread_server = MachThreadServer(self.ql) + self.envs = env_dict_to_array(self.env) - self.apples = self.ql.os.path.transform_to_relative_path(self.ql.path) + self.apples = [self.ql.os.path.transform_to_relative_path(self.ql.path)] self.ql.os.heap = QlMemoryHeap(self.ql, self.heap_address, self.heap_address + self.heap_size) # FIXME: Not working due to overlarge mapping, need to fix it @@ -131,10 +145,16 @@ def run(self): self.macho_file = MachoParser(self.ql, self.ql.path) self.is_driver = (self.macho_file.header.file_type == 0xb) self.loading_file = self.macho_file - self.slide = int(self.profile.get("LOADER", "slide"), 16) - self.dyld_slide = int(self.profile.get("LOADER", "dyld_slide"), 16) - self.string_align = 8 - self.ptr_align = 8 + if self.ql.arch.type == QL_ARCH.X86: + slide = int(self.profile.get("LOADER32", "slide"), 16) + dyld_slide = int(self.profile.get("LOADER32", "dyld_slide"), 16) + else: + slide = int(self.profile.get("LOADER", "slide"), 16) + dyld_slide = int(self.profile.get("LOADER", "dyld_slide"), 16) + self.slide = slide + self.dyld_slide = dyld_slide + self.string_align = 4 if self.ql.arch.type == QL_ARCH.X86 else 8 + self.ptr_align = 4 if self.ql.arch.type == QL_ARCH.X86 else 8 self.binary_entry = 0x0 self.proc_entry = 0x0 self.argvs = [self.ql.path] @@ -152,6 +172,74 @@ def run(self): self.init_sp = self.ql.arch.regs.arch_sp self.ql.os.macho_task.min_offset = page_align_end(self.vm_end_addr, PAGE_SIZE) + if self.ql.arch.type == QL_ARCH.X86: + # TODO: move to commpage? + + def commpage_install_syscall_jump(addr, func_num): + # b8 XX XX XX XX MOV EAX,func_num + self.ql.mem.write_ptr(addr, 0xb8, 1) + addr += 1 + self.ql.mem.write_ptr(addr, func_num, 4) + addr += 4 + # cd 80 INT 0x82 + self.ql.mem.write_ptr(addr, 0x82cd, 2) + addr += 2 + # c3 RET + self.ql.mem.write_ptr(addr, 0xc3, 1) + addr += 1 + + # address of "real" bzero + # ref. https://fdiv.net/2009/01/14/memset-vs-bzero-ultimate-showdown + commpage_install_syscall_jump(0xffff0600, 0x0000ffff) + + # address of "real" memcpy + commpage_install_syscall_jump(0xffff07a0, 0x0000fffe) + + # address of "real" mach_absolute_time + commpage_install_syscall_jump(0xffff1700, 0x0000fffd) + + # ___commpage_gettimeofday + addr = 0xffff02e0 + # b8 00 + self.ql.mem.write_ptr(addr, 0xb8, 1) + addr += 1 + # TODO: just returning 0 for now + self.ql.mem.write_ptr(addr, 0x00000000, 4) + addr += 4 + # c3 RET + self.ql.mem.write_ptr(addr, 0xc3, 1) + addr += 1 + + # OSAtomicCompareAndSwap64 invokes it via `call [0xffff00c0]`, so the slot + # must hold the address of the routine rather than the routine itself. + # + # the routine follows the commpage register ABI: + # edx:eax = old value, ecx:ebx = new value, esi = pointer to the value, + # ZF is set when the swap succeeds. + # that is exactly a `lock cmpxchg8b [esi]`, so emit it natively and let the + # CPU set ZF (and reload edx:eax on failure) as the caller expects. + slot = 0xffff00c0 + routine = slot + self.ql.arch.pointersize + # f0 0f c7 0e LOCK CMPXCHG8B [ESI] + # c3 RET + self.ql.mem.write(routine, b"\xf0\x0f\xc7\x0e\xc3") + self.ql.mem.write_ptr(slot, routine, self.ql.arch.pointersize) + + # OSAtomicCompareAndSwap32 is the 32-bit counterpart, invoked via + # `call [0xffff0080]`, so the slot again holds the routine address. + # + # its C wrapper (_OSAtomicCompareAndSwap32) loads the arguments into + # registers before the call, using a different ABI than the 64-bit one: + # eax = old value, edx = new value, ecx = pointer to the value, + # ZF is set when the swap succeeds (and eax is reloaded on failure). + # that is exactly a `lock cmpxchg [ecx], edx`, so emit it natively too. + slot = 0xffff0080 + routine = slot + self.ql.arch.pointersize + # f0 0f b1 11 LOCK CMPXCHG [ECX], EDX + # c3 RET + self.ql.mem.write(routine, b"\xf0\x0f\xb1\x11\xc3") + self.ql.mem.write_ptr(slot, routine, self.ql.arch.pointersize) + def loadDriver(self, stack_addr, loadbase = -1, argv = [], env = {}): self.import_symbols = {} PAGE_SIZE = 0x1000 @@ -353,7 +441,10 @@ def loadDriver(self, stack_addr, loadbase = -1, argv = [], env = {}): self.slide = loadbase def loadMacho(self, depth=0, isdyld=False): - mmap_address = int(self.profile.get("OS64", "mmap_address"), 16) + if self.ql.arch.type == QL_ARCH.X86: + mmap_address = int(self.profile.get("OS32", "mmap_address"), 16) + else: + mmap_address = int(self.profile.get("OS64", "mmap_address"), 16) # MAX load depth if depth > 5: @@ -386,7 +477,7 @@ def loadMacho(self, depth=0, isdyld=False): if pass_count == 2: if cmd.cmd_id == LC_SEGMENT: - pass + self.loadSegment64(cmd, isdyld) if cmd.cmd_id == LC_SEGMENT_64: self.loadSegment64(cmd, isdyld) @@ -414,6 +505,7 @@ def loadMacho(self, depth=0, isdyld=False): self.ql.log.info("Dyld entry point: {}".format(hex(self.entry_point))) else: self.entry_point = self.proc_entry + self.slide + self.ql.os.entry_point = self.entry_point self.ql.log.info("Binary Entry Point: 0x{:X}".format(self.binary_entry)) self.macho_entry = self.binary_entry + self.slide self.load_address = self.macho_entry @@ -578,9 +670,9 @@ def push_stack_addr(self, data): align = self.ptr_align if data == 0: - content = b'\x00\x00\x00\x00\x00\x00\x00\x00' + content = b'\x00\x00\x00\x00'if self.ql.arch.type == QL_ARCH.X86 else b'\x00\x00\x00\x00\x00\x00\x00\x00' else: - content = struct.pack(' Optional[int]: + if arch == QL_ARCH.X86: + return CPU_TYPE_X86 + if arch == QL_ARCH.X8664: + return CPU_TYPE_X8664 + elif arch == QL_ARCH.ARM64: + return CPU_TYPE_ARM64 + else: + return None + def getBinary(self, arch): + cpu_type = self.archToCPUType(arch) for item in self.binarys: - if item.cpu_type == CPU_TYPE_X8664: + if item.cpu_type == cpu_type: return item - elif item.cpu_type == CPU_TYPE_ARM64: - return item return None class FatInfo: diff --git a/qiling/loader/macho_parser/parser.py b/qiling/loader/macho_parser/parser.py index 2b38914cb..e91f41fb0 100644 --- a/qiling/loader/macho_parser/parser.py +++ b/qiling/loader/macho_parser/parser.py @@ -57,10 +57,10 @@ def parseHeader(self): self.ql.log.debug("Got a 64bit Header ") self.header = BinaryHeader(self.binary_file) - #elif self.magic in MAGIC_X86: - # # x86 - # ql.log.debug("Got a x86 Header") - # self.header = BinaryHeader(self.binary_file) + elif self.magic in MAGIC_32: + # x86 + self.ql.log.debug("Got a x86 Header") + self.header = BinaryHeader(self.binary_file) elif self.magic in MAGIC_FAT: # fat diff --git a/qiling/os/macos/const.py b/qiling/os/macos/const.py index e3799f014..aa12aaf90 100644 --- a/qiling/os/macos/const.py +++ b/qiling/os/macos/const.py @@ -488,9 +488,41 @@ HOST_PREFERRED_USER_ARCH = 12 -# commpage +# mach msg header bits +MACH_MSGH_BITS_COMPLEX = 0x80000000 + +# msgh_bits packs the remote and local port-right dispositions into the low two +# bytes of mach_msg_header_t.msgh_bits (osfmk/mach/message.h). MACH_MSGH_BITS() +# composes them the same way the kernel macro does. +MACH_MSGH_BITS_REMOTE_MASK = 0x000000ff +MACH_MSGH_BITS_LOCAL_MASK = 0x0000ff00 + +def MACH_MSGH_BITS(remote, local): + return (remote & MACH_MSGH_BITS_REMOTE_MASK) | ((local << 8) & MACH_MSGH_BITS_LOCAL_MASK) + +# mach msg descriptor types (mach_msg_descriptor_type_t) +MACH_MSG_PORT_DESCRIPTOR = 0 +MACH_MSG_OOL_DESCRIPTOR = 1 +MACH_MSG_OOL_PORTS_DESCRIPTOR = 2 +MACH_MSG_OOL_VOLATILE_DESCRIPTOR = 3 + +# mach msg type names (mach_msg_type_name_t), used as port dispositions +MACH_MSG_TYPE_MOVE_RECEIVE = 16 +MACH_MSG_TYPE_MOVE_SEND = 17 +MACH_MSG_TYPE_MOVE_SEND_ONCE = 18 +MACH_MSG_TYPE_COPY_SEND = 19 +MACH_MSG_TYPE_MAKE_SEND = 20 +MACH_MSG_TYPE_MAKE_SEND_ONCE = 21 +MACH_MSG_TYPE_COPY_RECEIVE = 22 + +# max number of ports registered per task (returned by mach_ports_lookup) +TASK_PORT_REGISTER_MAX = 3 + + +# commpage X8664_COMM_PAGE_START_ADDRESS = 0x7FFFFFE00000 ARM64_COMM_PAGE_START_ADDRESS = 0x0000000FFFFFC000 +X86_COMM_PAGE_START_ADDRESS = 0xffff0000 COMM_PAGE_SIGNATURE = 0x000 # first 16 bytes are a signature COMM_PAGE_CPU_CAPABILITIES64 = 0x010 # uint64_t _cpu_capabilities diff --git a/qiling/os/macos/kernel_func.py b/qiling/os/macos/kernel_func.py index b967e0b40..4225a30c8 100644 --- a/qiling/os/macos/kernel_func.py +++ b/qiling/os/macos/kernel_func.py @@ -24,7 +24,12 @@ def map_commpage(ql): addr_size = 0x100000 elif ql.arch.type == QL_ARCH.ARM64: addr_base = ARM64_COMM_PAGE_START_ADDRESS - addr_size = 0x1000 + addr_size = 0x1000 + elif ql.arch.type == QL_ARCH.X86: + addr_base = X86_COMM_PAGE_START_ADDRESS + addr_size = 0x2000 + else: + raise NotImplementedError ql.mem.map(addr_base, addr_size, info="[commpage]") time_lock_slide = 0x68 ql.mem.write(addr_base+time_lock_slide, ql.pack32(0x1)) diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index 75790e238..6cd87e426 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -109,6 +109,18 @@ def __init__(self, ql, my_port): self.semaphore_port = MachPort(0x903) self.special_port = MachPort(0x707) self.my_port = my_port + # unregistered slots are MACH_PORT_NULL (0). + self.registered_ports = [self.special_port, MachPort(0), MachPort(0)] + # names for dynamically allocated ports (e.g. mach_port_allocate), + # kept above the statically assigned port names to avoid collisions. + self.next_port_name = 0x1000 + self.allocated_ports = [] + + def alloc_port(self): + port = MachPort(self.next_port_name) + self.next_port_name += 1 + self.allocated_ports.append(port) + return port def deal_with_msg(self, msg, addr): @@ -119,12 +131,47 @@ def deal_with_msg(self, msg, addr): elif msg.header.msgh_id == 206: out_msg = self.ql.os.macho_host_server.host_get_clock_service(msg.header, msg.content) out_msg.write_msg_to_mem(addr) - elif msg.header.msgh_id == 3418: - out_msg = self.ql.os.macho_task_server.semaphore_create(msg.header, msg.content) + # 3400 (Task operations) + elif msg.header.msgh_id == 3404: + out_msg = self.ql.os.macho_task_server.mach_ports_lookup(msg.header, msg.content) out_msg.write_msg_to_mem(addr) elif msg.header.msgh_id == 3409: out_msg = self.ql.os.macho_task_server.get_special_port(msg.header, msg.content) out_msg.write_msg_to_mem(addr) + elif msg.header.msgh_id == 3418: + out_msg = self.ql.os.macho_task_server.semaphore_create(msg.header, msg.content) + out_msg.write_msg_to_mem(addr) + # 3200 (Mach port handling functions) + elif msg.header.msgh_id == 3204: + # 4 (mach_port_allocate) + out_msg = self.ql.os.macho_task_server.mach_port_allocate(msg.header, msg.content) + out_msg.write_msg_to_mem(addr) + elif msg.header.msgh_id == 3206: + # 6 (mach_port_deallocate) + out_msg = self.ql.os.macho_task_server.mach_port_deallocate(msg.header, msg.content) + out_msg.write_msg_to_mem(addr) + # 3600 (Thread operations) + elif msg.header.msgh_id == 3616: + # 16 (thread_policy) + out_msg = self.ql.os.macho_thread_server.thread_policy(msg.header, msg.content) + out_msg.write_msg_to_mem(addr) + # 3800 (Virtual memory operations) + elif msg.header.msgh_id == 3801: + # 1 (vm_allocate) + out_msg = self.ql.os.macho_task_server.vm_allocate(msg.header, msg.content) + out_msg.write_msg_to_mem(addr) + elif msg.header.msgh_id == 3802: + # 2 (vm_deallocate) + out_msg = self.ql.os.macho_task_server.vm_deallocate(msg.header, msg.content) + out_msg.write_msg_to_mem(addr) + elif msg.header.msgh_id == 3803: + # 3 (vm_protect) + out_msg = self.ql.os.macho_task_server.vm_protect(msg.header, msg.content) + out_msg.write_msg_to_mem(addr) + elif msg.header.msgh_id == 3812: + # 12 (vm_map) + out_msg = self.ql.os.macho_task_server.vm_map(msg.header, msg.content) + out_msg.write_msg_to_mem(addr) else: self.ql.log.info("Error Mach Msgid {} can not handled".format(msg.header.msgh_id)) raise Exception("Mach Msgid Not Found") diff --git a/qiling/os/macos/macos.py b/qiling/os/macos/macos.py index 3c09d68a4..7886e3982 100644 --- a/qiling/os/macos/macos.py +++ b/qiling/os/macos/macos.py @@ -6,10 +6,10 @@ from ctypes import sizeof from unicorn import UcError -from unicorn.x86_const import UC_X86_INS_SYSCALL +from unicorn.x86_const import UC_X86_INS_SYSCALL, UC_X86_INS_SYSENTER from qiling import Qiling -from qiling.arch.x86_utils import GDTManager, SegmentManager64 +from qiling.arch.x86_utils import GDTManager, SegmentManager64, SegmentManager86 from qiling.cc import intel from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE from qiling.os.fcall import QlFunctionCall @@ -18,7 +18,7 @@ from qiling.os.macos.events.macos_policy import QlMacOSPolicy from qiling.os.macos.events.macos_structs import mac_policy_list_t from qiling.os.macos.structs import kmod_info_t, POINTER64 -from qiling.os.posix.syscall.abi import arm +from qiling.os.posix.syscall.abi import arm, intel as intel_abi class QlOsMacos(QlOsPosix): @@ -31,6 +31,8 @@ def __init__(self, ql: Qiling): # here anyway for completion if ql.arch.type is QL_ARCH.ARM64: self.syscall_abi = arm.QlAArch64MacOS(ql.arch) + elif ql.arch.type is QL_ARCH.X86: + self.syscall_abi = intel_abi.QlIntel32MacOS(ql.arch) self.fcall = QlFunctionCall(ql, intel.macosx64(ql.arch)) @@ -164,11 +166,45 @@ def load(self): self.ql.hook_insn(self.hook_syscall, UC_X86_INS_SYSCALL) - + elif self.ql.arch.type == QL_ARCH.X86: + gdtm = GDTManager(self.ql) + + # setup gdt and segments selectors + segm = SegmentManager86(self.ql.arch, gdtm) + segm.setup_cs_ds_ss_es(0, 4 << 30) + + self.ql.hook_insn(self.hook_sysenter, UC_X86_INS_SYSENTER) + self.ql.hook_intno(self.hook_syscall, 0x80) + self.ql.hook_intno(self.hook_syscall, 0x82) # machdep + + def hook_syscall(self, ql, intno = None): return self.load_syscall() + def hook_sysenter(self, ql, intno=None): + # emulate a MacOS i386 fast system call. + # + # the '__sysenter_trap' trampoline pops the caller return address into + # edx and stores the user stack pointer in ecx before issuing sysenter: + # + # pop edx + # mov ecx, esp + # sysenter + # + # since sysenter does not push a return address, the kernel resumes user + # execution at edx (via sysexit) with esp restored from ecx. we capture + # both before running the syscall as its handler may clobber the regs. + edx = self.ql.arch.regs.edx + ecx = self.ql.arch.regs.ecx + + self.load_syscall() + + # emulate sysexit: return to the trampoline caller with the user stack + self.ql.arch.regs.arch_sp = ecx + self.ql.arch.regs.arch_pc = edx - 2 + + def hook_sigtrap(self, ql, intno): self.ql.log.info("Trap Found") self.emu_error() diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index feb1cd38b..ff302eb9e 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -9,12 +9,14 @@ def get_syscall_mapper(archtype: QL_ARCH): syscall_table = { QL_ARCH.X8664 : x8664_syscall_table, - QL_ARCH.ARM64 : arm64_syscall_table + QL_ARCH.ARM64 : arm64_syscall_table, + QL_ARCH.X86 : x86_syscall_table, }[archtype] syscall_fixup = { QL_ARCH.X8664 : lambda n: (n - 0x2000000) if 0x2000000 <= n <= 0x3000000 else n, - QL_ARCH.ARM64 : lambda n: (n - 0xffffffffffffff00) if n >= 0xffffffffffffff00 else n + QL_ARCH.ARM64 : lambda n: (n - 0xffffffffffffff00) if n >= 0xffffffffffffff00 else n, + QL_ARCH.X86 : lambda n: n, }[archtype] def __mapper(syscall_num: int) -> str: @@ -22,6 +24,46 @@ def __mapper(syscall_num: int) -> str: return __mapper +x86_syscall_table = { + # machdep + 0x3: 'thread_fast_set_cthread_self', + # syscalls + 20: 'getpid', + 25: 'geteuid', + 53: 'sigaltstack', + 197: 'mmap', + 202: 'sysctl', + 327: 'issetugid', + 0xc0003: 'read', + 0xc0005: 'open', + 0xc0036: 'ioctl', + 0xc004b: 'madvise', + 0xc005c: 'fcntl', + 0xc016e: 'bsdthread_register', + 0xc018c: 'read_nocancel', + 0xc018d: 'write_nocancel', + 0xc018e: 'open_nocancel', + 0x40001: 'exit', + 0x40006: 'close', + 0x4018f: 'close_nocancel', + 0xc0030: 'sigprocmask', + 0xc0196: 'fcntl_nocancel', + 0x80049: 'munmap', + 0x800bc: 'stat', + 0x80152: 'stat64', + 0x80153: 'fstat64', + 0x80154: 'lstat64', + 0x140099: 'pread', + 0xffffffe1: 'mach_msg_trap', + 0xffffffe3: 'host_self_trap', + 0xffffffe4: 'task_self_trap', + 0xffffffe5: 'thread_self_trap', + 0xffffffe6: 'mach_reply_port', + # my stuff... + 0x0000fffd: 'my_mach_absolute_time', + 0x0000fffe: 'my_memcpy', + 0x0000ffff: 'my_bzero', +} arm64_syscall_table = { 1: 'exit', @@ -146,7 +188,7 @@ def __mapper(syscall_num: int) -> str: 194: 'getrlimit', 195: 'setrlimit', 196: 'getdirentries', - 197: 'mmap2', + 197: 'mmap', 199: 'lseek', 200: 'truncate', 201: 'ftruncate', @@ -533,7 +575,7 @@ def __mapper(syscall_num: int) -> str: 194: 'getrlimit', 195: 'setrlimit', 196: 'getdirentries', - 197: 'mmap2', + 197: 'mmap', 199: 'lseek', 200: 'truncate', 201: 'ftruncate', diff --git a/qiling/os/macos/subsystems.py b/qiling/os/macos/subsystems.py index e7969c2cd..fec1dcb3d 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -15,6 +15,7 @@ from qiling.const import * from .mach_port import * from .const import * +from .utils import page_align_end class MachHostServer(): @@ -39,7 +40,7 @@ def host_info(self, in_header, in_content): # gen reply if flavor == HOST_BASIC_INFO: - out_msg.header.msgh_bits = 4608 + out_msg.header.msgh_bits = MACH_MSGH_BITS(0, MACH_MSG_TYPE_MOVE_SEND_ONCE) out_msg.header.msgh_size = 88 out_msg.header.msgh_remote_port = 0 out_msg.header.msgh_local_port = self.ql.os.macho_mach_port.name @@ -65,7 +66,7 @@ def host_info(self, in_header, in_content): out_msg.content += pack("= 12 else 0 + self.ql.log.debug("[mach] mach_port_deallocate(name: 0x%x)" % name) + + out_msg = MachMsg(self.ql) + out_msg.header.msgh_bits = MACH_MSGH_BITS(0, MACH_MSG_TYPE_MOVE_SEND_ONCE) + out_msg.header.msgh_size = 0x00000024 + out_msg.header.msgh_remote_port = 0x00000000 + out_msg.header.msgh_local_port = self.ql.os.macho_mach_port.name + out_msg.header.msgh_voucher_port = 0 + out_msg.header.msgh_id = 3306 + + out_msg.content += pack(" 0: + self.ql.os.macho_vmmap_end = self.ql.os.macho_vmmap_end - (self.ql.os.macho_vmmap_end & mask) + self.ql.os.macho_vmmap_end += mask + 1 + + vmmap_address = page_align_end(self.ql.os.macho_vmmap_end, PAGE_SIZE) + vmmap_end = page_align_end(vmmap_address + size, PAGE_SIZE) + self.ql.os.macho_vmmap_end = vmmap_end + self.ql.mem.map(vmmap_address, vmmap_end - vmmap_address) + + # Reply is a simple (non-complex) MIG message carrying the NDR record, + # the RetCode and the mapped address (__Reply__vm_map_t). + out_msg = MachMsg(self.ql) + out_msg.header.msgh_bits = MACH_MSGH_BITS(0, MACH_MSG_TYPE_MOVE_SEND_ONCE) + out_msg.header.msgh_size = 0x00000028 + out_msg.header.msgh_remote_port = 0x00000000 + out_msg.header.msgh_local_port = self.ql.os.macho_mach_port.name + out_msg.header.msgh_voucher_port = 0 + out_msg.header.msgh_id = 3912 + + out_msg.content += pack("> 32) & 0xffffffff # high dword + set_eflags_cf(ql, 0x0) # CF=0 -> success + +def ql_syscall_my_memcpy(ql, dest, src, size, *args, **kw): + ql.log.debug("my_memcpy(dest: 0x%x, src: 0x%x, size: %u)" % (dest, src, size)) + ql.mem.write(dest, bytes(ql.mem.read(src, size))) + return dest + +def ql_syscall_my_bzero(ql, ptr, n, *args, **kw): + ql.log.debug("my_bzero(ptr: 0x%x, n: %u)" % (ptr, n)) + ql.mem.write(ptr, bytes(n)) diff --git a/qiling/os/posix/const_mapping.py b/qiling/os/posix/const_mapping.py index dd95f717e..2a3184ad7 100644 --- a/qiling/os/posix/const_mapping.py +++ b/qiling/os/posix/const_mapping.py @@ -57,7 +57,8 @@ def get_open_flags_class(archtype: QL_ARCH, ostype: QL_OS) -> Union[Type[Flag], QL_OS.MACOS: { QL_ARCH.X86: macos_x86_open_flags, - QL_ARCH.X8664: macos_x86_open_flags + QL_ARCH.X8664: macos_x86_open_flags, + QL_ARCH.ARM64: macos_x86_open_flags, }, QL_OS.WINDOWS: { diff --git a/qiling/os/posix/syscall/abi/intel.py b/qiling/os/posix/syscall/abi/intel.py index 15d5ad200..cb909d498 100644 --- a/qiling/os/posix/syscall/abi/intel.py +++ b/qiling/os/posix/syscall/abi/intel.py @@ -2,6 +2,8 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework +from typing import Tuple + from unicorn.x86_const import ( UC_X86_REG_EAX, UC_X86_REG_EBX, UC_X86_REG_ECX, UC_X86_REG_EDX, UC_X86_REG_ESI, UC_X86_REG_EDI, UC_X86_REG_EBP, UC_X86_REG_RDI, @@ -21,6 +23,20 @@ class QlIntel32(QlSyscallABI): _retreg = UC_X86_REG_EAX +class QlIntel32MacOS(QlIntel32): + """System call ABI for Intel-based 32-bit MacOS. + Unlike Linux, MacOS follows the BSD calling convention in which syscall + arguments are passed on the stack rather than in registers. The syscall + number is still held in eax and the return value is set in eax. + """ + + def get_params(self, count: int) -> Tuple[int, ...]: + # the '__sysenter_trap' trampoline pops the return address into edx and + # points ecx (= esp) to the caller return address. the syscall arguments + # are laid out on the stack right after that return address slot. + return tuple(self.arch.stack_read((i + 1) * self.arch.pointersize) for i in range(count)) + + class QlIntel64(QlSyscallABI): """System call ABI for Intel-based 64-bit systems. """ diff --git a/qiling/os/posix/syscall/stat.py b/qiling/os/posix/syscall/stat.py index 5b153100a..1acdc0971 100644 --- a/qiling/os/posix/syscall/stat.py +++ b/qiling/os/posix/syscall/stat.py @@ -178,11 +178,42 @@ class MacOSStat(ctypes.Structure): ("st_qspare", ctypes.c_int64 * 2) ] - # No 32bit macos. _pack_ = 8 + +# 32bit macos (i386): sizeof(long) = sizeof(time_t) = 4, so every timespec field is +# 4 bytes wide. The i386 ABI packs 8-byte members (uint64/int64) on a 4-byte boundary. +class MacOSX86Stat(ctypes.Structure): + _fields_ = [ + ("st_dev", ctypes.c_int32), + ("st_mode", ctypes.c_uint16), + ("st_nlink", ctypes.c_uint16), + ("st_ino", ctypes.c_uint64), + ("st_uid", ctypes.c_uint32), + ("st_gid", ctypes.c_uint32), + ("st_rdev", ctypes.c_int32), + ("st_atime", ctypes.c_uint32), + ("st_atime_ns", ctypes.c_uint32), + ("st_mtime", ctypes.c_uint32), + ("st_mtime_ns", ctypes.c_uint32), + ("st_ctime", ctypes.c_uint32), + ("st_ctime_ns", ctypes.c_uint32), + ("st_birthtime", ctypes.c_uint32), + ("st_birthtime_ns", ctypes.c_uint32), + ("st_size", ctypes.c_int64), + ("st_blocks", ctypes.c_int64), + ("st_blksize", ctypes.c_int32), + ("st_flags", ctypes.c_uint32), + ("st_gen", ctypes.c_uint32), + ("st_lspare", ctypes.c_int32), + ("st_qspare", ctypes.c_int64 * 2) + ] + + _pack_ = 4 + # They are the same in source code. MacOSStat64 = MacOSStat +MacOSX86Stat64 = MacOSX86Stat # https://elixir.bootlin.com/linux/latest/source/arch/mips/include/uapi/asm/stat.h#L19 # @@ -1096,7 +1127,10 @@ def get_stat64_struct(ql: Qiling): elif ql.arch.type == QL_ARCH.PPC: return LinuxPPCStat64() elif ql.os.type == QL_OS.MACOS: - return MacOSStat64() + if ql.arch.type == QL_ARCH.X86: + return MacOSX86Stat64() + else: + return MacOSStat64() elif ql.os.type == QL_OS.QNX: return QNXARMStat64() ql.log.warning(f"Unrecognized arch && os with {ql.arch.type} and {ql.os.type} for stat64! Fallback to Linux x86.") @@ -1109,7 +1143,10 @@ def get_stat_struct(ql: Qiling): else: return FreeBSDX86Stat() elif ql.os.type == QL_OS.MACOS: - return MacOSStat() + if ql.arch.type == QL_ARCH.X86: + return MacOSX86Stat() + else: + return MacOSStat() elif ql.os.type == QL_OS.LINUX: if ql.arch.type == QL_ARCH.X8664: return LinuxX8664Stat() diff --git a/qiling/profiles/macos.ql b/qiling/profiles/macos.ql index b3624c0f1..0f6fdf9bf 100644 --- a/qiling/profiles/macos.ql +++ b/qiling/profiles/macos.ql @@ -1,8 +1,22 @@ +[LOADER32] +slide = 0x0000000 +dyld_slide = 0x5000000 + + [LOADER] slide = 0x0000000000000000 dyld_slide = 0x0000000500000000 +[OS32] +stack_address = 0xb8000000 +stack_size = 0x8000000 +vmmap_trap_address = 0x40000000 +mmap_address = 0x50000000 +heap_address = 0x5000000 +heap_size = 0x50000 + + [OS64] stack_address = 0x7ffcf0000000 stack_size = 0x19a00000 diff --git a/qiling/utils.py b/qiling/utils.py index 72e5c32df..e22c490cd 100644 --- a/qiling/utils.py +++ b/qiling/utils.py @@ -220,8 +220,9 @@ def __emu_env_from_macho(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], if ident[:4] in (macho_macos_sig32, macho_macos_sig64, macho_macos_fat): ostype = QL_OS.MACOS - # if ident[7] == 0: # 32 bit - # arch = QL_ARCH.X86 + if ident[7] == 0: # 32 bit + endian = QL_ENDIAN.EL + arch = QL_ARCH.X86 if ident[4] == 0x07 and ident[7] == 0x01: # X86 64 bit endian = QL_ENDIAN.EL diff --git a/qltool b/qltool index 5164468c3..3d416d3fd 100755 --- a/qltool +++ b/qltool @@ -274,6 +274,7 @@ def run(): }) ql = Qiling(**ql_args) + ql.add_fs_mapper("/dev/urandom", "/dev/urandom") # attach Qdb at entry point if options.qdb: