您当前的位置: 首页 > 技术文章 > 移动开发

iOS逆向之深入解析如何Hook所有+load方法及Category的处理

作者: 时间:2022-04-06阅读数:人阅读

一、类方法 +load

  • iOS 有四种方法可方便的在 premain 阶段执行代码:
    • Objective C 类的 +load 方法;
    • C++ static initializer;
    • C/C++ attribute(constructor) functions;
    • 动态库中的上面三种方法。
  • 所有类的 +load 方法是在 main 函数之前、在主线程,以串行方式调用,因此任何一个 +load 方法的耗时大小将直接影响到 App 的启动耗时。
  • Objective C Runtime 如下:
/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void) {
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    // Destroy the detached list.
    if (classes) free(classes);
}
  • 直接通过遍历 loadable_classes 全局变量,逐个调用。全局变量的定义如下:
// List of classes that need +load called (pending superclass +load)
// This list always has superclasses first because of the way it is constructed
static struct loadable_class *loadable_classes = nil;
static int loadable_classes_used = 0;
static int loadable_classes_allocated = 0;
  • 苹果的官方文档对 +load 的说明如下:
The order of initialization is as follows:
- All initializers in any framework you link to.
- All +load methods in your image.
- All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
- All initializers in frameworks that link to you.

二、运用 CaptainHook hook 类方法 +load

  • 由于 +load 方法调用时机已经很早,早于 C++ static initializer 等,但晚于 framework(动态库),那就可以把 hook 的代码写到动态库中,也就可以做到在主程序的 loadable_classes 全局变量初始化之前就把 +load hook 掉。
  • 创建一个动态库,使用 CaptainHook (只有一个头文件,使用也很简单):
#import "CaptainHook.h"

CHDeclareClass(MyClass);
CHClassMethod0(void, MyClass, load){
    CFTimeInterval start = CFAbsoluteTimeGetCurrent();
    CHSuper0(MyClass,load);
    CFTimeInterval end = CFAbsoluteTimeGetCurrent();
    // output: end - start
}

__attribute__((constructor)) static void entry(){
    NSLog(@"dylib loaded");
    CHLoadLateClass(MyClass);
    CHHook0(MyClass, load);
}
  • 把这个动态库链接到 App 主程序,就可以 hook 主程序中的 MyClass 类的 +load 方法。
  • 列出程序所有 +load 方法可以通过 Runtime 获取:
int numClasses;
Class * classes = NULL;
    
classes = NULL;
numClasses = objc_getClassList(NULL, 0);
    
if (numClasses > 0) {
   classes = (Class*)malloc(sizeof(Class) * numClasses);
   numClasses = objc_getClassList(classes, numClasses);
   
   for(int idx = 0; idx < numClasses; ++idx){
       Class cls = *(classes + idx);
       
       const char *className = object_getClassName(cls);
       Class metaCls = objc_getMetaClass(className);
       
       BOOL hasLoad = NO;
       unsigned int methodCount = 0;
       Method *methods = class_copyMethodList(metaCls, & methodCount);
       if(methods){
           for(int j = 0; j < methodCount; ++j){
               Method method = *(methods + j);
               SEL name = method_getName(method);
               NSString *methodName = NSStringFromSelector(name);
               if([methodName isEqualToString:@"load"]){
                   hasLoad = YES;
                   break;
               }
           }
       }
       
       if(hasLoad){
           NSLog(@"has load : %@", NSStringFromClass(cls));
       }else{
//                NSLog(@"not has load : %@", NSStringFromClass(cls));
       }
   }
   free(classes);
}
  • 经过测试可以发现,如果一个类存在 Category,上面的方法只能 hook Category 中的 +load,多个 Category 也只能 hook 一个;并且 CaptainHook 方法需要先静态分析(使用 Hopper)来看到所有 +load 方法,或者使用 objc runtime 的方法获取所有包含 +load 方法的类名,非常麻烦,那么该怎么处理和改进呢?

三、Hook 所有 +load 方法(包括 Category)

① hook 目的

  • 假设 App 包含两个自动链接的动态库,文件如下:

在这里插入图片描述

  • 我们的目的就是 hook 这三个 MachO 文件中的所有 Objective C +load 方法,并统计出耗时,打印出来。

② 新增动态库

  • 为了让 Hook 代码加载的比这两个动态库早,需要新增一个动态库 LoadRuler.dylib,链接的顺序很重要,要把 LoadRuler 第一个链接(App 启动时也就会第一个加载,以及第一个执行 macho 中的 +load 方法):

在这里插入图片描述

③ 获取 App 的所有 MachO

  • 首先获取所有加载的 MachO 可以这样:
static void AppendAllImagePaths(std::vector<std::string> & image_paths){
    uint32_t imageCount = _dyld_image_count();
    for(uint32_t imageIndex = 0; imageIndex < imageCount; ++imageIndex){
        const char * path = _dyld_get_image_name(imageIndex);
        image_paths.push_back(std::string(path));
    }
}
  • 然后可以根据路径区分出 App 中的所有 MachO(动态库和可执行的主二进制文件):
static void AppendProductImagePaths(std::vector<std::string> & product_image_paths){
    NSString *mainBundlePath = [NSBundle mainBundle].bundlePath;
    std::vector<std::string> all_image_paths;
    AppendAllImagePaths(all_image_paths);
    for(auto path: all_image_paths){
        NSString *imagePath = [NSString stringWithUTF8String:path.c_str()];
        if([imagePath containsString:mainBundlePath] ||[imagePath containsString:@"Build/Products/"]){
            product_image_paths.push_back(path);
        }
    }
}
  • 其中 Build/Products/ 是为了适配开发模式,例如上图的工程配置下 FirstDylib 的目录是在:
