Apple designed Objective-C to:
“defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically.“
Read the full Apple article. This repo was written using runtime
Apple APIs to swap in custom code into a normal, jailed iOS device. The code used a technique called Method Swizzling
. https://nshipster.com/method-swizzling/ is an excellent article on this topic.
Originally this repo generated a TinySwizzle.framework
to find Dormant Swift
or ObjC
iOS code. But now you can pick and choose what Swizzles you want. It works for any of the below methods. It is simple to add other Instance
or Class
methods.
Class | Description | Method |
---|---|---|
NSURL | Observed all URLs being invoked | initWithString: |
UILabel | Observed all strings set on a UILabel. Only set a custom string on a UILabel that was added onto a UIView | setText: |
NSHTTPCookie | Easy way to read all properties set on a cookie (no WKWebView yet) | initWithProperties: |
UITabBarController | Created a Tar Bar button that invoked a Dormant ViewController | viewWillAppear: |
N/A | Find name of classes to Swizzle | objc_copyClassList |
UIViewController | Loaded dormant Storyboard, XIB file or 100% coded ViewControllers. Added a UIBarButton to a navigationController to access the ViewController | viewDidAppear: |
UIApplication | Written to troubleshoot whether to load third party code inside / outside the app | application:continueUserActivity:restorationHandler: |
URLSession | Bypass Cert Pinning code when loaded with URLSession | URLSession:didReceiveChallenge:completionHandler: |
URLSession | Bypass Cert Pinning code when somebody has used NSURLSession with a delegate to perform Security Checks. This effectively "defaults" the traffic | sessionWithConfiguration:delegate:delegateQueue: |
WKWebView | Bypass Cert Pinning code when loading a Web Journey | webView:didReceiveAuthenticationChallenge:completionHandler: |
WKNavigationDelegate | Attach a custom Delegate to a Web Journey. Ignores the original WKNavigationDelegate | setNavigationDelegate: |
To swizzle successfully, you need the correct Selector
name. The colons are important with ObjC
. Get these Method signatures from Xcode Developer Documentation
.
@selector(webView:didReceiveAuthenticationChallenge:completionHandler:); // WKWebView Auth Challenge
@selector(URLSession:didReceiveChallenge:completionHandler:); // NSURLSession Auth Challenge
@selector(initWithProperties:); // NSHTTPCookie
You could Method Swizzle
on Objective-C Properties
. Properties had a getter
and setter
. Internal calls and properties are not shared in Developer Documents
but you can trace
or use a debugger
to find the method names.
For example, I found the WKWebView Class
had a set Navigation Delegate
method. Each WebView had a Property
getter called WKWebView.setNavigationDelegate
. Using Delegates
was a common iOS programming pattern. The delegates made life easier by offerings lots of pre-canned methods.
Find the property
took analysis. Here is an example with a debugger:
(lldb) lookup setNavigationDe
****************************************************
2 hits in: WebKit
****************************************************
-[WKWebView setNavigationDelegate:]
WebKit::NavigationState::setNavigationDelegate(id<WKNavigationDelegate>)
(lldb) b -[WKWebView setNavigationDelegate:]
// breakpoint fires
(lldb) po $arg1
<WKWebView: 0x7f8e90875400; frame = (0 0; 0 0); layer = <CALayer: 0x600000fdfb20>>
(lldb) p (char *) $arg2
(char *) $13 = 0x00007fff51f655b3 "setNavigationDelegate:"
(lldb) po $arg3
<tinyDormant.YDWKViewController: 0x7f8e8f416350>
*/
know - without consulting documentation - I have the signature for the property's set method and I know what is passed into the method.
@selector(setNavigationDelegate:);
The Swizzle used the Objective-C runtime.h
APIs from Apple. Namely:
- class_addMethod
- class_replaceMethod
- method_exchangeImplementations
- objc_getClass
Due to Subclassing
, if you followed the StackOverflow recommendations [ and solely used method_exchangeImplementations
] you would create unexpected behaviour. Take the addFakeUIBarButton
example. You could place the fake UIBarButtons with the method_exchangeImplementations
without using class_addMethod
and class_replaceMethod
. But the fake viewDidLoad
got called on lots of other classes when you only targeted the UIViewController
class.
You will see the Swizzle code inherited from NSObject
. If you didn't, you would often see:
🍭Stopped swizzle. Class: WKWebView, originalMethod: 0x10318bef0 swizzledMethod: 0x0
I could fix this, in some future commit.
If you wanted to target a piece of dormant
code, you first needed to know the Class
name. Tick the Target Membership
box to include the dumpClasses.m
file inside of the iOS app's Target
. Then it will run the app and print the found classes.
[*] 🌠 Started Class introspection...
[*]tinyDormant.AppDelegate
[*]tinyDormant.PorgViewController
[*]tinyDormant.YDJediVC
[*]tinyDormant.YDSithVC
[*]tinyDormant.YDMandalorianVC
[*]tinyDormant.YDPorgImageView
The above log was from a Swift app. Notice the Module name
. This was an important, subtle difference between a Swift
class and an Objective-C
class.
If you wanted to invoke dormant ViewControllers
, you needed to find the View Controller. It was even better if you knew whether it was:
- A storyboard file (or a single Main.storyboard)
- A
XIB
file - 100% code-only
Clone the repo. Create a YDSwizzlePlist.plist
file. Check the Target Membership
tickbox. This must be ticked so the plist file ships inside the framework. An example plist:
<plist version="1.0">
<array>
<dict>
<key>storyboardClassName</key>
<string>tinyDormant.YDChewyVC</string>
<key>storyboardID</key>
<string>chewyStoryboardID</string>
<key>storyboardFile</key>
<string>Main</string>
</dict>
</array>
</plist>
Then, select the "Target Membership" the Swizzle you want. I normally used the UIViewController
when it was embedded inside a navigationController
. This would add a button that would invoke the dormant code.
The project contained two Targets
. An iOS app and a simple framework. The app just demonstrated what the Swizzle framework could do. This app worked with a Simulator or real device.
The framework could be repackaged inside a real iOS app.
PLEASE USE IT FOR GOOD.
The process was summarised as :
- Unzipping the IPA
- Adding the Swizzle framework
- Adding a load command, so the app knew to load the new framework
- Zipping the app contents
- Code signing the modified IPA
The commands were as follows:
optool install -c load -p "@executable_path/Frameworks/tinySwizzle.framework/tinySwizzle" -t Payload/MyApp.app/MyApp
jtool -arch arm64 -l Payload/MyApp.app/MyApp
7z a unsigned.ipa Payload
applesign -7 -i < DEV CODE SIGNING ID > -m embedded.mobileprovision unsigned.ipa -o ready.ipa
The Sith
ViewController was 100% code generated. The dynamic nature of Objective-C let you create a class at runtime
:
Class SithClass = objc_getClass(dormantClassStr);
id sithvc = class_createInstance(SithClass, 0);
After you select the FakeUIBarButtonItem
it loaded the ViewController
.
But to invoke the PorgViewController
you need to find the ViewController and
the XIB
file. Then you can run this line:
let porgvc = YDPorgVC(nibName: "PorgViewController", bundle: nil)
Notice how you use the ViewController (YDPorgVC
) to cast the XIB file. After that, you can choose what option you want to show the code.
self.navigationController?.pushViewController(porgvc, animated: true)
This worked on Objective-C and Swift code.
- A different API for swizzling: https://blog.newrelic.com/engineering/right-way-to-swizzle/
- I only tried with Swift code that inherits from
NSObject
. - Try the technique on
SwiftUI
.