Hooking Swift Functions by Modifying the Virtual Table (VTable)
This article explains a novel Swift hooking technique that modifies the virtual function table (VTable) to replace method implementations, detailing Swift's runtime structures such as TypeContext, Metadata, OverrideTable, and providing concrete ARM64 assembly and Swift code examples.
The article introduces a new approach to hook Swift methods by directly editing the virtual function table (VTable) instead of relying on Objective‑C message forwarding, and explains why this is necessary as Swift adoption grows in large iOS applications.
Principle Overview – Swift method calls can be performed via three mechanisms: Objective‑C message dispatch (for @objc dynamic methods), VTable lookup, and direct address calls. The VTable is treated as an array of function pointers, and hooking requires replacing the pointer at a specific index.
Because VTable entries lack symbolic names, the index must be derived from the OverrideTable, which records the original class, the overridden method, and the new implementation pointer.
class MyTestClass : NSObject {
@objc dynamic func helloWorld() {
print("call helloWorld() in MyTestClass")
}
}
let myTest = MyTestClass.init()
myTest.helloWorld()The compiled assembly shows a call to objc_msgSend , confirming the Objective‑C path.
0x1042b8824 <+120>: bl 0x1042b9578 ; type metadata accessor for SwiftDemo.MyTestClass
0x1042b8848 <+156>: bl 0x1042bce88 ; symbol stub for: objc_msgSendWhen the @objc attribute is removed, the call switches to VTable lookup, as demonstrated by the following assembly snippet:
0x1026207ec <+120>: bl 0x102621548 ; type metadata accessor for SwiftDemo.MyTestClass
-> 0x102620ab0 <+136>: ldr x8, [x0]
-> 0x102620ab4 <+140>: ldr x8, [x8, #0x50]
-> 0x102620ac0 <+152>: blr x8Direct address calls appear when the compiler optimization level is set to Optimize for Size , producing a bl instruction that jumps straight to the function address.
0x1048c2120 <+52>: bl 0x1048c2388 ; SwiftDemo.MyTestClass.helloWorld()The article then delves into the internal layout of Swift’s ClassContextDescriptor and Metadata structures, showing how flags indicate generic types and how the VTable and OverrideTable are stored.
struct ClassContextDescriptor {
uint32_t Flag;
uint32_t Parent;
int32_t Name;
int32_t AccessFunction;
...
VTableList[];
OverrideTableList[];
}By reading the Flag bits, one can determine whether a class is generic and locate the VTable offset. The VTable resides in a read‑only __TEXT segment, so the article uses a remap technique to make it writable and replace function pointers.
However, because VTable indices can shift between versions, the OverrideTable is used to map original methods to their replacements regardless of order changes.
struct SwiftOverrideMethod {
uint32_t OverrideClass; // class being overridden
uint32_t OverrideMethod; // method index in VTable
uint32_t Method; // new implementation address
}Using this mapping, the author demonstrates a successful hook where a subclass overrides helloWorld() and the original implementation is swapped at runtime.
class MyTestClass {
func helloWorld() { print("call helloWorld() in MyTestClass") }
}
class HookTestClass: MyTestClass {
override func helloWorld() {
print("\n********** call helloWorld() in HookTestClass **********")
super.helloWorld()
print("********** call helloWorld() in HookTestClass end **********\n")
}
}
let myTest = MyTestClass.init()
myTest.helloWorld()
WBOCTest.replace(HookTestClass.self)
myTest.helloWorld()The runtime output confirms that the method has been replaced.
Conclusion – By exposing Swift’s VTable, Metadata, and OverrideTable, the article provides a deep look into Swift’s binary layout and offers a practical hooking strategy that can be extended to other runtime manipulation scenarios.
58 Tech
Official tech channel of 58, a platform for tech innovation, sharing, and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.