iOS APP 启动优化(六):在指定的 segment 和 section 中存入数据

 日常拷问,学习底层到底有没有用,很多人认为学习底层知识只是为了应付面试,日常开发中根本使用不到,事实真的是这样吗?今天我们就总结一些 mach-o 的知识点在日常开发中的一些使用场景,来验证一下我们学习底层知识点到底有没有用。

在指定的 segment 和 section 中存入数据

 在前面学习 mach-o 和 dyld 的过程中,我们看到了 dyld 任意的加载 mach-o 文件中指定 segment 的各个 section 中的内容,那么,我们可不可以干预 Xcode 生成 mach-o 文件的过程呢?那么,有没有一种方式,可以允许我们在 Xcode Build 过程中动态的在 mach-o 中插入新的 segment 和 section 呢?答案是可以的,下面我们直接揭晓答案:使用 __attribute__ section 将指定的数据储存到指定的 segment 和 section 中。

__attribute__ 知识点扩展

 下面我们首先做一个知识点的延展,看一下 __attribute__ 相关的信息,__attribute__ 可以用来设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。它的书写特征是:__attribute__ 前后都有两个下划线,并且后面会紧跟一对原括弧,括弧里面是相应的 __attribute__ 参数,语法格式:__attribute__((attribute-list)) 另外,它必须放于声明的尾部 ; 之前。下面我们看一些比较常用的 gcc Attribute syntax

  • __attribute__((format())) 按照指定格式进行参数检查。
  • __attribute__((__always_inline__)) 强制内联。
  • __attribute__((deprecated("Use xxx: instead") 这个可能是我们见的比较多的,用来标记某个方法已经被废弃了,需要用其它的方法代替。
  • __attribute__((__unused__)) 标记函数或变量可能不会用到。
  • __attribute__((visibility("visibility_type"))) 标记动态库符号是否可见,有以下取值:
  1. default 符号可见,可导出。
  2. hidden 符号隐藏,不可导出,只能在本动态库内调用。
  • __attribute__((objc_designated_initializer)) 明确指定用于初始化的方法。一个优秀的设计,初始化接口可以有多个,但最终多个初始化初始化接口都会调用 designed initializer 方法。

  • __attribute__((unavailable))__attribute__((unavailable("Must use xxx: instead."))); 标记方法被禁用,不能直接调用,但这并不意味着该方法不能被调用,在 Objective-C 中使用 runtime 依然可以调用。

  • __attribute__((section("segment,section"))) 将一个指定的数据储存到我们需要的 segment 和 section 中。

  • __attribute__((constructor))attribute((constructor)) 标记的函数,会在 main 函数之前或动态库加载时执行。在 mach-o 中,被 attribute((constructor)) 标记的函数会在 _DATA 段的 __mod_init_func 区中。当多个被标记 attribute((constructor)) 的方法想要有顺序的执行,怎么办?attribute((constructor)) 是支持优先级的:_attribute((constructor(1)))

  • __attribute__((destructor))attribute((constructor)) 相反:被 attribute((destructor)) 标记的函数,会在 main 函数退出或动态库卸载时执行。在 mach-o 中此类函数会放在 _DATA 段的 __mod_term_func 区中。

__attribute__((objc_root_class))

 这里我们再延伸一个可能被我们忽略了,但是还挺重要的知识点。我们大概一直都知道的 NSObject 作为根类(root_class),它的父类是 nil,我们日常使用的每个类都是 NSObject 的子类(NSProxy 除外,它是另外一个根类,它仅遵循 NSObject 协议,并不继承 NSObject 类。)那么我们能不能自己创建一个不继承 NSObject 的类来使用呢?这篇文章 不使用 NSOBJECT 的 OBJECTIVE-C CLASS 会给我们答案。

 作为根类使用的类会使用 NS_ROOT_CLASS 宏来声明,例如:

  • NSProxy
NS_ROOT_CLASS
@interface NSProxy <NSObject> {
    __ptrauth_objc_isa_pointer Class    isa;
}
...
复制代码
  • NSObject
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS // ⬅️ 这里有一个 OBJC_ROOT_CLASS 宏
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
复制代码

 我们看一下 OBJC_ROOT_CLASS 宏的定义,它其实就是 __attribute__((objc_root_class))

#if !defined(OBJC_ROOT_CLASS)
#   if __has_attribute(objc_root_class)
#       define OBJC_ROOT_CLASS __attribute__((objc_root_class))
#   else
#       define OBJC_ROOT_CLASS
#   endif
#endif
复制代码

__attribute__ ((section (“segment, section”))) 使用

 下面我们看一个示例代码:

struct __COM_easeapi {
    int count;
    const char *name;
};

// easeapi_section 是一个 __COM_easeapi 类型的结构体变量,然后用 __attribute__ 修饰使其位于 __COM 段的 __easeapi 区中
volatile struct __COM_easeapi easeapi_section __attribute__ ((section ("__COM, __easeapi"))) = {255, "name"};
复制代码

__attribute__ ((section ("segment, section"))) 只能声明 C (全局)函数、全局(静态)变量、Objective-C (全局)方法及属性。例如我们直接把其放在我们的 main 函数中使用,就会报这样的错误:'section' attribute only applies to functions, global variables, Objective-C methods, and Objective-C properties

 由于我们需要存储指定的信息,典型的做法就是像上述示例中使用结构体变量。这种方式看似解决了问题,但是有诸多限制:

  1. 新插入的 section 数据必须是静态或全局的,不能是运行时生成的。(不是动态数据,可以是全局函数的返回值。)
  2. __TEXT 段由于是只读的,其限制更大,仅支持绝对寻址,所以也不能使用字符串指针。如下代码:
char *tempString __attribute__((section("__TEXT, __customSection"))) = (char *)"customSection string value";
int tempInt __attribute__((section("__TEXT, __customSection"))) = 5;
复制代码

tempInt 能正常保存到 __TEXT 段的 __customSection 区中,也能正常读取到,而 tempString 的话则会直接报:Absolute addressing not allowed in arm64 code but used in '_string5' referencing 'cstring'

__attribute__ ((section ("segment, section"))) 其中 segment 可以是已知的段名,也可以是我们自定义的段名,然后读取时保证一致就好了。

__attribute__ section 的方式实际上是 mach-o 加载到内存后填充数据的,并不能直接填充至 mach-o 文件中的,例如上面示例代码中我们使用自定义的 segment 名字,然后打包后使用 MachOView 查看我们的可执行文件的结构,并不会有我们自定义的段,同样的我们使用现用的 __TEXT__DATA 段,也不会添加新的 section 区。

 下面我们看另一位大佬的示例代码,看下如何读取我们放在指定段和区中的值。iOS开发之runtime(12):深入 Mach-O

#import <Foundation/Foundation.h>

#import <dlfcn.h>
#import <mach-o/getsect.h>

#ifndef __LP64__
#define mach_header mach_header
#else
#define mach_header mach_header_64
#endif

const struct mach_header *machHeader = NULL;
static NSString *configuration = @"";

// 写入 __DATA, __customSection
char *string1 __attribute__((section("__DATA, __customSection"))) = (char *)"__DATA, __customSection1";
char *string2 __attribute__((section("__DATA, __customSection"))) = (char *)"__DATA, __customSection2";

// 写入 __CUSTOMSEGMENT, __customSection
char *string3 __attribute__((section("__CUSTOMSEGMENT, __customSection"))) = (char *)"__CUSTOMSEGMENT, __customSection1";
char *string4 __attribute__((section("__CUSTOMSEGMENT, __customSection"))) = (char *)"__CUSTOMSEGMENT, __customSection2";

// 在 __TEXT, __customSection 中写入字符串,则会报如下错误:
// Absolute addressing not allowed in arm64 code but used in '_string5' referencing 'cstring'

//char *string5 __attribute__((section("__TEXT, __customSection"))) = (char *)"customSection string value";

// 写入 __TEXT, __customSection
int tempInt __attribute__((section("__TEXT, __customSection"))) = 5;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // ⬇️ 直接在 main 函数中使用 __attribute__ section 会报如下错误:
        // 'section' attribute only applies to functions, global variables, Objective-C methods, and Objective-C properties
        // int tempInt2 __attribute__((section("__TEXT, __customSection"))) = 5;
        
        if (machHeader == NULL) {
            Dl_info info;
            dladdr((__bridge const void *)(configuration), &info);
            
            printf("??? dli_fname:%s\n", info.dli_fname);
            printf("??? dli_fbase:%p\n", info.dli_fbase);
            printf("??? dli_sname:%s\n", info.dli_sname);
            printf("??? dli_saddr:%p\n", info.dli_saddr);
            
            machHeader = (struct mach_header_64 *)info.dli_fbase;
        }
        
        unsigned long byteCount = 0;
        uintptr_t *data = (uintptr_t *)getsectiondata(machHeader, "__DATA", "__customSection", &byteCount);
        NSUInteger counter = byteCount/sizeof(void*);
        
        for (NSUInteger idx = 0; idx < counter; ++idx) {
            char *string = (char *)data[idx];
            NSString *str = [NSString stringWithUTF8String:string];
            NSLog(@"✳️ %@", str);
        }
        
        unsigned long byteCount1 = 0;
        uintptr_t *data1 = (uintptr_t *)getsectiondata(machHeader, "__CUSTOMSEGMENT", "__customSection", &byteCount1);
        NSUInteger counter1 = byteCount/sizeof(void*);
        
        for (NSUInteger idx = 0; idx < counter1; ++idx) {
            char *string = (char *)data1[idx];
            NSString *str = [NSString stringWithUTF8String:string];
            NSLog(@"✳️✳️ %@", str);
        }
        
        unsigned long byteCount2 = 0;
        uintptr_t *data2 = (uintptr_t *)getsectiondata(machHeader, "__TEXT", "__customSection", &byteCount2);
        NSUInteger counter2 = byteCount2/sizeof(int);
        
        for (NSUInteger idx = 0; idx < counter2; ++idx) {
            int intTemp = (int)data2[idx];
            NSLog(@"✳️✳️✳️ %d", intTemp);
        }
    }
    
    return 0;
}

