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.
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.
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) + "!"
}
}
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
.