熟悉Linux
和windows
開(kāi)發(fā)的同學(xué)都知道,ELF
是Linux
下可執(zhí)行文件的格式,PE32/PE32+
是windows
下可執(zhí)行文件的格式,下面我們要講的就OSX
和iOS
環(huán)境下可執(zhí)行文件的格式:mach-o
。
Mach-O 是 Mach object 文件格式的縮寫(xiě),它是一種用于記錄可執(zhí)行文件、對(duì)象代碼、共享庫(kù)、動(dòng)態(tài)加載代碼和內(nèi)存轉(zhuǎn)儲(chǔ)的文件格式。作為 .out 格式的替代品,Mach-O 提供了更好的擴(kuò)展性,并提升了符號(hào)表中信息的訪問(wèn)速度(from wiki)。
分析工具
- MachOView工具可以在Mac平臺(tái)上查看MachO文件格式信息,是一種可視化的方式,能更直觀的方便我們查看;
-
otool是命令行工具,該工具是
Xcode
自帶的,路徑是:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool
,具體操作可以使用man otool
查看。
結(jié)構(gòu)
從蘋(píng)果的官方文檔中我們可以知道,mach-o
文件主要包括以下三個(gè)區(qū)域:
-
Header
頭部區(qū)域,主要包括mach-o
文件的一些基本信息; -
LoadCommands
加載命令,是由多個(gè)Segment command
組成的; -
Data
包含Segement的具體數(shù)據(jù)。
下面我們通過(guò)工具看下具體區(qū)域的結(jié)構(gòu)。
Header
Header
位于mach-o
文件的開(kāi)始位置,不管是在32位架構(gòu)還是64位架構(gòu)。根據(jù)??開(kāi)源的代碼中我們可以看到以下數(shù)據(jù)結(jié)構(gòu)(下面的代碼默認(rèn)都是以64位為例):
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
首先我們通過(guò)otool
工具查看Alipay
的header
結(jié)構(gòu):
? ~ otool -hv /Alipay/Payload/AlipayWallet.app/AlipayWallet
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC ARM V7 0x00 EXECUTE 75 7524 NOUNDEFS DYLDLINK TWOLEVEL BINDS_TO_WEAK
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 75 8240 NOUNDEFS DYLDLINK TWOLEVEL BINDS_TO_WEAK PIE
二進(jìn)制文件包含arm64
和armv7s
兩個(gè)指令集,就是我們通常聽(tīng)到的胖二進(jìn)制結(jié)構(gòu)
或者多重指令集。下面我們對(duì)header的結(jié)構(gòu)做一個(gè)大致的分析:
-
magic
: 表示的是mach-o
文件的魔數(shù)#define MH_MAGIC_64 0xfeedfacf
。 -
cputype
和cupsubtype
: 標(biāo)識(shí)的是cpu的類型和其子類型,具體定義可以查看mach/machine.h
文件中的定義。
#define CPU_TYPE_I386 CPU_TYPE_X86 /* compatibility */
#define CPU_TYPE_X86_64 (CPU_TYPE_X86 | CPU_ARCH_ABI64)
#define CPU_TYPE_MC98000 ((cpu_type_t) 10)
#define CPU_TYPE_HPPA ((cpu_type_t) 11)
#define CPU_TYPE_ARM ((cpu_type_t) 12)
#define CPU_TYPE_ARM64 (CPU_TYPE_ARM | CPU_ARCH_ABI64)
-
filetype
文件類型,比如可執(zhí)行文件、庫(kù)文件、Dsym文件等,定義如下所示:
* Constants for the filetype field of the mach_header
*/
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static */
/* linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug */
/* sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
-
ncmds
:load commands
的個(gè)數(shù); -
sizeofcmds
:所有的load commands
的大小。 -
flags
:標(biāo)志位,定義有很多,具體可以看下mach-o/load.h
里面的定義,下面列出了上面Alipay
打印出來(lái)的header的flag:
#define MH_NOUNDEFS 0x1 /* 該文件沒(méi)有未定義的符號(hào)引用 */
#define MH_DYLDLINK 0x4 /* 該文件是dyld的輸入文件,無(wú)法被再次靜態(tài)鏈接 */
#define MH_TWOLEVEL 0x80 /* 該鏡像文件使用2級(jí)名稱空間 */
#define MH_BINDS_TO_WEAK 0x10000 /* 最后鏈接的鏡像文件使用弱符號(hào) */
#define MH_PIE 0x200000 /* 加載程序在隨機(jī)的地址空間,只在MH_EXECUTE中使用 */
二級(jí)名稱空間
這是dyld的一個(gè)獨(dú)有特性,說(shuō)是符號(hào)空間中還包括所在庫(kù)的信息,這樣子就可以讓兩個(gè)不同的庫(kù)導(dǎo)出相同的符號(hào),與其對(duì)應(yīng)的是平坦名稱空間。
Load Commands
load command
結(jié)構(gòu)緊跟著header
結(jié)構(gòu)的后面,并且它們?cè)谔摂M內(nèi)存中指定文件的邏輯結(jié)構(gòu)和文件的布局。 每個(gè)加載命令從指定命令類型和命令數(shù)據(jù)大小的字段開(kāi)始。
load command
的結(jié)構(gòu)如下所示:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
-
cmd
:load command
的類型,具體支持的類型如下圖所示; -
cmdsize
: 所有command
的大小。
下面列舉常見(jiàn)的command類型及用途:
命令類型 |
用途 |
---|---|
LC_UUID | 為鏡像文件或者dSYM 文件指定一個(gè)128位的UUID |
LC_SEGMENT | 定義該文件的一段,以映射到加載該文件的進(jìn)程的地址空間中。 它還包括該段所包含的所有sections 部分 |
LC_SEGMENT_64 | 同上,映射到64位的segment
|
LC_SYMTAB | 符號(hào)表,當(dāng)鏈接文件時(shí),靜態(tài)和動(dòng)態(tài)鏈接器都使用此信息,還可以通過(guò)調(diào)試器將符號(hào)映射到生成符號(hào)的原始源代碼文件 |
LC_DYSYMTAB | 動(dòng)態(tài)鏈接器使用的附加符號(hào)表信息 |
LC_THREAD LC_UNIXTHREAD | 對(duì)于一個(gè)可執(zhí)行文件,LC_UNIXTHREAD 定義了主線程初始化的線程狀態(tài) |
LC_LOAD_DYLIB | 定義此文件鏈接的動(dòng)態(tài)共享庫(kù)的名稱 |
LC_ID_DYLIB | 指定安裝的動(dòng)態(tài)共享庫(kù)名字 |
LC_PREBOUND_DYLIB | 對(duì)于共享庫(kù),該可執(zhí)行文件與Prebound對(duì)齊,指定共享庫(kù)中使用的模塊 |
LC_LOAD_DYLINKER | 指定內(nèi)核執(zhí)行加載此文件的動(dòng)態(tài)鏈接器 |
LC_ID_DYLINKER | 標(biāo)識(shí)這個(gè)文件為動(dòng)態(tài)鏈接器 |
LC_ROUTINES | 包含共享庫(kù)初始化例程的地址 |
LC_ROUTINES_64 | 同上,64位 |
LC_TWOLEVEL_HINTS | 包含兩級(jí)命名空間查詢提示表 |
LC_SUB_FRAMEWORK | 標(biāo)識(shí)這個(gè)文件為一個(gè)umbrella framework 的subframework 的實(shí)現(xiàn),umbrella framework 的名字存儲(chǔ)在字符串參數(shù)中 |
LC_SUB_UMBRELLA | 標(biāo)識(shí)這個(gè)文件是umbrella framework 的一個(gè)subumbrella
|
這些加載命令在Mach-O文件加載解析時(shí),被內(nèi)核加載器或者動(dòng)態(tài)鏈接器調(diào)用,指導(dǎo)如何設(shè)置加載對(duì)應(yīng)的二進(jìn)制數(shù)據(jù)段,具體定義可以查看/usr/include/mach-o/loader.h
文件或者最后列出的??文檔。
可以使用命令otool -v -l AlipayWallet
查看APP當(dāng)前的所有的load command
。
段數(shù)據(jù)(Segments)
在上面load command
中有定義LC_SEGMENT
和LC_SEGMENT_64
兩種類型,它們實(shí)際上就是標(biāo)識(shí)的segments
,我們可以看下Alipay
的結(jié)構(gòu):
數(shù)據(jù)結(jié)構(gòu)定義:
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
每一個(gè)segment定義了一些Mach-O文件的數(shù)據(jù)、虛擬地址、文件偏移和內(nèi)存保護(hù)等屬性,這些數(shù)據(jù)在動(dòng)態(tài)鏈接器加載程序時(shí)被映射到了虛擬內(nèi)存中。每個(gè)段都有不同的功能,主要包括如下幾種類型:
- __PAGEZERO: 空指針陷阱段,映射到虛擬內(nèi)存空間的第一頁(yè),用于捕捉對(duì)NULL指針的引用,沒(méi)有保護(hù)標(biāo)志位;
- __TEXT: 包含了執(zhí)行代碼以及其他只讀數(shù)據(jù);
- __DATA: 包含了程序數(shù)據(jù),該段可讀寫(xiě);
- __LINKEDIT: 含有為動(dòng)態(tài)鏈接庫(kù)使用的原始數(shù)據(jù),比如符號(hào),字符串,重定位表?xiàng)l目等等, 該段可讀寫(xiě)。
- _RODATA:包含objc類名、方法等列表。
一般情況下__TEXT
和__DATA
段又分為幾種section
。
section
括號(hào)前面大寫(xiě)的字母代碼segment
的名稱,后面小寫(xiě)字母代表section
名稱。
下面列出__TEXT
和__DATA
下的section:
__TEXT section | 用途 |
---|---|
__text | 程序代碼 |
__stubs | 用于動(dòng)態(tài)鏈接的存根 |
__stub_helper | 用于動(dòng)態(tài)鏈接的存根 |
__const | 程序中const關(guān)鍵字修飾的常量變量 |
__objc_methname | objc方法名 |
__cstring | 程序中硬編碼的ANSI的字符串 |
__objc_classname | objc類名 |
__objc_methtype | objc方法類型 |
__gcc_except_tab | 異常處理相關(guān) |
__ustring | unicode字符串 |
__unwind_info | 異常處理 |
__eh_frame | 異常處理 |
__DATA section | 用途 |
---|---|
__nl_symbol_ptr | 動(dòng)態(tài)符號(hào)鏈接相關(guān),指針數(shù)組 |
__got | 全局偏移表, Global Offset Table |
__la_symbol_ptr | 動(dòng)態(tài)符號(hào)鏈接相關(guān),也是指針數(shù)組,通過(guò)dyld_stub_binder輔助鏈接 |
__mod_init_func | 初始化的全局函數(shù)地址,會(huì)在main之前被調(diào)用 |
__const | const修飾的常量 |
__cstring | 程序中硬編碼的ANSI的字符串 |
__cfstring | CF用到的字符串 |
__objc_classlist | objc類列表 |
__objc_nlclslist | objcload 方法列表 |
__objc_catlist | objc category 列表 |
__objc_protolist | objc protocol 列表 |
__objc_imageinfo | 鏡像信息 |
__objc_const | objc的常量 |
__objc_selrefs | objc引用的SEL 列表 |
__objc_protorefs | objc引用的protocol 列表 |
__objc_classrefs | objc引用的class 列表 |
__objc_superrefs | objc父類的引用列表 |
__objc_ivar | objcivar 信息 |
__objc_data |
class 信息 |
__bss | 未初始化的靜態(tài)變量區(qū) |
__data | 初始化的可變變量 |
隨機(jī)地址空間
進(jìn)程每一次啟動(dòng),地址空間都會(huì)簡(jiǎn)單地隨機(jī)化。該方式對(duì)APP的安全性具有重大的意義。如果采用傳統(tǒng)的方式,程序的每一次啟動(dòng)的虛擬內(nèi)存鏡像都是一致的,黑客很容易采取重寫(xiě)內(nèi)存的方式來(lái)破解程序。采用ASLR
可以有效的避免黑客的中間人攻擊等。
dyld
dyld動(dòng)態(tài)鏈接器,全名是dynamic link editer
,當(dāng)內(nèi)核執(zhí)行LC_DYLINK時(shí),連接器會(huì)啟動(dòng),查找進(jìn)程所依賴的所有的動(dòng)態(tài)庫(kù),并加載到內(nèi)存中,當(dāng)然,??內(nèi)部對(duì)這個(gè)步驟做了很多優(yōu)化,有興趣的可以查看WWDC
相關(guān)的視頻。
用途
上面我們列舉了mach-o
文件格式的組成部分及一些主要結(jié)構(gòu),通過(guò)分析我們的APP的可執(zhí)行文件,可以幫助我們更深層次的了解APP的一些信息,也可以做一些hack的事情,比如微信之前有出一篇博文,講的就是通過(guò)LinkMap
文件進(jìn)行iOS APP瘦身。這里注意,如果不使用LinkMap
文件而是APP可執(zhí)行文件的話,一定要是debug編譯出來(lái)的,因?yàn)閞elease編譯的話是已經(jīng)優(yōu)化過(guò)的二進(jìn)制文件,沒(méi)有我們想要的一些信息。
查找無(wú)用selector
以往C++在鏈接時(shí),沒(méi)有被用到的類和方法是不會(huì)編進(jìn)可執(zhí)行文件里。但Objctive-C不同,由于它的動(dòng)態(tài)性,它可以通過(guò)類名和方法名獲取這個(gè)類和方法進(jìn)行調(diào)用,所以編譯器會(huì)把項(xiàng)目里所有OC源文件編進(jìn)可執(zhí)行文件里,哪怕該類和方法沒(méi)有被使用到。
結(jié)合LinkMap文件的__TEXT.__text
,通過(guò)正則表達(dá)式([+|-][.+\s(.+)])
,我們可以提取當(dāng)前可執(zhí)行文件里所有objc類方法和實(shí)例方法(SelectorsAll)。再使用otool
命令otool -v -s __DATA __objc_selrefs
逆向__DATA.__objc_selrefs
段,提取可執(zhí)行文件里引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll里哪些方法是沒(méi)有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系統(tǒng)API的Protocol可能被列入無(wú)用方法名單里,如UITableViewDelegate
的方法,我們只需要對(duì)這些Protocol里的方法加入白名單過(guò)濾即可。
另外第三方庫(kù)的無(wú)用selector也可以這樣掃出來(lái)的。
查找無(wú)用oc類
查找無(wú)用oc類有兩種方式,一種是類似于查找無(wú)用資源,通過(guò)搜索"[ClassName alloc/new"、"ClassName *"、"[ClassName class]
"等關(guān)鍵字在代碼里是否出現(xiàn)。另一種是通過(guò)otool命令逆向__DATA.__objc_classlist
段和__DATA.__objc_classrefs
段來(lái)獲取當(dāng)前所有oc類和被引用的oc類,兩個(gè)集合相減就是無(wú)用oc類,具體分析如下:
通過(guò)命令行:otool -arch arm64 -o AlipayWallet > ~/Desktop/all.txt
保存所有的信息。
打開(kāi)文件可以看到類似下面的數(shù)據(jù):
/// 所有的信息
AlipayWallet:
Contents of (__DATA,__objc_classlist) section
00000001039d6d60 0x10442e250
isa 0x10442e228
superclass 0x10442e7a0
cache 0x0
vtable 0x0
data 0x1039f2378 (struct class_ro_t *)
flags 0x90
instanceStart 8
instanceSize 8
reserved 0x0
ivarLayout 0x0
name 0x104c5d014 APSettingsCanSchemeHandleParse
baseMethods 0x1039f2358 (struct method_list_t *)
entsize 24
count 1
name 0x10473c037 doParseURL:
types 0x104ca59c7 @24@0:8@16
imp
baseProtocols 0x0
ivars 0x0
weakIvarLayout 0x0
baseProperties 0x0
Meta Class
isa 0x0
superclass 0x10442e778
cache 0x0
vtable 0x0
data 0x1039f2310 (struct class_ro_t *)
flags 0x91 RO_META
instanceStart 40
instanceSize 40
reserved 0x0
ivarLayout 0x0
name 0x104c5d014 APSettingsCanSchemeHandleParse
baseMethods 0x0 (struct method_list_t *)
baseProtocols 0x0
ivars 0x0
weakIvarLayout 0x0
baseProperties 0x0
......
debug編譯出來(lái)的可執(zhí)行文件objc_classrefs
段:
...
0000000101ba2340 0x101befcb0 _OBJC_CLASS_$_QQObjectPasteboard
0000000101ba2348 0x101befd28 _OBJC_CLASS_$_QQArrayPasteboard
0000000101ba2350 0x101befe68 _OBJC_CLASS_$_QQApiURLDecoder
0000000101ba2358 0x101bf0200 _OBJC_CLASS_$_QQWebViewKit
0000000101ba2360 0x101befe40 _OBJC_CLASS_$_QQApiURLEncoder
0000000101ba2368 0x101befff8 _OBJC_CLASS_$_GetMessageFromQQReq
0000000101ba2370 0x101bf0048 _OBJC_CLASS_$_GetMessageFromQQResp
0000000101ba2378 0x101bf0138 _OBJC_CLASS_$_ShowMessageFromQQReq
0000000101ba2380 0x101bf0188 _OBJC_CLASS_$_ShowMessageFromQQResp
...
release編譯出來(lái)的可執(zhí)行文件objc_classrefs
段:
...
00000001043dc060 0x0 _OBJC_CLASS_$_NSDictionary
00000001043dc068 0x0 _OBJC_CLASS_$_NSNotificationCenter
00000001043dc070 0x10442e6b0
00000001043dc078 0x10442e340
00000001043dc080 0x10442e5c0
00000001043dc088 0x10442e610
00000001043dc090 0x10442e660
00000001043dc098 0x10442e520
00000001043dc0a0 0x0 _OBJC_CLASS_$_NSBundle
...
其中__objc_classlist
section保存了所有的類信息,objc_classrefs
section中保存了使用的類信息,objc_classrefs
第二列是代表偏移量,和__objc_classlist
中每個(gè)類開(kāi)頭00000001039d6d60 0x10442e250
第二列的值是相同的。所以我們可以通過(guò)腳本找出所有的類和所有使用的類,兩者相減就是沒(méi)有使用的類。
針對(duì)上面兩個(gè)小的功能,寫(xiě)了一個(gè)小的ruby腳本,自動(dòng)的找出沒(méi)有使用的selector
和class
,具體的使用及代碼大家可以查看我的GitHub.