Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c9395f8
WIP macos x86 support
ciciplusplus Jul 3, 2026
8468bce
Handle "real" bzero loaded at 0xffff0600
ciciplusplus Jul 5, 2026
6f13106
WIP proper sysenter ABI
ciciplusplus Jul 5, 2026
4330478
machdep thread_fast_set_cthread_self syscall for x86
ciciplusplus Jul 5, 2026
497a7f3
More syscalls for x86 macos
ciciplusplus Jul 5, 2026
6bd5238
Handle 3404 mach_ports_lookup and 3204 mach_port_allocate mach messages
ciciplusplus Jul 5, 2026
9a7e747
Refactor existing code to use constants for mach msg bit flags
ciciplusplus Jul 5, 2026
77d4c97
Handle 3206 mach_port_deallocate mach message
ciciplusplus Jul 5, 2026
811eecc
Handle 3802 vm_deallocate mach message
ciciplusplus Jul 5, 2026
a703afb
Fix mach_port_allocate
ciciplusplus Jul 5, 2026
ab47de9
Fix vmmap_trap_address load for x86 osx
ciciplusplus Jul 5, 2026
2364bd7
Handle 3812 vm_map mach message
ciciplusplus Jul 5, 2026
75504a1
Handle 3803 vm_protect mach message
ciciplusplus Jul 5, 2026
48983d0
Handle 3801 vm_allocate mach message
ciciplusplus Jul 7, 2026
6de4821
WIP /dev/urandom in qltool
ciciplusplus Jul 7, 2026
c208126
Fix gdb on non-linux platforms
ciciplusplus Jul 7, 2026
2f8899a
Fix fails on step and stepi in gdb
ciciplusplus Jul 7, 2026
88f913e
Fix Darwin's x86 gdb debugging
ciciplusplus Jul 7, 2026
55af9ed
WIP handle int 0x80 syscalls for x86 osx
ciciplusplus Jul 7, 2026
52a2599
Fix macOS entry point absence
ciciplusplus Jul 7, 2026
eb817a2
stat family functions for macOS x86
ciciplusplus Jul 7, 2026
020de6f
WIP open flags for arm64 macos host
ciciplusplus Jul 7, 2026
b112550
Increase commpage size to 2 pages for x86 macos
ciciplusplus Jul 7, 2026
2ae9a01
Fix ql_syscall_pread()
ciciplusplus Jul 7, 2026
be23f29
Fix ql_syscall_write_nocancel()
ciciplusplus Jul 7, 2026
72ccd4a
Implement ql_syscall_read_nocancel() and ql_syscall_close_nocancel()
ciciplusplus Jul 7, 2026
62e213b
Fix kernelrpc_mach_vm_map_trap and mach_vm_allocate_trap to be arch
ciciplusplus Jul 7, 2026
5f5f6ca
Fix apples string array for macos
ciciplusplus Jul 7, 2026
caeef82
fix my_bzero log
ciciplusplus Jul 7, 2026
0f59316
Handle more syscalls for x86 macos
ciciplusplus Jul 7, 2026
3a7f271
Refactor and add more cases for functions calls in the commpage
ciciplusplus Jul 7, 2026
f5436c5
Fix mmap for macOS
ciciplusplus Jul 7, 2026
6fb7c10
commpage OSAtomicCompareAndSwap32() implementation
ciciplusplus Jul 7, 2026
f917064
Handle mach msg 3616 thread_policy
ciciplusplus Jul 7, 2026
9cf370d
Other macOS x86 syscalls to complete hello world support!
ciciplusplus Jul 7, 2026
d60e165
Cleanup
ciciplusplus Jul 7, 2026
7e097ed
Macho string and pointer alignment based on the arch type
ciciplusplus Jul 8, 2026
e687308
Adjust dyld_slide for x86 macos
ciciplusplus Jul 8, 2026
98e7355
Load proper Macho header based on arch type
ciciplusplus Jul 8, 2026
a7abf83
Fix conditional push_stack_addr
ciciplusplus Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions qiling/debugger/gdb/gdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand Down
23 changes: 23 additions & 0 deletions qiling/debugger/gdb/xmlregs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
128 changes: 110 additions & 18 deletions qiling/loader/macho.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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')
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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('<Q', data)
content = struct.pack('<I', data) if self.ql.arch.type == QL_ARCH.X86 else struct.pack('<Q', data)

if len(content) != align:
self.ql.log.info('stack align error')
Expand Down
19 changes: 15 additions & 4 deletions qiling/loader/macho_parser/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -48,13 +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
return None

class FatInfo:
Expand Down
8 changes: 4 additions & 4 deletions qiling/loader/macho_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 33 additions & 1 deletion qiling/os/macos/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion qiling/os/macos/kernel_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading