iOS逆向--dyld加载过程

725 阅读10分钟

我们知道iOS的可执行文件是是通过dyld进行加载的,但是是怎么加载的呢,我们来分析一下,首先我们创建一个空工程创建一个Person类,在类里面写一个load方法,并且在load方法上面加一个断点

当断点断住后,我们通过bt命令查看一下当前的堆栈信息,发现是从_dyld_start函数开始的,同时左边栏中也可以看到是从start开始的

我们关掉左边下面的第一个选项,让它把所有的堆栈信息都展示出来:

然后再通过up指令在日志栏中查看每一步的详细,信息,从左边我们看到总共有九步,所以我们输入九次up达到start的详细信息,并且上面是每一步的详细汇编内容:

我们在上面找到bl指令,因为在汇编中bl指令是代表调用函数的意思,调用的是dyldbootstrap::start函数,一看我们就知道调用的是c++代码,从堆栈信息我们看到,程序最开始就是从这个函数开始的,

我们可以到dyld源码中先找到这个函数

我们先分析一下start函数的参数

uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
				intptr_t slide, const struct macho_header* dyldsMachHeader,
				uintptr_t* startGlue)

  • macho_header* appsMachHeader:上一篇我们说的在load.h里面有个结构体就是这个,并且在dyld里面做了区分,如果是64位就指向64位的结构体,如果是32位就指向32位结构体

  • intptr_t slide:有个叫ALSR,内存布局随机化,当每个macho加载到内存中的时候,系统会随机生成一个值,保证macho再不定的内存中分布

再看看函数内容

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,也就是header和LoadCommands直接的空白区域写上汇编代码,在汇编代码调用
    slide = slideOfMainExecutable(dyldsMachHeader);
    bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
    shouldRebase = true;
#endif
    if ( shouldRebase ) {
        //重定向dyld
        rebaseDyld(dyldsMachHeader, slide);
    }

	// allow dyld to use mach messaging
    //mach初始化
	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
	uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
    //进入了dyld的main函数
	return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyld::_main

我们再看一下dyld::_main函数:

配置相关环节的操作

读取macho文件信息

设置上下文信息和一些参数

检测环境变量

获取程序的基本信息

开始加载共享缓存库

加载共享缓存库之前要先检测一下是否允许加载共享缓存库,

进到函数里面我们发现,iOS方面共享缓存库是不允许被拒绝加载的

我们再进到加载共享缓存库的函数mapSharedCache里面

发现调用的是loadDyldCache函数进行加载的

加载共享缓冲库后,就开始实例化主程序了

实例化主程序

我们再看一下instantiateFromLoadedImage函数,

我们发现在这里面是:

  • 先拿到macho文件的头信息,判断是否兼容手机,
  • 如果兼容,然后再去实例化主程序,
  • 实例化主程序后并不马上加载,而是添加到AllImages中去,然后统一加载

我们进入到实例化主程序的函数instantiateMainExecutable看看,发现最终是依靠sniffLoadCommands来实例的,看看具体实现:

发现里面很长,只挑几个关键信息:

  • *compressed = false;//mackO里面的INFO_Only
  • *segCount = 0;//mackO里面segment里面的长度,长度不能超过255条
  • *libCount = 0;//根据mackO里面LOAD_DYLIB个数,不能超过4095个,也就是加载的库不能超过4095个
  • *codeSigCmd = NULL;//代码签名
  • *encryptCmd = NULL;//加壳信息

我们再进入到AllImages看看:

发现AllImages是一个队列,遵守先进先出原则,最开始添加进去的就是主程序

链接动态库

主程序实例化完成之后,下一步就是链接动态库了,

我们看到系统会通过环境变量DYLD_INSERT_LIBRARIES来判断,如果有值就会循环插入动态库,这个地方在越狱实话会用到,这也就是为啥当越狱手机下载一个插件后,为什么即使没有对APP做任何操作,会影响到手机里面安装的app

而真正链接动态库是在link函数里,而这里链接的就是MachO文件里面显示需要依赖的库

再往下:

标记完主程序链接完,我们发现系统还链接了弱绑定的库,也就是一些懒加载的库,到这个时候,动态库和三方库都加载完成了,但是主程序还没加载,下面就是加载主程序进行程序初始化了,也就是加载已经我们自己写的类了

关于上面的顺序,我们可以做一下验证:

创建一个程序,在里面创建两个类,每个类都实现下自己的+(void)load方法,并在里面做一下打印,在创建两个framework,在framework里面也各自创建一个类,实现+(void)load方法,编译运行一下,我们发现,framework里面的打印比自己创建的类的打印提前,并且两个framework里面打印跟在Builde Phases里面framework里面的顺序一样,自己创建的类也是按照这种原则

上面就start函数,下面就到了开始加载主程序的initializeMainExecutable函数

程序的初始化--- initializeMainExecutable

我们进到initializeMainExecutable函数实现里面看看

在里面我们发现了xcode左边栏中堆栈信息的runInitializers,我们再进入到runInitializers里面看看:

看到了xcode左边栏中堆栈信息的processInitializers

在这里面我们又看到了xcode左边栏中堆栈信息的recursiveInitialization

这个函数比较长,我们看一下xcode堆栈里面下一个调用的函数

