Repository: angr/cle (language: python)

Issue:
Support for LC_DYLD_CHAINED_FIXUPS
New Mach-O binaries targeting ios15 contain a new Load Command that for critical rebasing functionality and symbol binding. This PR implements the pointer "fixups" so that internal pointers are correct.

Needs
https://github.com/angr/binaries/pull/80 for testcase
https://github.com/angr/angr-doc/pull/385 for API doc


Discussion / hints:

Fix patch:
diff --git a/cle/backends/__init__.py b/cle/backends/__init__.py
index 87cd9031..e2238c04 100644
--- a/cle/backends/__init__.py
+++ b/cle/backends/__init__.py
@@ -20,7 +20,7 @@
 
 
 if typing.TYPE_CHECKING:
-    from .. import Loader
+    from .. import Loader, Relocation
 
 class FunctionHintSource:
     """
@@ -168,9 +168,9 @@ def __init__(self,
         self.sections_map = {}  # Mapping from section name to section
         self.symbols: 'sortedcontainers.SortedKeyList[Symbol]' = sortedcontainers.SortedKeyList(
             key=self._get_symbol_relative_addr)
-        self.imports = {}
+        self.imports: typing.Dict[str, "Relocation"] = {}
         self.resolved_imports = []
-        self.relocs = []
+        self.relocs: "List[Relocation]" = []
         self.irelatives = []    # list of tuples (resolver, destination), dest w/o rebase
         self.jmprel = {}
         self.arch = None # type: Optional[archinfo.Arch]
diff --git a/cle/backends/externs/simdata/__init__.py b/cle/backends/externs/simdata/__init__.py
index 3d604c06..0697725e 100644
--- a/cle/backends/externs/simdata/__init__.py
+++ b/cle/backends/externs/simdata/__init__.py
@@ -1,5 +1,5 @@
 from collections import defaultdict
-from typing import List
+from typing import List, Type, Optional
 
 from ...relocation import Relocation
 from ...symbol import Symbol, SymbolType
@@ -50,7 +50,8 @@ def relocations(self) -> List[Relocation]:
 
 registered_data = defaultdict(list)
 
-def register(simdata_cls):
+
+def register(simdata_cls: Type[SimData]):
     """
     Register the given SimData class with CLE so it may be used during loading
     """
@@ -58,7 +59,8 @@ def register(simdata_cls):
         return
     registered_data[simdata_cls.name].append(simdata_cls)
 
-def lookup(name, libname):
+
+def lookup(name: str, libname) -> Optional[Type[SimData]]:
     weak_option = None
     for simdata_cls in registered_data[name]:
         if type(libname) is type(simdata_cls.libname) is str and simdata_cls.libname.startswith(libname):
diff --git a/cle/backends/macho/__init__.py b/cle/backends/macho/__init__.py
index 8d98e97a..7774385d 100644
--- a/cle/backends/macho/__init__.py
+++ b/cle/backends/macho/__init__.py
@@ -1,1 +1,6 @@
 from .macho import MachO
+
+# Some type aliases
+FilePointer = int    # Offset into a raw binary file
+FileOffset = int     # Offset to another FilePointer
+MemoryPointer = int  # Offset into the mapped memory space
diff --git a/cle/backends/macho/binding.py b/cle/backends/macho/binding.py
index e42ccf54..d2457a24 100644
--- a/cle/backends/macho/binding.py
+++ b/cle/backends/macho/binding.py
@@ -3,23 +3,24 @@
 # Contributed December 2016 by Fraunhofer SIT (https://www.sit.fraunhofer.de/en/) and updated in September 2019.
 
 import struct
+from typing import Callable, Dict, Tuple, TYPE_CHECKING
+import logging
 
 
 from ..relocation import Relocation
-from .. import Backend, Symbol
-from typing import Callable, Dict, Tuple
+from .. import Backend
+
+from .symbol import BindingSymbol, AbstractMachOSymbol, SymbolTableSymbol, DyldBoundSymbol
 
-from .symbol import BindingSymbol, AbstractMachOSymbol, SymbolTableSymbol
 
-from typing import TYPE_CHECKING
 
-if TYPE_CHECKING:
-    from ... import MachO
 
 from ...errors import CLEInvalidBinaryError
 from ...address_translator import AT
 
-import logging
+if TYPE_CHECKING:
+    from ... import MachO
+
 l = logging.getLogger(name=__name__)
 
 OPCODE_MASK = 0xF0
@@ -98,7 +99,8 @@ def __init__(self, is_64):
         self.segment_index = 0
         self.address = 0
         self.seg_end_address = 0  # TODO: no rebasing support
-        self.wraparound = 2 ** 64  # address is expected to properly overflow and address is uintptr_t (unsigned long according to _uintptr_t.h)
+        # address is expected to properly overflow and address is uintptr_t (unsigned long according to _uintptr_t.h)
+        self.wraparound = 2 ** 64
         self.sizeof_intptr_t = 8 if is_64 else 4  # experimentally determined
         self.bind_handler = None  # function(state,binary) => None
 
@@ -371,17 +373,19 @@ def n_opcode_do_bind_uleb_times_skipping_uleb(s: BindingState, b: 'MachO', _i: i
 
 
 class MachORelocation(Relocation):
-
+    """
+    Generic Relocation for MachO. For now it just deals with symbols
+    """
     def __init__(self, owner: Backend, symbol: AbstractMachOSymbol, relative_addr: int, data):
         super().__init__(owner, symbol, relative_addr)
         self.data = data
 
     def resolve_symbol(self, solist, thumb=False, extern_object=None, **kwargs):
-        if isinstance(self.symbol, BindingSymbol):
-            raise NotImplementedError("Doesn't seem to happen?")
-        elif isinstance(self.symbol, SymbolTableSymbol):
+        if isinstance(self.symbol, (SymbolTableSymbol, BindingSymbol, DyldBoundSymbol)):
             if self.symbol.library_name in [so.binary_basename for so in solist]:
-                raise NotImplementedError("Symbol could actually be resolved because we have the required library, but that isn't implemented yet")
+                raise NotImplementedError(
+                    "Symbol could actually be resolved because we have the required library,"
+                    " but that isn't implemented yet")
             else:
                 # Create an extern symbol for it
                 new_symbol = extern_object.make_extern(self.symbol.name, sym_type=self.symbol._type, thumb=thumb)
diff --git a/cle/backends/macho/macho.py b/cle/backends/macho/macho.py
index b5f05135..29888b41 100644
--- a/cle/backends/macho/macho.py
+++ b/cle/backends/macho/macho.py
@@ -1,32 +1,52 @@
 # -*-coding:utf8 -*-
 # This file is part of Mach-O Loader for CLE.
 # Contributed December 2016 by Fraunhofer SIT (https://www.sit.fraunhofer.de/en/).
+import ctypes
+import typing
 from collections import defaultdict
 from os import SEEK_CUR, SEEK_SET
 import struct
 import sys
 from io import BytesIO, BufferedReader
-from typing import Optional, DefaultDict, List, Tuple
+from typing import Optional, DefaultDict, List, Tuple, Dict, Union
+import logging
+from sortedcontainers import SortedKeyList
 
+from packaging.version import Version, LegacyVersion
+from packaging import version
 
 import archinfo
 
+from .macho_load_commands import LoadCommands as LC
+
 from .section import MachOSection
-from .symbol import SymbolTableSymbol, AbstractMachOSymbol
+from .symbol import SymbolTableSymbol, AbstractMachOSymbol, DyldBoundSymbol
 from .segment import MachOSegment
-from .binding import BindingHelper, read_uleb
+from .binding import BindingHelper, read_uleb, MachORelocation
 from .. import Backend, register_backend, AT
 from ...errors import CLEInvalidBinaryError, CLECompatibilityError, CLEOperationError
+from .structs import (
+    dyld_chained_fixups_header, dyld_chained_starts_in_segment,
+    ChainedFixupPointerOnDisk,
+    DyldImportStruct,
+    DyldChainedPtrFormats,
+    DYLD_CHAINED_PTR_START_NONE,
+    FileOffset,
+    FilePointer,
+    MemoryPointer,
+)
 
-import logging
 l = logging.getLogger(name=__name__)
 
 __all__ = ('MachO', 'MachOSection', 'MachOSegment')
 
-from sortedcontainers import SortedKeyList
 
 # pylint: disable=abstract-method
 class SymbolList(SortedKeyList):
+    """
+    Special data structure that extends SortedKeyList to allow looking up a MachO library by name and ordinal quickly
+    without having to iterate over the whole list
+    """
     _symbol_cache: DefaultDict[Tuple[str, int],
                                List[AbstractMachOSymbol]]
 
@@ -65,7 +85,8 @@ class MachO(Backend):
     MH_CIGAM_64 = 0xcffaedfe
     MH_MAGIC = 0xfeedface
     MH_CIGAM = 0xcefaedfe
-
+    ncmds: int
+    sizeofcmds: int
 
     def __init__(self, *args, **kwargs):
         l.warning('The Mach-O backend is not well-supported. Good luck!')
@@ -79,9 +100,7 @@ def __init__(self, *args, **kwargs):
         self.cpusubtype = None
         self.filetype = None
         self.pie = None  # position independent executable?
-        self.ncmds = None  # number of load commands
         self.flags = None  # binary flags
-        self.sizeofcmds = None  # total size of load commands
         self.imported_libraries = ["Self"]  # ordinal 0 = SELF_LIBRARY_ORDINAL
         self.sections_by_ordinal = [None] # ordinal 0 = None == Self
         self.exports_by_name = {}  # note exports is currently a raw and unprocessed datastructure.
@@ -99,10 +118,16 @@ def __init__(self, *args, **kwargs):
         self.symtab_offset = None # offset to the symtab
         self.symtab_nsyms = None # number of symbols in the symtab
         self.binding_done = False # if true binding was already done and do_bind will be a no-op
+        self._dyld_chained_fixups_offset: Optional[int] = None
+        self._dyld_rebases: Dict[MemoryPointer, MemoryPointer] = {}
+        self._dyld_imports: List[AbstractMachOSymbol] = []
 
         # For some analysis the insertion order of the symbols is relevant and needs to be kept.
         # This is has to be separate from self.symbols because the latter is sorted by address
-        self._ordered_symbols = []
+        self._ordered_symbols: List[AbstractMachOSymbol] = []
+
+        # The minimum version encoded by the LC_BUILD_VERSION command
+        self._minimum_version: Union[Version, LegacyVersion, None] = None
 
         # Begin parsing the file
         try:
@@ -112,8 +137,16 @@ def __init__(self, *args, **kwargs):
             self.struct_byteorder = self._detect_byteorder(struct.unpack("=I", binary_file.read(4))[0])
 
             # parse the mach header:
-            # (ignore all irrelevant fiels)
-            self._parse_mach_header(binary_file)
+            # (ignore all irrelevant fields)
+            (_, self.cputype, self.cpusubtype, self.filetype, self.ncmds, self.sizeofcmds,
+             self.flags) = self._unpack("7I", binary_file, 0, 28)
+
+            self.pie = bool(self.flags & 0x200000)  # MH_PIE
+
+            if not bool(self.flags & 0x80):  # ensure MH_TWOLEVEL
+                l.error("Binary is not using MH_TWOLEVEL namespacing."
+                        "This isn't properly implemented yet and will degrade results in unpredictable ways."
+                        "Please open an issue if you encounter this with a binary you can share")
 
             # determine architecture
             arch_ident = self._detect_arch_ident()
@@ -133,7 +166,8 @@ def __init__(self, *args, **kwargs):
             # Load commands have a common structure: First 4 bytes identify the command by a magic number
             # second 4 bytes determine the commands size. Everything after this generic "header" is command-specific
             # this makes parsing the commands easy.
-            # The documentation for Mach-O is at http://opensource.apple.com//source/xnu/xnu-1228.9.59/EXTERNAL_HEADERS/mach-o/loader.h
+            # The documentation for Mach-O is at
+            # http://opensource.apple.com//source/xnu/xnu-1228.9.59/EXTERNAL_HEADERS/mach-o/loader.h
             count = 0
             offset = lc_offset
             while count < self.ncmds and (offset - lc_offset) < self.sizeofcmds:
@@ -141,34 +175,55 @@ def __init__(self, *args, **kwargs):
                 (cmd, size) = self._unpack("II", binary_file, offset, 8)
 
                 # check for segments that interest us
-                if cmd in [0x1, 0x19]:  # LC_SEGMENT,LC_SEGMENT_64
+                if cmd in [LC.LC_SEGMENT, LC.LC_SEGMENT_64]:  # LC_SEGMENT,LC_SEGMENT_64
                     l.debug("Found LC_SEGMENT(_64) @ %#x", offset)
                     self._load_segment(binary_file, offset)
-                elif cmd == 0x2:  # LC_SYMTAB
+                elif cmd == LC.LC_SYMTAB:  # LC_SYMTAB
                     l.debug("Found LC_SYMTAB @ %#x", offset)
                     self._load_symtab(binary_file, offset)
-                elif cmd in [0x22, 0x80000022]:  # LC_DYLD_INFO(_ONLY)
+                elif cmd in [LC.LC_DYLD_INFO, LC.LC_DYLD_INFO_ONLY]:  # LC_DYLD_INFO(_ONLY)
                     l.debug("Found LC_DYLD_INFO(_ONLY) @ %#x", offset)
                     self._load_dyld_info(binary_file, offset)
-                elif cmd in [0xc, 0x8000001c, 0x80000018]:  # LC_LOAD_DYLIB, LC_REEXPORT_DYLIB,LC_LOAD_WEAK_DYLIB
+                elif cmd in [LC.LC_LOAD_DYLIB, 0x8000001c, LC.LC_LOAD_WEAK_DYLIB]:
+                    # TODO: Old comment claimed that 0x8000001c is LC_REEXPORT_DYLIB
+                    #  but 0x8000001c should be LC_RPATH = 0x1C | LC_REQ_DYLD
+                    #  So there is something wrong here that might be harmless
+                    #  but it definitely doesn't seem correct
                     l.debug("Found LC_*_DYLIB @ %#x", offset)
                     self._load_dylib_info(binary_file, offset)
-                elif cmd == 0x80000028:  # LC_MAIN
+                elif cmd == LC.LC_MAIN:  # LC_MAIN
                     l.debug("Found LC_MAIN @ %#x", offset)
                     self._load_lc_main(binary_file, offset)
-                elif cmd == 0x5:  # LC_UNIXTHREAD
+                elif cmd == LC.LC_UNIXTHREAD:  # LC_UNIXTHREAD
                     l.debug("Found LC_UNIXTHREAD @ %#x", offset)
                     self._load_lc_unixthread(binary_file, offset)
-                elif cmd == 0x26:  # LC_FUNCTION_STARTS
+                elif cmd == LC.LC_FUNCTION_STARTS:  # LC_FUNCTION_STARTS
                     l.debug("Found LC_FUNCTION_STARTS @ %#x", offset)
                     self._load_lc_function_starts(binary_file, offset)
-                elif cmd == 0x29:  # LC_DATA_IN_CODE
+                elif cmd == LC.LC_DATA_IN_CODE:  # LC_DATA_IN_CODE
                     l.debug("Found LC_DATA_IN_CODE @ %#x", offset)
                     self._load_lc_data_in_code(binary_file, offset)
-                elif cmd in [0x21, 0x2c]:  # LC_ENCRYPTION_INFO(_64)
+                elif cmd in [LC.LC_ENCRYPTION_INFO, LC.LC_ENCRYPTION_INFO_64]:  # LC_ENCRYPTION_INFO(_64)
                     l.debug("Found LC_ENCRYPTION_INFO @ %#x", offset)
-                    self._assert_unencrypted(binary_file, offset)
-
+                    # self._assert_unencrypted(binary_file, offset)
+                elif cmd in [LC.LC_DYLD_CHAINED_FIXUPS]:
+                    l.info("Found LC_DYLD_CHAINED_FIXUPS @ %#x", offset)
+                    (_, _, dataoff, datasize) = self._unpack("4I", binary_file, offset, 16)
+                    self._dyld_chained_fixups_offset: int = dataoff
+                elif cmd in [LC.LC_BUILD_VERSION]:
+                    l.info("Found LC_BUILD_VERSION @ %#x", offset)
+                    (_, _, _platform, minos, _sdk, _ntools) = self._unpack("6I", binary_file, offset, 6 * 4)
+                    patch = (minos >> (8 * 0)) & 0xFF
+                    minor = (minos >> (8 * 1)) & 0xFF
+                    major = (minos >> (8 * 2)) & 0xFFFF
+                    self._minimum_version = version.parse(f"{major}.{minor}.{patch}")
+                    l.info("Found minimum version %s", self._minimum_version)
+                else:
+                    try:
+                        command_name = LC(cmd)
+                        l.warning(f"{str(command_name)} is not handled yet")
+                    except ValueError:
+                        l.error(f"Command {hex(cmd)} is not recognized!")
                 # update bookkeeping
                 offset += size
 
@@ -183,20 +238,27 @@ def __init__(self, *args, **kwargs):
 
         # File is read, begin populating internal fields
         self._resolve_entry()
+        l.info("Parsing exports")
         self._parse_exports()
+        l.info(f"Parsing {self.symtab_nsyms} symbols")
         self._parse_symbols(binary_file)
+        l.info("Parsing module init/term function pointers")
         self._parse_mod_funcs()
 
         text_segment = self.find_segment_by_name("__TEXT")
         if not text_segment is None:
-            self.mapped_base = text_segment.vaddr
+            self.mapped_base: MemoryPointer = text_segment.vaddr
         else:
             l.warning("No text segment found")
+        if self._dyld_chained_fixups_offset:
+            l.info("Parsing dyld bound symbols and fixup chains (ios15 and above)")
+            self._parse_dyld_chained_fixups()
+        else:
+            l.info("Parsing binding bytecode stream")
+            self.do_binding()
 
-        self.do_binding()
-
-    @staticmethod
-    def is_compatible(stream):
+    @classmethod
+    def is_compatible(cls, stream):
         stream.seek(0)
         identstring = stream.read(0x5)
         stream.seek(0)
@@ -231,8 +293,8 @@ def parse_mod_funcs_internal(s, target):
                 target.append(addr)
 
         for seg in self.segments:
+            seg: Union[MachOSection, MachOSegment]
             for sec in seg.sections:
-
                 if sec.type == 0x9:  # S_MOD_INIT_FUNC_POINTERS
                     l.debug("Section %s contains init pointers", sec.sectname)
                     parse_mod_funcs_internal(sec, self.mod_init_func_pointers)
@@ -244,6 +306,7 @@ def parse_mod_funcs_internal(s, target):
 
     def find_segment_by_name(self, name):
         for s in self.segments:
+            s: Union[MachOSection, MachOSegment]
             if s.segname == name:
                 return s
         return None
@@ -268,35 +331,17 @@ def _read(fp: BufferedReader, offset: int, size: int) -> bytes:
         fp.seek(offset)
         return fp.read(size)
 
-    def _unpack_with_byteorder(self, fmt, data):
+    def _unpack_with_byteorder(self, fmt, data) -> Tuple[typing.Any, ...]:
         """
         Appends self.struct_byteorder before fmt to ensure usage of correct byteorder
         :return: struct.unpack(self.struct_byteorder+fmt,input)
         """
         return struct.unpack(self.struct_byteorder + fmt, data)
 
-    def _unpack(self, fmt, fp, offset, size):
+    def _unpack(self, fmt: str, fp: BufferedReader, offset: FilePointer, size: int) -> Tuple[typing.Any, ...]:
         """Convenience"""
         return self._unpack_with_byteorder(fmt, self._read(fp, offset, size))
 
-    def _parse_mach_header(self, f):
-        """
-        Parses the mach-o header and sets
-        self.cputype, self.cpusubtype, self.pie, self.ncmds,self.flags,self.sizeofcmds and self.filetype
-
-        Currently ignores any type of information that is not directly relevant to analyses
-        :param f: The binary as a file object
-        :return: None
-        """
-        # this method currently disregards any differences between 32 and 64 bit code
-        (_, self.cputype, self.cpusubtype, self.filetype, self.ncmds, self.sizeofcmds,
-         self.flags) = self._unpack("7I", f, 0, 28)
-
-        self.pie = bool(self.flags & 0x200000)  # MH_PIE
-
-        if not bool(self.flags & 0x80):  # ensure MH_TWOLEVEL
-            l.error("Binary is not using MH_TWOLEVEL namespacing. This isn't properly implemented yet and will degrade results in unpredictable ways. Please open an issue if you encounter this with a binary you can share")
-
     @staticmethod
     def _detect_byteorder(magic):
         """Determines the binary's byteorder """
