在上篇文章代碼注入,竊取微信密碼中咱們已經簡單的提到了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指令查看文件的具體格式
目前已知的架構分為armv7,armv7s,arm64,i386,x86_64等等,MachO中其實也是這些架構的集合??梢噪S意建立一個空工程:Dome1(空工程就不給Demo了)
查看Build出的Dome1.ipa中的MachO
將最低版本設置為iOS 12,用release打包出的Dome1.ipa中的MachO
將最低版本設置為iOS 8,用release打包出的Dome1.ipa中的MachO
從上面三張圖就可以確定MachO可以是多架構的二進制文件,稱之為「通用二進制文件」
通用二進制文件是蘋果公司提出的一種程序代碼。能同時適用多種架構的二進制文件
a. 同一個程序包中同時為多種架構提供最理想的性能。
b. 因為需要儲存多種代碼,通用二進制應用程序通常比單一平臺二進制的程序要大。
c. 但是由于兩種架構有共通的非執行資源,所以并不會達到單一版本的兩倍之多。
d. 而且由于執行中只調用一部分代碼,運行起來也不需要額外的內存。
注:其實除了更改最低版本號可以改變MachO的架構,在XCode的中也可以主動設置
3、拆分、重組MachO
// 使用lipo -info 可以查看MachO文件包含的架構
$ lipo -info MachO文件
// 使用lipo –thin 拆分某種架構
$ lipo MachO文件 –thin 架構 –output 輸出文件路徑
// 使用lipo -create 合并多種架構
$ lipo -create MachO1 MachO2 -output 輸出文件路徑
二、MachO的文件結構
先上一張官網圖:
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是一張包含很多內容的表。
內容包括區域的位置、符號表、動態符號表等。
上圖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_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
不會主動加載,這也是上篇文章,代碼注入的關鍵所在!
3、Data
Data 通常是對象文件中最大的部分,包含Segement的具體數據,如靜態C字符串,帶參數/不帶參數的OC方法,帶參數/不帶參數的C函數。
在Demo1中編寫一下代碼
- 靜態C字符串
- 靜態OC字符串
- 帶參數的OC方法
- 不帶參數的OC方法
- 帶參數的C函數
- 不帶參數的C函數
如圖:
代碼.png
查看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
這么一個重要的東西進行一個初探。
三、從DYLD源碼的角度看APP啟動流程
1、在main函數中斷點查看
首先思考,在main函數中掛斷點能不能查看到APP啟動對應的堆棧?
這部分其實靠想,靠猜測很難有答案,我們直接用XCode直接嘗試:
可以看到在main函數斷點并不能看到啟動的對應堆棧,說明main函數也是被別人調用的,而不是處于app啟動的堆棧中。
既然main查不到啟動堆棧,那么比app更早執行的load方式是否可以找得到呢?
2、在load方法中斷點查看
同樣的,直接XCode調試:
在這可以發現更多的信息,比如在堆棧底部的匯編(這里用的是手機調試,所以是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行了!
我們抓住其中的關鍵代碼,足步分析在main函數之前dyld到底幫我們做了哪一些事情。
1、配置環境變量
從main函數的初始,到函數getHostInfo()
之前都是在配置一些環境變量,已經一些線程相關的,涉及內容太過底層,這就不一一分析了(其實是能力不及??)
在這一步中有很多
if
判斷,其實里面都是對應的環境變量,這些都是可以在XCode進行相關的配置,進行對應的操作(如Log相關信息)。
2、加載共享緩存庫
在iOS系統中,每個程序依賴的動態庫都需要通過dyld(位于/usr/lib/dyld)一個一個加載到內存,然而如果在每個程序運行的時候都重復的去加載一次,勢必造成運行緩慢,為了優化啟動速度和提高程序性能,共享緩存機制就應運而生。所有默認的動態鏈接庫被合并成一個大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構保存分別保存著。其中包括UIKit,Foundation等基礎庫。
在源碼中可以看到在我們iOS系統中,共享緩存庫被明確一定會被加載。
因為這種機制的存在,使得iOS在的對這些基礎庫的加載的時候時間和內存都得到節約!
但是有時因為共享緩存庫的機制的存在使得iOS在共享緩存庫里面的C函數,也就是系統C函數變的不是那么靜態,有了些許OC運行時的特性!
這部分內容將會在下一篇文章著重講解!從不一樣的角度看Runtime!
3、實例化主程序
加載主程序其實就是對MachO文件中LoadCommons段的一些列加載!
我們繼續對代碼的跟進,如下6張圖:
補充:實例化完之后調用addImage(image),將實例化出來的鏡像加入所有的鏡像列表sAllImages,主程序永遠是sAllImages的第一個對象!
從源代碼可以看出,加載主程序這一步其實很簡單,就是將MachO文件中的部分信息一步一步的放入內存。
其中從最后一張圖可以了解到:
- 最大的segment數量為256個!
- 最大的動態庫(包括系統的個自定義的)個數為4096個!
4、加載動態鏈接庫
加載動態鏈接庫,如XCode的ViewDebug、MainThreadChecker,我們之后代碼注入的庫也是通過這種形式添加的!
5、鏈接主程序
link函數里面其實就是對之前的imges(不是圖片,這是鏡像)進行一些內核操作,這部分Apple沒有開源出來,只能看到些許源碼,有興許的同學可以自行查閱:
6、加載Load和特定的C++的構造函數方法
無論是從之前斷點load方法還是我們現在一步步對源碼的根據,都能了解到,dyld
的initializeMainExecutable
就是就加載load的入口:
并且最后都能接到一個結論:
由dyld
的notifySingle
函數經過一系列的跳轉,最終會跳轉到objc
源碼中的call_load_methods
函數??!
那么這中間的的過程到底是怎么樣的呢?看下方的gif:
[圖片上傳失敗...(image-1cbe4e-1554211741014)]
簡書坑爹總是吞我圖:(如果上面gif又顯示不出來就點這直接看流程圖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也是有的。
7、尋找APP的main函數并調用
當上面的load和C++方法加載完成之后就會回到dyld的main方法里面,尋找APP的main函數并調用。
最終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應用逆向工程》沙梓社,吳航 著 ,機械工業出版社