// ⬇️ 控制台打印:
// header 信息
??? dli_fname:/Users/hmc/Library/Developer/Xcode/DerivedData/objc-efzravoaasjkrvghpezsjgrtdmuy/Build/Products/Debug/KCObjc
??? dli_fbase:0x100000000
??? dli_sname:GCC_except_table1
??? dli_saddr:0x100003d0c

 ✳️ __DATA, __customSection1
 ✳️ __DATA, __customSection2
 ✳️✳️ __CUSTOMSEGMENT, __customSection1
 ✳️✳️ __CUSTOMSEGMENT, __customSection2
 ✳️✳️✳️ 5
复制代码

 有人会觉得,设置 section 的数据的意义是什么,也许在底层库的设计中可能会用到,但我们的日常开发中有使用场景吗?答案是肯定的。
 这主要是由其特性决定的:设置 section 的时机在 main 函数之前。这么靠前的位置,其实可能帮助我们做一些管理性的工作,比如 APP 的启动器管理:在任何一个想要独立启动的模块中,声明其模块名,并写入对应的 section 中,那么 APP 启动时,就可以通过访问指定 section 中的内容来实现加载启动模块的功能。iOS开发之runtime(12):深入 Mach-O

dladdr 介绍

 示例代码中 Dl_info 结构体和 dladdr 函数我们可能比较陌生,它们两者都是在 dlfcn.h 中声明。上面 main 函数开头的 if (machHeader == NULL) { ... } 中正是使用 dladdr 来获取 header,然后拿到 header 以后作为 getsectiondata 函数的参数, 去取指定段和区中的数据。

