探究Mach-O文件

較爲詳細的解析了Mach-O文件格式,並着重闡述了動態連接相關的知識點,開始吧~saonian~html

簡述

進程是可執行文件在內存中加載獲得的結果,這種文件必須是操做系統理解的格式,這樣操做系統才能解析文件,簡歷所須要的依賴(如庫),初始化運行環境並執行。ios

Mach-O(Mach Object File Format)是macOS上的可執行文件,Linux和大部分Unix系統採用的是原生格式 ELF(Extensible Firmware Interface),windows支持的格式爲PE32/PE32+macOS支持三種可執行文件格式:解釋器腳本文件、通用二進制格式和Mach-O格式,以下圖所示:git

可執行格式 magic 用途
腳本 \x7FELF 主要用於 shell 腳本,可是也經常使用語其餘解釋器,如 Perl, AWK 等。也就是咱們常見的腳本文件中在 #! 標記後的字符串,即爲執行命令的指令方式,以文件的 stdin 來傳遞命令
通用二進制格式 0xcafebabe 0xbebafeca 包含多種架構支持的二進制格式,只在 macOS 上支持
Mach-O 0xfeedface(32 位) 0xfeedfacf(64 位) macOS 的原生二進制格式

通用二進制格式(Universal Binary)也稱爲「胖二進制格式(Fat Binary)」,主要是解決歷史問題,以支持Power PC(PPC)架構以及Inter架構,是一種對多架構的二進制文件的打包集合。github

其中常見的包括:可執行文件、動態庫文件、動態連接器等都是Mach-O格式,具體可經過file命令查看具體的可執行文件格式,以下圖:shell

Mach-O文件格式

其結構以下圖所示,主要包括四部分組成:windows

img
  • Header頭部緩存

    描述了該文件的CPU類型、文件類型、加載命令等信息;數據結構

  • Load commands加載命令架構

    描述了文件中數據的具體組織結構,不一樣數據類型如何使用不一樣的加載命令表示;app

  • Data數據段

    存放了包括代碼、字符常量、類、方法等代碼和數據,而且擁有多個Segment段,每一個Segment段都包含零到多個Section節;

  • Loader info連接信息及其餘

    文件末端包含了一系列連接信息,如動態連接器用來連接可執行文件或者依賴所需使用的符號表、字符串表等,以及簽名信息等;

爲什麼Segment段中存在Section節?

分段的目的主要:不一樣段可被映射到不一樣虛擬存儲區域,便於讀寫權限管理;利用現代CPU緩存體系及程序的局部性原理,將指令和數據緩存分離有利用提高緩存命中率;指令或數據共享,有利於提高內存空間利用率。而分節主要是能夠不徹底按照page的大小進行內存對齊,提高內存空間利用率。

Header

Mach-O文件頭部具體的數據結構以下(區分32位和64位架構):

//32bit
struct mach_header {
    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 */
};
//64bit
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 */
};
複製代碼

32位和64位架構頭部結構沒有大的區別,只是64位多了一個保留字段,具體的字段名稱以下:

  • magic:魔數,用於確認該文件是32位仍是64位

  • cputype,CPU類型,如armx86_64

  • cpusubtype,CPU具體類型,如arm64armv7

  • filetype,文件類型,如可執行文件、庫文件、動態連接器、符號文件和調試信息等,其中MH_EXECUTE表明可執行文件,具體的文件類型定義以下:

    /* 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 */
    #define MH_DSYM 0xa /* companion file with only debug */
    #define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
    複製代碼
  • ncmd,加載命令條數

  • sizeofcmds,全部加載命令在文件中佔用地址空間大小

  • reserved,保留字段

  • flags,標誌位,具體的定義以下:

    #define MH_NOUNDEFS 0x1 // 目前沒有未定義的符號,不存在連接依賴
    #define MH_DYLDLINK 0x4 // 該文件是dyld的輸入文件,沒法被再次靜態連接
    #define MH_PIE 0x200000 // 加載程序在隨機的地址空間,只在 MH_EXECUTE中使用
    #define MH_TWOLEVEL 0x80 // 兩級名稱空間
    複製代碼

除了用MachOView能查看MachO文件信息,還能夠經過otool命令查看,咱們先來分析Header中的內容:otool -h xxx來查看。

Load commands

Load commands緊跟在頭部以後(以下圖),這些加載指令清晰地告訴加載器如何處理二進制數據,有些命令是由內核處理的,有些是由動態連接器處理的,常見的加載命令以下:

  • LC_SEGMENT/LC_SEGMENT_64: 將該段(32/64位)映射到進程地址空間中,包含了Segment中全部Section加載信息;

    其中_PAGEZERO段不具備訪問權限,用來處理空指針,其值爲0;TEXT爲代碼段,_DATA/_DATA_CONST爲可讀寫的數據段;_LINKEDIT連接段包含了一些符號表、間接符號表、rebase操做碼、綁定操做碼、導出符號、函數啓動信息、數據表、代碼簽名、字符串表等數據,該加載命令下沒有Section,須要配合LC_SYMTAB來解析symbol tablestring table

    _LINKEDIT加載命令信息中的文件偏移爲0x4000(十進制16384)正好對應Dynamic Loader Info起始地址,文件大小爲0x5840(十進制22592)=0x9840(0x9830+10)-0x4000,正好對應從Dynamic Loader Info到文件末尾的數據部分;

  • LC_DYLD_INFO_ONLY:加載動態連接庫信息(重定向地址、弱引用綁定、懶加載綁定、開放函數等的偏移值等信息)

  • LC_SYMTAB:載入符號表地址

  • LC_DYSYMTAB:載入動態符號表地址

  • LC_LOAD_DYLINKER:加載動態加載庫

  • 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:獲取應用簽名信息

具體的加載命令的數據結構以下(64位格式,與32位格式差異不大):

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 */
};
複製代碼
  • cmd:就是Load commands的類型,這裏LC_SEGMENT_64表明將文件中64位的段映射到進程的地址空間;
  • cmdsize:表明load command的大小
  • segment name:段的名稱
  • VM Address :段的虛擬內存地址
  • VM Size : 段的虛擬內存大小
  • file offset:段在文件中偏移量
  • file size:段在文件中的大小
  • nsects:標示了Segment中有多少secetion

除了使用MachOView查看,還能夠經過otool -l xxx查看,以下圖所示:

對於段的地址大小可經過size -l -m xxx查看,以下圖:

下面重點闡述幾個重要的加載命令,便於後續理解整個程序啓動、動態加載、逆向等知識點。

LC_SEGMENT_64(__PAGEZERO)

該加載命令的內容以下圖所示:

其中虛擬地址範圍爲0x0~0x100000000正好對應4GB空間,該文件的起始虛擬地址空間也是從0x100000000開始,即全部代碼和數據都是被加載到4GB以後的地址。對應的文件內容大小爲0,即在該文件中不佔用實際空間,且具備不可讀寫不可執行權限,這樣內核就能夠識別到空指針或指針截斷的錯誤的範圍該地址空間的調用而拋出段異常,如EXC_BAD_ACCESS異常。

LC_SEGMENT_64(__LINKEDIT) & LC_DYLD_INFO_ONLY

__LINKEDIT包含了動態連接相關的信息,如虛擬地址空間地址及文件偏移、文件權限等,而LC_DYLD_INFO_ONLY加載命令,包含了重定位、綁定及導出等偏移/大小信息。

LC_SYMTAB

對於LC_SYMTAB加載命令,其數據結構定義以下:

struct symtab_command {
    uint32_t cmd;     /* LC_SYMTAB */
    uint32_t cmdsize; /* sizeof(struct symtab_command) */
    uint32_t symoff;  /* symbol table offset */
    uint32_t nsyms;   /* number of symbol table entries */
    uint32_t stroff;  /* string table offset */
    uint32_t strsize; /* string table size in bytes */
};
複製代碼

這個命令告訴了連接器(包括靜態連接器或動態連接器)Symbol TableString Table的位置及大小信息。

其中符號的結構由內核定義,以下:

