Learning tweak development has been a very giving and exciting experience. I’ve not only been able to create tools which would have never been possible on non-jailbroken devices, but also had the possibility to tweak 3rd party apps, as well as testing vulnerabilities on projects I’ve been involved in. But since more and more projects are moving over to Swift, tweak development is becoming harder to do (for now). This was something that became painfully obvious after Swift 4, where the compiler became much more conservative in making Swift methods accessible in Objective-C; something that certainly wasn’t the case in previous Swift versions.

Methods in Objective-C vs Swift

There’s a large fundamental difference between methods in Objective-C and in Swift, which in the context of development, tweak or not, gives a trade-off between convenience and safety.

Objective-C

Objective-C uses message dispatch, a type of dynamic dispatch which resolves methods dynamically at runtime. As we can read in Apple’s Documentation:

The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.

This opens up a lot of possibilities for manipulation during runtime: creating new classes, adding new methods, manipulating existing methods (method swizzling), etc. While this approach is very convenient, it’s also easier to crash during runtime, for instance if a selector turns out to not exist.

Swift

Depending on the situation, Swift will use either direct dispatch, table dispatch or message dispatch.

From Thuyen’s corner - Method dispatch in Swift:

  • Direct dispatch is prioritized.
  • If overriding is needed, table dispatch is the next candidate.
  • Need both overriding and visibility to Objective-C? Then message dispatch.

So when dealing with classes, we’ll most likely either encounter table dispatch or message dispatch (for methods inherited from an Objective-C class).

Table dispatch in Swift works just like C++, where Swift classes contain a vtable member, created at compile time, which lists the available methods. The compiler uses that lookup table to translate method calls to appropriate function pointers.

Since Swift 4, the compiler will only use the Objective-C compatible message dispatch when absolutely needed. This can however be forced by explicitly marking classes or methods with the dynamic or @objc label.

As mentioned in Objective-C vs Swift messages dispatch:

Another way to force Swift to use dynamic dispatch is to mark a class member declaration with the dynamic keyword. Those declarations will be dispatched using Objective-C runtime. Those will be also implicitly marked with the @objc keyword.

This doesn’t necessarily guarantee dynamic dispatch however, as the Swift Documentation explains: While the @objc attribute exposes your Swift API to the Objective-C runtime, it does not guarantee dynamic dispatch of a property, method, subscript, or initialiser. The Swift compiler may still devirtualise or inline member access to optimise the performance of your code, bypassing the Objective-C runtime

Finding the Swift function symbol

For the following instructions you need to have Xcode and Theos installed, as well as a jailbroken device.

Fire up Xcode and create a new Single View App for iOS. Make sure to choose Swift as development language and give it a random name. I’ll name my project HookExampleApp.

Build the project.

In the left sidebar, right click you compiled app under the product folder. Select Show in Finder.

Press cmd + i to get the info tab. Copy your project path.

Now open the terminal and navigate to said path.

cd <pasted project path>/HookExampleApp.app

Using nm, we can dump the Swift symbols. Let’s take a look at some methods in ViewController:

nm HookExampleApp
..
T __T014HookExampleApp14ViewControllerC11viewDidLoadyyF
t __T014HookExampleApp14ViewControllerC11viewDidLoadyyFTo
T __T014HookExampleApp14ViewControllerC23didReceiveMemoryWarningyyF
t __T014HookExampleApp14ViewControllerC23didReceiveMemoryWarningyyFTo
..

Here’s how the symbols look unmangled.

nm HookExampleApp | xcrun swift-demangle
..
T _HookExampleApp.ViewController.viewDidLoad() -> ()
t _@objc HookExampleApp.ViewController.viewDidLoad() -> ()
T _HookExampleApp.ViewController.didReceiveMemoryWarning() -> ()
t _@objc HookExampleApp.ViewController.didReceiveMemoryWarning() -> ()
..

Both of the above methods, which are inherited from the Objective-C class UIViewController, are both exposed to Swift and to Objective-C. So hooking them through logos is no problem.

Creating the tweak

Create a new theos tweak in the directory of your choice.

