在上篇文章代碼注入硫痰,竊取微信密碼中咱們已經(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ī)矩秦士,片頭先上福利:點(diǎn)擊下載demo
這篇文章會(huì)用到的工具有:
廢話不多說(shuō)缺厉,本篇文章將會(huì)從以下幾點(diǎn)細(xì)說(shuō)到底什么是MachO!
- 什么是MachO
- MachO的文件結(jié)構(gòu)
- 從DYLD源碼的角度看APP啟動(dòng)流程 (重點(diǎn)N榛隆Q克馈!)
一次洼、什么是MachO
Mach-O其實(shí)是Mach Object文件格式的縮寫关贵,是mac以及iOS上可執(zhí)行文件的格式, 類似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)
1卖毁、常見的MachO文件
a揖曾、目標(biāo)文件:.o
b落萎、庫(kù)文件:.a .dylib Framework
c、可執(zhí)行文件:dyld .dsym
2炭剪、如何查看文件格式
我們可以通過file指令查看文件的具體格式
目前已知的架構(gòu)分為armv7,armv7s,arm64,i386,x86_64等等练链,MachO中其實(shí)也是這些架構(gòu)的集合∨梗可以隨意建立一個(gè)空工程:Dome1(空工程就不給Demo了)
查看Build出的Dome1.ipa中的MachO
將最低版本設(shè)置為iOS 12媒鼓,用release打包出的Dome1.ipa中的MachO
將最低版本設(shè)置為iOS 8,用release打包出的Dome1.ipa中的MachO
從上面三張圖就可以確定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è)置
3动遭、拆分、重組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 輸出文件路徑
二沽损、MachO的文件結(jié)構(gòu)
先上一張官網(wǎng)圖:
MachO分為三部分結(jié)構(gòu):Header、Load Commons循头、Data
1绵估、Header
Header 包含該二進(jìn)制文件的一般信息
字節(jié)順序、架構(gòu)類型卡骂、加載指令的數(shù)量等国裳。
使得可以快速確認(rèn)一些信息,比如當(dāng)前文件用于32位還是64位全跨,對(duì)應(yīng)的處理器是什么缝左、文件類型是什么
本文從兩個(gè)視角分析Header,分別是「用MachOView可視化后直觀的查看」和「系統(tǒng)源碼解析」
-
用MachOView可視化后直觀的查看
上篇文章已經(jīng)講過使用MacOView可以直接查看一個(gè)MachO文件浓若,如下圖
-
系統(tǒng)源碼解析
在MachO的源碼文件中同樣有對(duì)應(yīng)的字段渺杉。如下圖:
2、Load Commons
Load commands是一張包含很多內(nèi)容的表挪钓。
內(nèi)容包括區(qū)域的位置是越、符號(hào)表、動(dòng)態(tài)符號(hào)表等碌上。
上圖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_DYLINKER
和LC_LOAD_DYLIB
LC_LOAD_DYLINKER
該字段標(biāo)明我們的MachO是被誰(shuí)加載進(jìn)去的天梧。
可以理解為L(zhǎng)C_LOAD_DYLINKER指向的地址是微信APP加載小程序的引擎盔性,而我們的MachO是小程序。在上圖中可以看到我們的Demo1的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)鍵所在饭豹!
3、Data
Data 通常是對(duì)象文件中最大的部分务漩,包含Segement的具體數(shù)據(jù)拄衰,如靜態(tài)C字符串,帶參數(shù)/不帶參數(shù)的OC方法饵骨,帶參數(shù)/不帶參數(shù)的C函數(shù)翘悉。
在Demo1中編寫一下代碼
- 靜態(tài)C字符串
- 靜態(tài)OC字符串
- 帶參數(shù)的OC方法
- 不帶參數(shù)的OC方法
- 帶參數(shù)的C函數(shù)
- 不帶參數(shù)的C函數(shù)
如圖:
查看MachO中對(duì)應(yīng)的Data段:cstring
,methname
,如下兩圖:
可以看到,全局靜態(tài)C字符(myCString
)居触,方法里面的字符串(myCFuncAString:%d
,myCFuncString
,%s
,myOCFuncAString:%s
,myOCFuncString:%s
)都被保存在data段的cstring
里了妖混,哪怕是%d
,%s
等等這樣的參數(shù)類型字符串也被保存在內(nèi)。但所有同樣的字符串只會(huì)被保存一次轮洋。
同樣所有的OC方法都被保存在methname
里了制市。
這里有個(gè)問題:
在這兩個(gè)表中并沒有看到全局的靜態(tài)OC字符串(myOCString
)和C函數(shù)(myCFuncA(int a)
,myCFunc()
)這里為什么沒有?他們應(yīng)該會(huì)被以是形式保存在哪里弊予?
上面用cstring
和methname
距離了data段的作用祥楣,同樣的所有類名,協(xié)議名等也是以同樣形式存儲(chǔ)在這汉柒。
上面已經(jīng)對(duì)MachO有了一個(gè)大概的了解误褪,接下來(lái)本文就對(duì)dyld
這么一個(gè)重要的東西進(jìn)行一個(gè)初探。
三碾褂、從DYLD源碼的角度看APP啟動(dòng)流程
1兽间、在main函數(shù)中斷點(diǎn)查看
首先思考,在main函數(shù)中掛斷點(diǎn)能不能查看到APP啟動(dòng)對(duì)應(yīng)的堆棧斋扰?
這部分其實(shí)靠想渡八,靠猜測(cè)很難有答案啃洋,我們直接用XCode直接嘗試:
可以看到在main函數(shù)斷點(diǎn)并不能看到啟動(dòng)的對(duì)應(yīng)堆棧,說(shuō)明main函數(shù)也是被別人調(diào)用的屎鳍,而不是處于app啟動(dòng)的堆棧中宏娄。
既然main查不到啟動(dòng)堆棧,那么比app更早執(zhí)行的load方式是否可以找得到呢逮壁?
2孵坚、在load方法中斷點(diǎn)查看
同樣的,直接XCode調(diào)試:
在這可以發(fā)現(xiàn)更多的信息窥淆,比如在堆棧底部的匯編(這里用的是手機(jī)調(diào)試卖宠,所以是arm64架構(gòu))可以很明顯的發(fā)現(xiàn),是調(diào)用了用dyld中的dyldbootstrap文件中的start方法忧饭。
馬不停蹄扛伍,打開dyld源碼,找到對(duì)應(yīng)的dyldbootstrap文件中的start函數(shù)词裤。
點(diǎn)擊這里下載dyld源碼
3刺洒、在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的)并返回抹剩。同樣的,我們不能只蹭一蹭栏饮,要進(jìn)去干吧兔!
4、在dlyd中查看main函數(shù)
這個(gè)函數(shù)厲害了袍嬉,如下圖境蔼,足足快500行了!
我們抓住其中的關(guān)鍵代碼伺通,足步分析在main函數(shù)之前dyld到底幫我們做了哪一些事情箍土。
1、配置環(huán)境變量
從main函數(shù)的初始罐监,到函數(shù)getHostInfo()
之前都是在配置一些環(huán)境變量吴藻,已經(jīng)一些線程相關(guān)的,涉及內(nèi)容太過底層弓柱,這就不一一分析了(其實(shí)是能力不及??)
在這一步中有很多
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ù)都需要通過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ù)耕突。
在源碼中可以看到在我們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張圖:
補(bǔ)充:實(shí)例化完之后調(diào)用addImage(image),將實(shí)例化出來(lái)的鏡像加入所有的鏡像列表sAllImages晤锥,主程序永遠(yuǎn)是sAllImages的第一個(gè)對(duì)象掉蔬!
從源代碼可以看出,加載主程序這一步其實(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ù)也是通過這種形式添加的荐操!
5、鏈接主程序
link函數(shù)里面其實(shí)就是對(duì)之前的imges(不是圖片珍策,這是鏡像)進(jìn)行一些內(nèi)核操作托启,這部分Apple沒有開源出來(lái),只能看到些許源碼膛壹,有興許的同學(xué)可以自行查閱:
6驾中、加載Load和特定的C++的構(gòu)造函數(shù)方法
無(wú)論是從之前斷點(diǎn)load方法還是我們現(xiàn)在一步步對(duì)源碼的根據(jù),都能了解到模聋,dyld
的initializeMainExecutable
就是就加載load的入口:
并且最后都能接到一個(gè)結(jié)論:
由dyld
的notifySingle
函數(shù)經(jīng)過一系列的跳轉(zhuǎn)肩民,最終會(huì)跳轉(zhuǎn)到objc
源碼中的call_load_methods
函數(shù)!链方!
那么這中間的的過程到底是怎么樣的呢持痰?看下方的gif:
[圖片上傳失敗...(image-1cbe4e-1554211741014)]
簡(jiǎn)書坑爹總是吞我圖:(如果上面gif又顯示不出來(lái)就點(diǎn)這直接看流程圖gif)
最后找到函數(shù)_dyld_objc_notify_register
,就在全局都找不到一個(gè)調(diào)用的地方了祟蚀,其實(shí)這個(gè)函數(shù)本身就不是給dyld
調(diào)用的工窍,而是提供給外部調(diào)用的。怎么找到是誰(shuí)調(diào)用了_dyld_objc_notify_register
呢前酿?
繼續(xù)打開之前的Demo1患雏,在工程中加上_dyld_objc_notify_register
的符號(hào)斷點(diǎn)看看。
運(yùn)行工程罢维,斷住之后再次查看函數(shù)調(diào)用棧:
這就可以很清晰的看到淹仑,原來(lái)是
objc_init
調(diào)用了咱們的_dyld_objc_notify_register
函數(shù)。
同樣打開objc
的源碼(點(diǎn)擊下載objc源碼 )
快速定位_dyld_objc_notify_register
的調(diào)用位置肺孵。如圖:
這樣dyld是如何加載咱們的load方法就被找到了匀借。
期間如果有細(xì)心的同學(xué)可能看到了在notifySingle
后面緊跟著doInitialization
這樣一個(gè)函數(shù),這是一個(gè)系統(tǒng)特定的C++構(gòu)造函數(shù)的調(diào)用方法平窘。
這種C++構(gòu)造函數(shù)有特定的寫法吓肋,如下:
__attribute__((constructor)) void CPFunc(){
printf("C++Func1");
}
有興趣的同學(xué)可以嘗試實(shí)現(xiàn)一次,在MachO文件中找到對(duì)應(yīng)的方法瑰艘!
當(dāng)然是鬼,這在Demo1也是有的。
7磅叛、尋找APP的main函數(shù)并調(diào)用
當(dāng)上面的load和C++方法加載完成之后就會(huì)回到dyld的main方法里面屑咳,尋找APP的main函數(shù)并調(diào)用。
最終dyld的main函數(shù)中的主要流程就已經(jīng)走完了弊琴,當(dāng)然這7個(gè)步驟是一條主線兆龙,期間還會(huì)有很多其他的步驟,過程非常繁瑣,這就不一一舉例了紫皇。大家可以通過閱讀dyld的源碼盡收眼底慰安。
四、總結(jié)
本文講述了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
中LC_LOAD_DYLIB的(加載動(dòng)態(tài)鏈接庫(kù))存在,為我們逆向注入代碼提供了無(wú)限可能现诀。
MachO中其實(shí)還有一些符號(hào)表夷磕,為系統(tǒng)提供查詢對(duì)應(yīng)的方法名稱提供了路徑,這些在下一張文章中將會(huì)更加詳細(xì)的講到仔沿。
五坐桩、參考
1、Dynamic Linking of Imported Functions in Mach-O
2封锉、《iOS應(yīng)用逆向工程》沙梓社撕攒,吳航 著 ,機(jī)械工業(yè)出版社