綠樹(shù)陰濃夏日長(zhǎng),樓臺(tái)倒影入池塘括堤。--《唐高駢·山亭夏日》
mach-o文件和進(jìn)程的映像(image)
iOS系統(tǒng)生成的可執(zhí)行程序或者動(dòng)態(tài)庫(kù)文件的存儲(chǔ)布局格式被稱(chēng)之為mach-o格式抹估。文件中存放著程序的代碼和數(shù)據(jù)溉潭,而程序運(yùn)行時(shí)系統(tǒng)會(huì)為其建立一個(gè)進(jìn)程,以及分配虛擬內(nèi)存空間揍瑟。同時(shí)會(huì)把程序文件中的內(nèi)容加載到虛擬內(nèi)存地址空間中去白翻,這種加載的方法一般采用內(nèi)存映射文件的技術(shù)來(lái)實(shí)現(xiàn)。所謂的映像可以理解為將一個(gè)程序文件的內(nèi)容加載到進(jìn)程虛擬內(nèi)存中的內(nèi)容绢片,也就是說(shuō)進(jìn)程的映像就是程序磁盤(pán)文件在內(nèi)存中的一個(gè)副本滤馍。 一般來(lái)說(shuō)一個(gè)進(jìn)程中映像的內(nèi)容和內(nèi)存布局結(jié)構(gòu)會(huì)和程序文件的內(nèi)容以及存儲(chǔ)布局結(jié)構(gòu)一致,映像的首地址是一個(gè)struct mach_header
的結(jié)構(gòu)體指針底循。映像中內(nèi)容的排列布局和程序文件都是以段(Segment)為單位進(jìn)行排列的巢株。但是有一些情況映像的內(nèi)存布局和內(nèi)容可能會(huì)和程序文件的內(nèi)存布局和內(nèi)容不一致:
映像中的數(shù)據(jù)段部分,因?yàn)閿?shù)據(jù)段部分大多是可以被讀寫(xiě)訪問(wèn)的熙涤,也就是說(shuō)可以在運(yùn)行時(shí)被修改阁苞,或者某些信息會(huì)進(jìn)行rebase處理困檩。因此數(shù)據(jù)段不能被進(jìn)程之間共享,而是每個(gè)進(jìn)程單獨(dú)維護(hù)一份那槽。當(dāng)然為了效率和性能系統(tǒng)會(huì)采用一種稱(chēng)之為Copy on write的技術(shù)來(lái)實(shí)現(xiàn)單獨(dú)副本的拷貝的悼沿。通常只有不可變的代碼段部分才會(huì)是內(nèi)存和文件中的內(nèi)容保持一致,并且多進(jìn)程共享骚灸。一個(gè)很常見(jiàn)的例子就是進(jìn)程中加載的動(dòng)態(tài)庫(kù)和框架中的代碼段部分通常都是所有進(jìn)程共享糟趾。
即使是代碼段也有可能映像中的內(nèi)容和程序文件中的內(nèi)容不一致。有一些映像中的某些段的內(nèi)容會(huì)是系統(tǒng)中緩存的段甚牲,而不是程序文件對(duì)應(yīng)的段义郑。一個(gè)很有代表性的例子就是CoreLocation這個(gè)庫(kù),當(dāng)這個(gè)庫(kù)被加載時(shí)你就會(huì)發(fā)現(xiàn)其映像中的有一些代碼段的內(nèi)容其實(shí)是系統(tǒng)緩存的內(nèi)容而不是程序文件中的內(nèi)容丈钙。
所以說(shuō)程序文件和程序被加載后在內(nèi)存中映像之間并不是一一對(duì)應(yīng)的非驮。程序文件和映像之間的關(guān)系就如程序和進(jìn)程之間的關(guān)系是一樣的。在程序運(yùn)行后對(duì)其在進(jìn)程中所有的mach-o數(shù)據(jù)結(jié)構(gòu)的訪問(wèn)都是基于映像而不是基于程序文件的著恩。
Slide機(jī)制
構(gòu)建一個(gè)程序時(shí)為了方便計(jì)算和處理會(huì)為這個(gè)程序設(shè)定一個(gè)默認(rèn)在內(nèi)存中加載的基地址。這樣在程序中所有涉及到地址存儲(chǔ)的代碼中的地址變量都是以這個(gè)基地址為標(biāo)準(zhǔn)的蜻展。比如我們?cè)诖a中有變量保存一個(gè)函數(shù)的地址或者在rumtime中的OC類(lèi)的方法結(jié)構(gòu)體:struct method_t
中的imp保存的函數(shù)的地址等等喉誊。正常情況下如果我們的程序加載時(shí)也是按照程序中指定的基地址加載到虛擬內(nèi)存中對(duì)應(yīng)的地址時(shí)則一切都正常而且也不需要做任何的改變。但實(shí)際情況則不同:
- 任何一個(gè)庫(kù)或者可執(zhí)行程序在構(gòu)建時(shí)都會(huì)指定一個(gè)加載的基地址纵顾,但是卻無(wú)法保證這個(gè)基地址的唯一性伍茄。和無(wú)法保證程序映像的地址區(qū)間不產(chǎn)生重疊。因此有可能出現(xiàn)多個(gè)庫(kù)加載到內(nèi)存時(shí)的重疊覆蓋的情況施逾。
- iOS系統(tǒng)為保證的應(yīng)用安全采用了一種稱(chēng)之為ASLR(Address space layout randomization)的技術(shù)敷矫。這種技術(shù)會(huì)使得每個(gè)程序或者庫(kù)每次運(yùn)行加載到內(nèi)存中時(shí)的基地址都不是固定而是隨機(jī)的,這種機(jī)制會(huì)增加黑客的破解難度汉额。
上面的兩種情況表明一個(gè)程序或者庫(kù)加載到內(nèi)存時(shí)的真實(shí)的基地址和程序構(gòu)建時(shí)指定的基地址是不一樣的曹仗。系統(tǒng)會(huì)為可執(zhí)行程序和每個(gè)庫(kù)選擇不重疊的區(qū)域進(jìn)行加載。但是這樣就會(huì)出現(xiàn)在程序中所有以構(gòu)建時(shí)基地址為標(biāo)準(zhǔn)的那些地址指針出現(xiàn)訪問(wèn)異常蠕搜,因?yàn)檫@些地址值并不是真實(shí)在內(nèi)存中的地址值怎茫。
為了解決這個(gè)問(wèn)題系統(tǒng)會(huì)在構(gòu)建的程序或庫(kù)中添加一個(gè)特殊的load command命令:LC_DYLD_INFO或者LC_DYLD_INFO_ONLY。這部分信息用來(lái)記錄所有需要進(jìn)行地址調(diào)整的位置妓灌。這樣當(dāng)程序被加載到內(nèi)存時(shí)轨蛤,加載器就會(huì)將需要調(diào)整的地址分別進(jìn)行調(diào)整處理,以便轉(zhuǎn)化為真實(shí)的內(nèi)存地址虫埂。這個(gè)過(guò)程稱(chēng)之為基地址重定向(rebase)祥山。
假設(shè)程序構(gòu)建時(shí)指定的基地址為A,程序中某處保存的一個(gè)函數(shù)指針地址為x掉伏,而程序被加載到內(nèi)存時(shí)的真實(shí)基地址為B缝呕。也就是說(shuō)真實(shí)的基地址和構(gòu)建時(shí)的基地址的偏移差就是B-A澳窑。我們稱(chēng)這個(gè)偏移差值為Slide值。因此真實(shí)的地址x被調(diào)整后應(yīng)該是: x + (B - A)了岳颇。
一個(gè)程序在構(gòu)建時(shí)的基地址值可以在程序的第一個(gè)名為_(kāi)_TEXT的代碼段描述結(jié)構(gòu)體struct segment_command
中的vmaddr數(shù)據(jù)成員中獲取照捡,而程序被加載后的得到的映像的mach-o頭部結(jié)構(gòu)體struct mach_header
指針則是映像被加載的真實(shí)的基地址,因此:
映像的Slide值 = 映像的mach_header結(jié)構(gòu)體指針 - 映像的第一個(gè)__TEXT代碼段描述結(jié)構(gòu)體struct segmeng_command
中的vmaddr數(shù)據(jù)成員的值话侧。
當(dāng)然系統(tǒng)也提供了接口API來(lái)獲取可執(zhí)行程序或者庫(kù)的映像的Slide值栗精。這個(gè)將會(huì)在下面介紹。
段(Segment)和節(jié)(Section)
mach-o文件由諸多的load command組成瞻鹏,每個(gè)load command所代表的是一種數(shù)據(jù)類(lèi)型悲立。比如有的load command是用來(lái)存放程序代碼和全局變量數(shù)據(jù),有的load command是用來(lái)存放符號(hào)表新博,有的load command是用來(lái)存放代碼簽名信息等薪夕。每種load command都是結(jié)構(gòu)體struct load_command
的擴(kuò)展結(jié)構(gòu)體。其中的cmd字段用來(lái)描述這種load command的類(lèi)型赫悄。
類(lèi)型為L(zhǎng)C_SEGMENT或者為L(zhǎng)C_SEGMENT_64的load command被稱(chēng)之為段(Segment)原献。一個(gè)可執(zhí)行程序中的代碼和全局變量數(shù)據(jù)都保存在段中。描述段的信息是一個(gè)struct segment_command
結(jié)構(gòu)體埂淮。一個(gè)程序中可以存在著很多的段姑隅,每個(gè)段有一個(gè)唯一的段名(segment name)。比如一個(gè)可執(zhí)行程序中所有的代碼都保存在名字為:__TEXT的代碼段中倔撞,而所有的數(shù)據(jù)都保存在名字為:__DATA的數(shù)據(jù)段中讲仰。段以頁(yè)為邊界進(jìn)行對(duì)齊。
每個(gè)段則由多個(gè)節(jié)(Section)組成痪蝇。節(jié)是內(nèi)容分類(lèi)的最小管理單元鄙陡。每個(gè)節(jié)的描述信息是一個(gè)稱(chēng)之為:struct section
的結(jié)構(gòu)體。每個(gè)節(jié)有一個(gè)唯一的名稱(chēng)用來(lái)標(biāo)識(shí)這個(gè)節(jié)躏啰。比如代碼段中有一個(gè)名為:__text的節(jié)用來(lái)保存程序中用戶(hù)編寫(xiě)的源代碼對(duì)應(yīng)的機(jī)器指令趁矾,而一個(gè)名為:__stub_helper的節(jié)則保存所有調(diào)用的外部函數(shù)的樁代碼。下面的一張圖展示的就是程序中的段和節(jié)的結(jié)構(gòu)布局:
進(jìn)程映像(Image)操作API
對(duì)映像進(jìn)行操作的API都在<mach-o/dyld.h>
中聲明给僵。你可以import這個(gè)頭文件來(lái)使用里面定義的函數(shù)愈魏。下面我會(huì)分別介紹這些函數(shù)。
1.獲取當(dāng)前進(jìn)程中加載的映像的數(shù)量
//函數(shù)返回當(dāng)前進(jìn)程中加載的映像的數(shù)量
uint32_t _dyld_image_count(void)
2.獲取某個(gè)映像的mach-o頭部信息結(jié)構(gòu)體指針
const struct mach_header* _dyld_get_image_header(uint32_t image_index)
函數(shù)的入?yún)橛诚裨谶M(jìn)程當(dāng)中的索引號(hào)想际,函數(shù)返回的值是一個(gè)映像的mach-o頭部信息struct mach_header
結(jié)構(gòu)體指針培漏,如果是64位系統(tǒng)則返回的是struct mach_header_64
結(jié)構(gòu)體指針。你可以通過(guò)這個(gè)函數(shù)返回的映像的頭部結(jié)構(gòu)體來(lái)遍歷和訪問(wèn)映像中的所有信息和數(shù)據(jù)胡本。
一個(gè)映像的頭部信息結(jié)構(gòu)體指針其實(shí)就是映像在內(nèi)存中加載的基地址牌柄。
一般情況下索引為0的映像是dyld庫(kù)的映像,而索引為1的映像就是當(dāng)前進(jìn)程的可執(zhí)行程序映像侧甫。
系統(tǒng)還提供一個(gè)沒(méi)有在頭文件中聲明的函數(shù):
const struct mach_header* _NSGetMachExecuteHeader()
這個(gè)函數(shù)返回當(dāng)前進(jìn)程的可執(zhí)行程序映像的頭部信息結(jié)構(gòu)體指針珊佣。因?yàn)檫@個(gè)函數(shù)沒(méi)有在某個(gè)具體的頭文件中被聲明蹋宦,所以當(dāng)你要使用這個(gè)函數(shù)時(shí)需要在源代碼文件的開(kāi)頭進(jìn)行聲明處理:
extern const struct mach_header* _NSGetMachExecuteHeader();
3.獲取進(jìn)程中某個(gè)映像加載的Slide值
intptr_t _dyld_get_image_vmaddr_slide(uint32_t image_index)
函數(shù)的入?yún)橛诚裨谶M(jìn)程當(dāng)中的索引號(hào),函數(shù)的返回值是映像加載的Slide值咒锻。關(guān)于Slide值的介紹已經(jīng)在上面有詳細(xì)說(shuō)明冷冗。在mach-o格式程序中的結(jié)構(gòu)體描述信息中凡是涉及到指針字段都應(yīng)該加上這個(gè)值才是真實(shí)的內(nèi)存地址。
4.獲取進(jìn)程中某個(gè)映像的名稱(chēng)
const char* _dyld_get_image_name(uint32_t image_index)
函數(shù)的入?yún)橛诚裨谶M(jìn)程當(dāng)中的索引號(hào)惑艇,函數(shù)的返回值是映像對(duì)應(yīng)庫(kù)的全路徑名稱(chēng)蒿辙,返回的字符串我們不能修改也不必去銷(xiāo)毀它。
5.注冊(cè)映像加載和卸載的回調(diào)通知函數(shù)
void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))
void _dyld_register_func_for_remove_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))
如果你通過(guò)函數(shù)_dyld_register_func_for_add_image
注冊(cè)了一個(gè)映像被加載時(shí)的回調(diào)函數(shù)時(shí)滨巴,那么每當(dāng)后續(xù)一個(gè)新的映像被加載但未初始化前就會(huì)調(diào)用注冊(cè)的回調(diào)函數(shù)思灌,回調(diào)函數(shù)的兩個(gè)入?yún)⒎謩e表示加載的映像的頭結(jié)構(gòu)和對(duì)應(yīng)的Slide值。如果在調(diào)用_dyld_register_func_for_add_image
時(shí)系統(tǒng)已經(jīng)加載了某些映像恭取,則會(huì)分別對(duì)這些加載完畢的每個(gè)映像調(diào)用注冊(cè)的回調(diào)函數(shù)泰偿。
如果你通過(guò)函數(shù)_dyld_register_func_for_remove_image
注冊(cè)了一個(gè)映像被卸載時(shí)的回調(diào)函數(shù)時(shí),那么每當(dāng)一個(gè)映像被卸載前都會(huì)調(diào)用注冊(cè)的回調(diào)函數(shù)蜈垮,回調(diào)函數(shù)的兩個(gè)入?yún)⒎謩e表示卸載的映像的頭結(jié)構(gòu)和對(duì)應(yīng)的Slide值耗跛。
這兩個(gè)函數(shù)的作用通常用來(lái)做程序加載映像的監(jiān)控以及一些統(tǒng)計(jì)處理。
6.獲取某個(gè)庫(kù)鏈接時(shí)和運(yùn)行時(shí)的版本號(hào)
//獲取庫(kù)運(yùn)行時(shí)的版本號(hào)
int32_t NSVersionOfRunTimeLibrary(const char* libraryName)
//獲取庫(kù)鏈接時(shí)的版本號(hào)
int32_t NSVersionOfLinkTimeLibrary(const char* libraryName)
我們?cè)赬CODE工程中鏈接一些系統(tǒng)動(dòng)態(tài)庫(kù)時(shí)攒发,有時(shí)候會(huì)選擇某個(gè)具體版本的動(dòng)態(tài)庫(kù)调塌,但是有些操作系統(tǒng)可能不一定會(huì)提供對(duì)應(yīng)版本的動(dòng)態(tài)庫(kù),這樣就會(huì)導(dǎo)致程序運(yùn)行時(shí)加載的動(dòng)態(tài)庫(kù)版本和鏈接時(shí)指定的動(dòng)態(tài)庫(kù)的版本不一致晨继。還有一種場(chǎng)景就是工程中并沒(méi)有鏈接對(duì)應(yīng)的動(dòng)態(tài)庫(kù)烟阐,但是因?yàn)槠渌麕?kù)會(huì)鏈接對(duì)應(yīng)的動(dòng)態(tài)庫(kù)搬俊,就會(huì)出現(xiàn)雖然沒(méi)有直接鏈接對(duì)應(yīng)的動(dòng)態(tài)庫(kù)但是還是會(huì)加載對(duì)應(yīng)的動(dòng)態(tài)庫(kù)的情況紊扬。
因此系統(tǒng)提供了這兩個(gè)API可以獲取某個(gè)動(dòng)態(tài)庫(kù)鏈接和加載運(yùn)行時(shí)的版本號(hào)。這兩個(gè)函數(shù)的入?yún)⒍际莿?dòng)態(tài)庫(kù)的名稱(chēng)唉擂,這個(gè)名稱(chēng)是不帶路徑和擴(kuò)展名以及不帶lib前綴的庫(kù)名稱(chēng)餐屎。函數(shù)返回庫(kù)對(duì)應(yīng)的版本號(hào),如果庫(kù)不存在或者沒(méi)有被加載或者沒(méi)有被鏈接則返回-1玩祟。比如下面的代碼:
//這里的名稱(chēng)c++其實(shí)是指的libc++.dylib這個(gè)庫(kù)腹缩。
uint32_t v1 = NSVersionOfRunTimeLibrary("c++");
uint32_t v2 = NSVersionOfLinkTimeLibrary("c++");
如果我們的程序并沒(méi)有顯示的鏈接libc++.dylib則后者函數(shù)會(huì)返回-1。而前者則一般都會(huì)返回一個(gè)對(duì)應(yīng)的libc++的版本號(hào)空扎。
這兩個(gè)函數(shù)的主要用來(lái)做一些庫(kù)分析和運(yùn)行監(jiān)測(cè)等功能藏鹊,比如可以檢測(cè)某個(gè)庫(kù)是否是一個(gè)在運(yùn)行時(shí)被加載而不是顯示鏈接進(jìn)來(lái)的動(dòng)態(tài)庫(kù)。
7.獲取當(dāng)前進(jìn)程可執(zhí)行程序的路徑文件名
int _NSGetExecutablePath(char* buf, uint32_t* bufsize)
函數(shù)的入?yún)uf和bufsize指明保存可執(zhí)行文件路徑名的緩存和緩存的尺寸转锈,其中的bufsize是要指明緩存的尺寸盘寡,并且會(huì)輸出可執(zhí)行文件路徑名稱(chēng)的真實(shí)尺寸。如果函數(shù)調(diào)用返回正確則返回0撮慨,否則返回-1竿痰。就比如下面的例子:
char buf[256];
uint32_t bufsize = sizeof(buf)/sizeof(char);
_NSGetExecutablePath(buf, &bufsize);
8.注冊(cè)當(dāng)前線程結(jié)束時(shí)的回調(diào)函數(shù)
void _tlv_atexit(void (*termFunc)(void* objAddr), void* objAddr)
有時(shí)候我們想監(jiān)控線程的結(jié)束事件脆粥,那么就可以用這個(gè)函數(shù)來(lái)實(shí)現(xiàn)。這個(gè)函數(shù)用來(lái)監(jiān)控當(dāng)前線程的結(jié)束影涉,當(dāng)線程結(jié)束或者終止時(shí)就會(huì)調(diào)用注冊(cè)的回調(diào)函數(shù)变隔,_tlv_atexit
函數(shù)有兩個(gè)參數(shù):第一個(gè)是一個(gè)回調(diào)函數(shù)指針,第二個(gè)是一個(gè)擴(kuò)展參數(shù)蟹倾,作為回調(diào)函數(shù)的入?yún)?lái)使用匣缘。
不明白為什么這個(gè)函數(shù)會(huì)放在<mach-o/dyld.h>中聲明,完全不搭界喊式!
段(Segment)和節(jié)(Section)操作API
對(duì)段和節(jié)進(jìn)行操作的API都在import <mach-o/getsect.h>
中聲明孵户。你可以import這個(gè)頭文件來(lái)使用里面定義的函數(shù)。當(dāng)然如果你了解mach-o的文件格式的話可以不用這些API岔留,而是直接根據(jù)映像的頭部結(jié)構(gòu)體struct mach_header
來(lái)遍歷和訪問(wèn)這些段和節(jié)夏哭。不過(guò)既然系統(tǒng)已經(jīng)提供相關(guān)的API,那么還是優(yōu)先考慮用它們最合適了献联。下面我會(huì)分別介紹這些函數(shù)竖配。
段和節(jié)操作的API在系統(tǒng)的libmacho.dylib庫(kù)中實(shí)現(xiàn),這個(gè)庫(kù)暫時(shí)還沒(méi)有開(kāi)源出來(lái)里逆。
1. 獲取進(jìn)程中映像的某段中某個(gè)節(jié)的非Slide的數(shù)據(jù)指針和尺寸
//獲取進(jìn)程中可執(zhí)行程序映像的某個(gè)段中某個(gè)節(jié)的數(shù)據(jù)指針和尺寸进胯。
char *getsectdata(const char *segname, const char *sectname, unsigned long *size)
//獲取進(jìn)程加載的庫(kù)的segname段和sectname節(jié)的數(shù)據(jù)指針和尺寸。
char *getsectdatafromFramework(const char *FrameworkName, const char *segname, const char *sectname, unsigned long *size);
這兩個(gè)函數(shù)返回進(jìn)程中可執(zhí)行程序映像或者某個(gè)加載的動(dòng)態(tài)庫(kù)中的某個(gè)段中某個(gè)節(jié)的數(shù)據(jù)指針和尺寸原押。這兩個(gè)函數(shù)其實(shí)就是返回對(duì)應(yīng)的節(jié)描述信息結(jié)構(gòu)struct section
中的addr和size兩個(gè)數(shù)據(jù)成員的值胁镐。需要注意的是返回的地址值是沒(méi)有加上Slide值的指針,因此當(dāng)我們要在進(jìn)程中訪問(wèn)真實(shí)的地址時(shí)需要加上對(duì)應(yīng)的Slide值诸衔,下面就是一個(gè)實(shí)例代碼:
//一般索引為1的都是可執(zhí)行文件映像
intptr_t slide = _dyld_get_image_vmaddr_slide(1);
unsigned long size = 0;
char *paddr = getsectdata("__TEXT", "__text", &size);
char *prealaddr = paddr + slide; //這才是真實(shí)要訪問(wèn)的地址盯漂。
getsectdata函數(shù)的代碼實(shí)現(xiàn)如下:
//假設(shè)是64位的系統(tǒng)
char *getsectdata(const char *segname, const char *sectname, unsigned long *size)
{
const struct mach_header_64 *mhp = _NSGetMachExecuteHeader();
//這個(gè)函數(shù)會(huì)在下面介紹到。
return getsectdatafromheader_64(mhp, segname, sectname, size);
}
個(gè)人不建議用這個(gè)函數(shù)而是用下面會(huì)介紹到的getsectiondata函數(shù)更合適笨农。
2.獲取段和節(jié)的邊界信息
//獲取當(dāng)前進(jìn)程可執(zhí)行程序映像的最后一個(gè)段的數(shù)據(jù)后面的開(kāi)始地址就缆。
unsigned long get_end(void);
//獲取當(dāng)前進(jìn)程可執(zhí)行程序映像的第一個(gè)__TEXT段的__text節(jié)的數(shù)據(jù)后面的開(kāi)始地址。
unsigned long get_etext(void);
//獲取獲取當(dāng)前進(jìn)程可執(zhí)行程序映像的第一個(gè)_DATA段的__data節(jié)的數(shù)據(jù)后面的開(kāi)始地址
unsigned long get_edata(void);
這幾個(gè)函數(shù)主要用來(lái)獲取指定段和節(jié)的結(jié)束位置谒亦,以及用來(lái)確定某個(gè)地址是否在指定的邊界內(nèi)竭宰。需要注意的是這幾個(gè)函數(shù)返回的邊界值是并未加Slide值的邊界值。下面是這幾個(gè)函數(shù)的內(nèi)部實(shí)現(xiàn):
unsigned long get_end()
{
unsigned long end = 0;
const struct mach_header_64 *mhp = _NSGetMachExecuteHeader();
struct segment_command_64 *psegcmd = mhp + 1;
for (int i = 0; i < mhp->ncmds; i++)
{
if (psegcmd->cmd != LC_SEGMENT_64)
break;
end = psegcmd->vmaddr + psegcmd->vmsize;
psegcmd += 1;
}
return end;
}
unsigned long get_etext()
{
const struct section_64 *sec = getsectbyname("__TEXT","__text");
return sec->addr + sec->size;
}
unsigned long get_edata()
{
const struct section_64 *sec = getsectbyname("__DATA","__data");
return sec->addr + sec->size;
}
3.獲取進(jìn)程中可執(zhí)行程序映像的段描述信息
//獲取進(jìn)程中可執(zhí)行程序映像的指定段名的段描述信息
const struct segment_command *getsegbyname(const char *segname)
//上面函數(shù)的64位版本
const struct segment_command_64 *getsegbyname(const char *segname)
這兩個(gè)函數(shù)返回進(jìn)程中可執(zhí)行程序映像的某個(gè)段的段描述信息份招。段描述信息是一個(gè)struct segment_command
或者struct segment_command_64
結(jié)構(gòu)體切揭。
比如下面代碼返回進(jìn)程中可執(zhí)行程序映像代碼段__TEXT的段信息。
const struct segment_command_64 *psegment = getsegbyname("__TEXT");
4.獲取進(jìn)程中可執(zhí)行程序映像的某個(gè)段中某個(gè)節(jié)的描述信息
//獲取進(jìn)程中可執(zhí)行程序映像的某個(gè)段中某個(gè)節(jié)的描述信息锁摔。
const struct section *getsectbyname(const char *segname, const char *sectname)
//上面對(duì)應(yīng)函數(shù)的64位系統(tǒng)版本
const struct section_64 *getsectbyname(const char *segname, const char *sectname)
這兩個(gè)函數(shù)分別返回32位系統(tǒng)和64位系統(tǒng)中的進(jìn)程中可執(zhí)行程序映像的segname段中的sectname節(jié)的描述信息廓旬。節(jié)的描述信息是一個(gè)struct section
或者struct section_64
的結(jié)構(gòu)體。比如下面的代碼返回代碼段__TEXT中的代碼節(jié)__text的描述信息:
struct section_64 *psection = getsectbyname("__TEXT","__text");
5.獲取進(jìn)程中映像的段的數(shù)據(jù)
//獲取指定映像的指定段的數(shù)據(jù)鄙漏。
uint8_t *getsegmentdata(const struct mach_header *mhp, const char *segname, unsigned long *size)
//上面函數(shù)的64位版本
uint8_t *getsegmentdata(const struct mach_header_64 *mhp, const char *segname, unsigned long *size)
函數(shù)返回進(jìn)程內(nèi)指定映像mhp中的段segname中內(nèi)容的地址指針嗤谚,而整個(gè)段的尺寸則返回到size所指的指針當(dāng)中棺蛛。這個(gè)函數(shù)的內(nèi)部實(shí)現(xiàn)就是返回段描述信息結(jié)構(gòu)struct segment_command
中的vmaddr數(shù)據(jù)成員的值加上映像mhp的slide值。而size中返回的就是段描述信息結(jié)構(gòu)中的vmsize數(shù)據(jù)成員巩步。
因?yàn)樵谇懊嬷v過(guò)因?yàn)橛诚窦虞d時(shí)的slide值的緣故旁赊,所以映像中的各種mach-o結(jié)構(gòu)體中涉及到地址的數(shù)據(jù)成員的值都需要加上slide值才能得到映像在內(nèi)存中的真實(shí)加載地址。
進(jìn)程中每個(gè)映像中的第一個(gè)__TEXT段的數(shù)據(jù)的地址其實(shí)就是這個(gè)映像的mach_header頭結(jié)構(gòu)的地址椅野。這是一個(gè)比較特殊的情況终畅。
下面的代碼演示的是獲取進(jìn)程中第0個(gè)索引位置映像的__DATA段的數(shù)據(jù)。
struct mach_header_64 *mhp = _dyld_get_image_header(0);
unsigned long size = 0;
uint8_t *pdata = getsegmentdata(mhp, "__DATA", &size);
6.獲取進(jìn)程映像的某段中某節(jié)的數(shù)據(jù)
//獲取進(jìn)程映像中的某段中某節(jié)的數(shù)據(jù)地址和尺寸竟闪。
uint8_t *getsectiondata(const struct mach_header *mhp, const char *segname, const char *sectname, unsigned long *size)
//上面函數(shù)的64位版本
uint8_t *getsectiondata(const struct mach_header_64 *mhp, const char *segname, const char *sectname, unsigned long *size)
函數(shù)返回進(jìn)程內(nèi)指定映像mhp中的段segname中sectname節(jié)中內(nèi)容的地址指針离福,而整個(gè)節(jié)的尺寸則返回到size所指的指針當(dāng)中。這個(gè)函數(shù)的內(nèi)部實(shí)現(xiàn)就是返回節(jié)描述信息結(jié)構(gòu)struct section
中的addr數(shù)據(jù)成員的值加上映像mhp的slide值炼蛤。而size中返回的就是段描述信息結(jié)構(gòu)中的size數(shù)據(jù)成員的值妖爷。
因?yàn)樵谇懊嬷v過(guò)因?yàn)橛诚窦虞d時(shí)的slide值的緣故,所以映像中的各種mach-o結(jié)構(gòu)體中涉及到地址的數(shù)據(jù)成員的值都需要加上slide值才能得到映像在內(nèi)存中的真實(shí)加載地址理朋。
下面的例子獲取進(jìn)程中第0個(gè)映像的"__TEXT"段中的"__text"節(jié)的數(shù)據(jù)地址指針和尺寸:
struct mach_header_64 *mhp = _dyld_get_image_header(0);
unsigned long size = 0;
uint8_t *pdata = getsectiondata(mhp, "__TEXT", "__text", &size);
7.獲取mach-O文件中的某個(gè)段中某個(gè)節(jié)的描述信息
//獲取指定mach-o文件中的某個(gè)段中某個(gè)節(jié)中的描述信息
const struct section *getsectbynamefromheader(const struct mach_header *mhp, const char *segname, const char *sectname)
//獲取指定mach-o文件中的某個(gè)段中某個(gè)節(jié)中的描述信息絮识。fSwap傳NXByteOrder枚舉值。
const struct section *getsectbynamefromheaderwithswap(struct mach_header *mhp, const char *segname, const char *sectname, int fSwap)
//上面對(duì)應(yīng)函數(shù)的64位系統(tǒng)版本
const struct section_64 *getsectbynamefromheader_64(const struct mach_header_64 *mhp, const char *segname, const char *sectname)
//上面對(duì)應(yīng)函數(shù)的64位系統(tǒng)版本
const struct section *getsectbynamefromheaderwithswap_64(struct mach_header_64 *mhp, const char *segname, const char *sectname, int fSwap)
這一系列函數(shù)分別返回32位系統(tǒng)和64位系統(tǒng)的mach-o文件的節(jié)的描述信息嗽上。每個(gè)函數(shù)都有segname和sectname分別指明要獲取的段名和節(jié)名次舌。參數(shù)mhp則表明mach-o文件的頭部結(jié)構(gòu)指針。對(duì)于有一些系統(tǒng)或者mach-o文件中的數(shù)值采用big-endian來(lái)編碼兽愤,因此對(duì)于這些采用big-endian編碼的結(jié)構(gòu)來(lái)說(shuō)就需要傳遞fSwap來(lái)確定是否交換這些編碼彼念。
這一系列函數(shù)中的mhp結(jié)構(gòu)不局限于進(jìn)程中的映像的頭部結(jié)構(gòu),針對(duì)mach-o文件的頭部結(jié)構(gòu)也適用浅萧,如果你不了解映像和文件的區(qū)別則請(qǐng)看文章中的開(kāi)頭的介紹逐沙。
因?yàn)椴还苁沁M(jìn)程中的映像的Section的排列以及mach-o文件中的Section的排列都是一致的,因此其實(shí)上述的getsectbyname的實(shí)現(xiàn)就是借助本節(jié)提供的函數(shù)實(shí)現(xiàn)的惯殊,其實(shí)現(xiàn)的代碼如下:
const struct section_64 *getsectbyname(
const char *segname,
const char *sectname)
{
const struct mach_header_64 *mhp = _NSGetMachExecuteHeader();
return getsectbynamefromheader_64(mhp, segname, sectname);
}
8.獲取mach-o文件中的某段中的某個(gè)節(jié)的數(shù)據(jù)指針和尺寸
//獲取指定mach-o文件中的某個(gè)段中的某個(gè)節(jié)的數(shù)據(jù)指針和尺寸
char *getsectdatafromheader(const struct mach_header *mhp, const char *segname, const char *sectname, uint32_t *size)
//64位系統(tǒng)函數(shù)
char *getsectdatafromheader_64(const struct mach_header_64 *mhp, const char *segname, const char *sectname, uint64_t *size)
這兩個(gè)函數(shù)返回32位系統(tǒng)或者64位系統(tǒng)中的某個(gè)mach-o文件中的某個(gè)段中某個(gè)節(jié)的數(shù)據(jù)指針和尺寸酱吝。這兩個(gè)函數(shù)其實(shí)就是返回對(duì)應(yīng)的節(jié)描述信息結(jié)構(gòu)struct section中的addr值和size值也殖。因?yàn)檫@兩個(gè)函數(shù)是針對(duì)mach-o文件的土思,但是也可以用在對(duì)應(yīng)的庫(kù)映像中,當(dāng)應(yīng)用在庫(kù)映像中時(shí)就要記得對(duì)返回的結(jié)果加上對(duì)應(yīng)的slide值才是真實(shí)的節(jié)數(shù)據(jù)所對(duì)應(yīng)的地址忆嗜!
地址和符號(hào)查詢(xún)API
在一些場(chǎng)景中需要根據(jù)某個(gè)地址查詢(xún)其所歸屬的函數(shù)名稱(chēng),以及某個(gè)變量的符號(hào)名稱(chēng)這就要借助地址和符號(hào)查詢(xún)的函數(shù)己儒,這個(gè)函數(shù)定義在頭文件dlfcn.h中。
1.獲取地址歸屬的庫(kù)以及最近的符號(hào)信息捆毫。
int dladdr(const void *, Dl_info *);
函數(shù)的入?yún)⑹且粋€(gè)地址值闪湾,而輸出的Dl_info信息則是地址所歸屬的庫(kù)和符號(hào)信息,如果某個(gè)地址能夠找到對(duì)應(yīng)的符號(hào)信息則返回非0绩卤,否則返回0途样。Dl_info這個(gè)結(jié)構(gòu)體定義如下:
typedef struct dl_info {
const char *dli_fname; /* 地址歸屬的映像庫(kù)文件名稱(chēng) */
void *dli_fbase; /* 地址歸屬的庫(kù)在內(nèi)存中的基地址 */
const char *dli_sname; /* 離地址最近的符號(hào)名稱(chēng) */
void *dli_saddr; /* 離地址最近的符號(hào)名稱(chēng)的開(kāi)始地址 */
} Dl_info;
這里值得關(guān)注的就是dli_fbase是指的庫(kù)在內(nèi)存中的映像的首地址江醇,我們知道m(xù)ach-o文件的格式的頭部是一個(gè)mach_header結(jié)構(gòu),因此這里的dli_fbase就是指向?qū)?yīng)庫(kù)頭部的mach_header結(jié)構(gòu)體指針何暇。比如下面的代碼我想獲取objc_msgSend這個(gè)函數(shù)所在的庫(kù)以及對(duì)應(yīng)的函數(shù)名時(shí)就可以如下獲忍找埂:
#import <dlfcn.h>
#import <objc/message.h>
#import <mach-o/loader.h>
Dl_info info;
memset(&info, 0, sizeof(info));
if (dladdr(objc_msgSend, &info) != 0)
{
printf("dli_fname=%s\ndli_fbase=%p\ndli_sname=%s\ndl_saddr=%p\n", info.dli_fname, info.dli_fbase, info.dli_sname, info.dli_saddr);
struct mach_header_64 *pheader = (struct mach_header_64*)info.dli_fbase;
//...
}
通過(guò)這個(gè)函數(shù)可以獲取某個(gè)函數(shù)指針?biāo)鶎?duì)應(yīng)的字符串名稱(chēng),以及通過(guò)地址能夠獲取對(duì)應(yīng)的映像頭結(jié)構(gòu)信息裆站。
一個(gè)非常有用的DEMO
iOS系統(tǒng)提供了所謂方法交換(method swizzling)的黑魔法機(jī)制条辟。它可以在運(yùn)行時(shí)替換掉某個(gè)類(lèi)的某個(gè)方法的默認(rèn)實(shí)現(xiàn)。然而技術(shù)有兩面性宏胯,對(duì)于越獄系統(tǒng)來(lái)說(shuō)羽嫡,惡意開(kāi)發(fā)人員可以通過(guò)動(dòng)態(tài)庫(kù)注入并利用方法交換的技巧來(lái)改變程序運(yùn)行的原有邏輯,從而可以跨過(guò)一些常規(guī)檢測(cè)而謀取非法利益肩袍。
凡事有攻就有守杭棵,通過(guò)本文中介紹的API函數(shù)就可以在一定程度上檢測(cè)某個(gè)類(lèi)中的某個(gè)方法是否被非法HOOK。以可執(zhí)行程序中的某個(gè)類(lèi)的實(shí)例方法為例氛赐⊙胀溃可執(zhí)行程序中定義的類(lèi)的實(shí)例方法的實(shí)現(xiàn)地址總是在可執(zhí)行程序映像的地址區(qū)間范圍內(nèi),即使是這個(gè)方法被可執(zhí)行程序中的其他方法HOOK了鹰祸,這個(gè)HOOK的方法地址仍然是在可執(zhí)行程序的映像地址區(qū)間范圍內(nèi)甫窟,我們?nèi)匀徽J(rèn)為這是一個(gè)合法的HOOK。如果可執(zhí)行程序中的類(lèi)的實(shí)例方法被惡意攻擊者通過(guò)動(dòng)態(tài)庫(kù)注入并以方法交換的形式來(lái)HOOK原有方法的實(shí)現(xiàn)時(shí)蛙婴,因?yàn)镠OOK的方法地址是在惡意注入的動(dòng)態(tài)庫(kù)映像的地址區(qū)間范圍內(nèi)粗井,所以我們就可以通過(guò)檢測(cè)這個(gè)類(lèi)的實(shí)例方法的實(shí)現(xiàn)地址是否在可執(zhí)行程序的映像的地址區(qū)間范圍內(nèi)來(lái)判斷這個(gè)方法是否被惡意HOOK了。下面就是這種檢測(cè)的具體實(shí)現(xiàn)代碼街图,建議檢測(cè)的代碼用C函數(shù)來(lái)實(shí)現(xiàn)而不是用OC類(lèi)的方法來(lái)實(shí)現(xiàn)浇衬,否則這個(gè)檢測(cè)邏輯也有可能被HOOK。
//Author by 歐陽(yáng)大哥
#import <mach-o/dyld.h>
#import <mach-o/getsect.h>
BOOL checkMethodBeHooked(Class class, SEL selector)
{
//你也可以借助runtime中的C函數(shù)來(lái)獲取方法的實(shí)現(xiàn)地址
IMP imp = [class instanceMethodForSelector:selector];
if (imp == NULL)
return NO;
//計(jì)算出可執(zhí)行程序的slide值餐济。
intptr_t pmh = (intptr_t)_NSGetMachExecuteHeader();
intptr_t slide = 0;
#ifdef __LP64__
const struct segment_command_64 *psegment = getsegbyname("__TEXT");
#else
const struct segment_command *psegment = getsegbyname("__TEXT");
#endif
intptr_t slide = pmh - psegment->vmaddr
unsigned long startpos = (unsigned long) pmh;
unsigned long endpos = get_end() + slide;
unsigned long imppos = (unsigned long)imp;
return (imppos < startpos) || (imppos > endpos);
}
??【返回目錄】
歡迎大家訪問(wèn)我的github地址和簡(jiǎn)書(shū)地址