[Video] How Apple Hooks Entire Frameworks
Six years ago (!), I wrote a post about reverse engineering Apple’s Main Thread Checker.
That post essentially ended with the age-old question:
Can we do better?
I left that question alone at the time, but it turns out, the answer is yes. Apple’s implementation scales incredibly well, but it hits limits eventually — it can swizzle tens of thousands of methods, but it can’t really handle millions, not without some significant tradeoffs in binary size.
This video is an investigation into how to really push past that limit.
A more polished version of the library we make in that video is available on GitHub. As a brief substitute for having an actual text post here (which, sorry — maybe I’ll come back to add one, because I like them too — but this whole project has already taken… several months longer than expected), this excerpt from the readme explains the methodology pretty well:
Swizzling a single method is easy; swizzling many methods is tricky. To deal with different parameter and return types, we replace each method with a unique trampoline that bounces us to a shared assembly handler.
That handler then uses that unique trampoline to figure out what method we were going to, saves all parameter registers onto the stack, calls whatever overall callback we registered, restores the parameter registers, and then branches back to the original implementation.
That’s equivalent to how Apple’s Main Thread Checker already works, but the standard setup involves knowing in advance exactly how many trampolines you need, which limits the possibilities of the framework.
Instead, this framework uses a technique inspired by
imp_implementationWithBlock(shoutout Landon Fuller for the awesome write-up) when that runtime feature was first added) to start with a fixed number of trampolines in the binary and map more of them into memory as needed. This involves a couple tricks to make data accessible and workaround iOS codesigning requirements, but the end result is being able to support an arbitrarily large number of methods.
Finally, the framework optionally supports using the private
class_replaceMethodsBulkfunction to replace all the methods on a single class while only acquiring the runtime lock once. Because this is a private method, its usage is gated behind a compiler flag. The Xcode project is set up to produce two frameworks —SwizzleEverythingandSwizzleEverythingPrivateAPIs— to make it easy to pull in either version, depending on your use case.
The original blog post) is still worth a read if you want a deep dive
into how the trampoline side works — but if you want the updated arm64 version, or want to see how
we can use memory mapping to create more trampolines at runtime, definitely check out the video
or the implementation on GitHub!
