五 MachO文件解析

作者: 蚂蚁也疯狂 | 来源:发表于2020-06-16 09:56 被阅读0次
    nx_001.jpeg

    上一篇文章中,咱们已经简单的提到了MachO,在用Framework做代码注入的时候,必须先向MachO的Load Commons中插入该Framework的的相对路径,让我们的iPhone在执行MachO的时候能够识别并加载Framework!

    窥一斑而知全豹,从这些许内容其实已经可以了解到MachO在我们APP中的地位是多么的重要。同样,在咱们逆向的实践中,MachO也是一道绕不过去门槛!

    接下来本文会从以下几点进行阐述:

    • MachO文件
    • MachO文件结构
    • 从DYLD源码的角度看APP启动流程 (重点!!!)

    1.什么是MachO文件

    Mach-O其实是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式, 类似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)。它是一种用于可执行文件、目标代码、动态库的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性。

    1.1、常见的MachO文件

    1. 目标文件.o
    2. 库文件(.a、.dylib、Framework、可执行文件、dyld、*.dsym)文件。

    1.2、如何查看文件格式

    我们可以通过file指令查看文件的具体格式。 五 file命令1.png
    file 文件路径
    file WeChat // 查看微信的可执行文件类型
    file GPUImage.framework/GPUImage //GPUImage的文件类型
    

    目前已知的架构分为armv7,armv7s,arm64,i386,x86_64等等,MachO中其实也是这些架构的集合。

    可以随意建立一个空工程:Dome1

    选择Debug,然后Build一下,在Products->显示包内容中查看可执行文件的类型。这个是模拟Debug模式下模拟器的可执行文件的类型。 五 MachO1.png
    g)

    编辑Edit scheme,将Run下的Debug修改成Relase,此时目标文件还是选择iPhone模拟器。会看到下面的可执行文件类型。


    五 Edit scheme1.png 五 MachO2.png

    将目标文件设置设置成手机真机运行,Edit scheme还是上一步的保持不变,这时会出现以下的可执行文件类型。


    五 Edit scheme2.png 五 MachO3.png

    从上面三张图就可以确定MachO可以是多架构的二进制文件,称之为「通用二进制文件」

    通用二进制文件是苹果公司提出的一种程序代码。能同时适用多种架构的二进制文件
    a. 同一个程序包中同时为多种架构提供最理想的性能。
    b. 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大。
    c. 但是由于两种架构有共通的非执行资源,所以并不会达到单一版本的两倍之多。
    d. 而且由于执行中只调用一部分代码,运行起来也不需要额外的内存。

    注:其实除了更改最低版本号可以改变MachO的架构,在XCode的中也可以主动设置。

    注意:这里可以查看下最低版本设置为iOS 8,用release打包出的*.ipa中的MachO

    五 MachO4.png

    1.2、拆分、重组MachO

    // 使用lipo -info 可以查看MachO文件包含的架构
    lipo -info MachO文件
    
    // 使用lipo –thin 拆分某种架构
    lipo MachO文件 –thin 架构 –output 输出文件路径
    
    // 使用lipo -create  合并多种架构
    lipo -create MachO1  MachO2  -output 输出文件路径
    

    2、MachO文件结构

    五 文件结构1.png

    2.1、Mach-O 的组成结构如图所示包括了,Header、load commands、Data部分。

    Header包含该二进制文件的一般信息。
    1.字节顺序、架构类型、加载指令的数量等。
    2.使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么。
    
    Load commands 一张包含很多内容的表。
    1.内容包括区域的位置、符号表、动态符号表等。
    
    Data 通常是对象文件中最大的部分
    1.包含Segement的具体数据。
    

    本文从两个视角分析Header,分别是「用MachOView可视化后直观的查看」和「系统源码解析」

    • 用MachOView可视化后直观的查看。如下图: 五 文件结构2.png
      五 文件结构3.png
    • 系统源码解析 五 MachO header.png

    2.2、Load Commons

    Load commands是一张包含很多内容的表。
    内容包括区域的位置、符号表、动态符号表等。

    五 MachO load commands1.png

    上图Load Commons中的大部分字段在下表中可以找到相关的含义。

    名称 含义
    LC_SEGMENT_64 将文件中(32位或64位)的段映射到进程地址空间中
    LC_DYLD_INFO_ONLY 动态链接相关信息
    LC_SYMTAB 符号地址
    LC_DYSYMTAB 动态符号表地址
    LC_LOAD_DYLINKER 使用谁加载,我们使用dyld
    LC_UUID 文件的UUID
    LC_VERSION_MIN_MACOSX 支持最低的操作系统版本
    LC_SOURCE_VERSION 源代码版本
    LC_MAIN 设置程序主线程的入口地址和栈大小
    LC_LOAD_DYLIB 依赖库的路径,包含三方库
    LC_FUNCTION_STARTS 函数起始地址表
    LC_CODE_SIGNATURE 代码签名

    其中LC_LOAD_DYLINKERLC_LOAD_DYLIB

    • LC_LOAD_DYLINKER 该字段标明我们的MachO是被谁加载进去的。可以理解为LC_LOAD_DYLINKER指向的地址是微信APP加载小程序的引擎,而我们的MachO是小程序。在上图中可以看到我们的MachODemo1的LC_LOAD_DYLINKER指向的地址就是dylddyld确实是用来加载我们app的,在下面一节将会对dyld的源码进行分析,讲述dyld是如何对MachO进行加载的。
    • LC_LOAD_DYLIB 该字段标记了所有动态库的地址,只有在LC_LOAD_DYLIB中有标记,我们MachO外部的动态库(如:Framework)才能被dyld正确的引用,否则dyld不会主动加载,这也是上篇文章,代码注入的关键所在!

    2.3、Data

    Data 通常是对象文件中最大的部分,包含Segement的具体数据,如静态C字符串,带参数/不带参数的OC方法,带参数/不带参数的C函数。

    在DemoMachO1中编写一下代码:

    • 静态C字符串
    • 静态OC字符串
    • 带参数的OC方法
    • 不带参数的OC方法
    • 带参数的C函数
    • 不带参数的C函数

    在我们的项目中添加以下代码(在ViewController)中

    #import "ViewController.h"
    
    /*静态C字符串*/
    static const char *cString = "c_string";
    
    /*静态OC字符串*/
    static const NSString *ocString = @"oc_string";
    
    @interface ViewController ()
    @end
    
    /*C方法(无参数)*/
    void CFunc(){
        printf("c_func");
    }
    
    /*C方法(有参数)*/
    void CFunc1(int a){
        printf("c_func:%d",a);
    }
    @implementation ViewController
    /*OC方法(无参数)*/
    -(void)ocFunc{
        NSLog(@"%s",__func__);
        NSLog(@"ocString:%@",ocString);
    }
    /*OC方法(有参数)*/
    -(void)ocFunc1:(NSInteger)a{
        NSLog(@"%s",__func__);
        NSLog(@"ocString:%@",ocString);
    }
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        self.view.backgroundColor = [UIColor yellowColor];
        
        CFunc();
        CFunc1(1);
        
        [self ocFunc];
        [self ocFunc1:1];
    }
    
    @end
    

    使用MachOView查看MahcO文件。


    五 MachO 字符串.png 五 MachO 方法名.png

    可以看到,全局静态C字符,方法里面的字符串都被保存在data段的cstring里了。但所有同样的字符串只会被保存一次。
    同样所有的OC方法都被保存在methname里了。

    上面用cstring和methname距离了data段的作用,同样的所有类名,协议名等也是以同样形式存储在这。

    上面已经对MachO有了一个大概的了解,接下来本文就对dyld这么一个重要的东西进行一个初探。

    3.DYLD(从源码的角度看APP启动流程)(重点)

    dyld (the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。

    3.1、在main函数中断点查看

    首先思考,在main函数中挂断点能不能查看到APP启动对应的堆栈?
    这部分其实靠想,靠猜测很难有答案,我们直接用XCode直接尝试:


    五 main断点1.png

    可以看到在main函数断点并不能看到启动的对应堆栈,说明main函数也是被别人调用的,而不是处于app启动的堆栈中。
    既然main查不到启动堆栈,那么比app更早执行的load方式是否可以找得到呢?

    3.2、在load方法中断点查看

    1.首先在空工程的ViewController文件中添加以下代码。
    2.然后在load函数前设置断点

    #import "ViewController.h"
    @interface ViewController ()
    @end
    
    @implementation ViewController
    +(void)load{
        NSLog(@"");
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
    }
    @end
    
    五 load断点.png 五 load start.png

    在这可以发现更多的信息,可以很明显的发现,是调用了用dyld中的dyldbootstrap文件中的start方法。
    马不停蹄,打开dyld源码,找到对应的dyldbootstrap文件中的start函数。
    点击这里下载dyld源码

    3.3、在dyldbootstrap中查看start函数

    1.打开 dyld 项目,搜索 dyldbootstrap 文件。
    2.在dyldbootstrap文件中搜索start函数。

    //
    //  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
    //  In dyld we have to do this manually.
    //
    uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
                    intptr_t slide, const struct macho_header* dyldsMachHeader,
                    uintptr_t* startGlue)
    {
        // if kernel had to slide dyld, we need to fix up load sensitive locations
        // we have to do this before using any global variables
        // 滑块,ASLR技术,地址偏移,是MachO文件在内存中的地址重定向
        slide = slideOfMainExecutable(dyldsMachHeader);
        bool shouldRebase = slide != 0;
    #if __has_feature(ptrauth_calls)
        shouldRebase = true;
    #endif
        if ( shouldRebase ) {
            // 重定向
            rebaseDyld(dyldsMachHeader, slide);
        }
    
        // allow dyld to use mach messaging
        // 消息初始化
        mach_init();
    
        // kernel sets up env pointer to be just past end of agv array
        const char** envp = &argv[argc+1];
        
        // kernel sets up apple pointer to be just past end of envp array
        const char** apple = envp;
        while(*apple != NULL) { ++apple; }
        ++apple;
    
        // set up random value for stack canary
        // 栈溢出保护
        __guard_setup(apple);
    
    #if DYLD_INITIALIZER_SUPPORT
        // run all C++ initializers inside dyld
        runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
    #endif
    
        // now that we are done bootstrapping dyld, call dyld's main
        // 正在的启动函数,在dyld中的_main函数中
        uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
        return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
    }
    

    start函数的源码可得知道:dlyd会内存中找到一块地址给MachO使用,也就是ASLR,内存偏移。

    最后start函数执行了一个main函数(这个可以不是我们app中的main函数,而是dyld的)并返回。

    3.4、在dlyd中查看main函数

    由于这一段代码较多,我们也没有必要把每一句代码都弄清楚,这里我们抓住其中的关键代码,足步分析在main函数之前dyld到底帮我们做了哪一些事情。

    1.配置环境变量
    main函数的初始,到函数getHostInfo()之前都是在配置一些环境变量,已经一些线程相关的,涉及内容太过底层,这就不一一分析了(其实是能力不及😆)。

    五 配置环境.png

    在这一步中有很多if判断,其实里面都是对应的环境变量,这些都是可以在XCode进行相关的配置,进行对应的操作(如Log相关信息)。

    2、加载共享缓存库

    在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着。其中包括UIKit,Foundation等基础库。 五、加载缓存库.png
    五、必须加载的缓存库.png

    在源码中可以看到在我们iOS系统中,共享缓存库被明确一定会被加载。
    因为这种机制的存在,使得iOS在的对这些基础库的加载的时候时间和内存都得到节约!
    但是有时因为共享缓存库的机制的存在使得iOS在共享缓存库里面的C函数,也就是系统C函数变的不是那么静态,有了些许OC运行时的特性!
    这部分内容将会在下一篇文章着重讲解!从不一样的角度看Runtime!

    3、实例化主程序
    加载主程序其实就是对MachO文件中LoadCommons段的一些列加载!
    我们继续对代码的跟进,如下6张图:
    图片1

    图片2

    补充:实例化完之后调用addImage(image),将实例化出来的镜像加入所有的镜像列表sAllImages,主程序永远是sAllImages的第一个对象!

    图片3

    图片4

    图片5

    图片6

    从源代码可以看出,加载主程序这一步其实很简单,就是将MachO文件中的部分信息一步一步的放入内存。
    其中从最后一张图可以了解到:

    • 最大的segment数量为256个!
    • 最大的动态库(包括系统的个自定义的)个数为4096个!

    4、加载动态链接库
    加载动态链接库,如Xcode的ViewDebug、MainThreadChecker,我们之后代码注入的库也是通过这种形式添加的!

    插入动态链接库

    5、链接主程序
    链接主程序

    link函数里面其实就是对之前的imges(不是图片,这是镜像)进行一些内核操作,这部分Apple没有开源出来,只能看到些许源码,有兴许的同学可以自行查阅:
    link

    6、加载Load和特定的C++的构造函数方法
    无论是从之前断点load方法还是我们现在一步步对源码的根据,都能了解到,dyldinitializeMainExecutable就是就加载load的入口:
    initializeMainExecutable1

    initializeMainExecutable2

    并且最后都能接到一个结论:
    dyldnotifySingle函数经过一系列的跳转,最终会跳转到objc源码中的call_load_methods函数!!

    最后找到函数_dyld_objc_notify_register,就在全局都找不到一个调用的地方了,其实这个函数本身就不是给dyld调用的,而是提供给外部调用的。怎么找到是谁调用了_dyld_objc_notify_register呢?
    继续打开之前的DemoMachO1,在工程中加上_dyld_objc_notify_register的符号断点看看。

    五 符号断点.png

    运行工程,断住之后再次查看函数调用栈:

    五 _dyld_objc_notify_register1.png

    这就可以很清晰的看到,原来是objc_init调用了咱们的_dyld_objc_notify_register函数。

    同样打开objc的源码(点击下载objc源码 )
    快速定位_dyld_objc_notify_register的调用位置。如图:
    图片地址

    图片地址2

    这样dyld是如何加载咱们的load方法就被找到了。
    期间如果有细心的同学可能看到了在notifySingle后面紧跟着doInitialization这样一个函数,这是一个系统特定的C++构造函数的调用方法。
    doInitialization

    doModInitFunctions

    ImageLoadMachO

    这种C++构造函数有特定的写法,如下:

    __attribute__((constructor)) void CPFunc(){
        printf("C++Func1");
    }
    

    7、寻找APP的main函数并调用
    寻找APP的main函数并调用

    最终dyld的main函数中的主要流程就已经走完了,当然这7个步骤是一条主线,期间还会有很多其他的步骤,过程非常繁琐,这就不一一举例了。大家可以通过阅读dyld的源码尽收眼底。

    4.代码资料

    代码——暂时未上传

    本文讲述了MachO的概述,文件结构,在从其中Load Commons中的LC_LOAD_DYLINKER引出dyld,接下根据dyld源码分析了APP的启动流程。分别是:
    1、配置环境变量
    2、加载共享缓存库
    3、实例化主程序
    4、加载动态链接库
    5、链接主程序
    6、加载Load和特定的C++的构造函数方法
    7、寻找APP的main函数并调用

    如图:


    五 dyld流程.png

    另外dyld中LC_LOAD_DYLIB的(加载动态链接库)存在,为我们逆向注入代码提供了无限可能。
    MachO中其实还有一些符号表,为系统提供查询对应的方法名称提供了路径。

    参考文章:
    作者:一缕清风扬万里
    原文地址:https://www.jianshu.com/p/95896fb96a03

    相关文章

      网友评论

        本文标题:五 MachO文件解析

        本文链接:https://www.haomeiwen.com/subject/ozklxktx.html