17、iOS Runtime分析:Method Swizzling、isa指针、消息转发机制、LLDB动态调试
Runtime,说白了就是Objective-C这门语言的「运行时系统」。我刚开始学iOS逆向时,总觉得Runtime是个很玄乎的东西。后来踩的坑多了,才明白——不理解Runtime,你连App怎么跑起来的都搞不清楚。
这一章,我会带你从四个核心角度切入:isa指针、消息转发、Method Swizzling、以及LLDB动态调试。嗯,这四个东西是互相关联的,咱们一个一个拆。
核心认知:Objective-C的方法调用,本质上是给对象发消息。消息怎么发、发到哪里、能不能拦截——全由Runtime说了算。
17.1 isa指针——对象的身份证
每个Objective-C对象,内存布局的第一个字段就是isa指针。它指向什么呢?指向这个对象的类对象。
你想想看,当你调用[obj doSomething]时,Runtime怎么知道该执行哪段代码?它先通过obj->isa找到类对象,然后在类对象的方法列表里查找doSomething对应的IMP。
// 对象结构体简化版
struct objc_object {
Class isa; // 指向类对象
};
// 类对象结构体简化版
struct objc_class {
Class isa; // 指向元类
Class super_class; // 指向父类
method_list_t *methods; // 方法列表
// ... 还有其他成员
};
这里有个关键点:类对象本身也是一个对象。所以类对象也有isa指针,它指向的是元类(Meta Class)。元类里存的是类方法。我当年刚接触这个时,被「元类」绕晕了好几天。后来画了一张图才理清楚:实例对象→类对象→元类→根元类→自己,形成了一个闭环。
个人经验:我在逆向一个金融App时,发现它用isa混淆来对抗hook。它动态修改了对象的isa指向一个伪造的类。这种情况下,常规的class方法返回的是假类名。怎么破?直接读内存里的isa指针值,绕过OC的API。
17.2 消息转发——三次拯救机会
当给一个对象发送它没有实现的消息时,Runtime不会直接崩溃。它会给你三次机会来挽救。
| 阶段 | 方法 | 说明 |
|---|---|---|
| 第一次 | +resolveInstanceMethod: |
动态添加方法实现 |
| 第二次 | -forwardingTargetForSelector: |
将消息转发给另一个对象 |
| 第三次 | -forwardInvocation: |
完整的消息转发,可以修改参数、返回值 |
为什么会设计这么复杂的机制?说白了,这是OC给开发者留的「后门」。比如你想给所有NSObject的子类加一个日志功能,不用一个个去改,直接hook消息转发就行。
// 动态添加方法示例
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"动态添加的方法被调用了");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(someDynamicMethod)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
避坑指南:我曾经在逆向一个直播App时,发现它利用消息转发做了方法调用的全量统计。它重写了forwardInvocation:,把所有未识别的方法都记录到日志里。如果你在逆向时发现某个方法「不存在」但App又能正常调用,十有八九是走了消息转发。
17.3 Method Swizzling——偷梁换柱的艺术
Method Swizzling,说白了就是交换两个方法的IMP指针。这是逆向工程中最常用的hook手段之一。
#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(my_viewDidLoad);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 交换实现
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)my_viewDidLoad {
NSLog(@"Hook到了viewDidLoad: %@", self);
// 调用原始实现
[self my_viewDidLoad];
}
@end
这段代码看起来简单,但坑不少。我建议你注意三点:
- 只在+load中执行:+load是线程安全的,且只执行一次。别放+initialize里,会被多次调用。
- 用dispatch_once保证唯一性:防止多线程环境下重复交换。
- 调用原始方法时别写错:交换后,
my_viewDidLoad这个SEL对应的IMP已经是原始实现了。所以调用[self my_viewDidLoad]实际上是在调用原始的viewDidLoad。
实战场景:我在逆向一个社交App时,想拦截它的网络请求。直接hook NSURLSession的方法会被检测。后来我换了个思路——hook UIViewController的viewDidLoad,在控制器加载时注入网络监控代码。嗯,绕过了它的反hook检测。
17.4 LLDB动态调试——现场取证
LLDB是Xcode自带的调试器,但在逆向工程中,我们经常脱离Xcode使用它。你想想看,一个App已经跑在手机上了,你不能重新编译,怎么调试?用LLDB attach上去。
# 在终端中
(lldb) process attach --name "WeChat" --waitfor
# 查看isa指针
(lldb) po [self class]
(lldb) po object_getClass(self)
# 查看方法列表
(lldb) po [self methodSignatureForSelector:@selector(sendMessage:)]
# 动态调用方法
(lldb) expression -l objc -O -- [self performSelector:@selector(somePrivateMethod)]
# 设置符号断点
(lldb) breakpoint set -n "-[ViewController viewDidLoad]"
(lldb) breakpoint set -r ".*sendMessage.*" # 正则匹配
我个人最常用的LLDB技巧是动态修改方法实现。比如我想让某个方法直接返回NO,不用重新编译App:
// 先找到方法的IMP地址
(lldb) po class_getMethodImplementation([self class], @selector(shouldShowAd))
// 然后写一段汇编,让它直接返回0
(lldb) memory write -s 4 0x102345678 0x4e800020 // ARM64: mov w0, #0; ret
// 或者更简单,用expression动态替换
(lldb) expression -l objc -O --
@import Foundation;
IMP newIMP = imp_implementationWithBlock(^(id self) { return NO; });
class_replaceMethod([self class], @selector(shouldShowAd), newIMP, "B@:");
我的习惯:在逆向分析时,我会先通过LLDB的image list查看App加载了哪些动态库,然后用image lookup -rn搜索特定符号。这比静态分析快得多。有一次我找某个加密函数的实现,静态分析翻了一小时没找到,LLDB正则搜索三秒定位。
17.5 四个知识点串联起来
咱们把这四个东西串起来看:
- isa指针是Runtime的基石。没有它,消息就找不到类。
- 消息转发是方法调用的「异常处理」机制。它依赖isa找到类后,发现方法不存在才触发。
- Method Swizzling是在方法查找阶段做手脚。它直接修改了类对象的方法列表。
- LLDB调试是验证以上三者的工具。你可以用LLDB查看isa、触发消息转发、验证Swizzling是否生效。
举个例子。你在逆向时发现某个App的viewDidLoad行为异常。先用LLDB断住,po [self class]看看是不是被isa混淆了。然后po class_getInstanceMethod([self class], @selector(viewDidLoad))看看方法实现地址。如果地址不对,说明被Swizzle了。再查查是不是走了消息转发——po [self forwardingTargetForSelector:@selector(viewDidLoad)]。
曾经踩过的坑:我在用LLDB动态修改方法实现时,忘了考虑ARC内存管理。Block捕获了外部变量,导致野指针崩溃。后来我改用imp_implementationWithBlock时,都会用__bridge处理好内存。嗯,细节决定成败。
Runtime这东西,光看书是学不会的。我建议你打开Xcode,建个空项目,把上面的代码跑一遍。然后用LLDB attach上去,一条条命令敲。只有亲手看到isa指针的值、亲眼看到方法交换前后的IMP地址变化,你才算真正入门了。
公众号:蓝海资料掘金营,微信deep3321