@@ -326,7 +371,6 @@ def _detect_byteorder(magic):
                 l.debug("Not a mach-o file")
                 raise CLECompatibilityError()
 
-
     def do_binding(self):
         # Perform binding
 
@@ -341,9 +385,7 @@ def do_binding(self):
             l.info("Found weak binding blob. According to current state of knowledge, weak binding "
                    "is only sensible if multiple binaries are involved and is thus skipped.")
 
-
-        self.binding_done=True
-
+        self.binding_done = True
 
     def _parse_exports(self):
         """
@@ -416,7 +458,7 @@ def _parse_exports(self):
                         tmp = read_uleb(blob, blob_f.tell())
                         blob_f.seek(tmp[1], SEEK_CUR)
                         symbol_offset = tmp[0] + self.segments[1].vaddr
-                        l.info("Found normal export %r: %#x", sym_str, symbol_offset)
+                        l.debug("Found normal export %r: %#x", sym_str, symbol_offset)
                         self.exports_by_name[sym_str.decode()] = (flags, symbol_offset)
 
                 child_count = struct.unpack("B", blob_f.read(1))[0]
@@ -526,7 +568,8 @@ def _load_lc_unixthread(self, f, offset):
         # TODO: This is the place to add x86 and x86_64 thread states
         if flavor == 1 and self.arch.bits != 64:  # ARM_THREAD_STATE or ARM_UNIFIED_THREAD_STATE or ARM_THREAD_STATE32
             blob = self._unpack("16I", f, offset + 16, 64)  # parses only until __pc
-        elif flavor == 1 and self.arch.bits == 64 or flavor == 6:  # ARM_THREAD_STATE or ARM_UNIFIED_THREAD_STATE or ARM_THREAD_STATE64
+        elif flavor == 1 and self.arch.bits == 64 or flavor == 6:
+            # ARM_THREAD_STATE or ARM_UNIFIED_THREAD_STATE or ARM_THREAD_STATE64
             blob = self._unpack("33Q", f, offset + 16, 264)  # parses only until __pc
         else:
             l.error("Unknown thread flavor: %d", flavor)
@@ -570,6 +613,14 @@ def _load_symtab(self, f, offset):
         # load string table
         self.strtab = self._read(f, stroff, strsize)
 
+        # Create Dictionary of offsets to strings for quick lookups e.g. during later symbol creation
+        _indexed_strtab: Dict[int, bytes] = {}
+        idx = 0
+        for s in self.strtab.split(b"\x00"):
+            _indexed_strtab[idx] = s
+            idx += len(s) + 1
+        self._indexed_strtab = _indexed_strtab
+
         # store symtab info
         self.symtab_nsyms = nsyms
         self.symtab_offset = symoff
@@ -596,10 +647,14 @@ def _parse_symbols(self,f):
             self.symbols.add(sym)
             self._ordered_symbols.append(sym)
 
-            l.debug("Symbol # %d @ %#x is '%s'",i,offset, sym.name)
+            l.debug("Symbol # %d @ %#x is '%s'", i, offset, sym.name)
 
     def get_string(self, start):
         """Loads a string from the string table"""
+
+        if start in self._indexed_strtab:
+            return self._indexed_strtab[start]
+
         end = start
         if end > len(self.strtab):
             raise ValueError()
@@ -610,7 +665,7 @@ def get_string(self, start):
             end += 1
         return self.strtab[start:]
 
-    def parse_lc_str(self, f, start, limit=None):
+    def parse_lc_str(self, f, start, limit: Optional[int] = None):
         """Parses a lc_str data structure"""
         tmp = self._unpack("c", f, start, 1)[0]
         s = b''
@@ -675,18 +730,20 @@ def _load_segment(self, f, offset):
             # Create section
             sec = MachOSection(section_foff, section_vaddr, section_vsize, section_vsize, section_segname,
                                section_sectname,
-                               section_align, section_reloff, section_nreloc, section_flags, r1, r2)
+                               section_align, section_reloff, section_nreloc, section_flags, r1, r2, parent_segment=seg)
 
             # Store section
             seg.sections.append(sec)
+            self.sections.append(sec)
 
         # add to sections_by_ordinal
         self.sections_by_ordinal.extend(seg.sections)
 
         if segname == b"__PAGEZERO":
             # TODO: What we actually need at this point is some sort of smart on-demand string or memory
-            # This should not cause trouble because accesses to __PAGEZERO are SUPPOSED to crash (segment has access set to no access)
-            # This optimization is here as otherwise several GB worth of zeroes would clutter our memory
+            #  This should not cause trouble because accesses to __PAGEZERO are SUPPOSED to crash
+            #  (segment has access set to no access)
+            #  This optimization is here as otherwise several GB worth of zeroes would clutter our memory
             l.info("Found PAGEZERO, skipping backer for memory conservation")
         elif seg.filesize > 0:
             # Append segment data to memory
@@ -696,13 +753,124 @@ def _load_segment(self, f, offset):
 
             # for some reason seg.offset is not always the same as seg.vaddr - baseaddress
             # when they differ the vaddr seems to be the correct choice according to loaders like in Ghidra
-            # but there isn't necessarily a clear definition of a baseaddress because the vmaddr just specifies the the address in the global memory space
+            # but there isn't necessarily a clear definition of a baseaddress
+            # because the vmaddr just specifies the the address in the global memory space
             vaddr_offset = AT.from_mva(seg.vaddr, self).to_rva()
             self.memory.add_backer(vaddr_offset, blob)
 
         # Store segment
         self.segments.append(seg)
 
+    S = typing.TypeVar("S", bound=Union[ctypes.Structure,ctypes.Union])
+    def _get_struct(self, struct: typing.Type[S], offset: int) -> S:
+        data = self._read(self._binary_stream, offset, ctypes.sizeof(struct))
+        return struct.from_buffer_copy(data)
+
+    def _read_cstring_from_file(self, start: FilePointer):
+        end = -1
+        buffer = b""
+        while end == -1:
+            buffer += self._read(self._binary_stream, start, 1024)
+            end = buffer.find(b'\x00')
+        return buffer[:end]
+
+
+    def _parse_dyld_chained_fixups(self):
+
+        header: dyld_chained_fixups_header = self._get_struct(dyld_chained_fixups_header, self._dyld_chained_fixups_offset)
+
+        if header.symbols_format != 0:
+            raise NotImplementedError("Dyld fixup symbols are compressed, this isn't supported yet")
+
+        # Address of Array of dyld_chained_import* structs
+        imports_start_addr: FilePointer = self._dyld_chained_fixups_offset + header.imports_offset
+        symbols_start_addr: FilePointer = self._dyld_chained_fixups_offset + header.symbols_offset
+
+        import_struct = DyldImportStruct.get_struct(header.imports_format)
+        # Parse Imports
+        for i in range(header.imports_count):
+            import_addr = imports_start_addr + i * ctypes.sizeof(import_struct)
+            imp = self._get_struct(import_struct, import_addr)
+            sym_name_addr = symbols_start_addr + imp.name_offset
+            sym_name = self._read_cstring_from_file(sym_name_addr).decode("utf-8")
+            symbols = self.symbols.get_by_name_and_ordinal(sym_name, imp.lib_ordinal)
+            if len(symbols) == 1:
+                self._dyld_imports.append(symbols[0])
+            elif len(symbols) == 0:
+                try:
+                    l.debug("Creating DyldBoundSymbol with name %s for library %s",
+                            sym_name, self.imported_libraries[imp.lib_ordinal])
+                except IndexError:
+                    l.debug("Creating DyldBoundSymbol with name %s and library ordinal %s (unknown library)",
+                            sym_name, imp.lib_ordinal)
+                sym = DyldBoundSymbol(self, sym_name, imp.lib_ordinal)
+                self.symbols.add(sym)
+                self._dyld_imports.append(sym)
+            else:
+                raise NotImplementedError(
+                    f"Multiple symbols with name {sym_name}"
+                    f"for library {self.imported_libraries[imp.lib_ordinal]}."
+                )
+
+        # Address of the dyld_chained_starts_in_image struct
+        segs_addr: FilePointer = self._dyld_chained_fixups_offset + header.starts_offset
+
+        # The struct isn't straightforward to parse with ctypes, so we do it manually
+        seg_count = self._unpack("I", self._binary_stream, segs_addr, 4)[0]
+
+        segs: List[FileOffset] = []
+        for i in range(seg_count):
+            s = self._unpack("I", self._binary_stream, (i * 4) + segs_addr + 4, 4)[0]
+            segs.append(s)
+
+            if segs[i] == 0:
+                continue
+
+            starts_addr: FilePointer = (
+                    segs_addr + segs[i]
+            )
+            starts = self._get_struct(dyld_chained_starts_in_segment, starts_addr)
+
+            page_starts_data = self._read(self._binary_stream, starts_addr + 22, starts.page_count * 2)
+            page_starts = struct.unpack("<" + ("H" * starts.page_count), page_starts_data)
+
+            pointer_format: DyldChainedPtrFormats = starts.pointer_format
+            l.info("Page has pointer_format: %s", pointer_format)
+            for (j, start) in enumerate(page_starts):
+                if start == DYLD_CHAINED_PTR_START_NONE:
+                    continue
+                chain_entry_addr = (starts.segment_offset + (j * starts.page_size) + start)
+                current_chain_addr = chain_entry_addr
+                l.info("Reading chain at %x", current_chain_addr)
+
+                while True:
+                    chained_rebase_ptr: ChainedFixupPointerOnDisk = self._get_struct(ChainedFixupPointerOnDisk, current_chain_addr)
+                    bind = chained_rebase_ptr.isBind(pointer_format)
+                    rebase = chained_rebase_ptr.isRebase(pointer_format, self.mapped_base)
+                    if bind is not None:
+                        libOrdinal, addend = bind
+                        import_symbol = self._dyld_imports[libOrdinal]
+                        reloc = MachORelocation(self, import_symbol, current_chain_addr, None)
+                        self.relocs.append(reloc)
+                        l.debug("Binding for %s found at %x", import_symbol, current_chain_addr)
+                    elif rebase is not None:
+                        target = self.mapped_base + rebase
+                        location: MemoryPointer = self.mapped_base + current_chain_addr
+                        self._dyld_rebases[location] = target
+                        # TODO: Technically this is basically a relocation, i.e. relevant for rebasing
+                        # But it isn't clear to me currently how relocations without a corresponding symbol would
+                        # be handled
+                        self.memory.store(location, struct.pack("Q", target))
+                        l.debug("Rebase to %x found at %x", target, location)
+
+                    else:
+                        raise CLEInvalidBinaryError("FixupPointer was neither bind nor rebase, that shouldn't happen")
+
+                    skip = chained_rebase_ptr.generic64.rebase.next * 4
+                    current_chain_addr += skip
+                    if skip == 0:
+                        break
+
     def get_symbol_by_address_fuzzy(self, address):
         """
         Locates a symbol by checking the given address against sym.addr, sym.bind_xrefs and
