五 MachO文件解析

nx_001.jpeg

上一篇文章中,咱們已經(jīng)簡(jiǎn)單的提到了MachO,在用Framework做代碼注入的時(shí)候,必須先向MachO的Load Commons中插入該Framework的的相對(duì)路徑,讓我們的iPhone在執(zhí)行MachO的時(shí)候能夠識(shí)別并加載Framework!

窺一斑而知全豹,從這些許內(nèi)容其實(shí)已經(jīng)可以了解到MachO在我們APP中的地位是多么的重要。同樣,在咱們逆向的實(shí)踐中,MachO也是一道繞不過(guò)去門檻!

接下來(lái)本文會(huì)從以下幾點(diǎn)進(jìn)行闡述:

  • MachO文件
  • MachO文件結(jié)構(gòu)
  • 從DYLD源碼的角度看APP啟動(dòng)流程 (重點(diǎn)?。?!)

1.什么是MachO文件

Mach-O其實(shí)是Mach Object文件格式的縮寫,是mac以及iOS上可執(zhí)行文件的格式, 類似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)。它是一種用于可執(zhí)行文件、目標(biāo)代碼、動(dòng)態(tài)庫(kù)的文件格式。作為a.out格式的替代,Mach-O提供了更強(qiáng)的擴(kuò)展性。

1.1、常見(jiàn)的MachO文件

  1. 目標(biāo)文件.o
  2. 庫(kù)文件(.a、.dylib、Framework、可執(zhí)行文件、dyld、*.dsym)文件。

1.2、如何查看文件格式

我們可以通過(guò)file指令查看文件的具體格式。
五 file命令1.png
file 文件路徑
file WeChat // 查看微信的可執(zhí)行文件類型
file GPUImage.framework/GPUImage //GPUImage的文件類型

目前已知的架構(gòu)分為armv7,armv7s,arm64,i386,x86_64等等,MachO中其實(shí)也是這些架構(gòu)的集合。

可以隨意建立一個(gè)空工程:Dome1

選擇Debug,然后Build一下,在Products->顯示包內(nèi)容中查看可執(zhí)行文件的類型。這個(gè)是模擬Debug模式下模擬器的可執(zhí)行文件的類型。
五 MachO1.png

g)

編輯Edit scheme,將Run下的Debug修改成Relase,此時(shí)目標(biāo)文件還是選擇iPhone模擬器。會(huì)看到下面的可執(zhí)行文件類型。


五 Edit scheme1.png
五 MachO2.png

將目標(biāo)文件設(shè)置設(shè)置成手機(jī)真機(jī)運(yùn)行,Edit scheme還是上一步的保持不變,這時(shí)會(huì)出現(xiàn)以下的可執(zhí)行文件類型。


五 Edit scheme2.png
五 MachO3.png

從上面三張圖就可以確定MachO可以是多架構(gòu)的二進(jìn)制文件,稱之為「通用二進(jìn)制文件」

通用二進(jìn)制文件是蘋果公司提出的一種程序代碼。能同時(shí)適用多種架構(gòu)的二進(jìn)制文件
a. 同一個(gè)程序包中同時(shí)為多種架構(gòu)提供最理想的性能。
b. 因?yàn)樾枰獌?chǔ)存多種代碼,通用二進(jìn)制應(yīng)用程序通常比單一平臺(tái)二進(jìn)制的程序要大。
c. 但是由于兩種架構(gòu)有共通的非執(zhí)行資源,所以并不會(huì)達(dá)到單一版本的兩倍之多。
d. 而且由于執(zhí)行中只調(diào)用一部分代碼,運(yùn)行起來(lái)也不需要額外的內(nèi)存。

注:其實(shí)除了更改最低版本號(hào)可以改變MachO的架構(gòu),在XCode的中也可以主動(dòng)設(shè)置。

注意:這里可以查看下最低版本設(shè)置為iOS 8,用release打包出的*.ipa中的MachO

五 MachO4.png

1.2、拆分、重組MachO

// 使用lipo -info 可以查看MachO文件包含的架構(gòu)
lipo -info MachO文件

// 使用lipo –thin 拆分某種架構(gòu)
lipo MachO文件 –thin 架構(gòu) –output 輸出文件路徑

// 使用lipo -create  合并多種架構(gòu)
lipo -create MachO1  MachO2  -output 輸出文件路徑

2、MachO文件結(jié)構(gòu)

五 文件結(jié)構(gòu)1.png

2.1、Mach-O 的組成結(jié)構(gòu)如圖所示包括了,Header、load commands、Data部分。

Header包含該二進(jìn)制文件的一般信息。
1.字節(jié)順序、架構(gòu)類型、加載指令的數(shù)量等。
2.使得可以快速確認(rèn)一些信息,比如當(dāng)前文件用于32位還是64位,對(duì)應(yīng)的處理器是什么、文件類型是什么。

Load commands 一張包含很多內(nèi)容的表。
1.內(nèi)容包括區(qū)域的位置、符號(hào)表、動(dòng)態(tài)符號(hào)表等。

Data 通常是對(duì)象文件中最大的部分
1.包含Segement的具體數(shù)據(jù)。

本文從兩個(gè)視角分析Header,分別是「用MachOView可視化后直觀的查看」和「系統(tǒng)源碼解析」

  • 用MachOView可視化后直觀的查看。如下圖:
    五 文件結(jié)構(gòu)2.png

    五 文件結(jié)構(gòu)3.png
  • 系統(tǒng)源碼解析
    五 MachO header.png

2.2、Load Commons

Load commands是一張包含很多內(nèi)容的表。
內(nèi)容包括區(qū)域的位置、符號(hào)表、動(dòng)態(tài)符號(hào)表等。

五 MachO load commands1.png

上圖Load Commons中的大部分字段在下表中可以找到相關(guān)的含義。

名稱 含義
LC_SEGMENT_64 將文件中(32位或64位)的段映射到進(jìn)程地址空間中
LC_DYLD_INFO_ONLY 動(dòng)態(tài)鏈接相關(guān)信息
LC_SYMTAB 符號(hào)地址
LC_DYSYMTAB 動(dòng)態(tài)符號(hào)表地址
LC_LOAD_DYLINKER 使用誰(shuí)加載,我們使用dyld
LC_UUID 文件的UUID
LC_VERSION_MIN_MACOSX 支持最低的操作系統(tǒng)版本
LC_SOURCE_VERSION 源代碼版本
LC_MAIN 設(shè)置程序主線程的入口地址和棧大小
LC_LOAD_DYLIB 依賴庫(kù)的路徑,包含三方庫(kù)
LC_FUNCTION_STARTS 函數(shù)起始地址表
LC_CODE_SIGNATURE 代碼簽名

其中LC_LOAD_DYLINKERLC_LOAD_DYLIB

  • LC_LOAD_DYLINKER 該字段標(biāo)明我們的MachO是被誰(shuí)加載進(jìn)去的??梢岳斫鉃長(zhǎng)C_LOAD_DYLINKER指向的地址是微信APP加載小程序的引擎,而我們的MachO是小程序。在上圖中可以看到我們的MachODemo1的LC_LOAD_DYLINKER指向的地址就是dyld。dyld確實(shí)是用來(lái)加載我們app的,在下面一節(jié)將會(huì)對(duì)dyld的源碼進(jìn)行分析,講述dyld是如何對(duì)MachO進(jìn)行加載的。
  • LC_LOAD_DYLIB 該字段標(biāo)記了所有動(dòng)態(tài)庫(kù)的地址,只有在LC_LOAD_DYLIB中有標(biāo)記,我們MachO外部的動(dòng)態(tài)庫(kù)(如:Framework)才能被dyld正確的引用,否則dyld不會(huì)主動(dòng)加載,這也是上篇文章,代碼注入的關(guān)鍵所在!

2.3、Data

Data 通常是對(duì)象文件中最大的部分,包含Segement的具體數(shù)據(jù),如靜態(tài)C字符串,帶參數(shù)/不帶參數(shù)的OC方法,帶參數(shù)/不帶參數(shù)的C函數(shù)。

在DemoMachO1中編寫一下代碼:

  • 靜態(tài)C字符串
  • 靜態(tài)OC字符串
  • 帶參數(shù)的OC方法
  • 不帶參數(shù)的OC方法
  • 帶參數(shù)的C函數(shù)
  • 不帶參數(shù)的C函數(shù)

在我們的項(xiàng)目中添加以下代碼(在ViewController)中

#import "ViewController.h"

/*靜態(tài)C字符串*/
static const char *cString = "c_string";

/*靜態(tài)OC字符串*/
static const NSString *ocString = @"oc_string";

@interface ViewController ()
@end

/*C方法(無(wú)參數(shù))*/
void CFunc(){
    printf("c_func");
}

/*C方法(有參數(shù))*/
void CFunc1(int a){
    printf("c_func:%d",a);
}
@implementation ViewController
/*OC方法(無(wú)參數(shù))*/
-(void)ocFunc{
    NSLog(@"%s",__func__);
    NSLog(@"ocString:%@",ocString);
}
/*OC方法(有參數(shù))*/
-(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

可以看到,全局靜態(tài)C字符,方法里面的字符串都被保存在data段的cstring里了。但所有同樣的字符串只會(huì)被保存一次。
同樣所有的OC方法都被保存在methname里了。

上面用cstring和methname距離了data段的作用,同樣的所有類名,協(xié)議名等也是以同樣形式存儲(chǔ)在這。

上面已經(jīng)對(duì)MachO有了一個(gè)大概的了解,接下來(lái)本文就對(duì)dyld這么一個(gè)重要的東西進(jìn)行一個(gè)初探。

3.DYLD(從源碼的角度看APP啟動(dòng)流程)(重點(diǎn))

dyld (the dynamic link editor)是蘋果的動(dòng)態(tài)鏈接器,是蘋果操作系統(tǒng)一個(gè)重要組成部分,在系統(tǒng)內(nèi)核做好程序準(zhǔn)備工作之后,交由dyld負(fù)責(zé)余下的工作。

3.1、在main函數(shù)中斷點(diǎn)查看

首先思考,在main函數(shù)中掛斷點(diǎn)能不能查看到APP啟動(dòng)對(duì)應(yīng)的堆棧?
這部分其實(shí)靠想,靠猜測(cè)很難有答案,我們直接用XCode直接嘗試:


五 main斷點(diǎn)1.png

可以看到在main函數(shù)斷點(diǎn)并不能看到啟動(dòng)的對(duì)應(yīng)堆棧,說(shuō)明main函數(shù)也是被別人調(diào)用的,而不是處于app啟動(dòng)的堆棧中。
既然main查不到啟動(dòng)堆棧,那么比app更早執(zhí)行的load方式是否可以找得到呢?

3.2、在load方法中斷點(diǎn)查看

1.首先在空工程的ViewController文件中添加以下代碼。
2.然后在load函數(shù)前設(shè)置斷點(diǎn)

#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斷點(diǎn).png
五 load start.png

在這可以發(fā)現(xiàn)更多的信息,可以很明顯的發(fā)現(xiàn),是調(diào)用了用dyld中的dyldbootstrap文件中的start方法。
馬不停蹄,打開(kāi)dyld源碼,找到對(duì)應(yīng)的dyldbootstrap文件中的start函數(shù)。
點(diǎn)擊這里下載dyld源碼

3.3、在dyldbootstrap中查看start函數(shù)

1.打開(kāi) dyld 項(xiàng)目,搜索 dyldbootstrap 文件。
2.在dyldbootstrap文件中搜索start函數(shù)。

//
//  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技術(shù),地址偏移,是MachO文件在內(nèi)存中的地址重定向
    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
    // 棧溢出保護(hù)
    __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
    // 正在的啟動(dòng)函數(shù),在dyld中的_main函數(shù)中
    uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

start函數(shù)的源碼可得知道:dlyd會(huì)內(nèi)存中找到一塊地址給MachO使用,也就是ASLR,內(nèi)存偏移。

最后start函數(shù)執(zhí)行了一個(gè)main函數(shù)(這個(gè)可以不是我們app中的main函數(shù),而是dyld的)并返回。

3.4、在dlyd中查看main函數(shù)

由于這一段代碼較多,我們也沒(méi)有必要把每一句代碼都弄清楚,這里我們抓住其中的關(guān)鍵代碼,足步分析在main函數(shù)之前dyld到底幫我們做了哪一些事情。

1.配置環(huán)境變量
main函數(shù)的初始,到函數(shù)getHostInfo()之前都是在配置一些環(huán)境變量,已經(jīng)一些線程相關(guān)的,涉及內(nèi)容太過(guò)底層,這就不一一分析了(其實(shí)是能力不及??)。

五 配置環(huán)境.png

在這一步中有很多if判斷,其實(shí)里面都是對(duì)應(yīng)的環(huán)境變量,這些都是可以在XCode進(jìn)行相關(guān)的配置,進(jìn)行對(duì)應(yīng)的操作(如Log相關(guān)信息)。

2、加載共享緩存庫(kù)

在iOS系統(tǒng)中,每個(gè)程序依賴的動(dòng)態(tài)庫(kù)都需要通過(guò)dyld(位于/usr/lib/dyld)一個(gè)一個(gè)加載到內(nèi)存,然而如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次,勢(shì)必造成運(yùn)行緩慢,為了優(yōu)化啟動(dòng)速度和提高程序性能,共享緩存機(jī)制就應(yīng)運(yùn)而生。所有默認(rèn)的動(dòng)態(tài)鏈接庫(kù)被合并成一個(gè)大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構(gòu)保存分別保存著。其中包括UIKit,F(xiàn)oundation等基礎(chǔ)庫(kù)。
五、加載緩存庫(kù).png
五、必須加載的緩存庫(kù).png

在源碼中可以看到在我們iOS系統(tǒng)中,共享緩存庫(kù)被明確一定會(huì)被加載。
因?yàn)檫@種機(jī)制的存在,使得iOS在的對(duì)這些基礎(chǔ)庫(kù)的加載的時(shí)候時(shí)間和內(nèi)存都得到節(jié)約!
但是有時(shí)因?yàn)楣蚕砭彺鎺?kù)的機(jī)制的存在使得iOS在共享緩存庫(kù)里面的C函數(shù),也就是系統(tǒng)C函數(shù)變的不是那么靜態(tài),有了些許OC運(yùn)行時(shí)的特性!
這部分內(nèi)容將會(huì)在下一篇文章著重講解!從不一樣的角度看Runtime!

3、實(shí)例化主程序
加載主程序其實(shí)就是對(duì)MachO文件中LoadCommons段的一些列加載!
我們繼續(xù)對(duì)代碼的跟進(jìn),如下6張圖:
圖片1

圖片2

補(bǔ)充:實(shí)例化完之后調(diào)用addImage(image),將實(shí)例化出來(lái)的鏡像加入所有的鏡像列表sAllImages,主程序永遠(yuǎn)是sAllImages的第一個(gè)對(duì)象!

圖片3

圖片4

圖片5

圖片6

從源代碼可以看出,加載主程序這一步其實(shí)很簡(jiǎn)單,就是將MachO文件中的部分信息一步一步的放入內(nèi)存。
其中從最后一張圖可以了解到:

  • 最大的segment數(shù)量為256個(gè)!
  • 最大的動(dòng)態(tài)庫(kù)(包括系統(tǒng)的個(gè)自定義的)個(gè)數(shù)為4096個(gè)!

4、加載動(dòng)態(tài)鏈接庫(kù)
加載動(dòng)態(tài)鏈接庫(kù),如Xcode的ViewDebug、MainThreadChecker,我們之后代碼注入的庫(kù)也是通過(guò)這種形式添加的!

插入動(dòng)態(tài)鏈接庫(kù)

5、鏈接主程序
鏈接主程序

link函數(shù)里面其實(shí)就是對(duì)之前的imges(不是圖片,這是鏡像)進(jìn)行一些內(nèi)核操作,這部分Apple沒(méi)有開(kāi)源出來(lái),只能看到些許源碼,有興許的同學(xué)可以自行查閱:
link

6、加載Load和特定的C++的構(gòu)造函數(shù)方法
無(wú)論是從之前斷點(diǎn)load方法還是我們現(xiàn)在一步步對(duì)源碼的根據(jù),都能了解到,dyldinitializeMainExecutable就是就加載load的入口:
initializeMainExecutable1

initializeMainExecutable2

并且最后都能接到一個(gè)結(jié)論:
dyldnotifySingle函數(shù)經(jīng)過(guò)一系列的跳轉(zhuǎn),最終會(huì)跳轉(zhuǎn)到objc源碼中的call_load_methods函數(shù)??!

最后找到函數(shù)_dyld_objc_notify_register,就在全局都找不到一個(gè)調(diào)用的地方了,其實(shí)這個(gè)函數(shù)本身就不是給dyld調(diào)用的,而是提供給外部調(diào)用的。怎么找到是誰(shuí)調(diào)用了_dyld_objc_notify_register呢?
繼續(xù)打開(kāi)之前的DemoMachO1,在工程中加上_dyld_objc_notify_register的符號(hào)斷點(diǎn)看看。

五 符號(hào)斷點(diǎn).png

運(yùn)行工程,斷住之后再次查看函數(shù)調(diào)用棧:

五 _dyld_objc_notify_register1.png

這就可以很清晰的看到,原來(lái)是objc_init調(diào)用了咱們的_dyld_objc_notify_register函數(shù)。

同樣打開(kāi)objc的源碼(點(diǎn)擊下載objc源碼 )
快速定位_dyld_objc_notify_register的調(diào)用位置。如圖:
圖片地址

圖片地址2

這樣dyld是如何加載咱們的load方法就被找到了。
期間如果有細(xì)心的同學(xué)可能看到了在notifySingle后面緊跟著doInitialization這樣一個(gè)函數(shù),這是一個(gè)系統(tǒng)特定的C++構(gòu)造函數(shù)的調(diào)用方法。
doInitialization

doModInitFunctions

ImageLoadMachO

這種C++構(gòu)造函數(shù)有特定的寫法,如下:

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

7、尋找APP的main函數(shù)并調(diào)用
尋找APP的main函數(shù)并調(diào)用

最終dyld的main函數(shù)中的主要流程就已經(jīng)走完了,當(dāng)然這7個(gè)步驟是一條主線,期間還會(huì)有很多其他的步驟,過(guò)程非常繁瑣,這就不一一舉例了。大家可以通過(guò)閱讀dyld的源碼盡收眼底。

4.代碼資料

代碼——暫時(shí)未上傳

本文講述了MachO的概述,文件結(jié)構(gòu),在從其中Load Commons中的LC_LOAD_DYLINKER引出dyld,接下根據(jù)dyld源碼分析了APP的啟動(dòng)流程。分別是:
1、配置環(huán)境變量
2、加載共享緩存庫(kù)
3、實(shí)例化主程序
4、加載動(dòng)態(tài)鏈接庫(kù)
5、鏈接主程序
6、加載Load和特定的C++的構(gòu)造函數(shù)方法
7、尋找APP的main函數(shù)并調(diào)用

如圖:


五 dyld流程.png

另外dyld中LC_LOAD_DYLIB的(加載動(dòng)態(tài)鏈接庫(kù))存在,為我們逆向注入代碼提供了無(wú)限可能。
MachO中其實(shí)還有一些符號(hào)表,為系統(tǒng)提供查詢對(duì)應(yīng)的方法名稱提供了路徑。

參考文章:
作者:一縷清風(fēng)揚(yáng)萬(wàn)里
原文地址:http://www.itdecent.cn/p/95896fb96a03

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容