Desktop: $THEOS/bin/nic.pl
Desktop: NIC 2.0 - New Instance Creator
------------------------------
  [1.] iphone/activator_event
  [2.] iphone/application_modern
  [3.] iphone/cydget
  [4.] iphone/flipswitch_switch
  [5.] iphone/framework
  [6.] iphone/ios7_notification_center_widget
  [7.] iphone/library
  [8.] iphone/notification_center_widget
  [9.] iphone/preference_bundle_modern
  [10.] iphone/tool
  [11.] iphone/tweak
  [12.] iphone/xpc_service
Choose a Template (required): 11
Project Name (required): HookExampleTweak
Package Name [com.yourcompany.hookexampletweak]: com.mbo42.hookexampletweak
Author/Maintainer Name [Mikael]: Mikael
[iphone/tweak] MobileSubstrate Bundle filter [com.apple.springboard]: com.yangmei.HookExampleApp
[iphone/tweak] List of applications to terminate upon installation (space-separated, '-' for none) [SpringBoard]: -
Instantiating iphone/tweak in hookexampletweak/...
Done.
Desktop: cd hookexampletweak

Open and replace everything in Tweak.xm with the following:

%hook ViewController

- (void)viewDidLoad {
    %orig;
    NSLog(@"VIEW DID LOAD");
}

%end

%ctor {
    %init(ViewController = objc_getClass("HookExampleApp.ViewController"));
}

Deploy the tweak to your device, and then run the App on your device through Xcode.

You should see your NSLog in the console.

VIEW DID LOAD

Adding a Swift method

Let’s add our own method to ViewController.swift:

func randomFunction() {
    print("randomFunction called")
}

If you build the project, and run nm again, you’ll notice that there’s no Objective-c equivalent:

nm <AppName>
..
T __T014HookExampleApp14ViewControllerC14randomFunctionyyF
..

nm <AppName> | xcrun swift-demangle
..
T _HookExampleApp.ViewController.randomFunction() -> ()
..

Change viewDidLoad in Tweak.xm to the following:

- (void)viewDidLoad {
    %orig;
    NSLog(@"VIEW DID LOAD");
    [self performSelector:@selector(randomFunction)];
}

Deploy the tweak to your device, and run the app in Xcode again.

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-\[HookExampleApp.ViewController randomFunction\]: unrecognized selector sent to instance 0x100e21e40'

The app now crashes since randomFunction is not exposed to Objective-C, hence no selector to be found.

Calling a Swift method

Through running nm previously, we found out the mangled signature of our new method:

__T014HookExampleApp14ViewControllerC14randomFunctionyyF 

Using MSFindSymbol we can find the function pointer to the Swift method, and call it.

Change viewDidLoad in Tweak.xm to the following:

- (void)viewDidLoad {
    %orig;
    NSLog(@"VIEW DID LOAD");
    void *symbol = MSFindSymbol(NULL, "__T014HookExampleApp14ViewControllerC14randomFunctionyyF");
   ((void (*)(void)) symbol)();
}

randomFunction should now get called. Check your console.

VIEW DID LOAD
random function

Hooking a Swift method

After finding the function pointer to a Swift method, we can use MSHookFunction to hook it.

Replace the following in %ctor with:

static void (*orig_ViewController_randomFunction)(void) = NULL;

void hook_ViewController_randomFunction() {
   orig_ViewController_randomFunction();
   NSLog(@"Hooked random function");
}

%ctor {
    %init(ViewController = objc_getClass("HookExampleApp.ViewController"));
    MSHookFunction(MSFindSymbol(NULL, "__T014HookExampleApp14ViewControllerC14randomFunctionyyF"),
                   (void*)hook_ViewController_randomFunction,
                   (void**)&orig_ViewController_randomFunction);
}

Deploy the tweak to your device, and run the app in Xcode again. Your method should be successfully hooked, and your console in Xcode should now look like this:

VIEW DID LOAD
random function
Hooked random function

Limitations

Trying to interact with native Swift objects will make your tweak crash - unless you do it in the above way (creating and modifying Swift objects by calling their function pointers). Unfortunately, this adds much more limitation and overhead when it comes to tweaking Swift apps. But as soon as (and if) Logos adds support for Swift, that problem will be solved. There has been talks about adding Swift compatibility in tweaks after Swift 5; a release which will hopefully bring ABI stability.

Sources