iOS逆向(5)-不知MachO怎敢说自己懂DYLD

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

推荐阅读:iOS开发——BAT面试题合集(持续更新中)

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

老规矩,片头先上福利:点击下载demo
这篇文章会用到的工具有:

废话不多说,本篇文章将会从以下几点细说到底什么是MachO!

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

一、什么是MachO

Mach-O其实是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式, 类似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)

1、常见的MachO文件

a、目标文件:.o
b、库文件:.a .dylib Framework
c、可执行文件:dyld .dsym

2、如何查看文件格式

我们可以通过file指令查看文件的具体格式

image

目前已知的架构分为armv7,armv7s,arm64,i386,x86_64等等,MachO中其实也是这些架构的集合。可以随意建立一个空工程:Dome1(空工程就不给Demo了)

查看Build出的Dome1.ipa中的MachO

image

将最低版本设置为iOS 12,用release打包出的Dome1.ipa中的MachO

image

将最低版本设置为iOS 8,用release打包出的Dome1.ipa中的MachO

image

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

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

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

image

3、拆分、重组MachO

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

image

二、MachO的文件结构

先上一张官网图:

image

MachO分为三部分结构:Header、Load Commons、Data

1、Header

Header 包含该二进制文件的一般信息
字节顺序、架构类型、加载指令的数量等。
使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么

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

  • 用MachOView可视化后直观的查看
    上篇文章已经讲过使用MacOView可以直接查看一个MachO文件,如下图

    image
  • 系统源码解析
    在MachO的源码文件中同样有对应的字段。如下图:

    image

2、Load Commons

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

image

上图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是小程序。在上图中可以看到我们的Demo1的LC_LOAD_DYLINKER指向的地址就是dylddyld确实是用来加载我们app的,在下面一节将会对dyld的源码进行分析,讲述dyld是如何对MachO进行加载的。

  • LC_LOAD_DYLIB
    该字段标记了所有动态库的地址,只有在LC_LOAD_DYLIB中有标记,我们MachO外部的动态库(如:Framework)才能被dyld正确的引用,否则dyld不会主动加载,这也是上篇文章,代码注入的关键所在!

3、Data

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

在Demo1中编写一下代码

  • 静态C字符串

  • 静态OC字符串

  • 带参数的OC方法

  • 不带参数的OC方法

  • 带参数的C函数

  • 不带参数的C函数
    如图:

    image

查看MachO中对应的Data段:cstring,methname,如下两图:

image
image

可以看到,全局静态C字符(myCString),方法里面的字符串(myCFuncAString:%d,myCFuncString,%s,myOCFuncAString:%s,myOCFuncString:%s)都被保存在data段的cstring里了,哪怕是%d,%s等等这样的参数类型字符串也被保存在内。但所有同样的字符串只会被保存一次。
同样所有的OC方法都被保存在methname里了。

这里有个问题:
在这两个表中并没有看到全局的静态OC字符串(myOCString)和C函数(myCFuncA(int a),myCFunc())这里为什么没有?他们应该会被以是形式保存在哪里?

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

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

三、从DYLD源码的角度看APP启动流程

1、在main函数中断点查看

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

image

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

2、在load方法中断点查看

同样的,直接XCode调试:

image

在这可以发现更多的信息,比如在堆栈底部的汇编(这里用的是手机调试,所以是arm64架构)可以很明显的发现,是调用了用dyld中的dyldbootstrap文件中的start方法。
马不停蹄,打开dyld源码,找到对应的dyldbootstrap文件中的start函数。
点击这里下载dyld源码

3、在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的)并返回。同样的,我们不能只蹭一蹭,要进去干!

4、在dlyd中查看main函数

这个函数厉害了,如下图,足足快500行了!

image

我们抓住其中的关键代码,足步分析在main函数之前dyld到底帮我们做了哪一些事情。

1、配置环境变量

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

image

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

2、加载共享缓存库

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

image
image

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

3、实例化主程序

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

image
image

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

image
image
image
image

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

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

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

image
5、链接主程序
image

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

image
6、加载Load和特定的C++的构造函数方法

无论是从之前断点load方法还是我们现在一步步对源码的根据,都能了解到,dyldinitializeMainExecutable就是就加载load的入口:

image
image

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

那么这中间的的过程到底是怎么样的呢?看下方的gif:
[图片上传失败...(image-e9e628-1552838495670)]

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

image

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

image

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

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

image
image

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

image
image
image

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

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

有兴趣的同学可以尝试实现一次,在MachO文件中找到对应的方法!
当然,这在Demo1也是有的。

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

当上面的load和C++方法加载完成之后就会回到dyld的main方法里面,寻找APP的main函数并调用。

image

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

四、总结

本文讲述了MachO的概述,文件结构,在从其中Load Commons中的LC_LOAD_DYLINKER引出dyld,接下根据dyld源码分析了APP的启动流程。分别是:
1、配置环境变量
2、加载共享缓存库
3、实例化主程序
4、加载动态链接库
5、链接主程序
6、加载Load和特定的C++的构造函数方法
7、寻找APP的main函数并调用
另外dyld中LC_LOAD_DYLIB的(加载动态链接库)存在,为我们逆向注入代码提供了无限可能。
MachO中其实还有一些符号表,为系统提供查询对应的方法名称提供了路径,这些在下一张文章中将会更加详细的讲到。

五、参考

1、Dynamic Linking of Imported Functions in Mach-O
2、《iOS应用逆向工程》沙梓社,吴航 著 ,机械工业出版社

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:638302184,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

群内提供数据结构与算法、底层进阶、swift、逆向、整合面试题等免费资料
附上一份收集的各大厂面试题(附答案) ! 群文件直接获取
各大厂面试题

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容