struct nlist_64 {
    union {
        uint32_t n_strx;   /* index into the string table */
    } n_un;
    uint8_t  n_type;       /* type flag, see below */
    uint8_t  n_sect;       /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};
複製代碼
  • n_un,符號的名字在字符串表中的序號(在一個 Mach-O 文件裏,具備惟一性)
  • n_sect,符號所在的 section index(內部符號有效值從 1 開始,最大爲 255;外部符號爲0)
  • n_value,符號的地址值(在連接過程當中,會隨着其 section 發生變化)
  • n_type是一個 8 bit 的複合字段:
    • bit[5:8]: 若是不爲 0,表示這是一個與調試有關的符號,值意義類型詳見mach-o/stab.h
    • bit[4:5]: 若爲 1,則表示該符號是私有的(外部符號)
    • bit[1:4]: 符號類型
      • N_UNDF (0x0): 未定義
      • N_ABS (0x2): 符號地址指向到絕對地址,連接器後期不會再修改
      • N_SECT (0xe): 本地符號,即符號定義於當前 Mach-O
      • N_PBUD (0xc): 預綁定符號
      • N_INDR (0xa): 表示該符號和另外一個符號是同一個,n_value指向到 string table,即該同名符號的名字
    • bit[0:1]: 表示這是外部符號,即該符號要麼定義在外部,要麼定義在本地可是能夠被外部使用;
LC_DYSYMTAB

對於LC_DYSYMTAB加載命令,其數據結構以下:

struct dysymtab_command {
    uint32_t cmd;	/* LC_DYSYMTAB */
    uint32_t cmdsize;	/* sizeof(struct dysymtab_command) */
    uint32_t ilocalsym;	/* index to local symbols */
    uint32_t nlocalsym;	/* number of local symbols */
    uint32_t iextdefsym;/* index to externally defined symbols */
    uint32_t nextdefsym;/* number of externally defined symbols */
    uint32_t iundefsym;	/* index to undefined symbols */
    uint32_t nundefsym;	/* number of undefined symbols */
    uint32_t tocoff;	/* file offset to table of contents */
    uint32_t ntoc;	/* number of entries in table of contents */
    uint32_t modtaboff;	/* file offset to module table */
    uint32_t nmodtab;	/* number of module table entries */
    uint32_t extrefsymoff;	/* offset to referenced symbol table */
    uint32_t nextrefsyms;	/* number of referenced symbol table entries */
    uint32_t indirectsymoff; /* file offset to the indirect symbol table */
    uint32_t nindirectsyms;  /* number of indirect symbol table entries */
    uint32_t extreloff;	/* offset to external relocation entries */
    uint32_t nextrel;	/* number of external relocation entries */
    uint32_t locreloff;	/* offset to local relocation entries */
    uint32_t nlocrel;	/* number of local relocation entries */
};
複製代碼

主要包含了本地、外部符號、未定義外部符號、間接符號表的位置及數目,其中indriectsymoff指定了Dynamic Symbol Table的文件偏移位置及數目;

可以使用otool -I xxx來獲取間接符號表內容;

其中間接符號包含了符號名、符號所處的節及符號間接地址,其所處的Section處在__stubs__got、及__la_symbol_ptr等節;

對於後續須要動態連接定位的符號頭部,如LC_SEGMENT_64中的_TEXT.__stubs_DATA_CONST.__got_DATA.__la_symbol_ptr,其頭部字段中包含了Indirect Sym Index(Reserverd1)字段,該字段指明在Indirect Symbol Table間接符號表中的條目序號,以下圖:

_la_symbol_ptr中的符號在間接符號表中的起始條目序號爲26。

LC_LOAD_DYLINKER

該加載命令包含了重要的程序啓動動態連接器的路徑,以下圖x86_64的爲/usr/lib/dyld

Segment & Section

Section的數據結構