@@ -713,7 +881,7 @@ def get_symbol_by_address_fuzzy(self, address):
                 return sym
         return None
 
-    def get_symbol(self, name, include_stab=False, fuzzy=False): # pylint: disable=arguments-differ
+    def get_symbol(self, name, include_stab=False, fuzzy=False):  # pylint: disable=arguments-differ
         """
         Returns all symbols matching name.
 
@@ -754,6 +922,7 @@ def get_segment_by_name(self, name):
         :return: MachOSegment or None
         """
         for seg in self.segments:
+            seg: Union[MachOSection, MachOSegment]
             if seg.segname == name:
                 return seg
 
diff --git a/cle/backends/macho/macho_load_commands.py b/cle/backends/macho/macho_load_commands.py
new file mode 100644
index 00000000..f092f64c
--- /dev/null
+++ b/cle/backends/macho/macho_load_commands.py
@@ -0,0 +1,67 @@
+from enum import IntEnum
+
+
+class LoadCommands(IntEnum):
+    """
+    Enum for all the Load Commands used inside the MachO Binary
+    """
+    # https://opensource.apple.com/source/cctools/cctools-949.0.1/include/mach-o/loader.h.auto.html
+    LC_REQ_DYLD = 0x80000000
+
+    LC_SEGMENT = 0x1
+    LC_SYMTAB = 0x2
+    LC_SYMSEG = 0x3
+    LC_THREAD = 0x4
+    LC_UNIXTHREAD = 0x5
+    LC_LOADFVMLIB = 0x6
+    LC_IDFVMLIB = 0x7
+    LC_IDENT = 0x8
+    LC_FVMFILE = 0x9
+    LC_PREPAGE = 0xA
+    LC_DYSYMTAB = 0xB
+    LC_LOAD_DYLIB = 0xC
+    LC_ID_DYLIB = 0xD
+    LC_LOAD_DYLINKER = 0xE
+    LC_ID_DYLINKER = 0xF
+    LC_PREBOUND_DYLIB = 0x10
+
+    LC_ROUTINES = 0x11
+    LC_SUB_FRAMEWORK = 0x12
+    LC_SUB_UMBRELLA = 0x13
+    LC_SUB_CLIENT = 0x14
+    LC_SUB_LIBRARY = 0x15
+    LC_TWOLEVEL_HINTS = 0x16
+    LC_PREBIND_CKSUM = 0x17
+
+    LC_LOAD_WEAK_DYLIB = 0x18 | LC_REQ_DYLD
+
+    LC_SEGMENT_64 = 0x19
+    LC_ROUTINES_64 = 0x1A
+    LC_UUID = 0x1B
+    LC_RPATH = 0x1C | LC_REQ_DYLD
+    LC_CODE_SIGNATURE = 0x1D
+    LC_SEGMENT_SPLIT_INFO = 0x1E
+    LC_REEXPORT_DYLIB = 0x1F | LC_REQ_DYLD
+    LC_LAZY_LOAD_DYLIB = 0x20
+    LC_ENCRYPTION_INFO = 0x21
+    LC_DYLD_INFO = 0x22
+    LC_DYLD_INFO_ONLY = 0x22 | LC_REQ_DYLD
+    LC_LOAD_UPWARD_DYLIB = 0x23 | LC_REQ_DYLD
+    LC_VERSION_MIN_MACOSX = 0x24
+    LC_VERSION_MIN_IPHONEOS = 0x25
+    LC_FUNCTION_STARTS = 0x26
+    LC_DYLD_ENVIRONMENT = 0x27
+
+    LC_MAIN = 0x28 | LC_REQ_DYLD
+    LC_DATA_IN_CODE = 0x29
+    LC_SOURCE_VERSION = 0x2A
+    LC_DYLIB_CODE_SIGN_DRS = 0x2B
+    LC_ENCRYPTION_INFO_64 = 0x2C
+    LC_LINKER_OPTION = 0x2D
+    LC_LINKER_OPTIMIZATION_HINT = 0x2E
+    LC_VERSION_MIN_TVOS = 0x2F
+    LC_VERSION_MIN_WATCHOS = 0x30
+    LC_NOTE = 0x31
+    LC_BUILD_VERSION = 0x32
+    LC_DYLD_EXPORTS_TRIE = 0x33 | LC_REQ_DYLD
+    LC_DYLD_CHAINED_FIXUPS = 0x34 | LC_REQ_DYLD
diff --git a/cle/backends/macho/section.py b/cle/backends/macho/section.py
index 28f052ce..7dc3d2b1 100644
--- a/cle/backends/macho/section.py
+++ b/cle/backends/macho/section.py
@@ -1,14 +1,16 @@
 # -*-coding:utf8 -*-
 # This file is part of Mach-O Loader for CLE.
 # Contributed December 2016 by Fraunhofer SIT (https://www.sit.fraunhofer.de/en/).