我们发现调用的是dyld的notifySingle,在recursiveInitialization里面我们搜一下,发现了这个函数

因为下一个是load_images,我们看看如何调到的,command + shift + o,搜一下

进到这个函数实现,我们发现找不到loadImages函数,但是在这个函数里面,我们看到了一个回调函数sNotifyObjCInit

我们在这个文件全局搜一下这个回调函数在什么地方设置的,然后我们发现是在registerObjCNotifiers函数里面设置的

我们再搜一下registerObjCNotifiers是在哪个地方调用的:

发现在_dyld_objc_notify_register里面,但是全局搜一下,我们找不到_dyld_objc_notify_register函数的调用地方,只有一个头文件的APIs里面有

这样我们就需要用到符号断点来进行调试,在Demo中下一个_dyld_objc_notify_register的符号断点

然后运行

通过函数调用栈,我们发现_dyld_objc_notify_register是在_objc_init里面调用的,并且在下面控制台里面我们没发现自己卸载load里面的打印,说明在调用_dyld_objc_notify_register函数时候动态库还没开始加载,这个时候我们在控制台看一下这个函数的两个参数是什么:

(lldb) register read x1
     x1 = 0x00000001ac8364e8  libobjc.A.dylib`load_images
(lldb) register read x2
     x2 = 0x00000001ac837358  libobjc.A.dylib`unmap_image
(lldb) 

发现传进来的是load_images和unmap_image;

而_objc_init是在objc里面的源码,我们打开objc的源码搜一下_objc_init

这样我们就找到了load_images,因为函数名称就是函数的指针,所以这个地方可以直接将函数名称传进去,下一步就是调用loadImages了

loadImages

我们再看一下实现

发现主要调用的是call_load_methods,我们再看一下具体实现:

在里面会调用call_class_loads方法,来加载我们的类,加载完成后就相当于程序加载完成了

我们再回到recursiveInitialization函数里面去

发现在调用notifySingle方法后会调用doInitialization函数,我们在进到doInitialization函数看看

会调用doModInitFunctions函数,而这个函数会调用系统的构造函数,我们可以做一个实验:

  • 授信对Demo进行编译,然后拿到可执行文件,通过MachOView打开

  • 我们再在类中写几个构造函数:

__attribute__((constructor)) void func1(){
   
   printf("func1来了");
   
}


__attribute__((constructor)) void func2(){
   printf("func2来了");
}

__attribute__((constructor)) void func3(){
   printf("func3来了");
}

然后再进行编译运行,发现打印:

打开可执行文件,通过MachOView打开

我们发现多了个_mod_init_func,这个就是上面的doModInitFunctions函数,并且里面有func1、func2、func3三个构造函数,也就是在doModInitFunctions里面,项目中所有带__attribute__((constructor)) 的函数都会去调用一下

主程序加载完成后,我们再往下看,发现了主程序main函数的入口

总结

  • 综合起来,方法执行顺序是load --- > 构造函数 ---> main

  • 库(image)的加载顺序:共享缓存库 --- > 插入库 ---> 第三方库

  • dyld加载所有库和可执行文件

  • 程序开始是从_dyld_start开始,然后调用dyldbootstrap::start,在这里面算出machO中间的偏移量,并且重定向dyld,初始化mach后调用dyld::_main函数

  • dyld::_main里面,

    • 先配置相关环节的操作,
    • 再设置上下文信息和一些参数回调等,
    • 然后检测环境变量以及进程是否受限,
    • 在获取程序的基本信息后,
    • 开始通过mapSharedCache函数加载共享缓存库,
    • 加载共享缓存库时候会判断是否允许加载,但是在iOS中是不允许被禁止的,
      • 在mapSharedCache发现是通过loadDyldCache进行加载的,
      • 在loadDyldCache会判断是否已经加载过,如果加载过了不管,没加载的话在加载
  • 加载完共享缓存库后,开始通过instantiateFromLoadedImage实例化主程序,

    • 在这里面会先获取MachO的头部信息,然后判断兼容性,如果兼容的话会通过instantiateMainExecutable实例化主程序后,
    • 将主程序加载器添加到数组AllImages(是队列结构)里,在后面跟第三方库库统一加载,
    • 在instantiateMainExecutable发现是通过调用sniffLoadCommands来进行实例化的
  • 主程序实例化完成后,下一步就是链接/加载动态库,

    • 链接前会进行标示一下,
    • 然后从AllImages里面拿出库加载器,通过link函数进行链接,这个时候就会把程序依赖的第三方库都加载完成了
    • 链接完第三方库,在进行一下弱绑定库,也就是加载一些懒加载的库
  • 链接完库后,就开始通过initializeMainExecutable进行程序的初始化,runInitializers-->processInitializers-->recursiveInitialization-->notifySingle-->load_images

  • 这个load_images其实是一个回调函数,是在objc源码中的_objc_init通过_dyld_objc_notify_register进行注册的

  • 在load_Image函数里面通过call_load_methods循环调用类的load方法,并加载我们自己的类

  • load_images掉完之后就又回到recursiveInitialization里面的了,然后调用doInitialization,在这里通过doModInitFunctions来调用系统的构造函数

  • 最后进入到程序的main函数