struct section { /* for 32-bit architectures */
    char        sectname[16];    /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint32_t    addr;        /* memory address of this section */
    uint32_t    size;        /* size in bytes of this section */
    uint32_t    offset;        /* file offset of this section */
    uint32_t    align;        /* section alignment (power of 2) */
    uint32_t    reloff;        /* file offset of relocation entries */
    uint32_t    nreloc;        /* number of relocation entries */
    uint32_t    flags;        /* flags (section type and attributes)*/
    uint32_t    reserved1;    /* reserved (for offset or index) */
    uint32_t    reserved2;    /* reserved (for count or sizeof) */
};
複製代碼
  • sectname:好比_textstubs
  • segname :section所屬的segment,好比_TEXT
  • addr :section在內存的起始位置
  • size:section的大小
  • offset:section的文件偏移
  • align :字節大小對齊
  • reloff :重定位入口的文件偏移
  • nreloc: 須要重定位的入口數量
  • flags:包含sectiontypeattributes

常見的 Section以下表所示:

Section 用途
_TEXT.__text 主程序代碼
_TEXT.__cstring C 語言字符串
_TEXT.__const const 關鍵字修飾的常量
_TEXT.__stubs 用於 Stub 的佔位代碼,不少地方稱之爲樁代碼,用於重定向到 lazynon-lazy 符號的 section,被標記爲 S_SYMBOL_STUBSTEXT Segment 裏代碼和 dylib 外部符號的引用地址對函數符號的引用都指向了 stubs。其中每項都是 jmp 代碼間接尋址,可跳到la_symbol_ptr Section 中。
_TEXT.__stubs_helper Stub 沒法找到真正的符號地址後的最終指向
_TEXT.__objc_methname Objective-C 方法名稱
_TEXT.__objc_methtype Objective-C 方法類型
_TEXT.__objc_classname Objective-C 類名稱
_TEXT.__eh_frame 調試輔助信息
_TEXT.__unwind_info 用於存儲處理異常狀況信息
_DATA.__data 初始化過的可變數據
_DATA.__la_symbol_ptr lazy binding 的指針表,表中的指針一開始都指向 __stub_helper
_DATA.nl_symbol_ptr lazy binding 的指針表,每一個表項中的指針都指向一個在裝載過程當中,被動態鏈機器搜索完成的符號
_DATA.__got 全局偏移表
_DATA.__const 沒有初始化過的常量
_DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs
_DATA.__bss BSS,存放爲初始化的全局變量,即常說的靜態內存分配
_DATA.__common 沒有初始化過的符號聲明
_DATA.__mod_init_func 初始化函數,在main以前調用
_DATA.__mod_term_func 終止函數,在main返回以後調用
_DATA.__objc_classlist Objective-C 類列表
_DATA.__objc_protolist Objective-C 原型
_DATA.__objc_imginfo Objective-C 鏡像信息
_DATA.__objc_selfrefs Objective-C self 引用
_DATA.__objc_protorefs Objective-C 原型引用
_DATA.__objc_superrefs Objective-C 超類引用
_DATA.__got

對於_DATA.__got節,其內容以下圖所示:

其相似一個表,每一個條目是一個地址值,定義的是Non-Lazy Symbol Pointers即非懶加載符號地址,全部條目的內容都是0。其引入的目的是解決程序在連接階段存放不能肯定目的地址的符號,當鏡像被加載時,動態連接器dyld會對每一個條目對應的符號進行重定位,將其真正的地址寫入,做爲條目的內容。對於dyld如何肯定符號信息的,能夠經過上面的Indirect Symbol Table中的符號看出,包含了符號名稱、間接符號地址。

_DATA.__la_symbol_ptr

與之對應的是_DATA.__la_symbol_ptr節,其內容以下圖所示:

其實際內容都指向了_TEXT.__stub_helper節,最終經過jumpq指令跳轉到了dyld_stub_binder符號,即__got節中的Non_Lazy Symbol Pointer中的條目,該符號爲一個函數,定義於dyld_stub_binder.S,由 dyld 提供。

dyld_stub_binder函數其大體邏輯是:內部會尋找鎖調用符號的真實地址,並寫入_la_symbol_ptr條目中,而後跳轉到真實地址執行;

_TEXT.__stubs

對於_TEXT.__stubs節,其內容以下:

該內容也是一個表,每一個條目都是一段數據,稱爲「符號樁」。經過otool -v xx -s _TEXT __stubs命令查看內容以下:

其內容都是jmpq跳轉指令,跳轉的地址以第一條地址爲例計算:

0x100003000 = 0x100001dbc(rip) + 0x1244
複製代碼

該地址指向的是__la_symbol_ptr節,而該節最終都指向了dyld_stub_binder

Loader info

連接加載信息包含了動態加載信息Dynamic Loader Info(包含了重定向地址、弱引用綁定、懶加載綁定、開放函數等的偏移值等信息,其加載命令爲LC_DYLD_INFO_ONLY),函數起始地址表Function Starts(其加載命令爲LC_FUNCTION_STARTS),符號表Symbol Table,動態符號表Dynamic Symbol Table,代碼段非指令表Data in Code Table,字符串表String Table(以空值爲終止符)及代碼簽名Code Signature,以下圖所示:

Dynamic Loader Info

因爲地址空間隨機化技術(ddress space layout randomization, ASLR)和地址無關可執行技術(position-indendent excutable, PIE),使得程序在內存的加載地址是隨機的,所以須要程序在動態連接階段將內部地址進行修正。Rebase 數據描述了哪些是對指向 MachO 內部的引用並將其修正,而 Bind 數據描述哪些是指向外部的引用並進行修正。Lazy Bind 數據描述了哪些符號須要延遲綁定,即僅在第一次使用時纔會綁定,不會在啓動時進行,提升啓動效率;Export數據描述了對外可見的符號。其內容都是以操做數(Opcodes)、當即數(immediate)以及採用uleb128/sleb128編碼的偏移值組成。

PIE(position-independent executable)是一種生成地址無關可執行程序的技術。若是編譯器在生成可執行程序的過程當中使用了PIE,那麼當可執行程序被加載到內存中時其加載地址存在不可預知性。PIE還有個孿生兄弟PIC(position-independent code)。其做用和PIE相同,都是使被編譯後的程序可以隨機的加載到某個內存地址。區別在於PIC是在生成動態連接庫時使用(Linux中的so),PIE是在生成可執行文件時使用。

Rebase舉例,其協議和操做就是找到地址後將其值加上偏移便可,具體的獲取操做數和當即數是經過REBASE_OPCODE_MASK(0xF0)REBASE_IMMEDIATE_MASK(0x0F)對數據進行與&操做,如0x100004000的數據字節0x11,其操做數爲0x10=0x11&0xF0對應的是REBASE_OPCODE_SET_TYPE_IMM,當即數0x01=0x11&0x0Ftype=1(REBASE_TYPE_POINTER),具體的操做數及當即數對應的邏輯可查閱dyld源碼。

注意:MachOView中標註的Actions存在誤導性,重定位、綁定等操做都是按照字節數據順序讀取並操做直至完整的讀取完全部的數據,其標註具體緣由未知,待確認補充!

Dynamic Symbol Table

對於Dynamic Symbol Table中的Indirect Symbols其內容爲一個表,每一個條目的內容爲其在Symbol Table中的序號,以下圖:

其內容爲0x3c=60,對應的就是符號表第60個符號,經過符號表中的起始地址0x4380,每一個符號佔用0x10,則0x4740=0x4380+0x10*0x3c,對應的就是_CFRunLoopAddSource符號地址。

String Table

對於字符串表String Table中內容爲全部的符號名稱,每一個名稱中間經過空字符串間隔,以下圖所示:

string table

Symbol Table中的String Table Index字段就是字符串表中對應的第index個字符串。

Reference

趣探 Mach-O:文件格式分析

MachO 文件結構詳解

Mach-O 文件格式探索

深刻剖析Macho (1)

深刻理解Macho文件(二)- 消失的__OBJC段與新生的_DATA段

《深刻理解Mac OSX & iOS操做系統》

loader.h

Mach-O 與動態連接

Mach-O 與靜態連接

Apple 操做系統可執行文件 Mach-O

iOS 應用的啓動過程

APP漏洞掃描器之未使用地址空間隨機化

相關文章
相關標籤/搜索