[iOS 逆向 5] 逆向儲備

本文主要介紹一些 iOS / Mac OS X 操作系統的東西,比如 DYLD,Mach-O,ARM 彙編。內容很枯燥。

ARM 彙編

逆向分析別人的應用時,因爲我們肯定不可能直接拿到別人的源代碼,所以只能從可執行文件,也就是機器代碼下手。先用工具將其反彙編爲彙編代碼,然後通過分析彙編代碼來了解程序邏輯。因此需要掌握分析的前提——ARM 彙編。

介紹

在 iOS 設備和幾乎所有的移動設備上,使用的都是基於 arm 架構的處理器,含 arm/thumb 指令集。根據 arm 架構的不同版本,分爲:armv6,armv7,armv7s,armv8(arm64)。在模擬器上,由於模擬器是運行在 Intel 處理器的電腦上,使用的是 x86 的指令集,所以底層庫一般都會爲不同架構寫兼容代碼。而普通開發者發佈 App 時,只支持 arm64 架構就夠了,因爲現在支持最高 armv7s 的 iPhone 5 早已淘汰了。arm64 架構的處理器爲兼容舊程序,提供 64 位運行狀態 AArch64 和 32 位運行狀態 AArch32,分別使用 64/32 位的地址、寄存器、指令集。下面也只介紹 AArch64 下的內容。

寄存器

逆向分析中常見的寄存器包括:

R0~R30 是 31 個 64 位通用寄存器,使用時,通過 X0-X30 訪問;通過 W0-W30 只訪問其低 32 位,即當作 32 位寄存器使用。
SP,Stack Pointer,保存棧頂地址,WSP 可訪問低 32 位。
PC,Program Counter,保存下一條指令地址。
LR = X30,Link Register,保存函數調用的返回地址。
FP = X29,Frame Pointer,保存棧底地址,用 X29 訪棧。
X0-X7 用於函數調用時參數傳遞;X0 用於傳遞返回值。
X8 用於間接尋址。

指令

這裏主要介紹逆向分析中經常見到的指令。不常見的指令可以隨時查閱 ARM 手冊。

數據傳輸指令:
MOV X1,X0 ; X1 = X0
(MOVZ,MOVN,MOVK?)

