ElleKit Documentation

repository·main·Indexed 18 days ago

https://github.com/tealbathingsuit/ellekit

A low-level hooking framework for arm64/arm64e XNU kernels on iOS and macOS. ElleKit provides high-performance C and Objective-C function hooking, an arm64 assembler, and reimplements Substrate and libhooker APIs. It supports iOS 15-27 (arm64), iOS 15-17 (arm64e), and macOS Ventura (arm64/arm64e), featuring a JIT inline assembly implementation for Swift and a specialized app launcher for tweaked applications.

Tokens
1.8K
Snippets
5
Records
10
Agent score
70%

What's inside ElleKit

  1. Overview of ElleKit capabilities

    main

    ElleKit is a multi-purpose hooking and assembly library designed for arm64/arm64e devices running the XNU kernel. Its primary functions include:

    • C Function Hooking: Patches memory pages directly to intercept C functions.
    • Objective-C Hooking: Supports hooking Objective-C messages (preserving the original implementation) and class pairs via MSHookClassPair.
    • Assembly & JIT: Provides an arm64 assembler and a JIT inline assembly implementation for Swift.
    • API Compatibility: Reimplements both the Substrate and libhooker APIs.

    Supported Platforms:

    • iOS 15-27 (arm64)
    • iOS 15-17 (arm64e)
    • macOS Ventura (arm64/arm64e) — Note: Currently unsupported on latest macOS due to Apple's codesigning changes.
  2. Understand ElleKit's C hooking technique

    main

    ElleKit uses two different strategies for C function hooking depending on the memory distance between the hook and the target:

    Within 128MB of address space

    ElleKit performs a simple branch instruction patch. This is the fastest method for hooking.

    Beyond 128MB of address space

    ElleKit uses an exception-based approach:

    1. It sets up an exception port to catch breakpoint exceptions.
    2. It patches the target function with a brk #1 instruction.
    3. When the exception is triggered, the handler catches it, redirects execution to the target function, and resumes execution. While this involves exception handling, it is optimized for performance.
  3. How ElleKit handles original functions

    main

    When a function is hooked, ElleKit preserves the original implementation by writing it to a new memory page and providing a pointer to it.

    If you use LHHookFunctions, ElleKit optimizes this by using a single page for all original functions rather than allocating a new page for every individual hook, which improves performance.

    The original functions are assembled using a sequence that loads the target address into x16, skips the patched instruction (the branch to the replacement), executes the first 4 unpatched bytes, and then jumps to the rest of the original function.

    // Insert address to target function
    movk x16, target_addr % 65536)
    movk x16, (target_addr / 65536) % 65536 lsl: 16
    movk x16, ((target_addr / 65536) / 65536) % 65536, lsl: 32
    movk x16, ((target_addr / 65536) / 65536) / 65536, lsl: 48
    
    // Jump first instruction (the branch to the replacement, aka what we patched)
    add x16, x16, 4 
    
    // Execute the skipped instruction
    [4 first unpatched bytes of the target function]
    
    // Call the target function
    br x16
  4. Build ElleKit

    main

    ElleKit can be built as a dynamic library, a Swift package, or via a Debian package.

    • Dynamic Library: Use Xcode 14.
    • Debian Package: Run make deb to build both the library and the package.
    • macOS Library: Set MAC=1 during the build process.
    • Swift Package: Can be used directly as a Swift package.
    # Build the library and the package
    make deb
    
    # Build specifically for macOS
    MAC=1 make deb
  5. Automate ElleKit loading on startup using a LaunchDaemon

    main

    To avoid manually running the loader script every time your Mac starts, you can install a LaunchDaemon. This ensures ElleKit is automatically injected as soon as you log in to your user account.

    Installation Steps

    1. Download and place the plist file: Download the com.evln.ellekit.startup.plist file and save it to the following system directory (this requires root privileges): /Library/LaunchDaemons/com.evln.ellekit.startup.plist

    2. Load the Daemon: Open a terminal and use launchctl to load the configuration:

      sudo launchctl load -w /Library/LaunchDaemons/com.evln.ellekit.startup.plist
    3. Verify: Reboot your Mac. ElleKit should now be injected automatically on startup.

    sudo launchctl load -w /Library/LaunchDaemons/com.evln.ellekit.startup.plist
  6. How to use ElleKit APIs

    main

    ElleKit can be integrated into your project using three primary methods:

    1. Substrate API: Use the standard Substrate headers.
    2. libhooker API: Use the libhooker interface.
    3. Swift Functions: Call the Swift functions directly for native integration.

    Depending on your target, you can hook most C functions (even those without symbols if you provide a direct pointer) and Objective-C messages.

  7. Hook C functions from Swift using the `hook` function

    main

    To hook a C function in Swift, you can use the hook function. You can either provide an original function pointer to allow for chaining/calling the original implementation, or provide only the target and replacement functions.

    When providing an orig function, the returned value is a pointer to the original implementation, which is relocated to a different memory page.

    // Hooking with an original function pointer
    let atoiC: @convention(c) (UnsafePointer<CChar>?) -> Int32 = atoi
    let repC: @convention(c) () -> Int32 = Replacement
    let orig = hook(
        unsafeBitCast(atoiC, to: UnsafeMutableRawPointer.self),
        unsafeBitCast(repC, to: UnsafeMutableRawPointer.self)
    )
    
    // Hooking without an original function pointer
    hook(
        target,
        replacement
    )
  8. Hook C functions using the Substrate API

    main

    If you need to use the Substrate API directly, you can call MSHookFunction. This requires allocating memory for the original function pointer (orig), which will hold the address of the original implementation after the hook is applied.

    let atoiC: @convention(c) (UnsafePointer<CChar>?) -> Int32 = atoi
    let repC: @convention(c) () -> Int32 = Replacement
    let orig = UnsafeMutablePointer<UnsafeMutableRawPointer?>.allocate(capacity: 10)
    MSHookFunction(
        unsafeBitCast(atoiC, to: UnsafeMutableRawPointer.self),
        unsafeBitCast(repC, to: UnsafeMutableRawPointer.self),
        orig
    )