Mach-O
什么Mach-O
Mach-O為Mach Object文件格式的縮寫,它是一種用于可執行文件,目標代碼,動態庫,內核轉儲的文件格式。作為a.out格式的替代,Mach-O提供了更強的擴展性,并提升了符號表中信息的訪問速度。Mach-O是iOS、mac系統中的可執行文件格式。
- MachO格式的常見文件
目標文件.o
庫文件
.a
.dylib
Framework
可執行文件
dyld
.dsym
- 常用命令
使用lifo -info 可以查看MachO文件包含的架構
lipo -info MachO文件
使用lifo –thin 拆分某種架構
lipo MachO文件 –thin 架構 –output 輸出文件路徑
使用lipo -create 合并多種架構
lipo -create MachO1 MachO2 -output 輸出文件路徑
Mach-O文件結構
每個Mach-O文件包括一個Mach-O頭,然后是一系列的載入命令,再是一個或多個塊,每個塊包括0到255個段。Mach-O使用REL再定位格式控制對符號的引用。Mach-O在兩級命名空間中將每個符號編碼成“對象-符號名”對,在查找符號時則采用線性搜索法。以下是蘋果官方關于Mach-O的結構圖:
蘋果官方圖片
通過MachOView可以查看更為詳細的Mach-O的結構。如下圖所示:
mac-o.jpg
- Mach64 Header: 包含該二進制文件的一般信息。包括字節順序、架構類型、加載指令的數量等。使得可以快速確認一些信息,比如當前文件用于32位還是64位,對應的處理器是什么、文件類型是什么。以下就是Header的數據結構:
struct mach_header_64 {
uint32_t magic; /* mach-o 格式的標識符 */
cpu_type_t cputype; /* cpu區分符 */
cpu_subtype_t cpusubtype; /* machine區分符 */
uint32_t filetype; /* 文件類型 */
uint32_t ncmds; /* 加載命令的個數 */
uint32_t sizeofcmds; /* 加載命令的字節數 */
uint32_t flags; /* 程序的標識位 */
uint32_t reserved; /* 保留字段 */
};
- Load Commands:是加載指令,描述的是文件的加載信息,內容包括區域的位置、符號表、動態符號表等。這個部分信息還是比較有用的,我們可以從這里獲取到符號表和字符串表的偏移量等。通過MachOView可以查看Load Commands詳情:
load_commands_detail.jpg
字段解析
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 代碼簽名
- 數據區:除了Header和Load Commands外所有的原始數據。數據區分為很多段(Section)。
text段是代碼段。它用來放程序代碼(code)。它通常是只讀的。
data段是數據段。它用來存放初始化了的(initailized)全局變量(global)和初始化了的靜態變量(static)。它是可讀可寫的。
bss段是全局變量數據段。它用來存放未初始化的(uninitailized)全局變量(global)和未初始化的靜態變量
接下來先介紹數據區幾個比較重要的模塊:
- (__TEXT,__text)
這里存放的是匯編后的代碼,當我們進行編譯時,每個.m文件會經過預編譯->編譯->匯編形成.o文件,稱之為目標文件。匯編后,所有的代碼會形成匯編指令存儲在.o文件的(__TEXT,__text)區。鏈接后,所有的.o文件會合并成一個文件,所有.o文件的(__TEXT,__text)數據都會按鏈接順序存放到應用文件的(__TEXT,__text)中。
_TEXT_text.jpg
- (__DATA,__data)
存儲數據的section,static在進行非零賦值后會存儲在這里,如果static 變量沒有賦值或者賦值為0,那么它會存儲在(__DATA,__bss)中。
_DATA_data.jpg
- Symbol Table
符號表,這個是重點中的重點,符號表是將地址和符號聯系起來的橋梁。符號表并不能直接存儲符號,而是存儲符號位于字符串表的位置。
SymbolTable.jpg
- String Table
字符串表所有的變量名、函數名等,都以字符串的形式存儲在字符串表中。
StringTable.jpg
- Dynamic Symbol Table
動態符號表存儲的是動態庫函數位于符號表的偏移信息。(__DATA,__la_symbol_ptr) section 可以從動態符號表中獲取到該section位于符號表的索引數組。動態符號表并不存儲符號信息,而是存儲其位于符號表的偏移信息。
DynamicSymbolTable.jpg
- Lazy Symbol Pointers
Lazy Symbol Pointers懶加載符號表。所謂懶加載是指在程序運行時需要訪問這些符號的時候再去綁定。這些符號一般來自程序依賴的動態庫。
LazySymbolPointers.jpg
-
Non Lazy Symbol Pointers
Non Lazy Symbol Pointers 非懶加載符號表。所謂非懶加載是指在程序一加載就綁定好的。這些符號一般來自程序依賴的動態庫。
Non-LazySymbolPointers.jpg -
Symbol Stubs
翻譯過來就是符號樁。它與Lazy Symbol Pointers是一一對應的,每次訪問外部符號時都會先訪問Symbol Stubs,然后執行樁代碼,最后去Lazy Symbol Pointers找到相應的符號地址執行下一步操作。
SymbolStubs.jpg Assembly
這里面其實就是符號綁定執行的匯編代碼。后面到符號綁定的時候再詳解。
Assembly.jpg
符號重定向
對于iOS程序來說,由于ASLR安全機制的原因,每次啟動程序系統都會分配一個隨機偏移值,程序啟動時會根據偏移值進行符號地址修正。假設程序首地址的0x00000000, 隨機偏移值是0x00008000, 那程序的首地址就變成0x00000000 + 0x00008000, 程序內某個符號的偏移地址是0x00001000,那么它的內存地址是0x00001000 + 0x00008000 = 0x00009000。符號重定向實際上就是在程序運行時,把符號的偏移地址加上啟動時的隨機偏移值得到符號的內存地址的一個過程。符號重定向針對的是程序內的符號。接下來我們通過下面的demo來進行驗證。首先我們新建一個類MyObject, 然后定義
一個方法doSomething,然后運行:
demo.jpeg
獲取編譯后的可執行文件:
1.jpeg
2.jpeg
3.jpeg
這個黑色icon的文件就是我們的可執行文件,把這個文件拖入MachOView中:
mach-o.jpeg
接下來演示一下重定向的過程。首先在運行前- (void)doSomething入口打個斷點,然后在xcode->Debug-> Debug WorkFlow -> Always show disassembly 進入匯編模式,運行然后就進入如下畫面:
重定向-獲取ASLR值.png
打開剛才的MachOView查看符號表,如下:
符號表.png
可以看到- (void)doSomething放的的符號-[MyObject doSomething]在文件中的偏移地址為5E28。接下來我們驗證一下重定向的過程:
重定向.png
由圖中可知方法doSomething的內存地址與程序的首地址的差值是不變的,而且是等于-[MyObject doSomething]在文件中的偏移值。
符號綁定
相對于符號重定向針對的是程序內的符號,符號綁定針對的是程序外的符號,比如所以依賴的動態庫等。由于編譯時并沒有把動態庫編譯到程序內,只是在連接階段生成動態符號表。但是生成這個動態符表的地址并不是符號的真是地址,只有在訪問這個符號時才會調用系統的符號綁定函數進行進行綁定,并獲取符號地址更新到符號表中。這個過程就叫符號綁定。
綁定流程
當程序首次訪問外部函數的時候,它首先會訪問外部函數的樁Symbol Stubs,并執行樁代碼(Symbol Stubs中的Data字段對應的值),而這個樁代碼執行后,最后會跳轉到Lazy Symbol Pointers對應符號的地址。首次訪問會根據這個地址在Assembly文件中找到相應的代碼執行,最后調用dyld_stub_binder函數進行符號綁定。綁定完成之后就會更新Lazy Symbol Pointers表中的值,將符號地址直接寫入到表中,再次訪問的時候就可以直接訪問這個地址而不需要在執行Assembly中的代碼。 大致流程圖如下:
綁定流程.jpg
下面我們利用NSLog(NSLog來自系統動態庫Foundation)的例來對符號綁定整個過程進行演示。我們同時通過MachOView工具以及程序運行會的匯編調試來展示這個過程。同樣的,以下面的demo為例:
demo.jpeg
- 進入匯編調試
首先,在NSLog入口處打個斷點,進入匯編調試,如下圖所示:
訪問SymbolStubs.jpg
紅圈部分可以看到確實是訪問了Simbol Stubs。
- 執行執行Symbol Stubs中的代碼
通過匯編調試執行bl指令進入如下頁面:
執行SymbolStubs中的代碼.jpg
這一步可以看到實際上是執行Symbol Stubs中的Data斷的代碼,這個段代碼最后會跳轉到0x000000010015651c,這個地址是通過Lazy Symbol Pointers獲取的。
訪問Lazy Symbol Pointers.jpg
- 執行Assembly中的代碼
通過image list命令可以獲取程序的偏移地址(首地址)為0x0000000100150000。偏移地址0x000000010015651c -0x0000000100150000 = 0x000000000000651c。然后根據這個偏移值到Assembly執行相應的代碼。執行br指令,跳轉到x16中的地址(實際上就是剛才0x000000010015651c),進入如下頁面:
訪問Assembly.png
Assembly.jpg
- 調用符號綁定函數dyld_stub_binder
在這里執行兩行代碼然后跳轉到6504這個這個地方。這個地方實際上就是調用符號綁定函數dyld_stub_binder的地方。以下通過匯編調試和Assembly中查看他們的匯編指令就可以看得出來。
dyld_stub_binder.jpeg
為了進一步驗證,執行br指令跳轉到x16中的地址(實際上就是dyld_stub_binder函數地址)。進入如下頁面:
dyld_stub_binder.jpeg
- Non-Lazy Symbol Pointers
這里有個疑問,就是dyld_stub_binder本身也是外部符號。那它是怎么綁定的呢?又是什么時候綁定的呢。實際上dyld_stub_binder是非懶加載符號,是在程序一開始運行就綁定的,它存儲在Non-Lazy Symbol Pointers里面,所以不需要再走一遍符號綁定流程。
Assembly尋找函數dyld_stub_binder過程.png
至此,NSlog函數第一次調用過程中的符號綁定流程就走完了。其他外部符號訪問流程都是一樣的。綁定成功之后會修改Lazy Symbol Pointers中的值為符號的內存地址,下次訪問時不需要再次綁定,可以直接在Lazy Symbol Pointers進行訪問。
第二次調用NSLog函數
查看Lazy Symbol Pointers中的編譯時偏移地址:
第二次調用的Lazy Symbol Pointers.png
接下來我們進行第二次訪問的驗證。首先在第二次調用處打個斷點,進入匯編調試,進入如下頁面:
第二次調用.png
繼續執行bl指令,跳轉一個地址,進入如下頁面:
第二次訪問NSLog時的內存地址.jpeg
第二次直接跳轉NSLog函數實現.png
最終看到第二次進入的時候Lazy Symbol Pointers中的值就已經是NSLog函數的地址了,程序就可以直接訪問了。
符號重綁定
有前面知道符號的綁定過程,我們知道符號綁定的本質就是將外部符號的地址跟新到Lazy Symbol Pointers表中。符號重綁定實際上也就是修改Lazy Symbol Pointers中的值。常用的第三方庫Fishhook之所以能夠hook系統代碼就是利用這個原理,直接修改了Lazy Symbol Pointers對應符號的地址值實現。這里我們以Hook系統函數NSLog為例,演示Fishhook工作的大致流程如下:
- 通過字符換找到符號。去Mach-O中尋找String Table(String Table中通過“.”字符將符號分割),得到偏移值(String Table Index)
重綁定—StringTable.jpeg
- 通過String Table Index去找Symbols(符號表),得到符號表的偏移值(Symbol Index)
重綁定-Symbols.jpeg
- 通過Symbol Index 去找indirect Symbols,得到符號的偏移值(Indirect Symbol Index)
重綁定Indirect Symbol.png
- 最后去修改Lazy Symbol Pointers 里面的值(因為有前面的分析可知,外部符號調用都在找樁,樁去尋找Lazy Symbol Pointers 里面的地址)
重綁定Lazy Symbol Pointers.png
接下來通過匯編調試驗證:
首先獲取NSLog符號在Lazy Symbol Pointers中的偏移值:
重綁定_NSLog偏移值.png
拿到符號偏移地址為0xC000,開始進入匯編調試:
重綁定-demo.jpeg
重綁定-首地址.png
重綁定-第一次調用NSLog.png
重綁定-NSLog后hook前.png
重綁定-hook之后地址.png
hook之后符號地址就改成了我們自定義的函數myNSlog的地址。