如何判斷method是否被swizzled(續)

上次的文章介紹了一種方法用來檢測Objective-C中Method是否被swizzled。但該方法只能檢測非系統的方法,即,必須在源文件中的目標方法中添加上述的宏才能Work,對於系統類的方法被Hook就機關用盡了。
代碼整理後我會放到個人githubhtml

突破口

回想一下Objective-C中Method Swizzling的原理,看以下代碼:git

@implementation UIViewController (Hook)
- (void)viewDidLoad2 {
    [self viewDidLoad2];
}
+ (void)load {
    Method ori_method = class_getInstanceMethod([UIViewController class], @selector(viewDidLoad));
    Method replace_method = class_getInstanceMethod([UIViewController class], @selector(viewDidLoad2));
    method_exchangeImplementations(ori_method, replace_method);
}
@end

Method Swizzling的核心是交換兩個Method的IMP,而IMP的定義則是:github

typedef id             (*IMP)(id, SEL, ...);

其實是函數指針。shell

那我能夠作一些推測(撞大運編程又開始了),當App的二進制格式不會被混淆的前提下,App加載的某個dylib或者編譯時期某個framework的.a文件,其二進制確定連續的,意思是在二進制文件中,某個dylib或者framework中符號確定是相對固定的。譬如,對於上述這個例子,UIViewController這個Class的symbol的地址相對於-[UIViewController viewDidLoad]IMP的地址偏移確定是固定的。編程

寫段代碼來證實一下:segmentfault

NSMutableDictionary *offsetDict = @{}.mutableCopy;
unsigned classCount = 0;
Class *allClasses = objc_copyClassList(&classCount);
for (unsigned classIndex = 0; classIndex < classCount; ++classIndex) {
    @autoreleasepool {
        Class cls = allClasses[classIndex];
        //只管UI NS開頭的類
        if ([NSStringFromClass(cls) hasPrefix:@"UI"] || [NSStringFromClass(cls) hasPrefix:@"NS"]) {
            unsigned methodCount = 0;
            Method *methods = class_copyMethodList(cls, &methodCount);
            for (unsigned methodIndex = 0; methodIndex < methodCount; ++methodIndex) {
                Method mtd = methods[methodIndex];
                NSString *mtdString = [NSString stringWithUTF8String:sel_getName(method_getName(mtd))];
                //_開頭的內部方法忽略
                if ([mtdString hasPrefix:@"_"]){
                    continue;
                }

                IMP imp = method_getImplementation(mtd);
                int offset = (int) cls - (int) imp;
                offsetDict[[NSString stringWithFormat:@"[%@ %s]", NSStringFromClass(cls), sel_getName(method_getName(mtd))]] = @(offset);
            }
        }
    }
}

因此我就拿到了全部類的全部方法相對於該類的偏移。這個offsetDict是在離線環境拿到的,必須是在純淨的(即保證沒有任何method swizzling的狀況下)環境下得到的。app

在實際項目環境中,再讀取這個offsetDict,用來和實際的offset比較,就能判斷該Method是否被swizzled或者overridden。代碼以下:dom

BOOL isSiwwzledOrOverridden(Class cls, SEL selector) {
    //省略部分代碼。。。
    if (offsetDict){
        NSNumber *num = offsetDict[[NSString stringWithFormat:@"[%@ %s]", NSStringFromClass(cls), sel_getName(selector)]];
        if (num == nil){
            NSLog(@"Could not find selector!");
            return NO;
        }
        IMP imp = [cls instanceMethodForSelector:selector];
        int offset = (int) cls - (int) imp;

        if (offset != [num integerValue])
            return YES;
    }
    return NO;
}

試了一下,果真有用!對於下列示例:iphone

@implementation UIViewController (YouDonKnow)
- (void)viewDidLoad2 {
    [self viewDidLoad2];
}
+ (void)load {
    Method ori_method = class_getInstanceMethod([UIViewController class], @selector(viewDidLoad));
    Method replace_method = class_getInstanceMethod([UIViewController class], @selector(viewDidLoad2));
    method_exchangeImplementations(ori_method, replace_method);
}
- (void)viewDidAppear:(BOOL)animated {
}
@end

咱們只須要調用:函數

BOOL ret = isSiwwzledOrOverridden([UIViewController class], @selector(viewDidLoad));//YES 這是swizzling的狀況
ret = isSiwwzledOrOverridden([UIViewController class], @selector(viewDidAppear:));//YES 這是overridden的狀況
ret = isSiwwzledOrOverridden([UIViewController class], @selector(viewWillAppear:))//NO

就能知道是否被替換或者被覆蓋了。

問題一大堆

  • x86 armv7 arm64 binary的format是不同的,經實驗的確如此,因此offsetdict須要三個,分別是三個平臺的。

  • 因爲OC有Category,如Foundation裏的Class某些Method的實現並非放在Foundation的二進制裏的。好比:NSObject的Accessibility。這些method也有可能會被認爲是swizzled。

  • 上面說了,這樣推斷只不過是撞大運編程大法的升級版,其實並無證據代表iOS的二進制結構知足這個規律(實際上有這些文檔與資料)。

  • 其實arm64或許有更簡單的方法:

#if TARGET_CPU_ARM64
BOOL isSiwwzledOrOverriddenOnArm64(Class cls, SEL selector) {
    IMP imp = [cls instanceMethodForSelector:selector];
    int offset = (int) cls - (int) imp;
    return offset < 0;
}
#endif

就不誤人子弟了,本身參悟一下吧,沒找到有確切證據,只是實驗出來的。

  • 只在DEBUG下試過,不太肯定是否會被ASLR影響。不過我持樂觀態度,由於:

  1. 該文章的目的並非要在真正的運行時去檢測Swizzling從而進行防護,由於若是是防護攻擊,在有可能被hook的前提下,上述方法自己也可以被Hook。

  2. ASLR應該只會作基址偏移(On program load, the address space offset of the program is randomized between 0x0 and 0x100000),不會影響上述offset的計算,沒看到說iOS的ASLR會將其餘lib的二進制全弄亂。

後記

其實總感受還有其餘方法。由於,在進行method swizzling以後在lldb中打印IMPpo imp,lldb會定位swizzling method的代碼的位置,好比對於上述被hook過的-[UIViewController viewDidLoad]。編譯器確定是知道的,但IMP是個指針,怎麼會有這些信息呢?

(lldb) po (IMP)class_getMethodImplementation([UIViewController class], @selector(viewDidLoad))
(SwizzleDetector`-[UIViewController(Hi) viewDidLoad2] at ViewController.m:16)

致歉

後面我會具體讀一下MacOS/iOS二進制的格式,以及Apple文檔 還有看看fishhook。

萬罪萬罪。

原做寫於segmentfault 連接

相關文章
相關標籤/搜索