概念——什麼是MachO?
蘋果開發者對它一定不陌生,特別是喜歡逆向的同學,對它的研究更是必不可少。在可安裝的每一個.app包中,都有一個與app同名的可執行檔案,它可能長這樣:(如果你碰到顯示為白色的MachO檔案,說明當前使用者對其沒有可執行許可權)
MachO其實是Mach Object的縮寫,是在Mac以及iOS上可執行的一種檔案格式,包括但不限於:可執行檔案(.out .o)、動態庫、靜態庫、dyld、目標檔案,官方文件中列舉如下: 它類似於Windows上的PE、Linux上的ELF格式,我們用file命令看上圖中 AlipayWallet 的檔案型別資訊: 可以看到,該示例的MachO檔案屬於通用二進位制檔案(蘋果提出的一種能同時適用多種架構二進位制檔案的程式程式碼),並支援在兩種架構上執行:arm_v7和arm64,包含了多種架構的MachO檔案可以通過lipo thin命令進行拆分:lipo AlipayWallet -thin armv7 -output alipayArmv7
,同樣也能用lipo create進行合併:lipo alipayArm64 alipayArmv7 -create -output AlipayWalletNew
,大家可以自己實踐一下。
構造——MachO的內部結構是怎樣的?
一:簡單的講,它包括以下四個組成部分:
- Header (頭部)
- 用於快速確認該檔案的CPU型別、檔案型別等資訊
- LoadCommands (載入命令)
- 用於告訴loader如何設定並載入二進位制資料
- Data (資料段 segment)
- 存放資料:程式碼、字元常量、類、方法等
- 可以擁有多個segment,每個segment可以有零到多個section。每個段都有一段虛擬地址對映到程式的地址空間
- Loader Info (連結資訊)
- 一個完整的使用者級MachO檔案的末端是一系列連結資訊。其中包含了動態載入器用來連結可執行檔案或者依賴所需使用的符號表、字串表等
來用MachOView驗證一下該示例的MachO檔案結構:
二:逐一探索:
1.Header
除了用MachOView能檢視MachO檔案資訊,還可以通過otool命令檢視,我們先來分析Header中的內容:otool -h AlipayWallet
:
- magic:MachO檔案的魔數,用來確定其屬於64位(0xfeedfacf)還是32位(0xfeedface)例子中有兩個Header,分別對應的是armv7和arm64的Header,前者32位,後者64位。
- cputype和cupsubtype代表的是cpu的型別和其子型別,例子中分別是12(c)與9、16777228(100000c)與0定義如下:
#define CPU_TYPE_ARM((cpu_type_t) 12)
#define CPU_SUBTYPE_ARM_V7((cpu_subtype_t) 9)
#define CPU_TYPE_ARM64((cpu_type_t) 16777228)
#define CPU_SUBTYPE_AR64M_ALL((cpu_subtype_t) 0)
複製程式碼
- 接著是filetype,2,代表可執行的檔案:
#define MH_EXECUTE 0x2 /* demand paged executable file */
複製程式碼
- ncmds 指的是載入命令(load commands)的數量,例子中一共75個,編號0-74
- sizeofcmds 表示75個load commands的總位元組大小, load commands區域是緊接著header區域的
- 最後個flags 標識二進位制檔案支援的功能,主要與系統的載入、連結有關。
這裡為你準備了 mach_header 的蘋果官方文件說明:(更多詳細定義請參考loader.h)
2.LoadCommands
我們繼續用命令檢視:otool -l alipayArm64
,該示例共有75個載入指令,我們只擷取一個作為代表,分別為出現在segment和section中的每一個引數進行註釋:
Load command 1
cmd LC_SEGMENT_64 // cmd 是load command的型別,LC_SEGMENT_64的含義是將這個64位的段對映到程式地址空間,即載入命令
cmdsize 712 // 代表load command的大小
segname __TEXT // 16位元組的段名字 __TEXT
vmaddr 0x0000000100000000 // 段的虛擬記憶體起始地址
vmsize 0x00000000036a4000 // 段的虛擬記憶體大小
fileoff 0 // 段在檔案中的偏移量
filesize 57294848 // 段在檔案中的大小
maxprot 0x00000005 // 段頁面所需要的最高記憶體保護(4=r,2=w,1=x)
initprot 0x00000005 // 段頁面初始的記憶體保護
nsects 8 // 段中包含section的數量
flags 0x0 // 其他雜項標誌位
Section
sectname __text // 第一個是__text ,就是主程式程式碼
segname __TEXT // 該section所屬的 segment名,第一個是__TEXT
addr 0x0000000100006110 // 該section在記憶體的啟始位置,0x100006110
size 0x000000000358a268 // size 該section的大小,0x358a268
offset 24848 // 24848 0x6110
align 2^4 (16) // 位元組大小對齊,16
reloff 0 // 重定位入口的檔案偏移 0
nreloc 0 // 需要重定位的入口數量 0
flags 0x80000400 // 包含section的type和attributes
reserved1 0 // ...保留用
reserved2 0 // ...保留用
複製程式碼
註釋完畢,我又為你準備了 segment和section 的蘋果官方文件說明:(更多詳細定義請參考loader.h)
我總結了最常見的載入命令如下:- LC_SEGMENT_64: 將該段(64位)對映到程式地址空間中
- LC_DYLD_INFO_ONLY:載入動態連結庫資訊(重定向地址、弱引用繫結、懶載入繫結、開放函式等的偏移值等資訊)
- LC_SYMTAB:載入符號表地址
- LC_DYSYMTAB:載入動態符號表地址
- LC_LOAD_DYLINKER:載入動態載入庫,可以看出示例使用的是/usr/lib/dyld
- LC_UUID:確定檔案的唯一標識,crash解析中也會有這個,去檢測dysm檔案和crash檔案是否匹配
- LC_VERSION_MIN_MACOSX/LC_VERSION_MIN_IPHONEOS:確定二進位制檔案要求的最低作業系統版本
- LC_SOURCE_VERSION:構建該二進位制檔案使用的原始碼版本
- LC_MAIN:設定程式主執行緒的入口地址和棧大小
- LC_ENCRYPTION_INFO_64:獲取加密資訊
- LC_LOAD_DYLIB:載入額外的動態庫
- LC_FUNCTION_STARTS:定義一個函式起始地址表,使偵錯程式和其他程式易於看到一個地址是否在函式內
- LC_DATA_IN_CODE:定義在程式碼段內的非指令的表
- LC_CODE_SIGNATURE:獲取應用簽名資訊
3.Data、連結資訊
如果說前面兩部分的主要作用,是讓kern核心知道如何讀取MachO檔案,並指定MachO檔案的動態連結器(dyly)用來完成後續的動態庫載入,然後設定好程式入口等一些列程式啟動前的資訊,那麼Data和連結資訊部分,就相當於當程式執行起來後,為每一個對映到虛擬記憶體中的指令操作提供真實的實體地址支援。詳細的過程會面會單獨寫一篇文章展開來講。
收穫——熟悉MachO可以做什麼
理解原理很重要,瞭解MachO格式的結構和載入執行,不僅可以幫助我們理解MacOS和iOS的app可執行檔案啟動過程,還能做且不限於:
- bitcode分析
- crash符號化
- 符號模組查詢
- 非OC函式switch
- 包支援架構分析
- 常量字串分析
- 程式啟動速度優化
- 學習經典的資料結構
希望你有所收穫!下篇我們主要圍繞dyld講動態載入過程,see you~
參考文章: