在上篇文章代码注入,窃取微信密码中我们已经简单的提到了MachO,在用Framework作代码注入的时候,必须先向MachO的Load Commons中插入该Framework的的相对路径,让咱们的iPhone在执行MachO的时候可以识别并加载Framework!linux
窥一斑而知全豹,从这些许内容其实已经能够了解到MachO在咱们APP中的地位是多么的重要。一样,在我们逆向的实践中,MachO也是一道绕不过去门槛!git
老规矩,片头先上福利:点击下载demo
这篇文章会用到的工具备:github
废话很少说,本篇文章将会从如下几点细说到底什么是MachO!bootstrap
Mach-O实际上是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式, 相似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)小程序
a、目标文件:.o
b、库文件:.a .dylib Framework
c、可执行文件:dyld .dsymwindows
咱们能够经过file指令查看文件的具体格式 缓存
目前已知的架构分为armv7,armv7s,arm64,i386,x86_64等等,MachO中其实也是这些架构的集合。能够随意创建一个空工程:Dome1(空工程就不给Demo了)bash
查看Build出的Dome1.ipa中的MachO 微信
将最低版本设置为iOS 12,用release打包出的Dome1.ipa中的MachO 架构
将最低版本设置为iOS 8,用release打包出的Dome1.ipa中的MachO
从上面三张图就能够肯定MachO能够是多架构的二进制文件,称之为「通用二进制文件」
通用二进制文件是苹果公司提出的一种程序代码。能同时适用多种架构的二进制文件 a. 同一个程序包中同时为多种架构提供最理想的性能。 b. 由于须要储存多种代码,通用二进制应用程序一般比单一平台二进制的程序要大。 c. 可是因为两种架构有共通的非执行资源,因此并不会达到单一版本的两倍之多。 d. 并且因为执行中只调用一部分代码,运行起来也不须要额外的内存。
注:其实除了更改最低版本号能够改变MachO的架构,在XCode的中也能够主动设置
// 使用lipo -info 能够查看MachO文件包含的架构
$ lipo -info MachO文件
// 使用lipo –thin 拆分某种架构
$ lipo MachO文件 –thin 架构 –output 输出文件路径
// 使用lipo -create 合并多种架构
$ lipo -create MachO1 MachO2 -output 输出文件路径
复制代码
先上一张官网图:
Header 包含该二进制文件的通常信息 字节顺序、架构类型、加载指令的数量等。 使得能够快速确认一些信息,好比当前文件用于32位仍是64位,对应的处理器是什么、文件类型是什么
本文从两个视角分析Header,分别是「用MachOView可视化后直观的查看」和「系统源码解析」
Load commands是一张包含不少内容的表。 内容包括区域的位置、符号表、动态符号表等。
名称 | 含义 |
---|---|
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_DYLINKER
和LC_LOAD_DYLIB
LC_LOAD_DYLINKER 该字段标明咱们的MachO是被谁加载进去的。
能够理解为LC_LOAD_DYLINKER指向的地址是微信APP加载小程序的引擎,而咱们的MachO是小程序。在上图中能够看到咱们的Demo1的LC_LOAD_DYLINKER指向的地址就是dyld
。dyld
确实是用来加载咱们app的,在下面一节将会对dyld
的源码进行分析,讲述dyld
是如何对MachO进行加载的。
LC_LOAD_DYLIB 该字段标记了全部动态库的地址,只有在LC_LOAD_DYLIB中有标记,咱们MachO外部的动态库(如:Framework)才能被dyld
正确的引用,不然dyld
不会主动加载,这也是上篇文章,代码注入的关键所在!
Data 一般是对象文件中最大的部分,包含Segement的具体数据,如静态C字符串,带参数/不带参数的OC方法,带参数/不带参数的C函数。
在Demo1中编写一下代码
查看MachO中对应的Data段:cstring
,methname
,以下两图:
能够看到,全局静态C字符(myCString
),方法里面的字符串(myCFuncAString:%d
,myCFuncString
,%s
,myOCFuncAString:%s
,myOCFuncString:%s
)都被保存在data段的cstring
里了,哪怕是%d
,%s
等等这样的参数类型字符串也被保存在内。但全部一样的字符串只会被保存一次。
一样全部的OC方法都被保存在methname
里了。
这里有个问题: 在这两个表中并无看到全局的静态OC字符串(
myOCString
)和C函数(myCFuncA(int a)
,myCFunc()
)这里为何没有?他们应该会被以是形式保存在哪里?
上面用cstring
和methname
距离了data段的做用,一样的全部类名,协议名等也是以一样形式存储在这。
上面已经对MachO有了一个大概的了解,接下来本文就对dyld
这么一个重要的东西进行一个初探。
首先思考,在main函数中挂断点能不能查看到APP启动对应的堆栈?
这部分其实靠想,靠猜想很难有答案,咱们直接用XCode直接尝试:
一样的,直接XCode调试:
在这能够发现更多的信息,好比在堆栈底部的汇编(这里用的是手机调试,因此是arm64架构)能够很明显的发现,是调用了用dyld中的dyldbootstrap文件中的start方法。
快马加鞭,打开dyld源码,找到对应的dyldbootstrap文件中的start函数。
点击这里下载dyld源码
//
// 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的)并返回。一样的,咱们不能只蹭一蹭,要进去干!
这个函数厉害了,以下图,足足快500行了!
咱们抓住其中的关键代码,足步分析在main函数以前dyld到底帮咱们作了哪一些事情。
从main函数的初始,到函数getHostInfo()
以前都是在配置一些环境变量,已经一些线程相关的,涉及内容太过底层,这就不一一分析了(实际上是能力不及😆)
if
判断,其实里面都是对应的环境变量,这些都是能够在XCode进行相关的配置,进行对应的操做(如Log相关信息)。
在iOS系统中,每一个程序依赖的动态库都须要经过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而若是在每一个程序运行的时候都重复的去加载一次,势必形成运行缓慢,为了优化启动速度和提升程序性能,共享缓存机制就应运而生。全部默认的动态连接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不一样的架构保存分别保存着。其中包括UIKit,Foundation等基础库。
加载主程序其实就是对MachO文件中LoadCommons段的一些列加载! 咱们继续对代码的跟进,以下6张图:
补充:实例化完以后调用addImage(image),将实例化出来的镜像加入全部的镜像列表sAllImages,主程序永远是sAllImages的第一个对象!
加载动态连接库,如XCode的ViewDebug、MainThreadChecker,咱们以后代码注入的库也是经过这种形式添加的!
link函数里面其实就是对以前的imges(不是图片,这是镜像)进行一些内核操做,这部分Apple没有开源出来,只能看到些许源码,有兴许的同窗能够自行查阅:
不管是从以前断点load方法仍是咱们如今一步步对源码的根据,都能了解到,dyld
的initializeMainExecutable
就是就加载load的入口:
而且最后都能接到一个结论:
由dyld
的notifySingle
函数通过一系列的跳转,最终会跳转到objc
源码中的call_load_methods
函数!!
那么这中间的的过程究竟是怎么样的呢?看下方的gif:
最后找到函数_dyld_objc_notify_register
,就在全局都找不到一个调用的地方了,其实这个函数自己就不是给dyld
调用的,而是提供给外部调用的。怎么找到是谁调用了_dyld_objc_notify_register
呢?
继续打开以前的Demo1,在工程中加上_dyld_objc_notify_register
的符号断点看看。
运行工程,断住以后再次查看函数调用栈:
objc_init
调用了我们的
_dyld_objc_notify_register
函数。
一样打开objc
的源码(点击下载objc源码 ) 快速定位_dyld_objc_notify_register
的调用位置。如图:
这样dyld是如何加载我们的load方法就被找到了。 期间若是有细心的同窗可能看到了在notifySingle
后面紧跟着doInitialization
这样一个函数,这是一个系统特定的C++构造函数的调用方法。
这种C++构造函数有特定的写法,以下:
__attribute__((constructor)) void CPFunc(){
printf("C++Func1");
}
复制代码
有兴趣的同窗能够尝试实现一次,在MachO文件中找到对应的方法! 固然,这在Demo1也是有的。
当上面的load和C++方法加载完成以后就会回到dyld的main方法里面,寻找APP的main函数并调用。
最终dyld的main函数中的主要流程就已经走完了,固然这7个步骤是一条主线,期间还会有不少其余的步骤,过程很是繁琐,这就不一一举例了。你们能够经过阅读dyld的源码一览无余。
本文讲述了MachO的概述,文件结构,在从其中Load Commons中的LC_LOAD_DYLINKER引出dyld
,接下根据dyld
源码分析了APP的启动流程。分别是:
一、配置环境变量
二、加载共享缓存库
三、实例化主程序
四、加载动态连接库
五、连接主程序
六、加载Load和特定的C++的构造函数方法
七、寻找APP的main函数并调用
另外dyld
中LC_LOAD_DYLIB的(加载动态连接库)存在,为咱们逆向注入代码提供了无限可能。
MachO中其实还有一些符号表,为系统提供查询对应的方法名称提供了路径,这些在下一张文章中将会更加详细的讲到。
一、Dynamic Linking of Imported Functions in Mach-O 二、《iOS应用逆向工程》沙梓社,吴航 著 ,机械工业出版社