iOS應用程式的脫殼實現原理淺析
對於諸多逆向愛好者來說,給一個app脫殼是一項必做的事情。基於安全性的考慮,蘋果對上架到appstore的應用都會進行加密處理,所以如果直接逆向一個從appstore下載的應用程式時,所能看到的“原始碼”將非常的晦澀難懂。為了能看懂應用程式的“原始碼”,就必須對應用程式進行解密,也就是所謂的脫殼。脫殼後的目的是可以分析應用程式的一些技術實現原理,或者利用一些漏洞進行攻擊和測試。
這篇文章不是一篇介紹如何利用工具去進行脫殼的教程,而只是簡單的分析這些常用脫殼工具的實現原理。要想了解脫殼原理,就要先去了解一個被加密的應用程式是如何被執行的。下面一張圖片簡單的介紹了一個被加殼後的應用程式被載入和執行的過程:

脫殼原理以及常見的工具
要對一個殼應用進行脫殼處理,無非就是採用 靜態脫殼 和 動態脫殼 兩種方法: 靜態脫殼 就是在已經掌握和了解到了殼應用的加密演算法和邏輯後在不執行殼應用程式的前提下將殼應用程式進行解密處理。靜態脫殼的方法難度大,而且加密方發現應用被破解後就可能會改用更加高階和複雜的加密技術; 動態脫殼 就是從執行在程序記憶體空間中的可執行程式映像(image)入手,來將記憶體中的內容進行轉儲(dump)處理來實現脫殼處理。這種方法實現起來相對簡單,且不必關心使用的是何種加密技術。從上面的殼應用程式執行的過程就可以看出無論殼程式如何被加密處理,最終執行後在程序中的程式碼映像(image)始終是被解密後的原始程式二進位制。所以只要一個程序記憶體空間中的程式碼映像(image)能被讀取和訪問就可以實現動態脫殼。下面要介紹的兩個工具就是巧妙的運用了兩種不同的訪問技巧來實現動態脫殼的。
一、利用動態庫注入來實現脫殼的dumpdecrypted/frida-ios-dump
dumpdecrypted和frida-ios-dump都是在github上開源的專案,下載地址分別為: ofollow,noindex">github.com/stefanesser… 關於使用這兩個工具來進行脫殼的文件非常之多。我們知道一個應用除了有一個可執行程式外,還會連結非常多的動態庫。動態庫載入後和可執行程式共享相同的程序記憶體空間,而且動態庫中的程式碼是可以訪問整個程序記憶體空間中的有許可權的區域的,包括可執行程式的image被載入到程序中的記憶體區域。因此只要想辦法讓應用程式載入某個特定的第三方動態庫,也就是讓這個第三方動態庫注入到應用程式的程序中去就可以實現將被解密過後的可執行程式在程序記憶體中的image資訊轉儲到檔案中去從而實現脫殼處理。對於一個越獄後的裝置來說主要可以通過兩種方法來實現第三方動態庫的注入:
-
設定環境變數DYLD_INSERT_LIBRARIES的值指向這個第三方動態庫的路徑。然後執行要脫殼的應用程式即可。 DYLD_INSERT_LIBRARIES環境變數的設定是一個作業系統提供的特性,所有執行的程式都會載入這個環境變數中所指向的動態庫檔案。
-
將第三方動態庫檔案儲存在越獄裝置的**/Library/MobileSubstrate/DynamicLibraries/**目錄下並編寫對應的庫的同名plist檔案,所有plist中指定的可執行程式一旦執行就會載入對應的動態庫(此目錄即Tweak外掛所在的目錄)。
還有一種直接修改對應mach-o格式的可執行檔案內容來實現動態庫注入。
動態庫載入的問題解決後就需要解決動態庫中程式碼執行的時機問題了。要想讓一個被載入的動態庫在載入後自動執行某一段程式碼可以有四種方法:
-
建立一個C++全域性物件,並在物件所屬類的建構函式中新增特定程式碼。
-
建立一個OC類,並在OC類的+load方法中新增特定的程式碼。
-
生成動態庫時指定一個初始化init入口函式,並在入口函式中新增特定的程式碼。
-
在動態庫中定義一個帶有_ attribute_ ((constructor))宣告的函式,並在函式內新增特定的程式碼。
如果你想更進一步的瞭解上述那些方法的載入的原理,請參考我的文章: 深入解構iOS系統下的全域性物件和初始化函式
dumpdecrypted這個工具就是通過建立一個名為dumpdecrypted.dylib的動態庫並在庫內部定義了一個
__attribute__((constructor)) void dumptofile(int argc, const char **argv, const char **envp, const char **apple, struct ProgramVars *pvars) 複製程式碼
函式來實現脫殼的。這個函式的大體實現會在後面繼續介紹。
二、利用父子程序關係來實現脫殼的Clutch
Clutch也是一個在github上開源的專案,下載地址為: github.com/KJCracks/Cl… 關於這個工具的使用教程也非常之多。我們知道在unix系列的作業系統中父程序可以通過 fork 或者 posix_spawnp 兩個函式來執行或者建立一個子程序的,這兩個函式都會返回對應的子程序ID(PID)。iOS系統則可以通過 task_for_pid 函式來從程序ID獲取程序在mach核心子系統中的mach port標識。得到mach port 標識後,就可以藉助 mach_vm_read_overwrite 函式來讀取指定程序空間中的任意虛擬記憶體區域中所儲存的內容。因此Clutch內部的實現就是Clutch這個程式對將要進行脫殼的程式檔案路徑呼叫 posix_spawnp 函式來執行從而成為其子程序,然後藉助 task_for_pid 以及 mach_vm_read_overwrite 函式來讀取脫殼程式子程序在記憶體中已經被解密後的可執行程式的image所對映的記憶體空間來達到脫殼的目的的。
一個思考:可能在實際中並不一定要求是父子程序關係,是否只要某個具有特權的程式或者執行在root使用者上的程式只要拿到了對應程序的PID就可以通過mach子系統提供的API來讀取其他程序記憶體空間中的資訊呢?
上述的兩種方法中不管是dumpdecrypted還是Clutch最終都是將被解密後的可執行程式的image在記憶體中的對映寫入到一個檔案中去來儲存脫殼後的內容。參考dumpdecrypted中的dumptofile函式實現以及Clutch中的Dumpers目錄下的實現程式碼就可以看出:一個可執行程式image在記憶體中對映的內容的結構和mach-o格式的可執行檔案結構基本上是保持一致的。都是有一個mach_header結構體頭還有諸多的load_command結構體組成。因此所謂的dump處理就是將記憶體中的這些結構和資料原封不動的寫入到檔案中去即完成了脫殼中的最核心的部分。如果想仔細的閱讀這部分程式碼的實現,建議先了解一下mach-o檔案格式的組成。
後記
當你瞭解了這些內部實現後,也許你會發覺其實它的原理很簡單。而且有可能你也能很快的去實現。可問題的關鍵是為什麼這些方法總是別人能想到,而我們卻想不到呢?這是否和國人的思維以及解決問題的方式相關呢?在我們的教育和實踐體系中更多的是拿來主義和實用主義,往往很少人會對問題進行深入的探索研究以及進行問題的關聯性思考。但願這種情況在未來能夠得到改進,尤其作為一個程式設計師,更加應該秉持探索求知的強烈意願而不是簡單複製和應用就滿足了。
最後還是要感謝《iOS應用逆向與安全》的作者: 劉培慶 。向他諮詢了逆向相關的一些知識後才得以寫出這篇文章。並推薦逆向的愛好者閱讀這本書。