加載儲存指令:
LDR X5,[X6,#0x08] ; 把地址 X6 + 0x08 上的內容傳送到 X5
STR X0, [SP, #0x8] ; 把 X0 傳送到地址爲 SP + 0x8 的內存上
注意:棧向低地址擴展。
STP x29, x30, [SP, #0xA0-0x10] ; 常用於入棧備份寄存器,相當於兩次 STR
LDP x29, x30, [SP, #0xA0-0x10] ; 常用於出棧還原寄存器,相當於兩次 LDR

算術指令:
ADD X0,X1,X2 ; X0 = X1 + X2
SUB X0,X1,X2 ; X0 = X1 - X2
CMP X0,#0x1 ; X0 和 1 相減,結果影響狀態位;CMN 是相加
ADDS/SUBS 相當於 ADD/SUB,結果影響狀態位

邏輯指令:
AND X0,X0,#0xF ; X0 = X0 & 0xF;有 ANDS
ORR X0,X0,#9 ; X0 = X0 | 0x9
EOR X0,X0,#0xF ; X0 = X0 ^ 0xF
TST X0,#0x8 ; X0 & 0x8 結果影響狀態位,常用於測試第 4 位是否爲 0

條件跳轉指令:
CBZ X0,label ; 如果 X0 = 0,則跳轉到 label;CBNZ
TBZ X0,#0,label ; 如果 X0[0] = 0,則跳轉到 label;TBNZ

無條件跳轉指令:
B label ; 跳轉到 label
BL label ; 將下一條指令地址寫入 X30,跳轉到 label
BR X0 ;
BLR X0 ;
RET ; 跳轉到 X30

地址偏移指令:
ADR X1,0x1234 ; 計算當前指令偏移,X1 = PC + 0x1234
ADR 只能計算小範圍內的偏移,ADRP 計算以頁爲單位的偏移(操作系統中一頁內存是 4KB):
ADRP X1,0xa ; 以頁爲單位,計算當前指令所在的頁偏移 0xa 頁的地址。
X1 = ((PC >> 12) + 0xa) << 12。經常使用 ADRP 頁偏移指令配合 LDR,例如:
ADRP X8,#symbol@PAGE
LDR X0,[X8,#symbol@PAGEOFF]
其中 @PAGE 是符號的頁地址偏移 PC 的頁地址的值,因此 X8 最終保存的是 symbol 這個符號所在的頁的基地址。@PAGEOFF 是 symbol 在頁內的偏移量,最終實現用兩條指令把 64 位的符號放入 X0。

移位指令:
ASR
LSL
LSR
ROR

函數與棧

看彙編看多了,就會發現函數內對棧的操作有一個共同的套路:

“申請”一塊棧空間
備份寄存器
設置 FP(X29),用 X29 代替 SP 訪棧
函數功能實現
還原寄存器
“釋放”棧空間
函數返回

關於結構體做返回值,實際上是調用者分配結構體的內存,把結構體地址作爲參數調用函數,函數最後拷貝內存到目標地址達到“返回結構體”的效果。此外需要再次提醒,OC 中由於前面提到過的消息機制的存在,方法調用實際上會轉換爲 objc_msgSend 族彙編函數的調用,有隱藏的默認參數 X0、X1,分別代表 self、SEL。

Mach-O 文件格式

介紹

每種操作系統都有自己的可執行文件格式。源代碼按照目標操作系統支持的格式進行編譯,加載程序時按照規則讀取程序內容。在 Windows 平臺上是 PE 格式,在 Linux 平臺上是 ELF 格式,在 Apple 平臺上是 Mach-O 格式。只有對 Mach-O 格式的完全瞭解,才能理解一部分逆向工具、程序加載的原理。

各種操作系統的可執行文件類型雖然不同,但是設計思想都差不多,核心內容可以分爲三部分:
第一部分是文件頭,簡單描述了可執行文件的基礎信息。
現代操作系統設計中,程序普遍分代碼段、數據段等,通過段基址加偏移量訪問目標內存,因此文件中需要寫明有哪些段及其相關的詳細信息。段內分節,即 section,段有讀寫屬性可以限制訪問權限,而 section 更像是給程序員使用,利於編程。文件中也需要描述有哪些節及其詳細信息。這些構成了第二部分的主要內容。
第三部分就是每個段內各個節包含的真正的數據。

在上述的三個核心部分基礎上,一般操作系統都增加了自己獨特的功能以及細節上的優化。下面詳細解讀 Mach-O 格式文件內容。

Mach-O 格式具體內容

蘋果設計了一種 Mach-O Universal Binary 格式文件,它打包了多種不同架構的 Mach-O 格式文件,在自己的文件頭中描述了包含的各個 Mach-O 文件支持的 CPU 架構及位置、大小等信息。在蘋果平臺上,一個可執行文件可以是包含多個架構的 Mach-O 通用二進制格式文件,也可以直接是 Mach-O 格式文件。使用 lipo 命令可以合併、拆分多個架構,例如從 xxx.dylib 中拆分出 64 位架構:lipo xxx.dylib -thin arm64 -output xxx64.dylib

對於 Mach-O 格式文件,其整體結構如圖:
在這裏插入圖片描述
可以通過軟件 MachOView 方便地查看 Mach-O 文件內容,其各部分詳細介紹如下。

Header

文件頭包含以下信息:
1 魔數,標識 Mach-O 文件
2 支持的 CPU 架構,由主類型和子類型組成,例如 arm + armv7
3 文件類型,是可執行文件、動態庫還是可重定位文件
4 加載命令個數、所有命令佔據的空間大小
5 標識位,flags,例如標記啓用地址空間佈局隨機化

Load Commands

Load Commands 描述了 Mach-O 文件中到底有哪些內容,包括程序中各個段和節的信息、要鏈接的動態庫信息、動態加載信息以及代碼簽名等,程序加載過程中根據加載命令執行相應的簽名驗證、動態鏈接、加載程序等操作。下面解讀幾個重要的加載命令並展開介紹相關內容。

LC_SEGMENT_64

描述一個段。操作系統在加載程序時,根據這條命令內容,將可執行文件內 fileoff 偏移處的 filesize 大小的內容加載到虛擬地址 vmaddr 上。蘋果給 Mach-O 文件格式設計了四種段:

  • __PAGEZERO:讓程序在 0-4G 的虛擬地址空間上沒有任何訪問權限。也就是說在訪問空指針時報錯屬於保護性異常,而對於很早版本的 Linux,訪問虛擬地址 0 由於頁表沒有映射,這時屬於缺頁異常。
  • __TEXT:代碼段,Readonly、Executable
  • __DATA:數據段,Readable,Writable
  • __LINKEDIT:動態鏈接庫信息,包括重定位信息、綁定信息、懶加載信息等。

段內可以分節(Section),段的加載命令中包含段內所有節的描述信息。我覺得節的作用就是讓程序員在邏輯上將程序劃分爲幾個部分,讓結構更清晰。 代碼段中的節包含以下幾個:可執行代碼,符號樁,樁輔助函數,方法名字符串,類名字符串,方法簽名字符串,其他只讀字符串。在數據段中包含:懶加載/非懶加載符號指針表,類指針列表,Protocol 指針列表,Category 指針列表等等。

懶加載與非懶加載

懶加載符號的出現是爲了加快系統啓動速度。在 DYLD 加載時,非懶加載符號會直接綁定真實的地址,而懶加載符號會在第一次被調用時才綁定真實的地址,之後的調用不需要再次綁定。

前面提到過,代碼段中包含一個 section:符號樁,或者說函數樁 stub。懶加載符號運行中被調用時實際上是在調用這個函數樁,樁內指令非常簡單,LDR 後 BR。這樣怎麼就能實現懶加載的呢?

BR 時寄存器中的地址,就是懶加載符號表中目標符號的地址,這個地址上的值,默認是樁輔助函數,到這裏後續流程應該大致可以猜出來了。Mach-O 格式文件中有一塊懶加載信息,裏面描述了各個懶加載符號在哪個動態庫中、符號在當前模塊的哪個段及在段中的偏移量是多少等信息。調用樁輔助函數,該函數取出懶加載信息後調用 dyld_stub_binder。有 Load Commands 描述了所有要鏈接的動態庫的信息。dyld_stub_binder 調用 _dyld_fast_stub_entry 執行真正的綁定:根據目標符號所在的動態庫,在動態庫中找到目標符號的真實地址;將找到的真實地址寫入當前模塊中該符號所在的段的該符號的偏移量處,這個位置也就是懶加載符號表中該符號的地址。

第二次調用目標符號時,雖然仍要調用到函數樁,但是這次懶加載符號表中目標符號位置上的地址已經被修改爲真正目標符號的地址了,函數樁內會直接調用到真實的目標符號。

Code Signature

描述了簽名數據在文件內的偏移量和大小。簽名數據由多個子條目組成,包括 Code Directory、Requirement、Entitlements、Certificate 等,可以用 brew 安裝 jtool,然後用 jtool 讀取 Mach-O 格式文件的簽名信息:jtool -arch arm64 -v --sig xxx

Data

Load Commands 描述了 Mach-O 文件包含的全部內容,就像是 Mach-O 文件有文件頭一樣,文件內各個部分也需要有 Header,這些頭組成了 Load Commands,而各部分的具體數據共同組成了文件體,也就是本部分。

DYLD

簡介

在編寫應用程序時,程序員看到的入口都是 main 函數,然後編譯、鏈接生成可執行文件,寫入到文件系統中。在最早的 Linux 系統中,用戶打開某個可執行文件時,通過系統調用讓操作系統先 fork 出一個子進程,然後調用 exec 族系統調用,根據文件頭加載文件內容到內存,加載完成後跳轉到程序的 main 函數或指定的其他地址。

iOS 系統整體上仍然應用這種思想,並添加了更復雜的功能。程序在運行時會依賴很多動態庫,當操作系統準備好啓動程序後,會跳轉到 _dyld_start,由 dyld 加載可執行文件、鏈接動態庫、符號綁定及完成後調用程序內真正的 _main 符號。因此,dyld(dynamic link editor) 就是介於操作系統加載與程序 main 函數之間的一段程序,源碼下載

由於系統動態庫很多程序都會用到,爲了優化程序加載速度,蘋果設計了動態庫共享緩存技術。dyld 的緩存可以在手機的 /System/Library/Caches/com.apple.dyld 目錄下找到,因爲舊應用都是 32 位,所以在最初的 64 位設備中可能有兩個緩存文件,分別對應不同架構的動態庫緩存。如果我們想分析系統庫原始的二進制文件,就需要從緩存文件中提取,有四種常見方法:

  • 先找到 dyld 源碼中的 launch-cache/dsc_extractor.cpp 文件,把最後面包圍 main 函數的 #if 0 改爲 #if 1,執行 clang++ -o extractor dsc_extractor.cpp dsc_iterator.cpp 生成提取工具,然後執行 extractor dyld_share_cach_arm64 result 即可輸出原始的動態庫文件。實際使用 Xcode 打開 dyld 源碼項目就可以看到有一個名爲 dsc_extractor 的 target,可以快速構建提取工具。
  • 使用 jtool 命令:jtool -extract UIKit dyld_share_cach_arm64 可以導出指定的模塊。
  • 下載 Mac 界面工具 dyld_cache_extract 以導出。
  • 在設備第一次連接到 Xcode 時,會自動提取系統庫等數據到 ~/Library/Developer/Xcode/iOS DeviceSupport 目錄下,選擇某一版本文件夾,進入 Symbols/System/Library/PrivateFrameworks/ 目錄即可找到原始的動態庫。

模擬器運行在 x86_64 CPU 的電腦上,因此動態庫和緩存都是 x86_64 架構的,其中動態庫緩存在 ~/Library/Developer/CoreSimulator/Caches/dyld 目錄下,所有的動態庫在 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/ 目錄下。

加載過程

操作系統將“接力棒”傳給 dyld,首先從 dyldStartup.s 文件中的 __dyld_start 函數開始,裏面調用 dyldbootstrap::start,內部又調用 dyld::_main,這個函數內完成了全部加載任務,主要包括以下內容。

1 設置環境模式

如果 App 的 entitlements 中設置了 get_task_allow 權限或處於 Debug 模式下,則允許 dyld 使用環境變量(不受限模式);否則會忽略環境變量或只允許與打印有關的環境變量(受限模式或僅打印模式)。前面文章提到過的也是目前最熟悉的環境變量就是 DYLD_INSERT_LIBRARIES,給 App 注入動態庫。

2 讀取環境變量

根據上一步設置的環境模式,讀取環境變量。如果當前是受限模式則直接忽略所有環境變量;如果是僅打印模式,則只處理 DYLD_PRINT_ 開頭的環境變量;如果是不受限模式,則處理所有環境變量。Xcode 可以在 Edit Scheme 頁面裏添加環境變量。

3 檢查共享緩存

iOS 系統必須使用共享緩存。如果當前進程是開機後第一個使用緩存的進程,則需要先把緩存映射到共享區域中。之後的進程發現緩存已映射,只需要檢查緩存區域,驗證緩存文件即可。

4 加載可執行文件

可執行文件也就是主程序,先檢查可執行文件的架構是否與當前設備兼容,並初步構造 ImageLoader 實例。然後讀取加載命令並校驗;根據加載命令內容做一些初始化工作,如把段映射到內存;完成構造。最後將實例添加到 Image 列表中。

5 加載動態庫

遍歷 DYLD_INSERT_LIBRARIES 指向的動態庫數組,對每個動態庫執行加載操作。
因爲動態庫可以存放在多個位置,所以對於給定的動態庫名,先擴展爲一個位置列表,並遍歷列表中的每個位置來搜索目標動態庫。必須保證兩個相同的動態庫絕對不可能同時被加載。搜索的路徑包括 DYLD_ROOT_PATH、LD_LIBRARY_PATH、DYLD_FRAMEWORK_PATH、文件本身路徑、DYLD_FALLBACK_LIBRARY_PATH,此外,還會去掉文件後綴搜索。如果沒有在緩存中找到目標動態庫,就需要打開該動態庫並加載。構造動態庫的 ImageLoader 實例時,也是讀取動態庫的加載命令並校驗。根據加載命令,執行一些準備工作,例如,如果該動態庫有代碼簽名加載命令,則驗證代碼簽名。如果動態庫處於被加密狀態,則調用 mremap_encrypted 讓內核解密。完成構造。遍歷 image 列表再次確認沒有重複加載後纔將實例加入 Image 列表。

6 鏈接、重定位、符號綁定

根據加載命令中描述的動態庫的依賴關係,遞歸地加載動態庫,被依賴的排在前面。然後遞歸地執行重定位(rebase)操作,也就是確定所謂的“模塊的基地址”(符號的虛擬地址 = 模塊被加載的基地址 + 符號在模塊內的偏移地址)。每個動態庫被鏈接時都會遞歸執行非懶加載符號的綁定,更新非懶加載符號表。

7 執行初始化方法

遞歸調用了各個動態庫和主程序的初始化方法,方法中比較重要的部分是調用 objc runtime 的回調:在 dyld 接手之前,操作系統調用了 _objc_init,向 dyld 註冊了 objc runtime 的回調函數。這個回調函數就是 runtime 源碼中的 load_images,函數內部調用了各個類、Category 的 +load 方法。初始化方法後面還會調用模塊源碼中 __attribute__((constructor)) 修飾的函數,這些符號被加載命令記錄在數據段模塊初始化節中。

8 跳轉到主程序入口

主程序入口地址以前由加載命令 LC_UNIXTHREAD 描述,新的源碼中會先從加載命令 LC_MAIN 中讀取,沒有時纔讀取 LC_UNIXTHREAD,然後跳轉到入口地址,也就是我們最熟悉的 main 函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章