綠樹陰濃夏日長,樓臺倒影入池塘。--《唐高駢·山亭夏日》 c++
iOS系統生成的可執行程序或者動態庫文件的存儲佈局格式被稱之爲mach-o格式。文件中存放着程序的代碼和數據,而程序運行時系統會爲其創建一個進程,以及分配虛擬內存空間。同時會把程序文件中的內容加載到虛擬內存地址空間中去,這種加載的方法通常採用內存映射文件的技術來實現。所謂的映像能夠理解爲將一個程序文件的內容加載到進程虛擬內存中的內容,也就是說進程的映像就是程序磁盤文件在內存中的一個副本。 通常來講一個進程中映像的內容和內存佈局結構會和程序文件的內容以及存儲佈局結構一致,映像的首地址是一個struct mach_header
的結構體指針。映像中內容的排列布局和程序文件都是以段(Segment)爲單位進行排列的。可是有一些狀況映像的內存佈局和內容可能會和程序文件的內存佈局和內容不一致:git
映像中的數據段部分,由於數據段部分大可能是能夠被讀寫訪問的,也就是說能夠在運行時被修改,或者某些信息會進行rebase處理。所以數據段不能被進程之間共享,而是每一個進程單獨維護一份。固然爲了效率和性能系統會採用一種稱之爲Copy on write的技術來實現單獨副本的拷貝的。一般只有不可變的代碼段部分纔會是內存和文件中的內容保持一致,而且多進程共享。一個很常見的例子就是進程中加載的動態庫和框架中的代碼段部分一般都是全部進程共享。github
即便是代碼段也有可能映像中的內容和程序文件中的內容不一致。有一些映像中的某些段的內容會是系統中緩存的段,而不是程序文件對應的段。一個頗有表明性的例子就是CoreLocation這個庫,當這個庫被加載時你就會發現其映像中的有一些代碼段的內容實際上是系統緩存的內容而不是程序文件中的內容。緩存
因此說程序文件和程序被加載後在內存中映像之間並非一一對應的。程序文件和映像之間的關係就如程序和進程之間的關係是同樣的。在程序運行後對其在進程中全部的mach-o數據結構的訪問都是基於映像而不是基於程序文件的。安全
構建一個程序時爲了方便計算和處理會爲這個程序設定一個默認在內存中加載的基地址。這樣在程序中全部涉及到地址存儲的代碼中的地址變量都是以這個基地址爲標準的。好比咱們在代碼中有變量保存一個函數的地址或者在rumtime中的OC類的方法結構體:struct method_t
中的imp保存的函數的地址等等。正常狀況下若是咱們的程序加載時也是按照程序中指定的基地址加載到虛擬內存中對應的地址時則一切都正常並且也不須要作任何的改變。但實際狀況則不一樣:bash
上面的兩種狀況代表一個程序或者庫加載到內存時的真實的基地址和程序構建時指定的基地址是不同的。系統會爲可執行程序和每一個庫選擇不重疊的區域進行加載。可是這樣就會出如今程序中全部以構建時基地址爲標準的那些地址指針出現訪問異常,由於這些地址值並非真實在內存中的地址值。數據結構
爲了解決這個問題系統會在構建的程序或庫中添加一個特殊的load command命令:LC_DYLD_INFO或者LC_DYLD_INFO_ONLY。這部分信息用來記錄全部須要進行地址調整的位置。這樣當程序被加載到內存時,加載器就會將須要調整的地址分別進行調整處理,以便轉化爲真實的內存地址。這個過程稱之爲基地址重定向(rebase)。框架
假設程序構建時指定的基地址爲A,程序中某處保存的一個函數指針地址爲x,而程序被加載到內存時的真實基地址爲B。也就是說真實的基地址和構建時的基地址的偏移差就是B-A。咱們稱這個偏移差值爲Slide值。所以真實的地址x被調整後應該是: x + (B - A)了。dom
一個程序在構建時的基地址值能夠在程序的第一個名爲__TEXT的代碼段描述結構體struct segment_command
中的vmaddr數據成員中獲取,而程序被加載後的獲得的映像的mach-o頭部結構體struct mach_header
指針則是映像被加載的真實的基地址,所以:ide
映像的Slide值 = 映像的mach_header結構體指針 - 映像的第一個__TEXT代碼段描述結構體struct segmeng_command
中的vmaddr數據成員的值。
固然系統也提供了接口API來獲取可執行程序或者庫的映像的Slide值。這個將會在下面介紹。
mach-o文件由諸多的load command組成,每一個load command所表明的是一種數據類型。好比有的load command是用來存放程序代碼和全局變量數據,有的load command是用來存放符號表,有的load command是用來存放代碼簽名信息等。每種load command都是結構體struct load_command
的擴展結構體。其中的cmd字段用來描述這種load command的類型。
類型爲LC_SEGMENT或者爲LC_SEGMENT_64的load command被稱之爲段(Segment)。一個可執行程序中的代碼和全局變量數據都保存在段中。描述段的信息是一個struct segment_command
結構體。一個程序中能夠存在着不少的段,每一個段有一個惟一的段名(segment name)。好比一個可執行程序中全部的代碼都保存在名字爲:__TEXT的代碼段中,而全部的數據都保存在名字爲:__DATA的數據段中。段以頁爲邊界進行對齊。
每一個段則由多個節(Section)組成。節是內容分類的最小管理單元。每一個節的描述信息是一個稱之爲:struct section
的結構體。每一個節有一個惟一的名稱用來標識這個節。好比代碼段中有一個名爲:__text的節用來保存程序中用戶編寫的源代碼對應的機器指令,而一個名爲:__stub_helper的節則保存全部調用的外部函數的樁代碼。下面的一張圖展現的就是程序中的段和節的結構佈局:
對映像進行操做的API都在<mach-o/dyld.h>
中聲明。你能夠import這個頭文件來使用裏面定義的函數。下面我會分別介紹這些函數。
//函數返回當前進程中加載的映像的數量
uint32_t _dyld_image_count(void)
複製代碼
const struct mach_header* _dyld_get_image_header(uint32_t image_index)
複製代碼
函數的入參爲映像在進程當中的索引號,函數返回的值是一個映像的mach-o頭部信息struct mach_header
結構體指針,若是是64位系統則返回的是struct mach_header_64
結構體指針。你能夠經過這個函數返回的映像的頭部結構體來遍歷和訪問映像中的全部信息和數據。
一個映像的頭部信息結構體指針其實就是映像在內存中加載的基地址。
通常狀況下索引爲0的映像是dyld庫的映像,而索引爲1的映像就是當前進程的可執行程序映像。
系統還提供一個沒有在頭文件中聲明的函數:
const struct mach_header* _NSGetMachExecuteHeader()
複製代碼
這個函數返回當前進程的可執行程序映像的頭部信息結構體指針。由於這個函數沒有在某個具體的頭文件中被聲明,因此當你要使用這個函數時須要在源代碼文件的開頭進行聲明處理:
extern const struct mach_header* _NSGetMachExecuteHeader();
複製代碼
intptr_t _dyld_get_image_vmaddr_slide(uint32_t image_index)
複製代碼
函數的入參爲映像在進程當中的索引號,函數的返回值是映像加載的Slide值。關於Slide值的介紹已經在上面有詳細說明。在mach-o格式程序中的結構體描述信息中凡是涉及到指針字段都應該加上這個值纔是真實的內存地址。
const char* _dyld_get_image_name(uint32_t image_index)
複製代碼
函數的入參爲映像在進程當中的索引號,函數的返回值是映像對應庫的全路徑名稱,返回的字符串咱們不能修改也沒必要去銷燬它。
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))
複製代碼
若是你經過函數_dyld_register_func_for_add_image
註冊了一個映像被加載時的回調函數時,那麼每當後續一個新的映像被加載但未初始化前就會調用註冊的回調函數,回調函數的兩個入參分別表示加載的映像的頭結構和對應的Slide值。若是在調用_dyld_register_func_for_add_image
時系統已經加載了某些映像,則會分別對這些加載完畢的每一個映像調用註冊的回調函數。
若是你經過函數_dyld_register_func_for_remove_image
註冊了一個映像被卸載時的回調函數時,那麼每當一個映像被卸載前都會調用註冊的回調函數,回調函數的兩個入參分別表示卸載的映像的頭結構和對應的Slide值。
這兩個函數的做用一般用來作程序加載映像的監控以及一些統計處理。
//獲取庫運行時的版本號
int32_t NSVersionOfRunTimeLibrary(const char* libraryName)
//獲取庫連接時的版本號
int32_t NSVersionOfLinkTimeLibrary(const char* libraryName)
複製代碼
咱們在XCODE工程中連接一些系統動態庫時,有時候會選擇某個具體版本的動態庫,可是有些操做系統可能不必定會提供對應版本的動態庫,這樣就會致使程序運行時加載的動態庫版本和連接時指定的動態庫的版本不一致。還有一種場景就是工程中並無連接對應的動態庫,可是由於其餘庫會連接對應的動態庫,就會出現雖然沒有直接連接對應的動態庫可是仍是會加載對應的動態庫的狀況。 所以系統提供了這兩個API能夠獲取某個動態庫連接和加載運行時的版本號。這兩個函數的入參都是動態庫的名稱,這個名稱是不帶路徑和擴展名以及不帶lib前綴的庫名稱。函數返回庫對應的版本號,若是庫不存在或者沒有被加載或者沒有被連接則返回-1。好比下面的代碼:
//這裏的名稱c++實際上是指的libc++.dylib這個庫。
uint32_t v1 = NSVersionOfRunTimeLibrary("c++");
uint32_t v2 = NSVersionOfLinkTimeLibrary("c++");
複製代碼
若是咱們的程序並無顯示的連接libc++.dylib則後者函數會返回-1。而前者則通常都會返回一個對應的libc++的版本號。
這兩個函數的主要用來作一些庫分析和運行監測等功能,好比能夠檢測某個庫是不是一個在運行時被加載而不是顯示連接進來的動態庫。
int _NSGetExecutablePath(char* buf, uint32_t* bufsize)
複製代碼
函數的入參buf和bufsize指明保存可執行文件路徑名的緩存和緩存的尺寸,其中的bufsize是要指明緩存的尺寸,而且會輸出可執行文件路徑名稱的真實尺寸。若是函數調用返回正確則返回0,不然返回-1。就好比下面的例子:
char buf[256];
uint32_t bufsize = sizeof(buf)/sizeof(char);
_NSGetExecutablePath(buf, &bufsize);
複製代碼
void _tlv_atexit(void (*termFunc)(void* objAddr), void* objAddr)
複製代碼
有時候咱們想監控線程的結束事件,那麼就能夠用這個函數來實現。這個函數用來監控當前線程的結束,當線程結束或者終止時就會調用註冊的回調函數,_tlv_atexit
函數有兩個參數:第一個是一個回調函數指針,第二個是一個擴展參數,做爲回調函數的入參來使用。
不明白爲何這個函數會放在<mach-o/dyld.h>中聲明,徹底不搭界!
對段和節進行操做的API都在import <mach-o/getsect.h>
中聲明。你能夠import這個頭文件來使用裏面定義的函數。固然若是你瞭解mach-o的文件格式的話能夠不用這些API,而是直接根據映像的頭部結構體struct mach_header
來遍歷和訪問這些段和節。不過既然系統已經提供相關的API,那麼仍是優先考慮用它們最合適了。下面我會分別介紹這些函數。
段和節操做的API在系統的libmacho.dylib庫中實現,這個庫暫時尚未開源出來。
//獲取進程中可執行程序映像的某個段中某個節的數據指針和尺寸。
char *getsectdata(const char *segname, const char *sectname, unsigned long *size)
//獲取進程加載的庫的segname段和sectname節的數據指針和尺寸。
char *getsectdatafromFramework(const char *FrameworkName, const char *segname, const char *sectname, unsigned long *size);
複製代碼
這兩個函數返回進程中可執行程序映像或者某個加載的動態庫中的某個段中某個節的數據指針和尺寸。這兩個函數其實就是返回對應的節描述信息結構struct section
中的addr和size兩個數據成員的值。須要注意的是返回的地址值是沒有加上Slide值的指針,所以當咱們要在進程中訪問真實的地址時須要加上對應的Slide值,下面就是一個實例代碼:
//通常索引爲1的都是可執行文件映像
intptr_t slide = _dyld_get_image_vmaddr_slide(1);
unsigned long size = 0;
char *paddr = getsectdata("__TEXT", "__text", &size);
char *prealaddr = paddr + slide; //這纔是真實要訪問的地址。
複製代碼
getsectdata函數的代碼實現以下:
//假設是64位的系統
char *getsectdata(const char *segname, const char *sectname, unsigned long *size)
{
const struct mach_header_64 *mhp = _NSGetMachExecuteHeader();
//這個函數會在下面介紹到。
return getsectdatafromheader_64(mhp, segname, sectname, size);
}
複製代碼
我的不建議用這個函數而是用下面會介紹到的getsectiondata函數更合適。
//獲取當前進程可執行程序映像的最後一個段的數據後面的開始地址。
unsigned long get_end(void);
//獲取當前進程可執行程序映像的第一個__TEXT段的__text節的數據後面的開始地址。
unsigned long get_etext(void);
//獲取獲取當前進程可執行程序映像的第一個_DATA段的__data節的數據後面的開始地址
unsigned long get_edata(void);
複製代碼
這幾個函數主要用來獲取指定段和節的結束位置,以及用來肯定某個地址是否在指定的邊界內。須要注意的是這幾個函數返回的邊界值是並未加Slide值的邊界值。下面是這幾個函數的內部實現:
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 psection->addr + psection->size;
}
unsigned long get_edata()
{
const struct section_64 *sec = getsectbyname("__DATA","__data");
return psection->addr + psection->size;
}
複製代碼
//獲取進程中可執行程序映像的指定段名的段描述信息
const struct segment_command *getsegbyname(const char *segname)
//上面函數的64位版本
const struct segment_command_64 *getsegbyname(const char *segname)
複製代碼
這兩個函數返回進程中可執行程序映像的某個段的段描述信息。段描述信息是一個struct segment_command
或者struct segment_command_64
結構體。
好比下面代碼返回進程中可執行程序映像代碼段__TEXT的段信息。
const struct segment_command_64 *psegment = getsegbyname("__TEXT");
複製代碼
//獲取進程中可執行程序映像的某個段中某個節的描述信息。
const struct section *getsectbyname(const char *segname, const char *sectname)
//上面對應函數的64位系統版本
const struct section_64 *getsectbyname(const char *segname, const char *sectname)
複製代碼
這兩個函數分別返回32位系統和64位系統中的進程中可執行程序映像的segname段中的sectname節的描述信息。節的描述信息是一個struct section
或者struct section_64
的結構體。好比下面的代碼返回代碼段__TEXT中的代碼節__text的描述信息:
struct section_64 *psection = getsectbyname("__TEXT","__text");
複製代碼
//獲取指定映像的指定段的數據。
uint8_t *getsegmentdata(const struct mach_header *mhp, const char *segname, unsigned long *size)
//上面函數的64位版本
uint8_t *getsegmentdata(const struct mach_header_64 *mhp, const char *segname, unsigned long *size)
複製代碼
函數返回進程內指定映像mhp中的段segname中內容的地址指針,而整個段的尺寸則返回到size所指的指針當中。這個函數的內部實現就是返回段描述信息結構struct segment_command
中的vmaddr數據成員的值加上映像mhp的slide值。而size中返回的就是段描述信息結構中的vmsize數據成員。
由於在前面講過由於映像加載時的slide值的緣故,因此映像中的各類mach-o結構體中涉及到地址的數據成員的值都須要加上slide值才能獲得映像在內存中的真實加載地址。
進程中每一個映像中的第一個__TEXT段的數據的地址其實就是這個映像的mach_header頭結構的地址。這是一個比較特殊的狀況。
下面的代碼演示的是獲取進程中第0個索引位置映像的__DATA段的數據。
struct mach_header_64 *mhp = _dyld_get_image_header(0);
unsigned long size = 0;
uint8_t *pdata = getsegmentdata(mhp, "__DATA", &size);
複製代碼
//獲取進程映像中的某段中某節的數據地址和尺寸。
uint8_t *getsectiondata(const struct mach_header *mhp, const char *segname, const char *sectname, unsigned long *size)
//上面函數的64位版本
uint8_t *getsectiondata(const struct mach_header_64 *mhp, const char *segname, const char *sectname, unsigned long *size)
複製代碼
函數返回進程內指定映像mhp中的段segname中sectname節中內容的地址指針,而整個節的尺寸則返回到size所指的指針當中。這個函數的內部實現就是返回節描述信息結構struct section
中的addr數據成員的值加上映像mhp的slide值。而size中返回的就是段描述信息結構中的size數據成員的值。
由於在前面講過由於映像加載時的slide值的緣故,因此映像中的各類mach-o結構體中涉及到地址的數據成員的值都須要加上slide值才能獲得映像在內存中的真實加載地址。
下面的例子獲取進程中第0個映像的"__TEXT"段中的"__text"節的數據地址指針和尺寸:
struct mach_header_64 *mhp = _dyld_get_image_header(0);
unsigned long size = 0;
uint8_t *pdata = getsectiondata(mhp, "__TEXT", "__text", &size);
複製代碼
//獲取指定mach-o文件中的某個段中某個節中的描述信息
const struct section *getsectbynamefromheader(const struct mach_header *mhp, const char *segname, const char *sectname)
//獲取指定mach-o文件中的某個段中某個節中的描述信息。fSwap傳NXByteOrder枚舉值。
const struct section *getsectbynamefromheaderwithswap(struct mach_header *mhp, const char *segname, const char *sectname, int fSwap)
//上面對應函數的64位系統版本
const struct section_64 *getsectbynamefromheader_64(const struct mach_header_64 *mhp, const char *segname, const char *sectname)
//上面對應函數的64位系統版本
const struct section *getsectbynamefromheaderwithswap_64(struct mach_header_64 *mhp, const char *segname, const char *sectname, int fSwap)
複製代碼
這一系列函數分別返回32位系統和64位系統的mach-o文件的節的描述信息。每一個函數都有segname和sectname分別指明要獲取的段名和節名。參數mhp則代表mach-o文件的頭部結構指針。對於有一些系統或者mach-o文件中的數值採用big-endian來編碼,所以對於這些採用big-endian編碼的結構來講就須要傳遞fSwap來肯定是否交換這些編碼。
這一系列函數中的mhp結構不侷限於進程中的映像的頭部結構,針對mach-o文件的頭部結構也適用,若是你不瞭解映像和文件的區別則請看文章中的開頭的介紹。
由於不論是進程中的映像的Section的排列以及mach-o文件中的Section的排列都是一致的,所以其實上述的getsectbyname的實現就是藉助本節提供的函數實現的,其實現的代碼以下:
const struct section_64 *getsectbyname(
const char *segname,
const char *sectname)
{
const struct mach_header_64 *mhp = _NSGetMachExecuteHeader();
return getsectbynamefromheader_64(mhp, segname, sectname);
}
複製代碼
//獲取指定mach-o文件中的某個段中的某個節的數據指針和尺寸
char *getsectdatafromheader(const struct mach_header *mhp, const char *segname, const char *sectname, uint32_t *size)
//64位系統函數
char *getsectdatafromheader_64(const struct mach_header_64 *mhp, const char *segname, const char *sectname, uint64_t *size)
複製代碼
這兩個函數返回32位系統或者64位系統中的某個mach-o文件中的某個段中某個節的數據指針和尺寸。這兩個函數其實就是返回對應的節描述信息結構struct section中的addr值和size值。由於這兩個函數是針對mach-o文件的,可是也能夠用在對應的庫映像中,當應用在庫映像中時就要記得對返回的結果加上對應的slide值纔是真實的節數據所對應的地址!
iOS系統提供了所謂方法交換(method swizzling)的黑魔法機制。它能夠在運行時替換掉某個類的某個方法的默認實現。然而技術有兩面性,對於越獄系統來講,惡意開發人員能夠經過動態庫注入並利用方法交換的技巧來改變程序運行的原有邏輯,從而能夠跨過一些常規檢測而謀取非法利益。
凡事有攻就有守,經過本文中介紹的API函數就能夠在必定程度上檢測某個類中的某個方法是否被非法HOOK。以可執行程序中的某個類的實例方法爲例。可執行程序中定義的類的實例方法的實現地址老是在可執行程序映像的地址區間範圍內,即便是這個方法被可執行程序中的其餘方法HOOK了,這個HOOK的方法地址仍然是在可執行程序的映像地址區間範圍內,咱們仍然認爲這是一個合法的HOOK。若是可執行程序中的類的實例方法被惡意攻擊者經過動態庫注入並以方法交換的形式來HOOK原有方法的實現時,由於HOOK的方法地址是在惡意注入的動態庫映像的地址區間範圍內,因此咱們就能夠經過檢測這個類的實例方法的實現地址是否在可執行程序的映像的地址區間範圍內來判斷這個方法是否被惡意HOOK了。下面就是這種檢測的具體實現代碼,建議檢測的代碼用C函數來實現而不是用OC類的方法來實現,不然這個檢測邏輯也有可能被HOOK。
//Author by 歐陽大哥
#import <mach-o/dyld.h>
#import <mach-o/getsect.h>
BOOL checkMethodBeHooked(Class class, SEL selector)
{
//你也能夠藉助runtime中的C函數來獲取方法的實現地址
IMP imp = [class instanceMethodForSelector:selector];
if (imp == NULL)
return NO;
//計算出可執行程序的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);
}
複製代碼
👉【返回目錄】
歡迎你們訪問個人github地址