/*
 * Structure filled in by dladdr().
 */
typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

extern int dladdr(const void *, Dl_info *);
复制代码

 下面我们对 dladdr 进行学习,dladdr 方法可以用来获取一个函数所在的模块、名称以及地址。下面我们继续看一个示例,这个示例是使用 dladdr 方法获取 NSArray 类的 description 函数的 dl_info 信息。

#import <dlfcn.h>
#include <objc/objc.h>
#include <objc/runtime.h>
#include <stdio.h>

int main(int argc, const char * argv[]) {

//    /*
//     * Structure filled in by dladdr().
//     */
//    typedef struct dl_info {
//            const char      *dli_fname;     /* Pathname of shared object */
//            void            *dli_fbase;     /* Base address of shared object */
//            const char      *dli_sname;     /* Name of nearest symbol */
//            void            *dli_saddr;     /* Address of nearest symbol */
//    } Dl_info;
//
//    extern int dladdr(const void *, Dl_info *);
    
    Dl_info info;
    IMP imp = class_getMethodImplementation(objc_getClass("NSArray"), sel_registerName("description"));
    
    printf("✳️✳️✳️ pointer %p\n", imp);
    
    if (dladdr((const void *)(imp), &info)) {
        printf("✳️✳️✳️ dli_fname: %s\n", info.dli_fname);
        printf("✳️✳️✳️ dli_fbase: %p\n", info.dli_fbase);
        printf("✳️✳️✳️ dli_sname: %s\n", info.dli_sname);
        printf("✳️✳️✳️ dli_saddr: %p\n", info.dli_saddr);
    } else {
        printf("error: can't find that symbol.\n");
    }
    
    return 0;
}

