靜態(tài)分析是指對二進制包進行反編譯,分析靜態(tài)的代碼邏輯。
本文內容包括:app 砸殼過程、工具和環(huán)境的坑、導出 OC 頭文件、使用 hopper 和 IDA 反編譯、arm 寄存器功能、靜態(tài)分析經(jīng)驗、推薦的 IDA 插件、如何分析系統(tǒng)庫。
對 app 砸殼解密
從 App Store 下載的 app 是經(jīng)過加密的,需要對其進行解密后,才能進行分析。如果你懶得砸殼,可以直接去各種蘋果助手下載越獄版 app,那些是已經(jīng)解密過的。但是如果要找的 app 在助手上沒有,就只能自己砸殼了。
砸殼可以使用 dumpdecrypted,也可以使用更簡單的 clutch。這里用 dumpdecrypted 講解。步驟如下。
1.下載 dumpdecrypted
從https://github.com/AloneMonkey/dumpdecrypted下載源碼,編譯出一個 dumpdecrypted.dylib 文件。這個版本的 dumpdecrypted 添加了對 framework 的 dump。
2.安裝 openSSH
iOS 9及以下系統(tǒng),在 Cydia 里安裝 openSSH 即可。
iOS 10越獄自帶了 openSSH,但是默認是關閉的,需要做一點修改。
如果是用的 yalu 越獄:
- 1.用蘋果助手或者其他工具進入 iOS 的
/private/var/containers/Bundle/Application/yalu102/yalu102.app/
。 - 2.用文本編輯器打開
dropbear.plist
文件。 - 3.替換 127.0.0.1:22 為 22。
- 4.重啟設備,重新使用越獄工具恢復越獄。
參考:http://bbs.iosre.com/t/make-package-ssh-ios10-2/7564
或者直接去 Cydia 里安裝 dropbear 插件。
3.連接到 iOS 設備
iOS 設備安裝了 openSSH 后,在 Mac 端打開終端,確保 Mac 和 iOS 設備連接到同一網(wǎng)絡,在終端里輸入命令:ssh root@iOSIP。iOS 設備的 ip 地址:
在終端中輸入命令:ssh root@10.5.53.182
,回車,接著輸入 ssh 的默認密碼alpine
后即可連接到 iOS 設備。
4.找到需要砸殼的 app
找到 app 所在目錄,格式為/var/mobile/Containers/Data/Application/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/
,可以使用同步助手、itools 等工具查找。
也可以在 Cydia 里安裝 ps 命令行工具后,使用ps –e
命令查找,方法是 ssh 成功后,關閉所有 app,打開需要砸殼的 app,輸入ps –e
命令,即可打印出所有進程,/var/mobile
開頭的那個目錄就是 app 所在的目錄。
5.進行砸殼
下面的砸殼是舊版 dumpdecrypted 的方法,比較繁瑣。AloneMonkey 的 這個 https://github.com/AloneMonkey/dumpdecrypted 更加簡單。
- 把
dumpdecrypted.dylib
拷貝到/usr/lib
。 iOS 9之前是拷貝到 app 的 Document 目錄的, iOS 9 之后出現(xiàn)了權限問題,所以拷貝到/usr/lib
- 修改 user 為
mobile
:su mobile
- 進入到某個具有寫權限的目錄,例如
cd /var/mobile/Documents
- 使用
DYLD_INSERT_LIBRARIES
加載動態(tài)庫到 app 上,格式為DYLD_INSERT_LIBRARIES='dumpdecrypted.dylib的目錄' '需要砸殼的app執(zhí)行文件的目錄'
,例如:DYLD_INSERT_LIBRARIES=/usr/lib/dumpdecrypted.dylib /var/mobile/Applications/F7753B03-3F06-4524-A735-5BF5B398C730/WeChat.app/WeChat
。這是系統(tǒng)的 dyld 提供的加載動態(tài)庫的功能,可以在 dyld 源代碼中看到這部分邏輯。
如果出現(xiàn)dyld: could not load inserted library 'dumpdecrypted.dylib' because no suitable image found. Did find: dumpdecrypted.dylib: required code signature missing for 'dumpdecrypted.dylib'
,需要對 dumpdecrypted.dylib 進行簽名。
在 Mac 上列出證書:security find-identity -v -p codesigning
,用列出的證書簽名:
codesign --force --verify --verbose --sign "iPhone Developer: xxx xxxx (xxxxxxxxxx)" dumpdecrypted.dylib
。把簽名后的dumpdecrypted.dylib
重新拷到 iOS 設備上,重新進行砸殼。
砸殼完畢后,在當前目錄會生成一個.decrypted
后綴的文件,這就是砸殼后的文件,將其拷貝到 Mac 上即可導入其頭文件、用反編譯工具打開分析。可以在 Mac 上使用 scp 命令拷貝越獄機上的文件:scp -P 端口號(默認22) root@iOSIP:/var/mobile/Documents/xxx.decrypted ~/Documents/xxx.decrypted
。如果拷貝的是文件夾,加上-r
參數(shù)。
dumpdecrypted
原理是 app 啟動后會被系統(tǒng)解密,因此可以把解密后的內存 dump 出來。但是如果要對 app extension 進行砸殼,由于 extension 是依賴于主 app 的,不能獨立啟動,所以砸殼方法就失效了。可以參考這個改進版對 extension 砸殼的方法:https://github.com/CarinaTT/dumpdecrypted
使用 class-dump 導出 app 的頭文件
Class-dump 是一個可以導出 Objective-C 頭文件的工具,官網(wǎng):http://stevenygard.com。
通過分析頭文件里的 API,可以簡單地分析一個類的實現(xiàn),或者查找一些私有 API。
class-dump 官網(wǎng)上的版本不能導出用 swift 編寫的工程的頭文件,當出現(xiàn)Error: Cannot find offset for address 0x3a546a04 in dataOffsetForAddress:
這樣的錯誤時,就說明這個 app 可能是用 swift 編寫的。
建議去 github 上手動編譯最新版的 class-dump,或者使用 class-dump-z 代替,下載地址:https://code.google.com/archive/p/networkpx/downloads。
把下載到的class-dump-z
執(zhí)行文件放到/usr/local/bin/
,賦予執(zhí)行權限chmod +x /usr/local/bin/class-dump-z
。這樣就可以在終端使用 class-dump 命令了:class-dump-z –H '需要導出頭文件的app目錄' –o '導出頭文件的存放目錄'
。
例如要 dump 系統(tǒng)自帶的計算器,導出它的頭文件,命令如下:
class-dump-z -H /Applications/Calculator.app -o ~/Documents/headers
。
拿到砸殼后的 .decrypted 文件后,直接使用class-dump-z
即可導出頭文件。
此時,使用之前 reveal 定位到的類名,即可找到對應的文件,查看類里面的方法。
可以看到,在掃一掃界面,微信使用了- (void)captureOutput: didOutputSampleBuffer: fromConnection:
這個方法,說明它是截取了視頻流的幀圖像,再對圖像進行二維碼分析,而不是用AVFoundiation
提供的二維碼識別方法。
如果還想進一步查看方法的邏輯,可以使用Hopper Disassembler
對 .decrypted 文件進行反編譯。
使用使用 Hopper Disassembler 靜態(tài)分析
一個專門反編譯 OC 程序的工具。官網(wǎng):http://www.hopperapp.com。試用版有功能限制,30分鐘退出一次,不能保存和導入反編譯后的文件,不能動態(tài)調試等。
打開 Hopper Disassembler,直接將 .decrypted 文件拖入,選擇對應的 CPU 架構類型即可,例如這個.decrypted 是從 iPad mini2 上生成的,那么就是 arm64。
打開后會自動進行分析,列出方法名、字符串等信息,但是大多數(shù)都是匯編語言。閱讀匯編語言,還需要了解對應架構寄存器功能的知識。
在左側可以搜索類名,方法名。
右側的 is referenced by 和 have reference to 可以看到方法之間的的交叉引用關系:
按空格鍵可以彈出方法的邏輯跳轉圖:
Hopper Disassembler 可以將匯編語言轉換為 OC 風格的偽代碼,但是舊版的 hopper 不能對 arm64 文件使用這個功能。建議使用 armv7s 以下的 iOS 設備的原因就在這里。以下是使用 iPad2 越獄設備反編譯后,生成的匯編代碼和對應的偽代碼,由于微信的代碼比較復雜,這里選用的是另外一個更簡單的二維碼 app 的代碼:
可以看到aptureOutput: didOutputSampleBuffer: fromConnection:
里,首先用取到的幀生成了一張圖片,再用createRotatedImage:degrees:
對圖片做了一次處理,最后用decodeImage:cgimg:
對圖片進行二維碼分析。要想查看這些方法,只需要再搜索對應的方法名就可以了。最新版 hopper 也可以雙擊直接跳轉。
另外一個反編譯工具 IDA 也可以反編譯 armv7 的 app ,使用方法類似,可以和 Hopper Disassembler 對照著看。需要注意的是 IDA 的 Pro 版才支持 arm64 的 app,而 Pro 版不支持免費試用。
靜態(tài)分析經(jīng)驗總結
追蹤調用流程
- 對于靜態(tài)函數(shù),直接用交叉引用功能
is referenced by
查看函數(shù)在哪里被引用。注意 hopper 面板里列出的引用不是完整的,可以用快捷鍵x
列出完整的引用 - 對于 OC 方法,由于 runtime 在調用時不是直接引用方法,而是引用了 selector,所以需要搜索方法名字符串和 selector,然后再用
is referenced by
查找哪些地址引用了此字符串或者 selector,來查找方法調用 - 通過寄存器的賦值操作回溯參數(shù)的傳遞
- 通過查找某些關鍵字符串,回溯到關鍵函數(shù)
注意,反匯編工具有時候會分析出錯誤的指令,所以有些函數(shù)體是丟失的,需要在反編譯時手動 undefined。
分析匯編代碼
- 使用 hopper 的偽代碼轉換功能,可以將 OC 方法的匯編代碼轉換為 OC 風格的偽代碼。此功能對 arm64 的支持不是很好,建議使用 armv7 或者 armv7s 的越獄機
- 在函數(shù)的開始,32 位 arm 上前四個參數(shù)存放在 r0-r3 中,其他參數(shù)存放在棧中,結束后,返回值放在 r0 中;在 arm64 上,前7個參數(shù)存放在 x0–x7 中,返回值存在 x0 中
- 有些代碼是被開發(fā)者故意混效過的,例如打亂執(zhí)行流程、加入冗余代碼,可以借助一些 IDA 插件處理后再分析,例如 CrowdDetox、optimice python plugin,不過只是分析 iOS 的話,很少會遇到這種情況
基本的匯編知識
你并不需要花時間理解每一條匯編指令,只需要梳理出關鍵點就能理清代碼的邏輯。
逆向中關鍵的指令:
-
ldr
,mov
,讀取指令,從地址讀取數(shù)據(jù)到寄存器。 -
str
,保存指令,保存數(shù)據(jù)到寄存器。 -
b
,跳轉指令,跳轉到某個地址。 -
cmp
,比較指令,說明這里有分支。
32 位 arm 的調用約定:
寄存器 | 描述 |
---|---|
r0-r3 | 傳遞參數(shù)與返回值。如果斷點在 OC 方法的第一行,那 r0 就是 self,r1 就是 cmd。如果超過四個參數(shù),或者一些例如結構體的參數(shù)超過了32位 bit,那么參數(shù)將會通過棧來傳遞;返回值一般都在 r0 上 |
r4-r6, r8, r10-r11 | 沒有特殊規(guī)定,通用寄存器 |
r7 | 棧幀指針寄存器(Frame Pointer),指向前一個保存的棧幀(stack frame)和鏈接寄存器(link register, lr)在棧上的地址 |
r9 | 操作系統(tǒng)保留 |
r12 | IP 寄存器(intra-procedure scratch) |
r13 | SP 寄存器(stack pointer),是棧頂指針 |
r14 | LR 寄存器(link register),存放函數(shù)返回后需要繼續(xù)執(zhí)行的指令地址 |
r15 | PC 寄存器(program counter),指向當前指令地址 |
CPSR | 當前程序狀態(tài)寄存器(Current Program State Register),在用戶狀態(tài)下存放像 condition 標志中斷禁用等標志 |
arm64 的調用約定:
arm64有 r0 - r30 是31個通用整形寄存器,PC 不能再作為寄存器直接訪問。每個寄存器可以存取一個64位大小的數(shù)。 當使用 x0 - x30 訪問時,它就是一個64位的數(shù)。當使用 w0 - w30 訪問時,訪問的是這些寄存器的低32位。
寄存器 | 描述 |
---|---|
x0–x7 | 傳遞參數(shù)與返回值。如果參數(shù)個數(shù)超過了8個,多余的參數(shù)會存在棧上;返回值一般都在 x0 上 |
x29 | 棧幀指針寄存器(Frame Pointer),指向前一個保存的棧幀(stack frame)和鏈接寄存器(link register, lr)在棧上的地址 |
x31 | SP 寄存器(stack pointer),是棧頂指針;根據(jù)不同指令,也有可能是 zero register |
x30 | LR 寄存器(link register),存放函數(shù)的返回地址 |
CPSR | 當前程序狀態(tài)寄存器(Current Program State Register),在用戶狀態(tài)下存放像 condition 標志中斷禁用等標志 |
x86-64 的調用約定:
x86-64 有16個64位寄存器,分別是:
rax,rbx,rcx,rdx,esi,edi,rbp,rsp,r8,r9,r10,r11,r12,r13,r14,r15
寄存器 | 描述 |
---|---|
rax | 作為函數(shù)返回值使用 |
rsp | 棧指針寄存器,指向棧頂 |
rdi,rsi,rdx,rcx,r8,r9 | 依次用作函數(shù)參數(shù);如果斷點在 OC 方法的第一行,那 rdi 就是 self,rsi 就是 cmd |
rbx,rbp,r10,r11,r12,r13,r14,r15 | 通用寄存器 |
棧幀相關的知識,可以參考:iOS開發(fā)同學的arm64匯編入門
匯編指令速查插件
有許多很有用的插件可以對靜態(tài)分析提供幫助。
有時候看到不了解的匯編指令,每次都去 Google 查找,是一件很低效的事。可以安裝插件,直接在 hopper 和 IDA 中顯示指令的功能。
Hopper 插件:hopperref
Hopper 可以使用 Python 編寫的擴展插件。安裝插件hopperref,把Show Instruction Reference.py``arm.sql``x86-64.sql
拷貝到~/Library/Application Support/Hopper/Scripts/
目錄下即可。之后就能在 hopper 界面的菜單欄Scripts
中找到Show Instruction Reference
選項,點擊即可輸出選中指令的詳細文檔。
mov
指令的文檔:
IDA 插件:idaref
hopperref 插件是源自 一個 IDA 的插件 idaref。
把idaref.py
拷貝到your_ida_path/ida.app/Contents/MacOS/plugins/
下,把archs
文件夾拷貝到your_ida_path/ida.app/Contents/MacOS/plugins/archs
。archs
文件夾里是匯編指令的文檔x86-64.sql``x86-64_old.sql``arm.sql``mips32.sql``xtensa.sql
。
之后打開 IDA,就可以在Edit
菜單中多出了idaref
選項,選擇Start Idaref
就開啟了自動提示,
當選中匯編指令時,對應的文檔就會顯示在Instruction Reference
窗口中。
IDA 插件:FRIEND
除了 idaref,還有另一個插件 FRIEND 也提供了匯編指令和寄存器的文檔功能。只要把鼠標停在指令或者寄存器上就會顯示文檔懸浮窗。
需要注意的是,編譯出來的 IDA dylib 插件是對應 IDA 版本的,如果要使用不同版本的 IDA,就需要重新編譯。把對應版本的FRIEND.dylib
和FRIEND64.dylib
拷貝到your_ida_path/ida.app/Contents/MacOS/plugins/
下,再打開 IDA 就會在Edit->Plugins
中多出FRIEND
選項。
點擊選項,打開 FRIEND 的設置。需要加載 FRIEND 提供的 XML 配置文件,對應二進制文件的 x86_64 或者 arm 平臺。例如x86_64.xml
配置中提供了x86_64 instructions
項,選中后,勾上下面的四個功能選項,點擊 OK 保存。
之后,當鼠標停在指令或者寄存器上就會顯示文檔懸浮窗。
識別庫函數(shù)
很多時候,二進制文件中的函數(shù)都被去掉了符號,因此只能看到很多sub_100017D90
這樣的函數(shù),難以直觀分析。而程序會使用到很多第三方庫,例如加密庫、壓縮庫、網(wǎng)絡庫,這些第三方庫一般都是開源的,可以得到函數(shù)符號,如果能恢復這部分函數(shù)的符號,就能避免浪費時間在分析這些開源代碼上,也能通過分析開源庫的交叉引用,追蹤程序自身的邏輯。
這部分代碼一般都是 C 和 C++ 函數(shù),OC 方法的名字都保存在 Mach-O 文件的符號表中,不會被去除符號。如果你需要分析 C++ 程序,可以使用下面的工具進行輔助。
FLIRT:庫快速識別和鑒定技術
IDA 提供了FLIRT Signature
功能,F(xiàn)LIRT 全稱是庫快速識別和鑒定技術,可以為帶有符號的庫文件中的函數(shù)生成簽名,再把簽名文件導入到分析后的 app 中,就會識別出匹配到的函數(shù),重命名為正確的符號。
但是生成正確的簽名并不容易。用于生成簽名的庫文件,編譯時的編譯器版本、配置和 app 中用到的庫的編譯器版本、配置需要相同。這樣才能生成相同的代碼,從而生成相同的代碼簽名。
具體的使用方法,可以在書籍IDA Pro 權威指南
中找到。
識別加密函數(shù)
類似的,有些 IDA 插件可以識別程序中用到的加密常數(shù)、加密方法和壓縮方法。例如 Find Crypt 可以尋找常用加密算法中的常數(shù),IDA signsrch 可以尋找二進制文件所使用的加密、壓縮算法,IDA scope 可以自動識別 windows 函數(shù)和壓縮、加密算法。
可以從這些關鍵函數(shù)入手,尋找程序中的關鍵邏輯。
如何分析系統(tǒng)庫
有時候在分析某個 crash 時,或者對某個系統(tǒng)功能感興趣時,會需要分析特定版本的 iOS 系統(tǒng)庫的實現(xiàn),例如UIKit.framework
Foundiation.framework
。
絕大部分時候,只需要分析模擬器版本的系統(tǒng)庫就可以了。因為模擬器的系統(tǒng)庫保留了所有的符號,查找交叉引用更直接。
不過有些系統(tǒng)庫只在真機上才有,或者你需要特定版本的庫用于分析 crash 時,可以從這里下載對應的系統(tǒng)庫。
真機的系統(tǒng)庫和模擬器的有些差別。系統(tǒng)庫在真機上經(jīng)過了很多編譯優(yōu)化,去除了大部分私有的函數(shù)符號,交叉引用也不像模擬器版本的那樣直接。真機上的所有系統(tǒng) framework 都被整合成了一個大文件,名為dyld_shared_cache_arm64
或者dyld_shared_cache_armv7
。函數(shù)在尋址時,是基于整個dyld_shared_cache_xxx
文件進行尋址的。
當你把真機連接到 Xcode,Xcode 會把真機上的系統(tǒng)庫拷貝到~/Library/Developer/Xcode/iOS DeviceSupport
,從dyld_shared_cache_xxx
中切分出每個單獨的 framework。但是當你反編譯這些 framework 時,會發(fā)現(xiàn)代碼里會使用很多無效地址的函數(shù)指針,難以分析。這是因為在dyld_shared_cache_xxx
中,一個 framework 引用另一個 framework 中的函數(shù)時,是相當于在一個庫中直接引用的,直接跳轉到對應的地址,而不是再用函數(shù)符號經(jīng)過 lazy binder 進行調用。當 framework 從dyld_shared_cache_xxx
中切分出來后,這些函數(shù)調用的地址就會指向 framework 外,無法追蹤。
所以在分析真機的系統(tǒng)庫時,最好是配合模擬器版本的系統(tǒng)庫輔助分析,可以看到私有的符號,也可以看到更明確的交叉引用。或者用 IDA 直接分析整個 dyld_shared_cache_xxx
文件,不過這樣做需要反匯編整個文件,耗時很大。
結尾
靜態(tài)分析的整個流程如上,剩下的就是積累經(jīng)驗了。通過靜態(tài)分析查看一些簡單函數(shù)的實現(xiàn),在大部分情況下都足夠了。不過靜態(tài)分析的信息是有限的,有時候很難找到想要的函數(shù),這時候就需要動態(tài)分析上場了。下一篇文章將講解動態(tài)分析。