/Users/everettjf/Library/Developer/Xcode/DerivedData/LoadCostSample-amfsvwltyimldeaxbquwejweulqd/Build/Products/Debug-iphonesimulator/FirstDylib.framework/FirstDylib
  • 为了把这种情况过滤出来,这里简单的通过 Build/Products 匹配下(没有用 DerivedData 是考虑到 DerivedData 目录在 Xcode 的设置中是可修改的)。

④ 获取所有类

unsigned int classCount = 0;
const char ** classNames = objc_copyClassNamesForImage(path.c_str(),&classCount);

for(unsigned int classIndex = 0; classIndex < classCount; ++classIndex) {
	NSString *className = [NSString stringWithUTF8String:classNames[classIndex]];
	Class cls = object_getClass(NSClassFromString(className));
  • 关键代码如下:
@interface LoadRuler : NSObject
@end
@implementation LoadRuler

+(void)LoadRulerSwizzledLoad0{
    LoadRulerBegin;
    [self LoadRulerSwizzledLoad0];
    LoadRulerEnd;
}

+(void)LoadRulerSwizzledLoad1{
    LoadRulerBegin;
    [self LoadRulerSwizzledLoad1];
    LoadRulerEnd;
}
+(void)LoadRulerSwizzledLoad2{
    LoadRulerBegin;
    [self LoadRulerSwizzledLoad2];
    LoadRulerEnd;
}
+(void)LoadRulerSwizzledLoad3{
    LoadRulerBegin;
    [self LoadRulerSwizzledLoad3];
    LoadRulerEnd;
}
+(void)LoadRulerSwizzledLoad4{
    LoadRulerBegin;
    [self LoadRulerSwizzledLoad4];
    LoadRulerEnd;
}

+(void)load{
    PrintAllImagePaths();
    
    SEL originalSelector = @selector(load);
    Class rulerClass = [LoadRuler class];
    
    std::vector<std::string> product_image_paths;
    AppendProductImagePaths(product_image_paths);
    for(auto path : product_image_paths){
        unsigned int classCount = 0;
        const char ** classNames = objc_copyClassNamesForImage(path.c_str(),&classCount);

        for(unsigned int classIndex = 0; classIndex < classCount; ++classIndex){
            NSString *className = [NSString stringWithUTF8String:classNames[classIndex]];
            Class cls = object_getClass(NSClassFromString(className));
            
            // 不要把自己hook了
            if(cls == [self class]){
                continue;
            }

            unsigned int methodCount = 0;
            Method * methods = class_copyMethodList(cls, &methodCount);
            NSUInteger currentLoadIndex = 0;
            for(unsigned int methodIndex = 0; methodIndex < methodCount; ++methodIndex){
                Method method = methods[methodIndex];
                std::string methodName(sel_getName(method_getName(method)));

                if(methodName == "load"){
                    SEL swizzledSelector = NSSelectorFromString([NSString stringWithFormat:@"LoadRulerSwizzledLoad%@",@(currentLoadIndex)]);
                    
                    Method originalMethod = method;
                    Method swizzledMethod = class_getClassMethod(rulerClass, swizzledSelector);
                    
                    BOOL addSuccess = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
                    // 添加成功,则说明不存在load。但动态添加的load,不会被调用。与load的调用方式有关
                    if(!addSuccess){
                        // 已经存在,则添加新的selector
                        BOOL didAddSuccess = class_addMethod(cls, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
                        if(didAddSuccess){
                            // 然后交换
                            swizzledMethod = class_getClassMethod(cls, swizzledSelector);
                            method_exchangeImplementations(originalMethod, swizzledMethod);
                        }
                    }
                    ++currentLoadIndex;
                }
            }
        }
    }
}

@end

⑤ Category 的处理

  • 工程中 FirstLoader 的类及几个 Category 是如下这样:
@implementation FirstLoader

+ (void)load{
    NSLog(@"first +load");
    usleep(1000 * 15);
}
@end

@implementation FirstLoader (FirstCategory)

+(void)load{
    NSLog(@"first category +load for FirstLoader");
    usleep(1000 * 45);
}

@end

@implementation  FirstLoader (SecondCategory)

+ (void)load{
    NSLog(@"second category +load for FirstLoader");
    usleep(1000 * 55);
}

@end
  • Hopper 中看到 Category 中的 +load,最终的符号没有体现出来:

在这里插入图片描述

  • 为了把一个类及对应 Category 中的所有 load 都 hook,上面的代码使用了 class_copyMethodList 或许所有类方法,然后逐个替换。为了代码实现的简单,可以创建 LoadRulerSwizzledLoad0、 LoadRulerSwizzledLoad1、 LoadRulerSwizzledLoad2、 LoadRulerSwizzledLoad3 等这样的方法,适配 N 个 Category 的情况。

四、完整示例

本站所有文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:licqi@yunshuaiweb.com

加载中~
如果您对我们的成果表示认同并且觉得对你有所帮助可以给我们捐赠。您的帮助是对我们最大的支持和动力!
捐赠我们
扫码支持 扫码支持
扫码捐赠,你说多少就多少
2
5
10
20
50
自定义
您当前余额:元
支付宝
微信
余额

打开支付宝扫一扫,即可进行扫码捐赠哦

打开微信扫一扫,即可进行扫码捐赠哦

打开QQ钱包扫一扫,即可进行扫码捐赠哦

天猫38节现货-全屋智能