// ⬇️ 控制台打印内容如下:
✳️✳️✳️ pointer 0x7fff203f44dd
✳️✳️✳️ dli_fname: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
✳️✳️✳️ dli_fbase: 0x7fff20387000
✳️✳️✳️ dli_sname: -[NSArray description]
✳️✳️✳️ dli_saddr: 0x7fff203f44dd
复制代码

 如控制台打印,我们仅需要将 NSArray 类的 description 函数的 IMP 作为参数传递给 dladdr 函数,它就能获取到此 IMP 所在的模块、对应的函数的名称以及地址,所以我们可以通过这种方式来判断一个函数是不是被非法修改了。

 那么我们下面看一个验证函数是否被修改的例子:

// ? 要用到的头文件
#include <objc/runtime.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#import <dlfcn.h>

static inline BOOL validate_methods(const char *cls,const char *fname) __attribute__ ((always_inline));

BOOL validate_methods(const char *cls, const char *fname) {
    // 根据类名获取类对象
    Class aClass = objc_getClass(cls);

    // 用于记录 aClass 类的方法列表
    Method *methods;
    // 用于记录方法列表数量
    unsigned int nMethods;
    // 获取指定
    Dl_info info;
    // 用于记录 method 的 IMP
    IMP imp;

    char buf[128];
    Method m;

    if (!aClass) return NO;

    // ? 获取 aClass 的所有方法
    methods = class_copyMethodList(aClass, &nMethods);

    // ? 循环验证方法列表中的每个 method
    while (nMethods--) {
        m = methods[nMethods];

        printf("✳️✳️✳️ validating [%s %s]\n", (const char *)class_getName(aClass), (const char *)method_getName(m));
        
        // ? 取得函数的 IMP
        imp = method_getImplementation(m);
        // imp = class_getMethodImplementation(aClass, sel_registerName("allObjects"));
        
        if (!imp) {
            // IMP 不存在的话报错并 return
            printf("✳️✳️✳️ error: method_getImplementation(%s) failed\n", (const char *)method_getName(m));

            free(methods);
            return NO;
        }
        
        // ? imp 做参数,通过 dladdr 函数获取 imp 的信息
        if (!dladdr((const void *)imp, &info)) {
            // 获取失败的话报错并 return
            printf("✳️✳️✳️ error: dladdr() failed for %s\n", (const char *)method_getName(m));

            free(methods);
            return NO;
        }

        // ? Validate image path(验证(比较)函数所在的模块名,如果不同的话,则 goto 语句执行 FAIL 中的内容,打印 info 的信息)
        if (strcmp(info.dli_fname, fname)) {
            goto FAIL;
        }

        // ? 通过 dladdr 函数取得的函数名不为 NULL,且也不等于 <redacted> 时,否则打印一句 "✳️✳️✳️ <redacted>" 继续下个循环
        //(<redacted> 涉及一些符号化相关的知识点,后续我们再进行详细学习)
        if (info.dli_sname != NULL && strcmp(info.dli_sname, "<redacted>") != 0) {
            
            // ? 我们先看一下 snprintf 函数的定义,它是一个 C 库函数。
            // ? C 库函数 int snprintf(char *str, size_t size, const char *format, ...) 
            // 设将可变参数 (...) 按照 format 格式化成字符串,并将字符串复制到 str 中,size 为要写入的字符的最大数目,超过 size 会被截断。
            // 返回值:
            // 1. 如果格式化后的字符串长度小于等于 size,则会把字符串全部复制到 str 中,并给其后添加一个字符串结束符 \0;
            // 2. 如果格式化后的字符串长度大于 size,超过 size 的部分会被截断,只将其中的 (size-1) 个字符复制到 str 中,并给其后添加一个字符串结束符 \0,返回值为欲写入的字符串长度。
            
            // ? Validate class name in symbol
            
            // 获取 aClass 类对象的名字,然后按 "[%s " 这个格式保存在 buf 中(buf 是前面声明的长度是 128 的 char 数组)
            snprintf(buf, sizeof(buf), "[%s ", (const char *)class_getName(aClass));
            
            // 这里的字符串比较。dli_sname 是一个 const char *,它加 1 后应该是 info.dli_saddr 吧?
            if (strncmp(info.dli_sname + 1, buf, strlen(buf))) {
            
                // 获取 aClass 类对象的名字,然后按 "[%s(" 这个格式保存在 buf 中(buf 是前面声明的长度是 128 的 char 数组)
                // 跟上面类似,只是格式发生了变化 
                snprintf(buf, sizeof(buf), "[%s(", (const char *)class_getName(aClass));
                
                // 字符串比较。info.dli_sname + 1 应该是 info.dli_saddr 吧?
                if (strncmp(info.dli_sname + 1, buf, strlen(buf))) {
                    goto FAIL;
                }
            }

            // ? Validate selector in symbol
            
            // 获取 m 方法的名字,然后按 " %s]" 这个格式保存在 buf 中(buf 是前面声明的长度是 128 的 char 数组)
            snprintf(buf, sizeof(buf), " %s]", (const char *)method_getName(m));

            if (strncmp(info.dli_sname + (strlen(info.dli_sname) - strlen(buf)), buf, strlen(buf))) {
                goto FAIL;
            }
            
        } else {
            printf("✳️✳️✳️ <redacted> \n");
        }
    }

    return YES;

FAIL:
    printf("??? method %s failed integrity test: \n", (const char *)method_getName(m));
    printf("???   dli_fname:%s\n", info.dli_fname);
    printf("???   dli_sname:%s\n", info.dli_sname);
    printf("???   dli_fbase:%p\n", info.dli_fbase);
    printf("???   dli_saddr:%p\n", info.dli_saddr);
    free(methods);
    return NO;
}
复制代码

 然后我们便可以通过前面示例中取得的 NSArray 所在的模块路径,调用 validate_methods 函数:validate_methods("NSArray", "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"); 由于打印的内容太长了,这里就不复制粘贴了,感兴趣的小伙伴可以把代码粘贴出来自己试一下。

 这里 iOS安全–验证函数地址,检测是否被替换,反注入 是原文,看原文应该会更加清晰一些!

参考链接

参考链接:?

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享