+from typing import Optional
 
-from .. import Region
+from .segment import MachOSegment
+from .. import Section
 
 TYPE_MASK = 0x000000ff
 ATTRIBUTES_MASK = 0xffffff00
 
 
-class MachOSection(Region):
+class MachOSection(Section):
     """
     Mach-O Section, only defined within the context of a Mach-O Segment.
 
@@ -25,9 +27,11 @@ class MachOSection(Region):
         - r1 and r2 are values for the reserved1 and reserved2 fields respectively
     """
 
-    def __init__(self, offset, vaddr, size, vsize, segname, sectname, align, reloff, nreloc, flags, r1, r2):
-        super().__init__(offset, vaddr, size, vsize)
-
+    def __init__(self, offset, vaddr, size, vsize, segname, sectname, align, reloff, nreloc, flags, r1, r2,
+                 parent_segment: Optional[MachOSegment] = None):
+        super().__init__(sectname.decode(), offset, vaddr, size)
+        self.filesize = size
+        self.memsize = vsize
         self.segname = segname.decode()
         self.sectname = sectname.decode()
         self.align = align
@@ -36,6 +40,7 @@ def __init__(self, offset, vaddr, size, vsize, segname, sectname, align, reloff,
         self.flags = flags
         self.reserved1 = r1
         self.reserved2 = r2
+        self.parent_segment = parent_segment
 
     @property
     def type(self):
@@ -45,6 +50,38 @@ def type(self):
     def attributes(self):
         return self.flags & ATTRIBUTES_MASK
 
+    @property
+    def is_readable(self):
+        """
+        Always true, because sections should always be readable
+        :return:
+        """
+        return True
+
+    @property
+    def is_writable(self):
+        """
+        Returns the permission of the parent segment, because MachO sections simply inherit that
+        :return:
+        """
+        return self.parent_segment.is_writable
+
+    @property
+    def is_executable(self):
+        """
+        Returns the permission of the parent segment, because MachO sections simply inherit that
+        :return:
+        """
+        return self.parent_segment.is_executable
+
+    @property
+    def only_contains_uninitialized_data(self):
+        """
+        I actually don't know if this is true, but it seems like a saner assumption than true
+        :return:
+        """
+        return False
+
     def __repr__(self):
         return "<Section: %s (part of Segment: %s)| offset %#x, vaddr %#x, size %#x>" % (
             self.sectname if self.sectname else "Unnamed",
diff --git a/cle/backends/macho/segment.py b/cle/backends/macho/segment.py
index 4b5ebd1e..c544dba5 100644
--- a/cle/backends/macho/segment.py
+++ b/cle/backends/macho/segment.py
@@ -2,10 +2,10 @@
 # This file is part of Mach-O Loader for CLE.
 # Contributed December 2016 by Fraunhofer SIT (https://www.sit.fraunhofer.de/en/).
 
-from .. import Region
+from .. import Segment
 
 
-class MachOSegment(Region):
+class MachOSegment(Segment):
     """
     Mach-O Segment
 
@@ -59,7 +59,6 @@ def is_writable(self):
     def is_executable(self):
         return ((self.initprot | self.maxprot) & 0x04) != 0
 
-
     def __repr__(self):
         return "<%s | offset %#x, vaddr %#x, size %#x>" % (
             self.segname if self.segname else "Unnamed",
diff --git a/cle/backends/macho/structs.py b/cle/backends/macho/structs.py
new file mode 100644
index 00000000..8a38cd43
--- /dev/null
+++ b/cle/backends/macho/structs.py
@@ -0,0 +1,424 @@
+# noinspection PyPep8Naming
+# This file takes some liberties to match the naming from the dyld implementation
+import ctypes
+import enum
+from ctypes import Structure, c_uint32, c_uint64, c_uint16
+from typing import Union, Type, Tuple, Optional
+
+# Some type aliases
+FilePointer = int  # Offset into a raw binary file
+FileOffset = int  # Offset to another FilePointer
+MemoryPointer = int  # Offset into the mapped memory space
+
+# All of this is based on the dyld-852.2 source code
+
+
+class HelperStruct(Structure):
+    """
+    Subclass of ctypes.Structure that adds a helpful repr method for debugging
+    """
+
+    def __repr__(self):
+        r = [f"<struct {self.__class__.__name__}"]
+        for name, *_ in self._fields_:
+
+            r.append(f"{name}={hex(getattr(self, name))}")
+        r.append(">")
+        return " ".join(r)
+
+
+class DyldImportFormats(enum.IntEnum):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L249-L254
+    """
+
+    DYLD_CHAINED_IMPORT = (1,)
+    DYLD_CHAINED_IMPORT_ADDEND = (2,)
+    DYLD_CHAINED_IMPORT_ADDEND64 = (3,)
+
+
+class DyldChainedPtrFormats(enum.IntEnum):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L89-L104
+
+    """
+
+    DYLD_CHAINED_PTR_ARM64E = 1  # stride 8, unauth target is vmaddr
+    DYLD_CHAINED_PTR_64 = 2  # target is vmaddr
+    DYLD_CHAINED_PTR_32 = 3
+    DYLD_CHAINED_PTR_32_CACHE = 4
+    DYLD_CHAINED_PTR_32_FIRMWARE = 5
+    DYLD_CHAINED_PTR_64_OFFSET = 6  # target is vm offset
+    # DYLD_CHAINED_PTR_ARM64E_OFFSET          =  7 # old name
+    DYLD_CHAINED_PTR_ARM64E_KERNEL = 7  # stride 4, unauth target is vm offset
+    DYLD_CHAINED_PTR_64_KERNEL_CACHE = 8
+    DYLD_CHAINED_PTR_ARM64E_USERLAND = 9  # stride 8, unauth target is vm offset
+    DYLD_CHAINED_PTR_ARM64E_FIRMWARE = 10  # stride 4, unauth target is vmaddr
+    DYLD_CHAINED_PTR_X86_64_KERNEL_CACHE = 11  # stride 1, x86_64 kernel caches
+    DYLD_CHAINED_PTR_ARM64E_USERLAND24 = 12  # stride 8, unauth target is vm offset, 24-bit bind
+
+
+# noinspection PyPep8Naming
+class dyld_chained_ptr_arm64e_auth_rebase(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L128-L138
+    """
+
+
+# noinspection PyPep8Naming
+class dyld_chained_ptr_arm64e_auth_bind(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L140-L151
+    """
+
+
+# noinspection PyPep8Naming
+class dyld_chained_ptr_arm64e_rebase(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L107-L115
+    """
+
+
+# noinspection PyPep8Naming
+class dyld_chained_ptr_arm64e_bind(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L117-L126
+    """
+
+
+# noinspection PyPep8Naming
+class dyld_chained_ptr_arm64e_bind24(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L164-L173
+    """
+
+
+# noinspection PyPep8Naming
+class dyld_chained_ptr_arm64e_auth_bind24(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L175-L186
+    """
+
+
+class Arm64e(ctypes.Union):
+    """
+    named after the Union `Arm64e` from dyld MachOLoaded.h
+    https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.h#L89-L103
+    """
+
+    authRebase: dyld_chained_ptr_arm64e_auth_rebase
+    authBind: dyld_chained_ptr_arm64e_auth_bind
+    rebase: dyld_chained_ptr_arm64e_rebase
+    bind: dyld_chained_ptr_arm64e_bind
+    bind24: dyld_chained_ptr_arm64e_bind24
+    authBind24: dyld_chained_ptr_arm64e_auth_bind24
+
+    _fields_ = [
+        ("authRebase", dyld_chained_ptr_arm64e_auth_rebase),
+        ("authBind", dyld_chained_ptr_arm64e_auth_bind),
+        ("rebase", dyld_chained_ptr_arm64e_rebase),
+        ("bind", dyld_chained_ptr_arm64e_bind),
+        ("bind24", dyld_chained_ptr_arm64e_bind24),
+        ("authBind24", dyld_chained_ptr_arm64e_auth_bind24),
+    ]
+
+    @staticmethod
+    def check_valid_pointer_format(pointer_format: DyldChainedPtrFormats) -> bool:
+        """
+        helper to check if a pointer format is relevant for this
+        :param pointer_format:
+        :return:
+        """
+        return pointer_format in [
+            DyldChainedPtrFormats.DYLD_CHAINED_PTR_ARM64E,
+            DyldChainedPtrFormats.DYLD_CHAINED_PTR_ARM64E_USERLAND,
+            DyldChainedPtrFormats.DYLD_CHAINED_PTR_ARM64E_USERLAND24,
+            DyldChainedPtrFormats.DYLD_CHAINED_PTR_ARM64E_KERNEL,
+            DyldChainedPtrFormats.DYLD_CHAINED_PTR_ARM64E_FIRMWARE,
+        ]
+
+
+# noinspection PyPep8Naming
+class dyld_chained_ptr_64_rebase(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L153-L161
+    """
+
+    target: Union[FilePointer, FileOffset]
+    high8: int
+    next: int
+    bind: int
+
+    _fields_ = [
+        ("target", c_uint64, 36),
+        ("high8", c_uint64, 8),
+        ("_reserved", c_uint64, 7),
+        ("next", c_uint64, 12),
+        ("bind", c_uint64, 1),
+    ]
+
+    @property
+    def unpackedTarget(self):
+        return self.high8 << 56 | self.target
+
+
+# noinspection PyPep8Naming
+class dyld_chained_ptr_64_bind(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L189-L197
+    """
+
+    ordinal: int
+    addend: int
+    next: int
+    bind: int
+
+    _fields_ = [
+        ("ordinal", c_uint64, 24),
+        ("addend", c_uint64, 8),
+        ("_reserved", c_uint64, 19),
+        ("next", c_uint64, 12),
+        ("bind", c_uint64, 1),
+    ]
+
+
+class Generic64(ctypes.Union):
+    """
+    named after the Union `Generic64` from dyld MachOLoaded.h
+    https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.h#L105-L111
+    """
+
+    rebase: dyld_chained_ptr_64_rebase
+    bind: dyld_chained_ptr_64_bind
+
+    _fields_ = [
+        ("rebase", dyld_chained_ptr_64_rebase),
+        ("bind", dyld_chained_ptr_64_bind),
+    ]
+
+    @staticmethod
+    def check_valid_pointer_format(pointer_format: DyldChainedPtrFormats) -> bool:
+        return pointer_format in [
+            DyldChainedPtrFormats.DYLD_CHAINED_PTR_64,
+            DyldChainedPtrFormats.DYLD_CHAINED_PTR_64_OFFSET,
+        ]
+
+
+# noinspection PyPep8Naming
+class ChainedFixupPointerOnDisk(ctypes.Union):
+    """
+    the ChainedFixupPointerOnDisk union from dyld MachOLoaded.h
+    https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.h#L87-L141
+    """
+
+    generic64: Generic64
+    arm64e: Arm64e
+
+    _fields_ = [("generic64", Generic64), ("arm64e", Arm64e)]
+
+    def isBind(self, pointer_format: DyldChainedPtrFormats) -> Optional[Tuple[int, int]]:
+        """
+        Port of ChainedFixupPointerOnDisk::isBind(uint16_t pointerFormat, uint32_t& bindOrdinal, int64_t& addend)
+        https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.cpp#L1098-L1147
+        Returns None if not a bind (so `if struct.isBind()` works),
+        :return:
+        """
+        # pylint: disable=no-else-raise
+        if Arm64e.check_valid_pointer_format(pointer_format):
+            # TODO: implement https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.cpp#L1107-L1124
+            raise NotImplementedError("Arm64e is not implemented yet")
+        elif Generic64.check_valid_pointer_format(pointer_format):
+            # https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.cpp#L1126-L1132
+            if self.generic64.bind.bind:
+                return self.generic64.bind.ordinal, self.generic64.bind.addend
+            else:
+                return None
+
+        else:
+            raise NotImplementedError(f"Not yet supported pointer format {pointer_format}")
+
+    def isRebase(
+        self, pointer_format: DyldChainedPtrFormats, preferredLoadAddress: MemoryPointer
+    ) -> Optional[MemoryPointer]:
+        """
+        port of ChainedFixupPointerOnDisk::isRebase(
+        uint16_t pointerFormat, uint64_t preferedLoadAddress, uint64_t& targetRuntimeOffset)
+        https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.cpp#L1046-L1096
+        :param pointer_format:
+        :param preferredLoadAddress: I think that's just the requested base address
+        :return:
+        """
+        # pylint: disable=no-else-raise
+        if Arm64e.check_valid_pointer_format(pointer_format):
+            # TODO: port https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.cpp#L1049-L1067
+            raise NotImplementedError("Arm64e is not implemented yet")
+        elif Generic64.check_valid_pointer_format(pointer_format):
+            # https://github.com/apple-opensource/dyld/blob/852.2/dyld3/MachOLoaded.cpp#L1068-L1076
+            rebase = self.generic64.rebase
+            if rebase.bind:
+                # Then this wasn't actually a rebase
+                return False
+            else:
+                targetRuntimeOffset = rebase.unpackedTarget
+                if pointer_format == DyldChainedPtrFormats.DYLD_CHAINED_PTR_64:
+                    targetRuntimeOffset -= preferredLoadAddress
+                return targetRuntimeOffset
+        else:
+            raise NotImplementedError(f"Not yet supported pointer format {pointer_format}")
+
+
+# DYLD_CHAINED_PTR_BASE: Dict[DyldChainedPtrFormats, Type[ChainedFixupPointerOnDisk]] = {
+#     DyldChainedPtrFormats.DYLD_CHAINED_PTR_ARM64E: Arm64e,
+#     DyldChainedPtrFormats.DYLD_CHAINED_PTR_ARM64E_USERLAND24: Arm64e,
+#     DyldChainedPtrFormats.DYLD_CHAINED_PTR_64: Generic64,
+#     DyldChainedPtrFormats.DYLD_CHAINED_PTR_64_OFFSET: Generic64
+#
+# }
+
+
+class DyldImportStruct(HelperStruct):
+    """
+    Meta Struct for the different kind of import structs and the fields they are all guaranteed to have
+    """
+
+    lib_ordinal: int
+    weak_import: bool
+    name_offset: FileOffset
+
+    @staticmethod
+    def get_struct(pointer: DyldImportFormats) -> Type["DyldImportStruct"]:
+        return {
+            DyldImportFormats.DYLD_CHAINED_IMPORT: dyld_chained_import,
+            DyldImportFormats.DYLD_CHAINED_IMPORT_ADDEND64: dyld_chained_import_addend64,
+            DyldImportFormats.DYLD_CHAINED_IMPORT_ADDEND: dyld_chained_import_addend,
+        }[pointer]
+
+
+# noinspection PyPep8Naming
+class dyld_chained_import(DyldImportStruct):
+    """
+    Struct for symbol format DYLD_CHAINED_IMPORT
+
+    """
+
+    _fields_ = [
+        ("lib_ordinal", c_uint32, 8),
+        ("weak_import", c_uint32, 1),
+        ("name_offset", c_uint32, 23),
+    ]
+
+
+# noinspection PyPep8Naming
+class dyld_chained_import_addend(DyldImportStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L264-L271
+    """
+
+    addend: int
+    _fields_ = [
+        ("lib_ordinal", c_uint32, 8),
+        ("weak_import", c_uint32, 1),
+        ("name_offset", c_uint32, 23),
+        ("addend", c_uint32),
+    ]
+
+
+# DYLD_CHAINED_IMPORT_ADDEND64
+# noinspection PyPep8Naming
+class dyld_chained_import_addend64(DyldImportStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L273-L281
+    """
+
+    addend: int
+    _fields_ = [
+        ("lib_ordinal", c_uint64, 16),
+        ("weak_import", c_uint64, 1),
+        ("reserved", c_uint64, 15),
+        ("name_offset", c_uint64, 32),
+        ("addend", c_uint64),
+    ]
+
+
+# noinspection PyPep8Naming
+class dyld_chained_fixups_header(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L36-L46
+    """
+
+    fixups_version: int
+    starts_offset: FileOffset
+    imports_offset: FileOffset
+    symbols_offset: FileOffset
+    imports_count: int
+    imports_format: DyldImportFormats
+    symbols_format: int
+
+    _fields_ = [
+        ("fixups_version", c_uint32),
+        # offset of dyld_chained_starts_in_image in chain_data
+        ("starts_offset", c_uint32),
+        # offset of imports table in chain_data
+        ("imports_offset", c_uint32),
+        # offset of symbol strings in chain_data
+        ("symbols_offset", c_uint32),
+        # number of imported symbol names
+        ("imports_count", c_uint32),
+        # DYLD_CHAINED_IMPORT*
+        ("imports_format", c_uint32),
+        # 0 => uncompressed, 1 => zlib compressed
+        ("symbols_format", c_uint32),
+    ]
+
+
+# noinspection PyPep8Naming
+class dyld_chained_starts_in_image(ctypes.Structure):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L48-L54
+    """
+
+    seg_count: int
+    seg_info_offset: ctypes.Array
+
+    _fields_ = [("seg_count", c_uint32), ("seg_info_offset", c_uint32 * 1)]
+
+
+DYLD_CHAINED_PTR_START_NONE = 0xFFFF  # used in page_start[] to denote a page with no fixups
+DYLD_CHAINED_PTR_START_MULTI = 0x8000  # used in page_start[] to denote a page which has multiple starts
+DYLD_CHAINED_PTR_START_LAST = 0x8000  # used in chain_starts[] to denote last start in list for page
+
+
+# noinspection PyPep8Naming
+class dyld_chained_starts_in_segment(HelperStruct):
+    """
+    https://github.com/apple-opensource/dyld/blob/852.2/include/mach-o/fixup-chains.h#L56-L72
+    """
+
+    # size: int
+    page_size: int
+    _pointer_format: int
+    segment_offset: int
+    max_valid_pointer: int
+    page_count: int
+    page_start: int
+
+    _fields_ = [
+        # size of this (amount kernel needs to copy)
+        ("_size", c_uint32),
+        # 0x1000 or 0x4000
+        ("page_size", c_uint16),
+        # DYLD_CHAINED_PTR_*
+        ("_pointer_format", c_uint16),
+        # offset in memory to start of segment
+        ("segment_offset", c_uint64),
+        ("max_valid_pointer", c_uint32),
+        ("page_count", c_uint16),
+        ("page_start", c_uint16),
+    ]
+
+    @property
+    def pointer_format(self) -> DyldChainedPtrFormats:
+        return DyldChainedPtrFormats(self._pointer_format)
+
+
+BIND_SPECIAL_DYLIB_WEAK_LOOKUP = -3
diff --git a/cle/backends/macho/symbol.py b/cle/backends/macho/symbol.py
index fa381fde..9bbf38b2 100644
--- a/cle/backends/macho/symbol.py
+++ b/cle/backends/macho/symbol.py
@@ -2,10 +2,14 @@
 
 # This file is part of Mach-O Loader for CLE.
 # Contributed December 2016 by Fraunhofer SIT (https://www.sit.fraunhofer.de/en/) and updated in September 2019.
+import logging
+from typing import TYPE_CHECKING
 
-from .. import Symbol, SymbolType
+from .. import Symbol, SymbolType, Backend
+
+if TYPE_CHECKING:
+    from . import MachO
 
-import logging
 l = logging.getLogger(name=__name__)
 
 # some constants:
@@ -16,10 +20,14 @@
 SYMBOL_TYPE_INDIR = 0xa
 
 LIBRARY_ORDINAL_SELF = 0x0
-LIBRARY_ORDINAL_MAX = 0xfd
 LIBRARY_ORDINAL_OLD_MAX = 0xfe
 LIBRARY_ORDINAL_DYN_LOOKUP = 0xfe
-LIBRARY_ORDINAL_EXECUTABLE = 0xff
+
+BIND_SPECIAL_DYLIB_SELF = 0x0
+BIND_SPECIAL_DYLIB_WEAK_LOOKUP = 0xfd
+BIND_SPECIAL_DYLIB_FLAT_LOOKUP = 0xfe
+BIND_SPECIAL_DYLIB_MAIN_EXECUTABLE = 0xff # technically -1
+
 
 class AbstractMachOSymbol(Symbol):
     """
@@ -27,8 +35,12 @@ class AbstractMachOSymbol(Symbol):
     Defines the minimum common properties all types of mach-o symbols must have
     """
 
-    def __init__(self, owner, name, relative_addr, size, sym_type):
-        super().__init__(owner,name,relative_addr,size,sym_type)
+    def __init__(self, owner: Backend,
+             name: str,
+             relative_addr: int,
+             size: int,
+             sym_type: SymbolType):
+        super().__init__(owner, name, relative_addr, size, sym_type)
 
         # additional properties
         self.bind_xrefs = []  # XREFs discovered during binding of the symbol
@@ -42,6 +54,10 @@ def library_ordinal(self):
     def is_stab(self):
         return False
 
+    @property
+    def library_name(self):
+        return None
+
 class SymbolTableSymbol(AbstractMachOSymbol):
     """
     "Regular" symbol. Made to be (somewhat) compatible with backends.Symbol.
@@ -53,15 +69,16 @@ class SymbolTableSymbol(AbstractMachOSymbol):
 
     Much of the code below is based on heuristics as official documentation is sparse, consider yourself warned!
     """
-
-    def __init__(self, owner, symtab_offset, n_strx, n_type, n_sect, n_desc, n_value):
+    owner: "MachO"
+    def __init__(self, owner: "MachO", symtab_offset, n_strx, n_type, n_sect, n_desc, n_value):
         # Note 1: Setting size = owner.arch.bytes has been directly taken over from the PE backend,
         # there is no meaningful definition of a symbol's size so I assume the size of an address counts here
-        # Note 2: relative_addr will be the address of a symbols __got or __nl_symbol_ptr entry, not the address of a stub
+        # Note 2: relative_addr will be the address of a symbols __got or __nl_symbol_ptr entry, not the addr of a stub
         # pointing to the symobl.
         # Stub addresses must be obtained through some sort of higher-level analysis
         # Note 3: A symbols name may not be unique!
-        # Note 4: The symbol type of all symbols is SymbolType.TYPE_OTHER because without docs I was unable to proplerly map Mach-O symbol types to CLE's notion of a symbol type
+        # Note 4: The symbol type of all symbols is SymbolType.TYPE_OTHER
+        # because without docs I was unable to proplerly map Mach-O symbol types to CLE's notion of a symbol type
 
         # store the mach-o properties, all these are raw values straight from the binary
         self.symtab_offset = symtab_offset # offset from the start of the symbol table
@@ -83,7 +100,9 @@ def __init__(self, owner, symtab_offset, n_strx, n_type, n_sect, n_desc, n_value
                 SymbolType.TYPE_OTHER)
 
         # set further fields
-        self.is_import = self.sym_type == SYMBOL_TYPE_UNDEF and self.is_external and self.library_ordinal != LIBRARY_ORDINAL_SELF
+        self.is_import = (self.sym_type == SYMBOL_TYPE_UNDEF
+                          and self.is_external
+                          and self.library_ordinal != LIBRARY_ORDINAL_SELF)
         self.is_export = self.name in self.owner.exports_by_name
 
     @property
@@ -131,9 +150,10 @@ def is_weak(self):
         # compare https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/MachOTopics/1-Articles/executing_files.html
         return self.is_weak_referenced
 
+    @property
     def is_function(self):
         # Incompatibility to CLE
-        l.warning("It is not possible to decide wether a symbol is a function or not for MachOSymbols")
+        l.debug("It is not possible to decide wether a symbol is a function or not for MachOSymbols")
         return False
 
     @property
@@ -210,6 +230,59 @@ def is_alt_entry(self):
         return self.n_desc & 0x0200
 
 
+class DyldBoundSymbol(AbstractMachOSymbol):
+    """
+    The new kind of symbol handling introduced with ios15
+    """
+    def __init__(self, owner, name, lib_ordinal):
+        """Based on the constructor of BindingSymbol"""
+
+        # store the mach-o properties, all these are raw values straight from the binary
+        self.lib_ordinal = lib_ordinal
+
+        super().__init__(owner,
+                         name,
+                         0,
+                         owner.arch.bytes,
+                         SymbolType.TYPE_OTHER)
+
+        # set further fields
+        self.is_import = True  # TODO: this is always an import for now
+        # with ios15 came a new load command LC_DYLD_EXPORTS_TRIE
+        # this isn't handled yet, so for now no symbol generated by the new dyld handling is an export
+        self.is_export = False
+
+    @property
+    def library_name(self):
+        if BIND_SPECIAL_DYLIB_FLAT_LOOKUP == self.lib_ordinal:
+            l.warning("BIND_SPECIAL_DYLIB_FLAT_LOOKUP found, cannot handle")
+            return None
+        elif BIND_SPECIAL_DYLIB_WEAK_LOOKUP == self.lib_ordinal:
+            return None
+        try:
+            return self.owner_obj.imported_libraries[self.lib_ordinal]
+        except IndexError:
+            l.error("Symbol %s has library ordinal %d, but there are only %d imported libraries", self,
+                    self.lib_ordinal, len(self.owner_obj.imported_libraries))
+            return None
+
+    @property
+    def is_function(self):
+        # Incompatibility to CLE
+        l.debug("It is not possible to decide wether a symbol is a function or not for MachOSymbols")
+        return False
+
+    @property
+    def rebased_addr(self):
+        return self.linked_addr
+
+    def demangled_name(self):
+        return self.name  # it is not THAT easy with Mach-O
+
+    @property
+    def library_ordinal(self):
+        return self.lib_ordinal
+
 class BindingSymbol(AbstractMachOSymbol):
     """
     "Binding" symbol. Made to be (somewhat) compatible with backends.Symbol.
@@ -227,7 +300,7 @@ class BindingSymbol(AbstractMachOSymbol):
     def __init__(self, owner, name,lib_ordinal):
         # Note 1: Setting size = owner.arch.bytes has been directly taken over from the PE backend,
         # there is no meaningful definition of a symbol's size so I assume the size of an address counts here
-        # Note 2: relative_addr will be the address of a symbols __got or __nl_symbol_ptr entry, not the address of a stub
+        # Note 2: relative_addr will be the address of a symbols __got or __nl_symbol_ptr entry, not the addr of a stub
         # pointing to the symobl.
         # Stub addresses must be obtained through some sort of higher-level analysis
         # Note 3: A symbols name may not be unique!
@@ -257,10 +330,10 @@ def library_name(self):
 
         return self.owner_obj.imported_libraries[self.lib_ordinal]
 
-
+    @property
     def is_function(self):
         # Incompatibility to CLE
-        l.warning("It is not possible to decide wether a symbol is a function or not for MachOSymbols")
+        l.debug("It is not possible to decide wether a symbol is a function or not for MachOSymbols")
         return False
 
     @property
diff --git a/cle/backends/region.py b/cle/backends/region.py
index 8bae3ad2..68b21701 100644
--- a/cle/backends/region.py
+++ b/cle/backends/region.py
@@ -64,7 +64,9 @@ def offset_to_addr(self, offset):
         return addr
 
     def __repr__(self):
-        return '<{} {}>'.format(self.__class__.__name__, ', '.join(['{}=0x{:x}'.format(k, v) for k, v in self.__dict__.items()]))
+        return '<{} {}>'.format(self.__class__.__name__,
+                                ', '.join(['{}=0x{:x}'.format(k, v) for k, v in self.__dict__.items()])
+                                )
 
     @property
     def max_addr(self):
@@ -95,15 +97,18 @@ def min_offset(self):
 
     # EDG says: Blobs now have segments, and SimOS will get upset if these don't exist.  See simos.py line 107 for
     # some code you should probably fix if you don't like it.
-    def is_readable(self):
+    @property
+    def is_readable(self) -> bool:
         # pylint: disable=no-self-use
         return True
 
-    def is_writable(self):
+    @property
+    def is_writable(self) -> bool:
         # pylint: disable=no-self-use
         return True
 
-    def is_executable(self):
+    @property
+    def is_executable(self) -> bool:
         # pylint: disable=no-self-use
         return True
 
diff --git a/cle/backends/symbol.py b/cle/backends/symbol.py
index 57ef6579..b2e82207 100644
--- a/cle/backends/symbol.py
+++ b/cle/backends/symbol.py
@@ -1,8 +1,12 @@
 from enum import Enum
 import logging
+from typing import TYPE_CHECKING
 
 from ..address_translator import AT
 
+if TYPE_CHECKING:
+    from .. import Backend
+
 l = logging.getLogger(name=__name__)
 
 
@@ -48,7 +52,7 @@ class Symbol:
     :ivar str resolvewith:  The name of the library we must use to resolve this symbol, or None if none is required.
     """
 
-    def __init__(self, owner, name, relative_addr, size, sym_type):
+    def __init__(self, owner: "Backend", name: str, relative_addr: int, size: int, sym_type: SymbolType):
         """
         Not documenting this since if you try calling it, you're wrong.
         """
@@ -115,7 +119,7 @@ def is_function(self):
 
     def resolve_forwarder(self):
         """
-        If this symbol is a forwarding export, return the symbol the forwarding refers to, or None if it cannot be found.
+        If this symbol is a forwarding export, return the symbol the forwarding refers to, or None if it cannot be found
         """
         return self
 


Test patch:
diff --git a/tests/test_macho_bindinghelper.py b/tests/test_macho_bindinghelper.py
index 3e9fd4f9..d2f92486 100644
--- a/tests/test_macho_bindinghelper.py
+++ b/tests/test_macho_bindinghelper.py
@@ -303,7 +303,7 @@ def test_bind_real_32(self):
 
         machofile = os.path.join(TEST_BASE, 'tests', 'armhf', 'FileProtection-05.armv7.macho')
         ld = cle.Loader(machofile, auto_load_libs=False)
-        macho = ld.main_object
+        macho: MachO = ld.main_object
         macho.do_binding()
 
         expected = {
diff --git a/tests/test_macho_dyld.py b/tests/test_macho_dyld.py
new file mode 100644
index 00000000..6e225ecc
--- /dev/null
+++ b/tests/test_macho_dyld.py
@@ -0,0 +1,146 @@
+from typing import cast
+from pathlib import Path
+
+import nose
+
+import cle
+from cle import MachO
+
+TEST_BASE = Path(__file__).resolve().parent.parent.parent / "binaries"
+
+
+
+def test_fixups():
+    """
+    Tests the pointer format DYLD_CHAINED_PTR_64_OFFSET
+    :return:
+    """
+    binary: MachO = cast(MachO,
+                         cle.Loader(str(TEST_BASE / "tests" / "aarch64" / "dyld_ios15.macho"))
+                         .main_object)
+    expected = {0x100008100: 0x100007a40,
+                0x1000081e0: 0x1000072b0,
+                0x1000081e8: 0x1000072dc,
+                0x1000081f0: 0x1000072e4,
+                0x1000081f8: 0x100007310,
+                0x100008200: 0x100007350,
+                0x100008208: 0x10000735c,
+                0x100008210: 0x10000738c,
+                0x100008218: 0x1000073e8,
+                0x100008238: 0x1000081e0,
+                0x100008248: 0x100007a40,
+                0x1000082a0: 0x100007afc,
+                0x1000082d8: 0x10000c0e8,
+                0x10000c018: 0x100007b90,
+                0x10000c060: 0x100007b90,
+                0x10000c068: 0x100007998,
+                0x10000c090: 0x100007c2a,
+                0x10000c0d0: 0x10000c000,
+                0x10000c0d8: 0x100007210,
+                0x10000c0e8: 0x10000c0b0,
+                0x10000c108: 0x10000c04a,
+                0x10000c128: 0x1000079f0}
+
+    nose.tools.assert_equals(len(binary._dyld_rebases), len(expected))
+    nose.tools.assert_dict_equal(binary._dyld_rebases, expected)
+
+
+def test_symbols():
+    l = cle.Loader(str(TEST_BASE / "tests" / "aarch64" / "dyld_ios15.macho"))
+    binary: MachO = cast(MachO, l.main_object)
+
+    expected = [(0x100008000, '_$s10Foundation5NSLogyySS_s7CVarArg_pdtF'),
+                (0x100008008,
+                 '_$s2os0A4_log_3dso0B04type_ys12StaticStringV_SVSgSo03OS_a1_B0CSo0a1_b1_D2_tas7CVarArg_pdtF'),
+                (0x100008010, '_$s7SwiftUI11WindowGroupV7contentACyxGxyXE_tcfC'),
+                (0x100008018, '_$s7SwiftUI11WindowGroupVMn'),
+                (0x100008020, '_$s7SwiftUI11WindowGroupVyxGAA5SceneAAMc'),
+                (0x100008028, '_$s7SwiftUI12SceneBuilderV10buildBlockyxxAA0C0RzlFZ'),
+                (0x100008030, '_$s7SwiftUI13_VStackLayoutVMn'),
+                (0x100008038, '_$s7SwiftUI13_VariadicViewO4TreeVMn'),
+                (0x100008040, '_$s7SwiftUI14_PaddingLayoutVMn'),
+                (0x100008048, '_$s7SwiftUI15ModifiedContentVMn'),
+                (0x100008050, '_$s7SwiftUI18LocalizedStringKeyV13stringLiteralACSS_tcfC'),
+                (0x100008058, '_$s7SwiftUI19HorizontalAlignmentV6centerACvgZ'),
+                (0x100008060, '_$s7SwiftUI3AppPAAE4mainyyFZ'),
+                (0x100008068, '_$s7SwiftUI4EdgeO3SetV3allAEvgZ'),
+                (0x100008070, '_$s7SwiftUI4TextVMn'),
+                (0x100008078,
+                 '_$s7SwiftUI4ViewPAAE05_makeC04view6inputsAA01_C7OutputsVAA11_GraphValueVyxG_AA01_C6InputsVtFZ'),
+                (0x100008080,
+                 '_$s7SwiftUI4ViewPAAE05_makeC4List4view6inputsAA01_cE7OutputsVAA11_GraphValueVyxG_AA01_cE6InputsVtFZ'),
+                (0x100008088, '_$s7SwiftUI4ViewPAAE14_viewListCount6inputsSiSgAA01_ceF6InputsV_tFZ'),
+                (0x100008090, '_$s7SwiftUI5StateV12wrappedValueACyxGx_tcfC'),
+                (0x100008098, '_$s7SwiftUI5StateVMn'),
+                (0x1000080a0, '_$s7SwiftUI6ButtonVA2A4TextVRszrlE_6actionACyAEGAA18LocalizedStringKeyV_yyctcfC'),
+                (0x1000080a8, '_$s7SwiftUI6ButtonVMn'),
+                (0x1000080b0, '_$s7SwiftUI6VStackVMn'),
+                (0x1000080b8, '_$s7SwiftUI6VStackVyxGAA4ViewAAMc'),
+                (0x1000080c0, '_$sSiN'),
+                (0x1000080c8, '_$sSo13os_log_type_ta0A0E7defaultABvgZ'),
+                (0x1000080d0, '_$sSo8NSObjectCs7CVarArg10ObjectiveCMc'),
+                (0x1000080d8, '_$sSo8NSStringC10FoundationE13stringLiteralABs12StaticStringV_tcfC'),
+                (0x1000080e0, '_$sSo9OS_os_logC0B0E7defaultABvgZ'),
+                (0x1000080e8, '_$ss23_ContiguousArrayStorageCMn'),
+                (0x1000080f0, '_$ss7CVarArgMp'),
+                (0x1000080f8, '___chkstk_darwin'),
+                (0x100008108, '__swiftEmptyArrayStorage'),
+                (0x100008110, '_objc_opt_self'),
+                (0x100008118, '_objc_release'),
+                (0x100008120, '_swift_allocObject'),
+                (0x100008128, '_swift_deallocClassInstance'),
+                (0x100008130, '_swift_getObjCClassMetadata'),
+                (0x100008138, '_swift_getOpaqueTypeConformance'),
+                (0x100008140, '_swift_getTypeByMangledNameInContext'),
+                (0x100008148, '_swift_getTypeByMangledNameInContextInMetadataState'),
+                (0x100008150, '_swift_getWitnessTable'),
+                (0x100008158, '_swift_release'),
+                (0x100008160, '_swift_retain'),
+                (0x100008168, '__swift_FORCE_LOAD_$_swiftObjectiveC'),
+                (0x100008170, '__swift_FORCE_LOAD_$_swiftDarwin'),
+                (0x100008178, '__swift_FORCE_LOAD_$_swiftos'),
+                (0x100008180, '__swift_FORCE_LOAD_$_swiftUniformTypeIdentifiers'),
+                (0x100008188, '__swift_FORCE_LOAD_$_swiftFoundation'),
+                (0x100008190, '__swift_FORCE_LOAD_$_swiftCoreFoundation'),
+                (0x100008198, '__swift_FORCE_LOAD_$_swiftDispatch'),
+                (0x1000081a0, '__swift_FORCE_LOAD_$_swiftCoreGraphics'),
+                (0x1000081a8, '__swift_FORCE_LOAD_$_swiftUIKit'),
+                (0x1000081b0, '__swift_FORCE_LOAD_$_swiftCoreImage'),
+                (0x1000081b8, '__swift_FORCE_LOAD_$_swiftMetal'),
+                (0x1000081c0, '__swift_FORCE_LOAD_$_swiftQuartzCore'),
+                (0x1000081c8, '__swift_FORCE_LOAD_$_swiftFileProvider'),
+                (0x1000081d0, '__swift_FORCE_LOAD_$_swiftDataDetection'),
+                (0x1000081d8, '__swift_FORCE_LOAD_$_swiftCoreData'),
+                (0x100008258, '_$s7SwiftUI4ViewMp'),
+                (0x100008260, '_$s7SwiftUI4ViewP4BodyAC_AaBTn'),
+                (0x100008268, '_$s4Body7SwiftUI4ViewPTl'),
+                (0x100008270,
+                 '_$s7SwiftUI4ViewP05_makeC04view6inputsAA01_C7OutputsVAA11_GraphValueVyxG_AA01_C6InputsVtFZTq'),
+                (0x100008278,
+                 '_$s7SwiftUI4ViewP05_makeC4List4view6inputsAA01_cE7OutputsVAA11_GraphValueVyxG_AA01_cE6InputsVtFZTq'),
+                (0x100008280, '_$s7SwiftUI4ViewP14_viewListCount6inputsSiSgAA01_ceF6InputsV_tFZTq'),
+                (0x100008288, '_$s7SwiftUI4ViewP4body4BodyQzvgTq'),
+                (0x100008290, '_$sytWV'),
+                (0x1000082a8, '_$s7SwiftUI3AppMp'),
+                (0x1000082b0, '_$s7SwiftUI3AppP4BodyAC_AA5SceneTn'),
+                (0x1000082b8, '_$s4Body7SwiftUI3AppPTl'),
+                (0x1000082c0, '_$s7SwiftUI3AppP4body4BodyQzvgTq'),
+                (0x1000082c8, '_$s7SwiftUI3AppPxycfCTq'),
+                (0x1000082d0, '_$s7SwiftUI5SceneMp'),
+                (0x10000c098, '_OBJC_CLASS_$_OS_os_log'),
+                (0x10000c0a0, '_OBJC_CLASS_$_NSString'),
+                (0x10000c0b0, '_OBJC_METACLASS_$__TtCs12_SwiftObject'),
+                (0x10000c0b8, '_OBJC_METACLASS_$__TtCs12_SwiftObject'),
+                (0x10000c0c0, '__objc_empty_cache'),
+                (0x10000c0e0, '_$sBoWV'),
+                (0x10000c0f0, '_OBJC_CLASS_$__TtCs12_SwiftObject'),
+                (0x10000c0f8, '__objc_empty_cache'),
+                (0x10000c138, '_swift_deletedMethodError'),
+                (0x10000c140, '_swift_deletedMethodError')]
+    result = [(r.dest_addr, r.resolvedby.name) for r in binary.relocs]
+    nose.tools.assert_list_equal(expected, result)
+
+
+if __name__ == '__main__':
+    test_fixups()
+    test_symbols()
diff --git a/tests/test_macho_dyld_structs.py b/tests/test_macho_dyld_structs.py
new file mode 100644
index 00000000..aea1d51b
--- /dev/null
+++ b/tests/test_macho_dyld_structs.py
@@ -0,0 +1,62 @@
+from cle.backends.macho.structs import dyld_chained_import, dyld_chained_ptr_64_rebase, Generic64, \
+    ChainedFixupPointerOnDisk
+
+
+def test_parsing_simple():
+    """
+    basically the same tests as test_struct_bitfield_simple from test_memview.py
+    :return:
+    """
+    data = [
+        (b"\x0e\x02\x00\x00", (14, 0, 1)),
+        (b"\x14T\x00\x00", (20, 0, 42)),
+        (b"\x04\n\x01\x00", (4, 0, 133)),
+        (b"\x04j\x01\x00", (4, 0, 181)),
+        (b"\x04\xa2\x01\x00", (4, 0, 209)),
+        (b"\x04\xf4\x01\x00", (4, 0, 250)),
+        (b"\x04\\\x02\x00", (4, 0, 302)),
+        (b"\x04\x98\x02\x00", (4, 0, 332)),
+        (b"\x04\xe0\x02\x00", (4, 0, 368)),
+        (b"\x04\x1e\x03\x00", (4, 0, 399)),
+    ]
+    for b, (lib_ordinal, weak_import, name_offset) in data:
+        struct = dyld_chained_import.from_buffer_copy(b)
+        assert struct.lib_ordinal == lib_ordinal
+        assert struct.weak_import == weak_import
+        assert struct.name_offset == name_offset
+
+
+def test_parsing_complex():
+    """
+    basically the same tests as test_struct_bitfield_complex from test_memview.py
+    :return:
+    """
+    data = b"\xb3\xc7\xe9|\xad\xd7\xee$"
+
+    # Check that parsing the struct directly works
+    struct = dyld_chained_ptr_64_rebase.from_buffer_copy(data)
+    assert struct.target == 0xD7CE9C7B3
+    assert struct.high8 == 0x7A
+    assert struct.next == 0x49D
+    assert struct.bind == 0
+
+    # Test that the Generic64 union works
+    generic_union = Generic64.from_buffer_copy(data)
+
+    struct = generic_union.rebase
+    assert struct.target == 0xD7CE9C7B3
+    assert struct.high8 == 0x7A
+    assert struct.next == 0x49D
+    assert struct.bind == 0
+
+    chained_union = ChainedFixupPointerOnDisk.from_buffer_copy(data)
+    struct = chained_union.generic64.rebase
+    assert struct.target == 0xD7CE9C7B3
+    assert struct.high8 == 0x7A
+    assert struct.next == 0x49D
+    assert struct.bind == 0
+
+
+if __name__ == "__main__":
+    test_parsing_simple()
+    test_parsing_complex()
diff --git a/tests/test_macho_reloc.py b/tests/test_macho_reloc.py
new file mode 100644
index 00000000..16748263
--- /dev/null
+++ b/tests/test_macho_reloc.py
@@ -0,0 +1,105 @@
+import os
+import cle
+
+TEST_BASE = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                         os.path.join('..', '..', 'binaries'))
+
+
+def test_basic_reloc_functionality():
+    machofile = os.path.join(TEST_BASE, 'tests', 'armhf', 'FileProtection-05.arm64.macho')
+    ld = cle.Loader(machofile, auto_load_libs=False)
+    assert isinstance(ld.main_object, cle.MachO)
+    macho: cle.MachO = ld.main_object
+    assert macho.binding_done
+
+    # This is the same data as in test_macho_bindinghelper
+    # but this checks that there are actually proper relocs
+    expected = {0x100009128: '_OBJC_CLASS_$_UIResponder',
+                0x1000090f8: '_OBJC_CLASS_$_UIScreen',
+                0x100009178: '_OBJC_CLASS_$_UIViewController',
+                0x1000090f0: '_OBJC_CLASS_$_UIWindow',
+                0x100009150: '_OBJC_METACLASS_$_UIResponder',
+                0x1000091a0: '_OBJC_METACLASS_$_UIViewController',
+                0x100009148: '_OBJC_METACLASS_$_NSObject',
+                0x100009198: '_OBJC_METACLASS_$_NSObject',
+                0x100009130: '__objc_empty_cache',
+                0x100009158: '__objc_empty_cache',
+                0x100009180: '__objc_empty_cache',
+                0x1000091a8: '__objc_empty_cache',
+                0x1000091d0: '__objc_empty_cache',
+                0x1000091f8: '__objc_empty_cache',
+                0x1000091d8: '__objc_empty_vtable',
+                0x100009200: '__objc_empty_vtable',
+                0x100008010: '_class_getName',
+                0x100008020: '_objc_allocateClassPair',
+                0x100008028: '_objc_copyClassNamesForImage',
+                0x100008030: '_objc_getClass',
+                0x100008038: '_objc_getMetaClass',
+                0x100008040: '_objc_getProtocol',
+                0x100008048: '_objc_getRequiredClass',
+                0x100008050: '_objc_lookUpClass',
+                0x100008058: '_objc_readClassPair',
+                0x100008060: '_object_getIndexedIvars',
+                0x100008068: '_protocol_getName',
+                0x100008000: '__DefaultRuneLocale',
+                0x100008008: '___stack_chk_guard',
+                0x100008070: 'dyld_stub_binder',
+                0x1000081f8: '___CFConstantStringClassReference',
+                0x100008218: '___CFConstantStringClassReference',
+                0x100008238: '___CFConstantStringClassReference',
+                0x100008018: '_kCFCoreFoundationVersionNumber',
+                0x100008080: '_UIApplicationMain',
+                0x100008088: '_NSSearchPathForDirectoriesInDomains',
+                0x100008090: '_NSStringFromClass',
+                0x100008098: '_class_addMethod',
+                0x1000080a0: '_class_addProperty',
+                0x1000080a8: '_class_addProtocol',
+                0x1000080b0: '_class_getInstanceMethod',
+                0x1000080b8: '_class_getInstanceVariable',
+                0x1000080c0: '_class_getSuperclass',
+                0x1000080c8: '_class_isMetaClass',
+                0x1000080d0: '_class_replaceMethod',
+                0x1000080d8: '_method_setImplementation',
+                0x1000080e0: '_objc_autoreleasePoolPop',
+                0x1000080e8: '_objc_autoreleasePoolPush',
+                0x1000080f0: '_objc_constructInstance',
+                0x1000080f8: '_objc_getClass',
+                0x100008100: '_objc_getMetaClass',
+                0x100008108: '_objc_getProtocol',
+                0x100008110: '_objc_getRequiredClass',
+                0x100008118: '_objc_initializeClassPair',
+                0x100008120: '_objc_lookUpClass',
+                0x100008128: '_objc_msgSend',
+                0x100008130: '_objc_msgSendSuper2',
+                0x100008138: '_objc_registerClassPair',
+                0x100008140: '_objc_setProperty_nonatomic',
+                0x100008148: '_object_getClass',
+                0x100008150: '_object_getIvar',
+                0x100008158: '_property_copyAttributeList',
+                0x100008160: '_protocol_getMethodDescription',
+                0x100008168: '_sel_getUid',
+                0x100008170: '___stack_chk_fail',
+                0x100008178: '__dyld_register_func_for_add_image',
+                0x100008180: '_asprintf',
+                0x100008188: '_bzero',
+                0x100008190: '_calloc',
+                0x100008198: '_free',
+                0x1000081a0: '_hash_create',
+                0x1000081a8: '_hash_search',
+                0x1000081b0: '_malloc',
+                0x1000081b8: '_memcmp',
+                0x1000081c0: '_memcpy',
+                0x1000081c8: '_pthread_mutex_lock',
+                0x1000081d0: '_pthread_mutex_unlock',
+                0x1000081d8: '_strcmp',
+                0x1000081e0: '_strlen',
+                0x1000081e8: '_strncmp',
+                0x1000081f0: '_CFStringGetCStringPtr'}
+
+    result = {reloc.dest_addr: reloc.resolvedby.name for reloc in macho.relocs}
+
+    assert expected == result
+
+
+if __name__ == "__main__":
+    test_basic_reloc_functionality()