欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程资源 > 编程问答 >内容正文

编程问答

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

发布时间:2024/5/28 编程问答 46 豆豆
生活随笔 收集整理的这篇文章主要介绍了 iOS逆向之深入解析如何Hook所有+load方法及Category的处理 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

一、类方法 +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){// 已经存在,则添加新的selectorBOOL 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 的情况。

四、完整示例

  • Objective C之Hook所有+load方法简单示例。
与50位技术专家面对面20年技术见证,附赠技术全景图

总结

以上是生活随笔为你收集整理的iOS逆向之深入解析如何Hook所有+load方法及Category的处理的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。