Skip to content

Instantly share code, notes, and snippets.

@leptos-null
Last active October 26, 2024 23:12
Show Gist options
  • Save leptos-null/1f7fc9de9f7ae018f149f13446f9c8ae to your computer and use it in GitHub Desktop.
Save leptos-null/1f7fc9de9f7ae018f149f13446f9c8ae to your computer and use it in GitHub Desktop.
Hooking Swift functions

This article aims to describe how to hook Swift functions.

Thanks to help from @NightwindDev for discussion and testing.

Overall, the idea is simple: Write our own Swift code that will have the same calling convention as the target code, then get a pointer to our own code and the target code, and call MSHookFunction with these values.

Code

For simplicity, let's say that we have the following code in the application we're hooking:

import SwiftUI

public struct ContentView: View {
    
    public func greet(name: String) -> String {
        "Hello \(name)"
    }
    
    public var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(Color.orange)
            
            Text(greet(name: "Nightwind"))
        }
        .padding()
    }
}

This particular code is in an app called "SwizzleSandbox".

Let's say that we want to hook the greet(name:) function.

The first thing we'll do is create a Swift file in our tweak. I'll name this file Tweak.swift.

We want to match the original source as much as possible, so we'll create a struct matching the original declaration, and a matching instance method:

import SwiftUI

public struct ContentView {
    public func greet_hook(name: String) -> String {
        "Hi \(name)"
    }
}

Next, we need to get the symbol names for the original function and our replacement function.

To get the name for the original function, I'll simply use nm with grep:

nm SwizzleSandbox.app/SwizzleSandbox | grep 'ContentView.*greet'

nm lists the symbol names in a binary. The grep parameter is ContentView for the name of the enclosing type, and then greet is the leading portion of the function name. .* matches any characters in-between. This command prints 1 result for me:

_$s14SwizzleSandbox11ContentViewV5greet4nameS2S_tF

We'll use the same technique for our own tweak:

nm .theos/obj/Tweak.dylib | grep 'ContentView.*greet'

This also prints 1 result:

_$s5Tweak11ContentViewV10greet_hook4nameS2S_tF

If you get multiple results, or just want to see what these strings mean, pipe them into swift demangle:

printf '_$s5Tweak11ContentViewV10greet_hook4nameS2S_tF' | swift demangle
Tweak.ContentView.greet_hook(name: Swift.String) -> Swift.String

Now that we have our symbols, we just want to hook the Swift function like a normal C function.

We'll create a Logos file- I'll call this file Hooks.x.

#import <assert.h>
#import <dlfcn.h>
#import <substrate.h>

%ctor {
    void $s5Tweak11ContentViewV10greet_hook4nameS2S_tF(void);
    
    void *const target = dlsym(RTLD_MAIN_ONLY, "$s14SwizzleSandbox11ContentViewV5greet4nameS2S_tF");
    assert(target != NULL);
    
    MSHookFunction(target, $s5Tweak11ContentViewV10greet_hook4nameS2S_tF, NULL);
}

We'll look at this line by line.

    void $s5Tweak11ContentViewV10greet_hook4nameS2S_tF(void);

This is the Swift function in Tweak.swift. Note that we do not place the leading underscore (_) in this declaration. We could use dlsym or MSFindSymbol, however since we're able to link against this symbol, I prefer to do that. This line is simply declaring that a function with this symbol name exists. Since this is C, the value of this function is a pointer to the code for the function.

    void *const target = dlsym(RTLD_MAIN_ONLY, "$s14SwizzleSandbox11ContentViewV5greet4nameS2S_tF");

On this line, we're doing the same thing - getting the address of the symbol we're looking for in the application we're hooking. Same as in the previous line, we do not put the leading underscore in the string. This time, we are using dlsym, since we cannot link against the application we're hooking.

    assert(target != NULL);

I check that the target function is found to make sure the symbol name is correct, since this won't be checked at link time, as the previous technique is. You may want to change this behavior, once you've verified a particular hook.

    MSHookFunction(target, $s5Tweak11ContentViewV10greet_hook4nameS2S_tF, NULL);

This is straight-forward: Replace the implementation of target with the implementation of $s5Tweak11ContentViewV10greet_hook4nameS2S_tF. This code passes NULL to the last parameter, which is the "original" function pointer. Currently, we don't need the original function, however we'll cover that next.

Calling orig

To support calling the original implementation of a Swift function that we've hooked, we'll start by adding another function with a similar signature:

    public func greet_orig(name: String) -> String {
        fatalError("This implementation should never be called")
    }

We get the symbol name the same way as before:

nm .theos/obj/Tweak.dylib | grep 'ContentView.*greet_orig'
_$s5Tweak11ContentViewV10greet_orig4nameS2S_tF

We then bring this symbol into Hooks.x:

%ctor {
    void $s5Tweak11ContentViewV10greet_hook4nameS2S_tF(void);
    void $s5Tweak11ContentViewV10greet_orig4nameS2S_tF(void);
    
    void *const target = dlsym(RTLD_MAIN_ONLY, "$s14SwizzleSandbox11ContentViewV5greet4nameS2S_tF");
    assert(target != NULL);
    
    void *next;
    MSHookFunction(target, $s5Tweak11ContentViewV10greet_hook4nameS2S_tF, &next);
    MSHookFunction($s5Tweak11ContentViewV10greet_orig4nameS2S_tF, next, NULL);
}

In this snippet, we first store the address of the original implementation in next, then we replace our "orig" function with next.

We have to take this approach because the original implementation is expected to be called with the Swift calling convention. As far as I know, Swift does not allow a function with the Swift calling convention to be stored in a variable. This is the reason we created another Swift function earlier: we replace the implementation of that new _orig function with the actual original implementation.

We can now use greet_orig(name:) wherever we would like in our Swift code:

public struct ContentView {
    public func greet_orig(name: String) -> String {
        fatalError("This implementation should never be called")
    }
    
    public func greet_hook(name: String) -> String {
        greet_orig(name: name) + "!"
    }
}

Hooking system code

This technique works the same way for system code.

In this demo, I'll hook SwiftUI.Color.orange, which has the following declaration:

extension Color {
    public static let orange: Color
}

In Tweak.swift, I'll add this code:

extension Color {
    public static var orange_hook: Color {
        .teal
    }
}

To get the symbol name from the SDK, I use

grep 'Color.*orange' $(xcrun --show-sdk-path)/System/Library/Frameworks/SwiftUI.framework/SwiftUI.tbd
_$s7SwiftUI5ColorV6orangeACvgZ

And the usual for our own symbol:

nm .theos/obj/Tweak.dylib | grep 'Color.*orange_hook'
_$s7SwiftUI5ColorV5TweakE11orange_hookACvgZ

We put this together in the same way as above, in Hooks.x:

    void $s7SwiftUI5ColorV5TweakE11orange_hookACvgZ(void);
    void $s7SwiftUI5ColorV6orangeACvgZ(void);
    
    MSHookFunction($s7SwiftUI5ColorV6orangeACvgZ, $s7SwiftUI5ColorV5TweakE11orange_hookACvgZ, NULL);

Since we can link against the symbol that we want to hook, I chose to do that instead of using dlsym.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment