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

一縷清風揚萬里發表於2019-03-17

在上篇文章程式碼注入,竊取微信密碼中我們們已經簡單的提到了MachO,在用Framework做程式碼注入的時候,必須先向MachO的Load Commons中插入該Framework的的相對路徑,讓我們的iPhone在執行MachO的時候能夠識別並載入Framework!

窺一斑而知全豹,從這些許內容其實已經可以瞭解到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指令檢視檔案的具體格式

file指令.png

目前已知的架構分為armv7,armv7s,arm64,i386,x86_64等等,MachO中其實也是這些架構的集合。可以隨意建立一個空工程:Dome1(空工程就不給Demo了)

檢視Build出的Dome1.ipa中的MachO

x86架構MachO.png

將最低版本設定為iOS 12,用release打包出的Dome1.ipa中的MachO

arm64架構MachO.png

將最低版本設定為iOS 8,用release打包出的Dome1.ipa中的MachO

多架構MachO.png

從上面三張圖就可以確定MachO可以是多架構的二進位制檔案,稱之為「通用二進位制檔案」

通用二進位制檔案是蘋果公司提出的一種程式程式碼。能同時適用多種架構的二進位制檔案 a. 同一個程式包中同時為多種架構提供最理想的效能。 b. 因為需要儲存多種程式碼,通用二進位制應用程式通常比單一平臺二進位制的程式要大。 c. 但是由於兩種架構有共通的非執行資源,所以並不會達到單一版本的兩倍之多。 d. 而且由於執行中只呼叫一部分程式碼,執行起來也不需要額外的記憶體。

注:其實除了更改最低版本號可以改變MachO的架構,在XCode的中也可以主動設定

主動修改架構.png

3、拆分、重組MachO

// 使用lipo -info 可以檢視MachO檔案包含的架構
$ lipo -info MachO檔案
// 使用lipo –thin 拆分某種架構
$ lipo MachO檔案 –thin 架構 –output 輸出檔案路徑
// 使用lipo -create  合併多種架構
$ lipo -create MachO1  MachO2  -output 輸出檔案路徑
複製程式碼

拆分,重組MachO.png

二、MachO的檔案結構

先上一張官網圖:

MachO的檔案結構.png
MachO分為三部分結構:Header、Load Commons、Data

1、Header

Header 包含該二進位制檔案的一般資訊 位元組順序、架構型別、載入指令的數量等。 使得可以快速確認一些資訊,比如當前檔案用於32位還是64位,對應的處理器是什麼、檔案型別是什麼

本文從兩個視角分析Header,分別是「用MachOView視覺化後直觀的檢視」和「系統原始碼解析」

  • 用MachOView視覺化後直觀的檢視 上篇文章已經講過使用MacOView可以直接檢視一個MachO檔案,如下圖
    MachO-Header.png
  • 系統原始碼解析 在MachO的原始碼檔案中同樣有對應的欄位。如下圖:
    MachO-Header原始碼.png

2、Load Commons

Load commands是一張包含很多內容的表。 內容包括區域的位置、符號表、動態符號表等。

MachO-LoadCommons.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是小程式。在上圖中可以看到我們的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函式
    如圖:
    程式碼.png

檢視MachO中對應的Data段:cstring,methname,如下兩圖:

MachO-字串.png
MachO-方法名.png

可以看到,全域性靜態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直接嘗試:

main斷點.png

可以看到在main函式斷點並不能看到啟動的對應堆疊,說明main函式也是被別人呼叫的,而不是處於app啟動的堆疊中。
既然main查不到啟動堆疊,那麼比app更早執行的load方式是否可以找得到呢?

2、在load方法中斷點檢視

同樣的,直接XCode除錯:

load斷點.png

在這可以發現更多的資訊,比如在堆疊底部的彙編(這裡用的是手機除錯,所以是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行了!

dyld的main函式.png

我們抓住其中的關鍵程式碼,足步分析在main函式之前dyld到底幫我們做了哪一些事情。

1、配置環境變數

從main函式的初始,到函式getHostInfo()之前都是在配置一些環境變數,已經一些執行緒相關的,涉及內容太過底層,這就不一一分析了(其實是能力不及?)

配置環境變數.png
在這一步中有很多if判斷,其實裡面都是對應的環境變數,這些都是可以在XCode進行相關的配置,進行對應的操作(如Log相關資訊)。

2、載入共享快取庫

在iOS系統中,每個程式依賴的動態庫都需要通過dyld(位於/usr/lib/dyld)一個一個載入到記憶體,然而如果在每個程式執行的時候都重複的去載入一次,勢必造成執行緩慢,為了優化啟動速度和提高程式效能,共享快取機制就應運而生。所有預設的動態連結庫被合併成一個大的快取檔案,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構儲存分別儲存著。其中包括UIKit,Foundation等基礎庫。

載入共享快取庫_1.png
載入共享快取庫_2.png
在原始碼中可以看到在我們iOS系統中,共享快取庫被明確一定會被載入。
因為這種機制的存在,使得iOS在的對這些基礎庫的載入的時候時間和記憶體都得到節約!
但是有時因為共享快取庫的機制的存在使得iOS在共享快取庫裡面的C函式,也就是系統C函式變的不是那麼靜態,有了些許OC執行時的特性!
這部分內容將會在下一篇文章著重講解!從不一樣的角度看Runtime!

3、例項化主程式

載入主程式其實就是對MachO檔案中LoadCommons段的一些列載入! 我們繼續對程式碼的跟進,如下6張圖:

載入主程式_1.png

載入主程式_2.png

補充:例項化完之後呼叫addImage(image),將例項化出來的映象加入所有的映象列表sAllImages,主程式永遠是sAllImages的第一個物件!

載入主程式_3.png

載入主程式_LoadCommons_1.png

載入主程式_LoadCommons_2.png

載入主程式_LoadCommons_3.png
從原始碼可以看出,載入主程式這一步其實很簡單,就是將MachO檔案中的部分資訊一步一步的放入記憶體。
其中從最後一張圖可以瞭解到:

  • 最大的segment數量為256個!
  • 最大的動態庫(包括系統的個自定義的)個數為4096個!
4、載入動態連結庫

載入動態連結庫,如XCode的ViewDebug、MainThreadChecker,我們之後程式碼注入的庫也是通過這種形式新增的!

插入動態連結庫.png

5、連結主程式

連結主程式.png

link函式裡面其實就是對之前的imges(不是圖片,這是映象)進行一些核心操作,這部分Apple沒有開源出來,只能看到些許原始碼,有興許的同學可以自行查閱:

Link.png

6、載入Load和特定的C++的建構函式方法

無論是從之前斷點load方法還是我們現在一步步對原始碼的根據,都能瞭解到,dyldinitializeMainExecutable就是就載入load的入口:

initializeMainExecutable_1.png

initializeMainExecutable_2.png

並且最後都能接到一個結論:
dyldnotifySingle函式經過一系列的跳轉,最終會跳轉到objc原始碼中的call_load_methods函式!!

那麼這中間的的過程到底是怎麼樣的呢?看下方的gif:

函式查詢過程.gif

最後找到函式_dyld_objc_notify_register,就在全域性都找不到一個呼叫的地方了,其實這個函式本身就不是給dyld呼叫的,而是提供給外部呼叫的。怎麼找到是誰呼叫了_dyld_objc_notify_register呢?
繼續開啟之前的Demo1,在工程中加上_dyld_objc_notify_register的符號斷點看看。

符號斷點.png

執行工程,斷住之後再次檢視函式呼叫棧:

符號斷點後的呼叫堆疊.png
這就可以很清晰的看到,原來是objc_init呼叫了我們們的_dyld_objc_notify_register函式。

同樣開啟objc的原始碼(點選下載objc原始碼 ) 快速定位_dyld_objc_notify_register的呼叫位置。如圖:

_objc_init.png

load_images.png

這樣dyld是如何載入我們們的load方法就被找到了。 期間如果有細心的同學可能看到了在notifySingle後面緊跟著doInitialization這樣一個函式,這是一個系統特定的C++建構函式的呼叫方法。

doInitialization_1.png

doModInitFunctions.png

ImageLoaderMachO_2.png

這種C++建構函式有特定的寫法,如下:

__attribute__((constructor)) void CPFunc(){
    printf("C++Func1");
}
複製程式碼

有興趣的同學可以嘗試實現一次,在MachO檔案中找到對應的方法! 當然,這在Demo1也是有的。

7、尋找APP的main函式並呼叫

當上面的load和C++方法載入完成之後就會回到dyld的main方法裡面,尋找APP的main函式並呼叫。

尋找APP的main函式並呼叫.png

最終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應用逆向工程》沙梓社,吳航 著 ,機械工業出版社

相關文章