From c9395f82ab5a65e26f8d76532864fd137b5b9019 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Sat, 4 Jul 2026 01:32:42 +0200 Subject: [PATCH 01/40] WIP macos x86 support --- qiling/loader/macho.py | 52 ++++++++++++++++++++-------- qiling/loader/macho_parser/header.py | 5 ++- qiling/loader/macho_parser/parser.py | 8 ++--- qiling/os/macos/const.py | 1 + qiling/os/macos/kernel_func.py | 7 +++- qiling/os/macos/macos.py | 15 ++++++-- qiling/os/macos/map_syscall.py | 9 +++-- qiling/profiles/macos.ql | 14 ++++++++ qiling/utils.py | 5 +-- 9 files changed, 88 insertions(+), 28 deletions(-) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index a7e7f87bc..4a820c18d 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -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 = None # 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 @@ -131,10 +144,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 + self.ptr_align = 4 self.binary_entry = 0x0 self.proc_entry = 0x0 self.argvs = [self.ql.path] @@ -353,7 +372,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 +408,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) @@ -578,9 +600,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' else: - content = struct.pack('= 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,9 @@ def __mapper(syscall_num: int) -> str: return __mapper +x86_syscall_table = { + 0xffffffe4: 'task_self_trap', +} arm64_syscall_table = { 1: 'exit', diff --git a/qiling/profiles/macos.ql b/qiling/profiles/macos.ql index b3624c0f1..e551c1cf1 100644 --- a/qiling/profiles/macos.ql +++ b/qiling/profiles/macos.ql @@ -1,8 +1,22 @@ +[LOADER32] +slide = 0x0000000 +dyld_slide = 0x0000000 + + [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 From 8468bce488ba652812894c8282f475aaf8e74c48 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Sun, 5 Jul 2026 13:13:22 +0200 Subject: [PATCH 02/40] Handle "real" bzero loaded at 0xffff0600 --- qiling/loader/macho.py | 20 ++++++++++++++++++++ qiling/os/macos/map_syscall.py | 2 ++ qiling/os/macos/syscall.py | 6 ++++++ 3 files changed, 28 insertions(+) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index 4a820c18d..5aec16cee 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -171,6 +171,26 @@ 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: + # address of "real" bzero + # ref. https://fdiv.net/2009/01/14/memset-vs-bzero-ultimate-showdown + addr = 0xffff0600 + # b8 ff ff 00 00 MOV EAX,0xffff + self.ql.mem.write_ptr(addr, 0xb8, 1) + addr += 1 + self.ql.mem.write_ptr(addr, 0x0000ffff, 4) + addr += 4 + sysenter_trap_addr = 0x8fe2b7ac + offset = (sysenter_trap_addr - addr - 5) & 0xffffffff; + # e8 a2 b1 e3 8f CALL __sysenter_trap + self.ql.mem.write_ptr(addr, 0xe8, 1) + addr += 1 + self.ql.mem.write_ptr(addr, offset, 4) + addr += 4 + # c3 RET + self.ql.mem.write_ptr(addr, 0xc3, 1) + addr += 1 + def loadDriver(self, stack_addr, loadbase = -1, argv = [], env = {}): self.import_symbols = {} PAGE_SIZE = 0x1000 diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index adcd86a53..a73cc98a5 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -26,6 +26,8 @@ def __mapper(syscall_num: int) -> str: x86_syscall_table = { 0xffffffe4: 'task_self_trap', + # my stuff... + 0x0000ffff: 'my_bzero', } arm64_syscall_table = { diff --git a/qiling/os/macos/syscall.py b/qiling/os/macos/syscall.py index 8e3c720d6..71ed6a73a 100644 --- a/qiling/os/macos/syscall.py +++ b/qiling/os/macos/syscall.py @@ -432,3 +432,9 @@ def ql_syscall_thread_fast_set_cthread_self64(ql, u_info_addr, *args, **kw): ql.log.debug("[mdep] thread fast set cthread self64(tsd_base:0x%x)" % (u_info_addr)) ql.arch.msr.write(IA32_GS_BASE_MSR, u_info_addr) return KERN_SUCCESS + +# Other + +def ql_syscall_my_bzero(ql, ptr, n, *args, **kw): + ql.log.debug("bzero(ptr: 0x%x, n: %u)" % (ptr, n)) + ql.mem.write(ptr, bytes(n)) From 6f13106e6ae64085fd06363e7b41f0952e356cb5 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Sun, 5 Jul 2026 13:15:11 +0200 Subject: [PATCH 03/40] WIP proper sysenter ABI --- qiling/os/macos/macos.py | 29 ++++++++++++++++++++++++++-- qiling/os/posix/syscall/abi/intel.py | 16 +++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/qiling/os/macos/macos.py b/qiling/os/macos/macos.py index c1410eeb9..638d5d42e 100644 --- a/qiling/os/macos/macos.py +++ b/qiling/os/macos/macos.py @@ -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)) @@ -171,13 +173,36 @@ def load(self): segm = SegmentManager86(self.ql.arch, gdtm) segm.setup_cs_ds_ss_es(0, 4 << 30) - self.ql.hook_insn(self.hook_syscall, UC_X86_INS_SYSENTER) + self.ql.hook_insn(self.hook_sysenter, UC_X86_INS_SYSENTER) 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/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. """ From 43304782dfc3dd979e42d0c1a4ce826d2744ddb6 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Sun, 5 Jul 2026 13:16:31 +0200 Subject: [PATCH 04/40] machdep thread_fast_set_cthread_self syscall for x86 --- qiling/os/macos/macos.py | 1 + qiling/os/macos/map_syscall.py | 2 ++ qiling/os/macos/syscall.py | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/qiling/os/macos/macos.py b/qiling/os/macos/macos.py index 638d5d42e..b975f16b5 100644 --- a/qiling/os/macos/macos.py +++ b/qiling/os/macos/macos.py @@ -174,6 +174,7 @@ def load(self): 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, 0x82) def hook_syscall(self, ql, intno = None): diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index a73cc98a5..f0de123b0 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -25,6 +25,8 @@ def __mapper(syscall_num: int) -> str: return __mapper x86_syscall_table = { + # machdep + 0x3: 'thread_fast_set_cthread_self', 0xffffffe4: 'task_self_trap', # my stuff... 0x0000ffff: 'my_bzero', diff --git a/qiling/os/macos/syscall.py b/qiling/os/macos/syscall.py index 71ed6a73a..36af494ec 100644 --- a/qiling/os/macos/syscall.py +++ b/qiling/os/macos/syscall.py @@ -433,6 +433,11 @@ def ql_syscall_thread_fast_set_cthread_self64(ql, u_info_addr, *args, **kw): ql.arch.msr.write(IA32_GS_BASE_MSR, u_info_addr) return KERN_SUCCESS +def ql_syscall_thread_fast_set_cthread_self(ql, u_info_addr, *args, **kw): + ql.log.debug("[mdep] thread fast set cthread self(tsd_base:0x%x)" % (u_info_addr)) + ql.arch.msr.write(IA32_GS_BASE_MSR, u_info_addr) # ?? + return KERN_SUCCESS + # Other def ql_syscall_my_bzero(ql, ptr, n, *args, **kw): From 497a7f32578cb83234f5f9fbc6432e67f5938ef5 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Sun, 5 Jul 2026 13:16:59 +0200 Subject: [PATCH 05/40] More syscalls for x86 macos --- qiling/os/macos/map_syscall.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index f0de123b0..c4aea4871 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -27,7 +27,12 @@ def __mapper(syscall_num: int) -> str: x86_syscall_table = { # machdep 0x3: 'thread_fast_set_cthread_self', + # syscalls + 0xc018d: 'write_nocancel', + 0xffffffe1: 'mach_msg_trap', + 0xffffffe3: 'host_self_trap', 0xffffffe4: 'task_self_trap', + 0xffffffe6: 'mach_reply_port', # my stuff... 0x0000ffff: 'my_bzero', } From 6bd5238a1a9aa7ab76ed28e0c0fb7f9b4d522a0a Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Sun, 5 Jul 2026 13:35:04 +0200 Subject: [PATCH 06/40] Handle 3404 mach_ports_lookup and 3204 mach_port_allocate mach messages --- qiling/os/macos/const.py | 23 +++++++++++- qiling/os/macos/mach_port.py | 15 ++++++-- qiling/os/macos/subsystems.py | 66 ++++++++++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/qiling/os/macos/const.py b/qiling/os/macos/const.py index 1fc3f6217..a91845617 100644 --- a/qiling/os/macos/const.py +++ b/qiling/os/macos/const.py @@ -487,8 +487,29 @@ HOST_CAN_HAS_DEBUGGER = 11 HOST_PREFERRED_USER_ARCH = 12 +# mach msg header bits +MACH_MSGH_BITS_COMPLEX = 0x80000000 -# commpage +# 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 diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index 75790e238..a08caeae7 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -109,6 +109,8 @@ 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)] def deal_with_msg(self, msg, addr): @@ -119,12 +121,21 @@ 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) 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/subsystems.py b/qiling/os/macos/subsystems.py index e7969c2cd..2392d06e4 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -159,4 +159,68 @@ def get_special_port(self, in_header, in_content): out_msg.trailer += pack(" Date: Sun, 5 Jul 2026 13:52:55 +0200 Subject: [PATCH 07/40] Refactor existing code to use constants for mach msg bit flags --- qiling/os/macos/const.py | 10 ++++++++++ qiling/os/macos/subsystems.py | 14 +++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/qiling/os/macos/const.py b/qiling/os/macos/const.py index a91845617..aa12aaf90 100644 --- a/qiling/os/macos/const.py +++ b/qiling/os/macos/const.py @@ -487,9 +487,19 @@ HOST_CAN_HAS_DEBUGGER = 11 HOST_PREFERRED_USER_ARCH = 12 + # 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 diff --git a/qiling/os/macos/subsystems.py b/qiling/os/macos/subsystems.py index 2392d06e4..e81d7b9b7 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -39,7 +39,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 +65,7 @@ def host_info(self, in_header, in_content): out_msg.content += pack(" Date: Sun, 5 Jul 2026 13:53:44 +0200 Subject: [PATCH 08/40] Handle 3206 mach_port_deallocate mach message --- qiling/os/macos/mach_port.py | 4 ++++ qiling/os/macos/subsystems.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index a08caeae7..0d3b932f9 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -136,6 +136,10 @@ def deal_with_msg(self, msg, addr): # 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) 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/subsystems.py b/qiling/os/macos/subsystems.py index e81d7b9b7..be547ccd4 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -181,6 +181,25 @@ def mach_port_allocate(self, in_header, in_content): return out_msg + def mach_port_deallocate(self, in_header, in_content): + # Request carries the NDR record followed by the mach_port_name_t to + # release. Reply is a simple message with only a kern_return_t. + name = unpack("= 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(" Date: Sun, 5 Jul 2026 14:07:14 +0200 Subject: [PATCH 09/40] Handle 3802 vm_deallocate mach message --- qiling/os/macos/mach_port.py | 5 +++++ qiling/os/macos/subsystems.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index 0d3b932f9..ec96e524b 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -140,6 +140,11 @@ def deal_with_msg(self, msg, addr): # 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) + # 3800 (Virtual memory operations) + 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) 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/subsystems.py b/qiling/os/macos/subsystems.py index be547ccd4..aeea90d16 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -200,6 +200,22 @@ def mach_port_deallocate(self, in_header, in_content): return out_msg + def vm_deallocate(self, in_header, in_content): + # vm_deallocate (vm_map subsystem, routine 2). Reply is a simple + # (non-complex) MIG message carrying only the NDR record and RetCode. + 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 = 3902 + + out_msg.content += pack(" Date: Sun, 5 Jul 2026 14:18:11 +0200 Subject: [PATCH 10/40] Fix mach_port_allocate --- qiling/os/macos/mach_port.py | 10 ++++++++++ qiling/os/macos/subsystems.py | 21 +++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index ec96e524b..646c43831 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -111,6 +111,16 @@ def __init__(self, ql, my_port): 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): diff --git a/qiling/os/macos/subsystems.py b/qiling/os/macos/subsystems.py index aeea90d16..e033cd4c4 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -161,23 +161,24 @@ def get_special_port(self, in_header, in_content): return out_msg def mach_port_allocate(self, in_header, in_content): + # mach_port_allocate(task, right, &name): allocate a new port right in + # the task's IPC space and return its name. Since the out parameter is a + # plain mach_port_name_t (not a transferred port right), the reply is a + # simple (non-complex) MIG message: NDR record + RetCode + name. + port = self.ql.os.macho_port_manager.alloc_port() + out_msg = MachMsg(self.ql) - out_msg.header.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(0, MACH_MSG_TYPE_MOVE_SEND_ONCE) - out_msg.header.msgh_size = 0x00000028 + out_msg.header.msgh_bits = MACH_MSGH_BITS(0, MACH_MSG_TYPE_MOVE_SEND_ONCE) 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 = 3304 - out_msg.content = pack(" Date: Sun, 5 Jul 2026 14:32:42 +0200 Subject: [PATCH 11/40] Fix vmmap_trap_address load for x86 osx --- qiling/loader/macho.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index 5aec16cee..0d49f1557 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -96,7 +96,7 @@ def run(self): 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 = None # int(self.profile.get("OS32", "vmmap_trap_address"), 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: From 2364bd749d73693f80b02cf80af2ce2840aee39a Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Sun, 5 Jul 2026 14:34:08 +0200 Subject: [PATCH 12/40] Handle 3812 vm_map mach message --- qiling/os/macos/mach_port.py | 4 ++++ qiling/os/macos/subsystems.py | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index 646c43831..aceb82847 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -155,6 +155,10 @@ def deal_with_msg(self, msg, addr): # 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 == 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/subsystems.py b/qiling/os/macos/subsystems.py index e033cd4c4..e827a73ec 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(): @@ -217,6 +218,49 @@ def vm_deallocate(self, in_header, in_content): return out_msg + def vm_map(self, in_header, in_content): + # vm_map (vm_map subsystem, routine 12, msgh_id 3812). The request is a + # complex message: a memory-entry port descriptor followed by the NDR + # record and the vm_map arguments. Request body layout (after the + # 24-byte header): + # [0x00] mach_msg_body_t: msgh_descriptor_count + # [0x04] mach_msg_port_descriptor_t (name + pad + disposition/type) + # [0x10] NDR record + # [0x18] address / [0x1c] size / [0x20] mask / [0x24] flags / ... + # We carve a fresh region out of the task's vm map, honoring the + # requested size and alignment mask, and report its base address back. + address = unpack(" 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(" Date: Sun, 5 Jul 2026 15:40:45 +0200 Subject: [PATCH 13/40] Handle 3803 vm_protect mach message --- qiling/os/macos/mach_port.py | 4 ++++ qiling/os/macos/subsystems.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index aceb82847..6b0d070e9 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -155,6 +155,10 @@ def deal_with_msg(self, msg, addr): # 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) diff --git a/qiling/os/macos/subsystems.py b/qiling/os/macos/subsystems.py index e827a73ec..7722c172b 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -218,6 +218,33 @@ def vm_deallocate(self, in_header, in_content): return out_msg + def vm_protect(self, in_header, in_content): + # vm_protect (vm_map subsystem, routine 3, msgh_id 3803). Request body + # layout (after the 24-byte header): + # [0x00] NDR record + # [0x08] address / [0x0c] size / [0x10] set_maximum / [0x14] new_protection + address = unpack(" Date: Tue, 7 Jul 2026 21:56:25 +0200 Subject: [PATCH 14/40] Handle 3801 vm_allocate mach message --- qiling/os/macos/mach_port.py | 4 ++++ qiling/os/macos/subsystems.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index 6b0d070e9..ff7a5cc18 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -151,6 +151,10 @@ def deal_with_msg(self, msg, addr): out_msg = self.ql.os.macho_task_server.mach_port_deallocate(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) diff --git a/qiling/os/macos/subsystems.py b/qiling/os/macos/subsystems.py index 7722c172b..03f648806 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -202,6 +202,40 @@ def mach_port_deallocate(self, in_header, in_content): return out_msg + def vm_allocate(self, in_header, in_content): + # vm_allocate (vm_map subsystem, routine 1, msgh_id 3801). Request body + # layout (after the 24-byte header): + # [0x00] NDR record + # [0x08] address / [0x0c] size / [0x10] flags + # We carve a fresh page-aligned region out of the task's vm map and + # report its base address back. + address = unpack(" Date: Tue, 7 Jul 2026 21:56:45 +0200 Subject: [PATCH 15/40] WIP /dev/urandom in qltool --- qltool | 1 + 1 file changed, 1 insertion(+) 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: From c208126715832c6c8cff6e7f427bc4fcf646f6f1 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 21:57:31 +0200 Subject: [PATCH 16/40] Fix gdb on non-linux platforms --- qiling/debugger/gdb/xmlregs.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/qiling/debugger/gdb/xmlregs.py b/qiling/debugger/gdb/xmlregs.py index 6bc2371f4..086cbfd5c 100644 --- a/qiling/debugger/gdb/xmlregs.py +++ b/qiling/debugger/gdb/xmlregs.py @@ -93,6 +93,24 @@ 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',) + + 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') From 2f8899a586be3f590719819ce8ac04d5179efda6 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 21:57:50 +0200 Subject: [PATCH 17/40] Fix fails on step and stepi in gdb --- qiling/debugger/gdb/gdb.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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}' From 88f913e08127214062ce5c3783d1538f32d1f133 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 21:58:12 +0200 Subject: [PATCH 18/40] Fix Darwin's x86 gdb debugging --- qiling/debugger/gdb/xmlregs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qiling/debugger/gdb/xmlregs.py b/qiling/debugger/gdb/xmlregs.py index 086cbfd5c..ac7e6b421 100644 --- a/qiling/debugger/gdb/xmlregs.py +++ b/qiling/debugger/gdb/xmlregs.py @@ -104,6 +104,11 @@ def __wrapped(href: str, parse, encoding=None): 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() From 55af9edafb93c260d898304e6097461c1a5663f0 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:02:04 +0200 Subject: [PATCH 19/40] WIP handle int 0x80 syscalls for x86 osx --- qiling/os/macos/macos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiling/os/macos/macos.py b/qiling/os/macos/macos.py index b975f16b5..7886e3982 100644 --- a/qiling/os/macos/macos.py +++ b/qiling/os/macos/macos.py @@ -174,7 +174,8 @@ def load(self): 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, 0x82) + self.ql.hook_intno(self.hook_syscall, 0x80) + self.ql.hook_intno(self.hook_syscall, 0x82) # machdep def hook_syscall(self, ql, intno = None): From 52a2599db0692fd5fdcee88a23046065d72d5ba1 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:03:17 +0200 Subject: [PATCH 20/40] Fix macOS entry point absence --- qiling/loader/macho.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index 0d49f1557..49a80d92e 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -456,6 +456,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 From eb817a2026fd1151006696f2fe6d249120fe3daa Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:06:12 +0200 Subject: [PATCH 21/40] stat family functions for macOS x86 --- qiling/os/posix/syscall/stat.py | 43 ++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) 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() From 020de6f23d343c90e88ee0b370312946f272324a Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:07:01 +0200 Subject: [PATCH 22/40] WIP open flags for arm64 macos host --- qiling/os/posix/const_mapping.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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: { From b1125506393e36efa6f52a6c59510f8b8fab7bb3 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:08:25 +0200 Subject: [PATCH 23/40] Increase commpage size to 2 pages for x86 macos --- qiling/os/macos/kernel_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/os/macos/kernel_func.py b/qiling/os/macos/kernel_func.py index b8228e11c..4225a30c8 100644 --- a/qiling/os/macos/kernel_func.py +++ b/qiling/os/macos/kernel_func.py @@ -27,7 +27,7 @@ def map_commpage(ql): addr_size = 0x1000 elif ql.arch.type == QL_ARCH.X86: addr_base = X86_COMM_PAGE_START_ADDRESS - addr_size = 0x1000 + addr_size = 0x2000 else: raise NotImplementedError ql.mem.map(addr_base, addr_size, info="[commpage]") From 2ae9a0174bdf2072dcd8e4ea00d8a56df4115957 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:34:05 +0200 Subject: [PATCH 24/40] Fix ql_syscall_pread() --- qiling/os/macos/syscall.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/qiling/os/macos/syscall.py b/qiling/os/macos/syscall.py index 36af494ec..a959d91b7 100644 --- a/qiling/os/macos/syscall.py +++ b/qiling/os/macos/syscall.py @@ -205,13 +205,23 @@ def ql_syscall_pread(ql, fd, buf, nbyte, offset, *args, **kw): fd, buf, nbyte, offset )) - if fd in range(MAX_FD_SIZE + 1): - ql.os.fd[fd].seek(offset) - data = ql.os.fd[fd].read(nbyte) - ql.mem.write(buf, data) + if fd not in range(MAX_FD_SIZE + 1) or ql.os.fd[fd] is None: + set_eflags_cf(ql, 0x1) + return EBADF + + f = ql.os.fd[fd] + + # pread must not change the file descriptor's current offset, so save + # the current position, read from the requested offset, then restore it. + pos = f.tell() + f.seek(offset) + data = f.read(nbyte) + f.seek(pos) + + ql.mem.write(buf, data) set_eflags_cf(ql, 0x0) - return nbyte + return len(data) # 0xa9 def ql_syscall_csops(ql, pid, ops, useraddr, usersize, *args, **kw): From be23f2970d0b7022a17e62ea5a544199172008f0 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:34:30 +0200 Subject: [PATCH 25/40] Fix ql_syscall_write_nocancel() --- qiling/os/macos/syscall.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiling/os/macos/syscall.py b/qiling/os/macos/syscall.py index a959d91b7..1c69eb911 100644 --- a/qiling/os/macos/syscall.py +++ b/qiling/os/macos/syscall.py @@ -359,7 +359,10 @@ def ql_syscall_write_nocancel(ql, write_fd, write_buf, write_count, *args, **kw) raise #if buf: # ql.log.info(buf.decode(errors='ignore')) - return 0 + # return the number of bytes written: callers (e.g. dyld's _simple_dprintf) loop + # writing the remainder until the syscall reports the full count. returning 0 makes + # the writer believe nothing was written. + return regreturn # 0x18e From 72ccd4a0f1bccfbd332ebf2675e2129b9af31f68 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:35:18 +0200 Subject: [PATCH 26/40] Implement ql_syscall_read_nocancel() and ql_syscall_close_nocancel() --- qiling/os/macos/syscall.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qiling/os/macos/syscall.py b/qiling/os/macos/syscall.py index 1c69eb911..59d64a93c 100644 --- a/qiling/os/macos/syscall.py +++ b/qiling/os/macos/syscall.py @@ -16,6 +16,8 @@ from .mach_port import * from .kernel_func import * from .utils import * +from ..posix.syscall import ql_syscall_read, ql_syscall_close + # TODO: We need to finish these syscall # there are three kinds of syscall, we often use posix syscall, mach syscall is used by handle mach msg @@ -335,6 +337,16 @@ def ql_syscall_thread_selfid(ql, *args, **kw): return thread_id +# 0x18c +def ql_syscall_read_nocancel(ql, fd, buf, length, *args, **kw): + return ql_syscall_read(ql, fd, buf, length) + + +# 0x18f +def ql_syscall_close_nocancel(ql, fd, *args, **kw): + return ql_syscall_close(ql, fd) + + # 0x18d def ql_syscall_write_nocancel(ql, write_fd, write_buf, write_count, *args, **kw): regreturn = 0 From 62e213b6714413dccfae5896a8ce7928de8971b9 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:39:56 +0200 Subject: [PATCH 27/40] Fix kernelrpc_mach_vm_map_trap and mach_vm_allocate_trap to be arch independent --- qiling/os/macos/syscall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/os/macos/syscall.py b/qiling/os/macos/syscall.py index 59d64a93c..3fe64eebb 100644 --- a/qiling/os/macos/syscall.py +++ b/qiling/os/macos/syscall.py @@ -85,7 +85,7 @@ def ql_syscall_kernelrpc_mach_vm_allocate_trap(ql, port, addr, size, flags, *arg ql.mem.write(mmap_address, b'\x00'*(mmap_end - mmap_address)) ql.os.macho_task.min_offset = mmap_end ql.log.debug("vm alloc form 0x%x to 0x%0x" % (mmap_address, mmap_end)) - ql.mem.write(addr, struct.pack(" Date: Tue, 7 Jul 2026 22:40:39 +0200 Subject: [PATCH 28/40] Fix apples string array for macos --- qiling/loader/macho.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index 49a80d92e..f3af83026 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -128,7 +128,7 @@ def run(self): self.ql.os.macho_task_server = MachTaskServer(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 From caeef8239f0e848729eed6af0bf848b600504674 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:41:05 +0200 Subject: [PATCH 29/40] fix my_bzero log --- qiling/os/macos/syscall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/os/macos/syscall.py b/qiling/os/macos/syscall.py index 3fe64eebb..91e2f34d5 100644 --- a/qiling/os/macos/syscall.py +++ b/qiling/os/macos/syscall.py @@ -466,5 +466,5 @@ def ql_syscall_thread_fast_set_cthread_self(ql, u_info_addr, *args, **kw): # Other def ql_syscall_my_bzero(ql, ptr, n, *args, **kw): - ql.log.debug("bzero(ptr: 0x%x, n: %u)" % (ptr, n)) + ql.log.debug("my_bzero(ptr: 0x%x, n: %u)" % (ptr, n)) ql.mem.write(ptr, bytes(n)) From 0f59316d9f8b5a73e59369b6cbd16b94aa131667 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:42:03 +0200 Subject: [PATCH 30/40] Handle more syscalls for x86 macos --- qiling/os/macos/map_syscall.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index c4aea4871..f16abf8ae 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -28,7 +28,28 @@ def __mapper(syscall_num: int) -> str: # machdep 0x3: 'thread_fast_set_cthread_self', # syscalls + 20: 'getpid', + 25: 'geteuid', + 53: 'sigaltstack', + 197: 'mmap2', + 202: 'sysctl', + 327: 'issetugid', + 0xc0003: 'read', + 0xc0005: 'open', + 0xc004b: 'madvise', + 0xc005c: 'fcntl', + 0xc018c: 'read_nocancel', 0xc018d: 'write_nocancel', + 0xc018e: 'open_nocancel', + 0x40006: 'close', + 0x4018f: 'close_nocancel', + 0xc0030: 'sigprocmask', + 0xc0196: 'fcntl_nocancel', + 0x800bc: 'stat', + 0x80152: 'stat64', + 0x80153: 'fstat64', + 0x80154: 'lstat64', + 0x140099: 'pread', 0xffffffe1: 'mach_msg_trap', 0xffffffe3: 'host_self_trap', 0xffffffe4: 'task_self_trap', From 3a7f271fbec4d752c89e51285625001d36ad1940 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Tue, 7 Jul 2026 22:43:34 +0200 Subject: [PATCH 31/40] Refactor and add more cases for functions calls in the commpage --- qiling/loader/macho.py | 53 +++++++++++++++++++++++++++------- qiling/os/macos/map_syscall.py | 3 ++ qiling/os/macos/syscall.py | 13 +++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index f3af83026..488536c3e 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -172,25 +172,58 @@ def run(self): 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 - addr = 0xffff0600 - # b8 ff ff 00 00 MOV EAX,0xffff + 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 - self.ql.mem.write_ptr(addr, 0x0000ffff, 4) - addr += 4 - sysenter_trap_addr = 0x8fe2b7ac - offset = (sysenter_trap_addr - addr - 5) & 0xffffffff; - # e8 a2 b1 e3 8f CALL __sysenter_trap - self.ql.mem.write_ptr(addr, 0xe8, 1) - addr += 1 - self.ql.mem.write_ptr(addr, offset, 4) + # 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) + def loadDriver(self, stack_addr, loadbase = -1, argv = [], env = {}): self.import_symbols = {} PAGE_SIZE = 0x1000 diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index f16abf8ae..7e189e7d1 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -55,6 +55,9 @@ def __mapper(syscall_num: int) -> str: 0xffffffe4: 'task_self_trap', 0xffffffe6: 'mach_reply_port', # my stuff... + 0x0000fffc: 'my_OSAtomicCompareAndSwap64', + 0x0000fffd: 'my_mach_absolute_time', + 0x0000fffe: 'my_memcpy', 0x0000ffff: 'my_bzero', } diff --git a/qiling/os/macos/syscall.py b/qiling/os/macos/syscall.py index 91e2f34d5..011f9ba37 100644 --- a/qiling/os/macos/syscall.py +++ b/qiling/os/macos/syscall.py @@ -4,6 +4,7 @@ # import struct +import time from qiling.exception import * from qiling.const import * @@ -465,6 +466,18 @@ def ql_syscall_thread_fast_set_cthread_self(ql, u_info_addr, *args, **kw): # Other +def ql_syscall_my_mach_absolute_time(ql, *args, **kw): + ql.log.debug("my_mach_absolute_time()") + val = time.process_time_ns() + ql.arch.regs.eax = val & 0xffffffff # low dword + ql.arch.regs.edx = (val >> 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)) From f5436c5c7989d9627b7612b5376c4e51f9ba31a9 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Wed, 8 Jul 2026 00:06:54 +0200 Subject: [PATCH 32/40] Fix mmap for macOS --- qiling/os/macos/map_syscall.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index 7e189e7d1..1b599c747 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -31,7 +31,7 @@ def __mapper(syscall_num: int) -> str: 20: 'getpid', 25: 'geteuid', 53: 'sigaltstack', - 197: 'mmap2', + 197: 'mmap', 202: 'sysctl', 327: 'issetugid', 0xc0003: 'read', @@ -184,7 +184,7 @@ def __mapper(syscall_num: int) -> str: 194: 'getrlimit', 195: 'setrlimit', 196: 'getdirentries', - 197: 'mmap2', + 197: 'mmap', 199: 'lseek', 200: 'truncate', 201: 'ftruncate', @@ -571,7 +571,7 @@ def __mapper(syscall_num: int) -> str: 194: 'getrlimit', 195: 'setrlimit', 196: 'getdirentries', - 197: 'mmap2', + 197: 'mmap', 199: 'lseek', 200: 'truncate', 201: 'ftruncate', From 6fb7c108e47a67d8cfdf76f1e21d1c5b8862c400 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Wed, 8 Jul 2026 00:07:39 +0200 Subject: [PATCH 33/40] commpage OSAtomicCompareAndSwap32() implementation --- qiling/loader/macho.py | 15 +++++++++++++++ qiling/os/macos/map_syscall.py | 1 + 2 files changed, 16 insertions(+) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index 488536c3e..29ab85f70 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -224,6 +224,21 @@ def commpage_install_syscall_jump(addr, func_num): 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 diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index 1b599c747..8c6ecbef8 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -55,6 +55,7 @@ def __mapper(syscall_num: int) -> str: 0xffffffe4: 'task_self_trap', 0xffffffe6: 'mach_reply_port', # my stuff... + 0x0000fffb: 'my_OSAtomicCompareAndSwap32', 0x0000fffc: 'my_OSAtomicCompareAndSwap64', 0x0000fffd: 'my_mach_absolute_time', 0x0000fffe: 'my_memcpy', From f9170649517c5fe60191471811fbcac45578b92c Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Wed, 8 Jul 2026 00:09:28 +0200 Subject: [PATCH 34/40] Handle mach msg 3616 thread_policy --- qiling/loader/macho.py | 5 +++-- qiling/os/macos/mach_port.py | 5 +++++ qiling/os/macos/subsystems.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index 29ab85f70..d75ffa12a 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 @@ -126,7 +126,8 @@ 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.ql.os.heap = QlMemoryHeap(self.ql, self.heap_address, self.heap_address + self.heap_size) diff --git a/qiling/os/macos/mach_port.py b/qiling/os/macos/mach_port.py index ff7a5cc18..6cd87e426 100644 --- a/qiling/os/macos/mach_port.py +++ b/qiling/os/macos/mach_port.py @@ -150,6 +150,11 @@ def deal_with_msg(self, msg, addr): # 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) diff --git a/qiling/os/macos/subsystems.py b/qiling/os/macos/subsystems.py index 03f648806..fec1dcb3d 100644 --- a/qiling/os/macos/subsystems.py +++ b/qiling/os/macos/subsystems.py @@ -365,3 +365,34 @@ def mach_ports_lookup(self, in_header, in_content): out_msg.header.msgh_size = out_msg.header.header_size + len(out_msg.content) return out_msg + + +class MachThreadServer(): + + def __init__(self, ql): + self.ql = ql + + def thread_policy(self, in_header, in_content): + # thread_policy (thread_act subsystem, routine 16, msgh_id 3616). + # Request body layout (after the 24-byte header): + # [0x00] NDR record + # [0x08] policy / [0x0c] baseCnt / [0x10] base[baseCnt] / [..] set_limit + policy = unpack(" Date: Wed, 8 Jul 2026 00:10:18 +0200 Subject: [PATCH 35/40] Other macOS x86 syscalls to complete hello world support! --- qiling/os/macos/map_syscall.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index 8c6ecbef8..d36a60b16 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -36,15 +36,19 @@ def __mapper(syscall_num: int) -> str: 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', @@ -53,6 +57,7 @@ def __mapper(syscall_num: int) -> str: 0xffffffe1: 'mach_msg_trap', 0xffffffe3: 'host_self_trap', 0xffffffe4: 'task_self_trap', + 0xffffffe5: 'thread_self_trap', 0xffffffe6: 'mach_reply_port', # my stuff... 0x0000fffb: 'my_OSAtomicCompareAndSwap32', From d60e1658367fd8df529118c9c4a984d379a79a28 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Wed, 8 Jul 2026 00:19:44 +0200 Subject: [PATCH 36/40] Cleanup --- qiling/os/macos/map_syscall.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qiling/os/macos/map_syscall.py b/qiling/os/macos/map_syscall.py index d36a60b16..ff302eb9e 100644 --- a/qiling/os/macos/map_syscall.py +++ b/qiling/os/macos/map_syscall.py @@ -60,8 +60,6 @@ def __mapper(syscall_num: int) -> str: 0xffffffe5: 'thread_self_trap', 0xffffffe6: 'mach_reply_port', # my stuff... - 0x0000fffb: 'my_OSAtomicCompareAndSwap32', - 0x0000fffc: 'my_OSAtomicCompareAndSwap64', 0x0000fffd: 'my_mach_absolute_time', 0x0000fffe: 'my_memcpy', 0x0000ffff: 'my_bzero', From 7e097ed76f8f9caf71c0f03c1d8d72a63508fc7b Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Thu, 9 Jul 2026 00:13:59 +0200 Subject: [PATCH 37/40] Macho string and pointer alignment based on the arch type --- qiling/loader/macho.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index d75ffa12a..928e7500e 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -153,8 +153,8 @@ def run(self): dyld_slide = int(self.profile.get("LOADER", "dyld_slide"), 16) self.slide = slide self.dyld_slide = dyld_slide - self.string_align = 4 - self.ptr_align = 4 + 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] From e687308b8a964f0f1dab902be32ee69003893887 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Thu, 9 Jul 2026 00:14:14 +0200 Subject: [PATCH 38/40] Adjust dyld_slide for x86 macos --- qiling/profiles/macos.ql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/profiles/macos.ql b/qiling/profiles/macos.ql index e551c1cf1..0f6fdf9bf 100644 --- a/qiling/profiles/macos.ql +++ b/qiling/profiles/macos.ql @@ -1,6 +1,6 @@ [LOADER32] slide = 0x0000000 -dyld_slide = 0x0000000 +dyld_slide = 0x5000000 [LOADER] From 98e73554231a37e2a0009ed687689f02f8754ede Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Thu, 9 Jul 2026 00:30:35 +0200 Subject: [PATCH 39/40] Load proper Macho header based on arch type --- qiling/loader/macho_parser/header.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/qiling/loader/macho_parser/header.py b/qiling/loader/macho_parser/header.py index 682e53159..3f1d68f26 100644 --- a/qiling/loader/macho_parser/header.py +++ b/qiling/loader/macho_parser/header.py @@ -4,9 +4,12 @@ # from struct import unpack +from typing import Optional from .utils import * from .const import * +from ...const import QL_ARCH + class Header: @@ -48,16 +51,21 @@ def __init__(self, data): FI = FatInfo(FR.read(4 * 5)) self.binarys.append(FI) - def getBinary(self, arch): + def archToCPUType(self, arch) -> 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 - elif item.cpu_type == CPU_TYPE_X86: - return item - return None class FatInfo: From a7abf83993dfa07901a7b79c8ebcb7d338819bf4 Mon Sep 17 00:00:00 2001 From: ciciplusplus Date: Thu, 9 Jul 2026 00:42:44 +0200 Subject: [PATCH 40/40] Fix conditional push_stack_addr --- qiling/loader/macho.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index 928e7500e..0a1c4e587 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -670,9 +670,9 @@ def push_stack_addr(self, data): align = self.ptr_align if data == 0: - content = b'\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('