From 3d12ada3ed416ce11b632ec134166f091c8b6203 Mon Sep 17 00:00:00 2001 From: songlei <474325919@qq.com> Date: Wed, 26 Nov 2025 21:18:42 +0800 Subject: [PATCH] feat(macos): implement dyld chained fixups for arm64 This commit adds support for dyld chained fixups for arm64 macOS binaries. This is essential for correctly loading and executing binaries on newer versions of macOS. The main changes include: - **Mach-O Parser**: - Added parsing for the `LC_DYLD_CHAINED_FIXUPS` load command. - Implemented new data structures to represent chained fixups information, including `DyldChainedHeader`, `DyldChainedStartsInSegment`, and `DyldChainedImport`. - **Mach-O Loader**: - Added the `dyld_chained_fixups` method in `QlLoaderMACHO` to process and apply chained fixups during loading. - This method resolves pointer chains, rebases addresses, and resolves imported symbols. - **macOS Emulation**: - Added the `hook_imports` method in `QlOsMacos` to intercept and handle calls to imported symbols resolved via chained fixups. - This allows providing custom implementations for these imported functions via `set_api`. - **Constants**: - Added constants related to chained fixups in `macho_parser/const.py`. These changes significantly enhance Qiling's emulation capabilities on arm64 macOS targets. --- qiling/loader/macho.py | 89 ++++++++++++++++- qiling/loader/macho_parser/const.py | 27 ++++- qiling/loader/macho_parser/data.py | 116 ++++++++++++++++++++++ qiling/loader/macho_parser/loadcommand.py | 12 ++- qiling/loader/macho_parser/parser.py | 2 + qiling/os/macos/macos.py | 32 +++++- 6 files changed, 271 insertions(+), 7 deletions(-) diff --git a/qiling/loader/macho.py b/qiling/loader/macho.py index a7e7f87bc..a98fc01db 100644 --- a/qiling/loader/macho.py +++ b/qiling/loader/macho.py @@ -422,8 +422,95 @@ def loadMacho(self, depth=0, isdyld=False): if self.ql.arch.type == QL_ARCH.X8664: load_commpage(self.ql) + if depth == 0 and self.is_driver is False and self.ql.arch.type == QL_ARCH.ARM64: + self.dyld_chained_fixups() + return self.proc_entry - + + def dyld_chained_fixups(self): + # Only support arm64 for now + all_imports = {} + + chained_fixups = self.macho_file.chained_fixups + if chained_fixups is None: + return + + can_resolve_binds = chained_fixups.header.imports_format == 1 # Only support format 1 for now + for starts_in_seg in chained_fixups.starts_in_segment: + if starts_in_seg is None: + continue + + pointer_format = starts_in_seg.pointer_format + for page_idx in range(starts_in_seg.page_count): + start_offset = starts_in_seg.page_start[page_idx] + if start_offset == 0xFFFF: + continue + + page_file_offset = starts_in_seg.segment_offset + (page_idx * starts_in_seg.page_size) + chain_cursor_ptr = self.load_address + self.slide + page_file_offset + start_offset + done = False + + while not done: + target_offset = 0 + if pointer_format == DYLD_CHAINED_PTR_64: + value = self.ql.unpack64(self.ql.mem.read(chain_cursor_ptr, 8)) + target = value & 0xFFFFFFFFF + high8 = (value >> 36) & 0xFF + next_stride = (value >> 51) & 0xFFF + is_bind = (value >> 63) & 0x1 == 1 + if is_bind is False: target_offset = target | (high8 << 36) + else: + raise QlErrorMACHOFormat("Unsupported pointer format in chained fixups: {}".format(pointer_format)) + + if is_bind is False: + corrected_addr = self.load_address + self.slide + target_offset + if pointer_format == DYLD_CHAINED_PTR_32: + self.ql.mem.write(chain_cursor_ptr, self.ql.pack32(corrected_addr)) + else: + self.ql.mem.write(chain_cursor_ptr, self.ql.pack64(corrected_addr)) + else: + if not can_resolve_binds: + raise QlErrorMACHOFormat("Cannot resolve binds in chained fixups") + + ordinal = 0 + if pointer_format == DYLD_CHAINED_PTR_64: + value = self.ql.unpack64(self.ql.mem.read(chain_cursor_ptr, 8)) + ordinal = value & 0xFFFFFF + else: + raise QlErrorMACHOFormat("Unsupported pointer format in chained fixups: {}".format(pointer_format)) + + if ordinal < chained_fixups.header.imports_count: + import_entry = chained_fixups.imports[ordinal] + all_imports[chain_cursor_ptr] = import_entry.symbol_name + + if next_stride == 0: + done = True + else: + chain_cursor_ptr += next_stride * 4 + + + self.import_symbols = {} + if len(all_imports) != 0: + self.static_addr = self.vm_end_addr + self.static_size = self.ql.mem.align_up(len(all_imports) * 4) + + self.ql.mem.map(self.static_addr, self.static_size, info="[STATIC]") + self.vm_end_addr += self.static_size + self.ql.log.info("Memory for external static symbol is created at 0x%x with size 0x%x" % (self.static_addr, + self.static_size)) + jump = self.static_addr + for fixup_addr in all_imports: + self.import_symbols[jump] = { + 'ptr': fixup_addr, + 'name': all_imports[fixup_addr] + } + + #self.ql.mem.write(jump, b'\x00\x00\x20\xD4') # brk #0 + self.ql.mem.write(jump, b'\xC0\x03\x5F\xD6') # ret + self.ql.mem.write(fixup_addr, self.ql.pack64(jump)) + jump += 4 + + def loadSegment64(self, cmd, isdyld): PAGE_SIZE = 0x1000 if isdyld: diff --git a/qiling/loader/macho_parser/const.py b/qiling/loader/macho_parser/const.py index 0726f374d..f7cecb87a 100644 --- a/qiling/loader/macho_parser/const.py +++ b/qiling/loader/macho_parser/const.py @@ -56,4 +56,29 @@ # UNIXTHREAD X86_THREAD_STATE32 = 0x00000001 X86_THREAD_STATE64 = 0x00000004 -ARM_THREAD_STATE64 = 0x00000006 \ No newline at end of file +ARM_THREAD_STATE64 = 0x00000006 + + +SECTION_TYPE = 0x000000ff + +# S_NON_LAZY_SYMBOL_POINTERS - Section with non-lazy symbol pointers. +S_NON_LAZY_SYMBOL_POINTERS = 0x06 + +# S_LAZY_SYMBOL_POINTERS - Section with lazy symbol pointers. +S_LAZY_SYMBOL_POINTERS = 0x07 + +INDIRECT_SYMBOL_ABS = 0x40000000 +INDIRECT_SYMBOL_LOCAL = 0x80000000 + +# Pointer Formats +DYLD_CHAINED_PTR_ARM64E = 1 +DYLD_CHAINED_PTR_64 = 2 +DYLD_CHAINED_PTR_32 = 3 +DYLD_CHAINED_PTR_32_CACHE = 4 +DYLD_CHAINED_PTR_32_FIRMWARE = 5 +DYLD_CHAINED_PTR_64_OFFSET = 6 +DYLD_CHAINED_PTR_ARM64E_OFFSET = 7 # aka KERNEL +DYLD_CHAINED_PTR_64_KERNEL_CACHE = 8 +DYLD_CHAINED_PTR_ARM64E_USERLAND24 = 9 +DYLD_CHAINED_PTR_ARM64E_SHARED_CACHE = 10 +DYLD_CHAINED_PTR_X86_64_KERNEL_CACHE = 11 diff --git a/qiling/loader/macho_parser/data.py b/qiling/loader/macho_parser/data.py index 7acda503a..cf1704e67 100644 --- a/qiling/loader/macho_parser/data.py +++ b/qiling/loader/macho_parser/data.py @@ -39,6 +39,9 @@ def __init__(self, lc, data): self.rel_offset = lc.relocations_offset self.rel_num = lc.number_of_relocations self.flags = lc.flags + self.reserved1 = lc.reserved1 + self.reserved2 = lc.reserved2 + self.reserved3 = lc.reserved3 self.content = data[self.offset : self.offset + self.size] # def __str__(self): @@ -235,3 +238,116 @@ def __init__(self, lc, data): def __str__(self): pass + +class DyldChainedHeader: + def __init__(self, data): + ''' + struct dyld_chained_header { + uint32_t fixups_version; + uint32_t starts_offset; + uint32_t imports_offset; + uint32_t symbols_offset; + uint32_t imports_count; + uint32_t imports_format; + uint32_t symbols_format; + }; + ''' + self.fixups_version = unpack(">= 8 + self.weak_import = tmp & 0x1 + tmp >>= 1 + self.name_offset = tmp & 0x7fffff + + self.symbol_name = None # to be filled later + +class ChainedFixups: + def __init__(self, lc, data): + self.offset = lc.data_offset + self.size = lc.data_size + self.content = data[self.offset : self.offset + self.size] + + self.header = DyldChainedHeader(self.content) + self.starts_in_image = DyldChainedStartsInImage(self.content[self.header.starts_offset:]) + self.starts_in_segment = [] + for i in range(self.starts_in_image.seg_count): + seg_offset = self.starts_in_image.seg_info_offset[i] + if seg_offset == 0: + self.starts_in_segment.append(None) + continue + + seg_data = self.content[self.header.starts_offset + seg_offset:] + self.starts_in_segment.append(DyldChainedStartsInSegment(seg_data)) + + self.imports = [] + symbol_pool = self.content[self.header.symbols_offset:] + for i in range(self.header.imports_count): + import_offset = self.header.imports_offset + i * 4 + import_data = self.content[import_offset : import_offset + 4] + + import_entry = DyldChainedImport(import_data) + import_entry.symbol_name = symbol_pool[import_entry.name_offset :].split(b'\0', 1)[0].decode() + self.imports.append(import_entry) + + + + # def __str__(self): + # return (" ChainedFixupsInfo: content {}".format(self.content)) diff --git a/qiling/loader/macho_parser/loadcommand.py b/qiling/loader/macho_parser/loadcommand.py index 1eb18d362..314ba20c0 100644 --- a/qiling/loader/macho_parser/loadcommand.py +++ b/qiling/loader/macho_parser/loadcommand.py @@ -45,7 +45,8 @@ def get_complete(self): LC_DYLD_CHAINED_FIXUPS : LoadDyldChainedFixups, LC_RPATH : LoadRPath, LC_ID_DYLIB : LoadIdDylib, - LC_BUILD_VERSION : LoadBuildVersion + LC_BUILD_VERSION : LoadBuildVersion, + LC_DYLD_CHAINED_FIXUPS : LoadDyldChainedFixups, } exec_func = cmd_map.get(self.cmd_id) @@ -537,4 +538,11 @@ def __init__(self, data): self.current_version = unpack("