注:這么長時間以來本文后續加入了新的內容以及一些修改,由于精力有限,所以不會同步更新到簡書上。請大家移步到本項目的Github: https://github.com/Urinx/iOSAppHook
從零到一,非越獄環境下iOS應用逆向研究,從dylib注入,應用重簽名到App Hook。文中用到的工具和編譯好的dylib可在Github上下載。
注意!本文所有操作均在以下環境下成功進行,不同平臺或環境可能存在某些問題,歡迎大家在issue中提出問題以及相互討論。
Mac OS X 10.11.6 (15G12a)
Xcode 7.3.1 (7D1014)
iPhone 5s, iOS 9.3.3 (13G21)
免費開發者賬號
示例App:微信 v6.3.19.18
前言
提到非越獄環境下App Hook
大家早就已經耳熟能詳,已經有很多大神研究過,這方面相關的資料和文章也能搜到很多。我最早是看到烏云知識庫上蒸米的文章才對這方面有所了解,當時就想試試,整個過程看似簡單(大神總是一筆帶過),然而當自己真正開始動手時一路上遇到各種問題(一臉懵逼),在iOSRE論壇上也看到大家遇到的各種問題,其實阻擾大家的主要是一些環境的搭建以及相關配置沒設置好,結果導致dylib編譯過程各種錯誤,重簽名不成功,各種閃退等。所以本文里的每一步操作都會很詳細的交代,確保大家都能操作成功。
應用脫殼
我們知道,App Store里的應用都是加密了的,直接拿上來擼是不正確的,所以在此之前一般會有這么一個砸殼的過程,其中用到的砸殼工具就是dumpdecrypted,其原理是讓app預先加載一個解密的dumpdecrypted.dylib,然后在程序運行后,將代碼動態解密,最后在內存中dump出來整個程序。然而砸殼實在越獄的環境下進行的,鑒于本文主要關注點在非越獄環境下,再者我手里也沒有越獄設備(有就不會這么蛋疼了)。
所以這里我們選擇的是直接從PP助手等各種xx助手里面下載,注意的是這里下載的是越獄應用(不是正版應用),也就是所謂的脫過殼的應用。
為了謹慎起見,這里我們還需要確認一下從xx助手里下載的應用是否已解密,畢竟有好多應用是只有部分架構被解密,還有就是Watch App以及一些擴展依然加密了,所以最好還是確認一下,否則的話,就算hook成功,簽名成功,安裝成功,app還是會閃退。
首先,找到應用對應的二進制文件,查看包含哪些架構:
> file WeChat.app/WeChat
WeChat.app/WeChat: Mach-O universal binary with 2 architectures
WeChat.app/WeChat (for architecture armv7): Mach-O executable arm
WeChat.app/WeChat (for architecture arm64): Mach-O 64-bit executable
可以看到微信包含兩個架構,armv7和arm64。關于架構與設備之間的對應關系可以從iOS Support Matrix上查看。理論上只要把最老的架構解密就可以了,因為新的cpu會兼容老的架構。
otool
可以輸出app的load commands,然后通過查看cryptid這個標志位來判斷app是否被加密。1代表加密了,0代表被解密了:
> otool -l WeChat.app/WeChat | grep -B 2 crypt
cmd LC_ENCRYPTION_INFO
cmdsize 20
cryptoff 16384
cryptsize 40534016
cryptid 0
--
cmd LC_ENCRYPTION_INFO_64
cmdsize 24
cryptoff 16384
cryptsize 43663360
cryptid 0
可以看到微信已經被解密了,第一個對應的是較老的armv7架構,后者則是arm64架構。
鑒于微信是一個多targets的應用,包含一個Watch App和一個分享擴展。所以同理,我們還需要依次確認以下二進制文件,這里就跳過了。
WeChat.app/Watch/WeChatWatchNative.app/WeChatWatchNative
WeChat.app/Watch/WeChatWatchNative.app/PlugIns/WeChatWatchNativeExtension.appex/WeChatWatchNativeExtension
WeChat.app/PlugIns/WeChatShareExtensionNew.appex/WeChatShareExtensionNew
應用重簽名
在第二部分我們將要進行的是應用重簽名,注意這里并不是按照整個操作流程的順序來講,而是跳過編譯dylib,因為我覺得如果現在你沒有把重簽名拿下的話,寫tweak寫hook都是白搭。所以從這里在開始,我們不需要對App進行任何修改和處理,僅僅對其進行重簽名,然后將其安裝到設備上能夠正常的運行。再次提醒的是重簽名用的是脫殼后的App,加密的App重簽名成功安到設備上也會閃退。
關于iOS應用重簽名的文章有很多,簽名方法都是一樣,個別地方會有些小出入,然后按照里面給出的步驟手動一步一步的來操作,不知道是由于免費的開發者證書的原因還是哪一步漏掉了或者是什么其它的原因,總之就是不成功。就在一籌莫展的時候,偶然發現了一個名為iOS App Signer的Mac上的應用,這款重簽名工具能夠用免費的開發者賬號重簽名應用。我試了下,用這個工具重簽名了一個應用,并且成功安裝到手機上,盡管打開時閃退,但至少總算能安裝到設備上去了。
看到簽名成功心里一陣高興,并且由于該應用開源,于是就到Github上閱讀源碼看其具體的實現。該應用是用Swift語音所寫,代碼量也不多,閱讀起來沒有問題。由于整個操作都是在終端下進行的,從終端到圖形界面來回切換實在是麻煩,所以在其代碼基礎上稍作修改寫了一個Command Line Tool
工具在命令行下使用。
下面就來具體交代下這個重簽名工具到底做了什么。
1. 獲取到本地上的開發者簽名證書和所有的Provisioning文件
獲取本機上的Provisioning文件主要是調用了populateProvisioningProfiles()
方法,該方法調用了ProvisioningProfile.getProfiles()
將結果封裝到ProvisioningProfile
結構體中以數組的形式返回,并對其結果做了一個刷選,去掉了那些過期的證書。
struct ProvisioningProfile {
...
static func getProfiles() -> [ProvisioningProfile] {
var output: [ProvisioningProfile] = []
let fileManager = NSFileManager()
if let libraryDirectory = fileManager.URLsForDirectory(.LibraryDirectory, inDomains: .UserDomainMask).first, libraryPath = libraryDirectory.path {
let provisioningProfilesPath = libraryPath.stringByAppendingPathComponent("MobileDevice/Provisioning Profiles") as NSString
if let provisioningProfiles = try? fileManager.contentsOfDirectoryAtPath(provisioningProfilesPath as String) {
for provFile in provisioningProfiles {
if provFile.pathExtension == "mobileprovision" {
let profileFilename = provisioningProfilesPath.stringByAppendingPathComponent(provFile)
if let profile = ProvisioningProfile(filename: profileFilename) {
output.append(profile)
}
}
}
}
}
return output
}
...
}
簡單點用shell表示就是,先列出所有的~/Library/MobileDevice/Provisioning Profiles
路徑下所有的后綴為.mobileprovision
的文件,然后依次獲取文件的信息(plist格式)。
security cms -D -i "~/Library/MobileDevice/Provisioning Profiles/xxx.mobileprovision"
獲取開發者簽名證書:
func populateCodesigningCerts() {
var output: [String] = []
let securityResult = NSTask().execute(securityPath, workingDirectory: nil, arguments: ["find-identity","-v","-p","codesigning"])
if securityResult.output.characters.count >= 1 {
let rawResult = securityResult.output.componentsSeparatedByString("\"")
for index in 0.stride(through: rawResult.count - 2, by: 2) {
if !(rawResult.count - 1 < index + 1) {
output.append(rawResult[index+1])
}
}
}
self.codesigningCerts = output
Log("Found \(output.count) Codesigning Certificates")
}
以上代碼相當于:
> security find-identity -v -p codesigning
1) 1234567890123456789012345678901234567890 "iPhone Developer: XXX (xxxxxxxxxx)"
2) 1234567890123456789012345678901234567890 "Mac Developer: XXX (xxxxxxxxxx)"
2. 一些準備工作
創建臨時目錄,makeTempFolder()
方法:
> mktemp -d -t com.eular.test
/var/folders/qr/8_n21zhd4f993khcsh_qll000000gp/T/com.eular.test.6aHPpdBZ
處理不同格式的輸入文件,包括deb
,ipa
,app
,xcarchive
:
deb -> ar -x xxx.deb -> tar -xf xxx.tar -> mv Applications/ -> Payload/
ipa -> unzip -> Payload/
app -> copy -> Payload/
xcarchive -> copy .xcarchive/Products/Applications/ -> Payload/
3. 重簽名
首先,復制provisioning文件到app目錄里:
cp xxx.mobileprovision Payload/xxx.app/embedded.mobileprovision
根據provisioning文件導出entitlements.plist:
if provisioningFile != nil || useAppBundleProfile {
print("Parsing entitlements")
if let profile = ProvisioningProfile(filename: useAppBundleProfile ? appBundleProvisioningFilePath : provisioningFile!){
if let entitlements = profile.getEntitlementsPlist(tempFolder) {
Log("–––––––––––––––––––––––\n\(entitlements)")
Log("–––––––––––––––––––––––")
do {
try entitlements.writeToFile(entitlementsPlist, atomically: false, encoding: NSUTF8StringEncoding)
Log("Saved entitlements to \(entitlementsPlist)")
} catch let error as NSError {
Log("Error writing entitlements.plist, \(error.localizedDescription)")
}
} else {
Log("Unable to read entitlements from provisioning profile")
warnings += 1
}
if profile.appID != "*" && (newApplicationID != "" && newApplicationID != profile.appID) {
Log("Unable to change App ID to \(newApplicationID), provisioning profile won't allow it")
cleanup(tempFolder); return
}
} else {
Log("Unable to parse provisioning profile, it may be corrupt")
warnings += 1
}
}
修復可執行文件的權限:
if let bundleExecutable = getPlistKey(appBundleInfoPlist, keyName: "CFBundleExecutable"){
NSTask().execute(chmodPath, workingDirectory: nil, arguments: ["755", appBundlePath.stringByAppendingPathComponent(bundleExecutable)])
}
替換所有Info.plist里的CFBundleIdentifier
:
if let oldAppID = getPlistKey(appBundleInfoPlist, keyName: "CFBundleIdentifier") {
func changeAppexID(appexFile: String){
func shortName(file: String, payloadDirectory: String) -> String {
return file.substringFromIndex(payloadDirectory.endIndex)
}
let appexPlist = appexFile.stringByAppendingPathComponent("Info.plist")
if let appexBundleID = getPlistKey(appexPlist, keyName: "CFBundleIdentifier"){
let newAppexID = "\(newApplicationID)\(appexBundleID.substringFromIndex(oldAppID.endIndex))"
print("Changing \(shortName(appexFile, payloadDirectory: payloadDirectory)) id to \(newAppexID)")
setPlistKey(appexPlist, keyName: "CFBundleIdentifier", value: newAppexID)
}
if NSTask().execute(defaultsPath, workingDirectory: nil, arguments: ["read", appexPlist,"WKCompanionAppBundleIdentifier"]).status == 0 {
setPlistKey(appexPlist, keyName: "WKCompanionAppBundleIdentifier", value: newApplicationID)
}
recursiveDirectorySearch(appexFile, extensions: ["app"], found: changeAppexID)
}
recursiveDirectorySearch(appBundlePath, extensions: ["appex"], found: changeAppexID)
}
...
然后對所有的dylib
,so
,0
,vis
,pvr
,framework
,appex
,app
以及egg
文件用codesign
命令進行簽名。代碼略長,此處不寫。
4. 打包
最后將上述目錄用zip打包成ipa文件就可以了。
5. 安裝ipa文件
這里用到的是mobiledevice
工具,執行下列命令安裝ipa文件到手機上:
./mobiledevice install_app xxx.ipa
當然,你也可以使用ideviceinstaller
工具。
./ideviceinstaller -i xxx.ipa
總之,上述步驟較多,主要集中在前4個步驟上,不建議自己操作,你可以選擇使用圖形界面的iOS App Signer
應用,也可以使用我提供的根據其開源代碼寫的命令行工具,AppResign
,你可以直接下載編譯好的二進制文件。
使用方法:
./AppResign input out
這里以微信為例,我們一開始直接對其重簽名,總是不成功,我猜問題主要在里面的Watch App上。于是乎便采取的最簡單粗暴的方法,解壓ipa文件,將WeChat.app
里面的Watch
文件夾,連同PlugIns
文件夾一起刪去。再用AppResign
重簽名,如圖所示:
這時候將其安裝到設備上,成功,并能夠正常的打開。至此,這一步就大功告成了。
最后一點要注意的是,由于中國的開發者利用免費的證書大量對應用進行重簽名,所以目前蘋果加上了許多限制,免費開發者的provisioning證書有效時間從之前的30天改為7天,過期后需要重新簽名。另外就是一個星期內最多只能申請到10個證書。
安裝iOSOpenDev
如果上面的重簽名步驟你能夠成功的進行,接下來便開始考慮搭建編寫dylib的越獄開發環境了。這里我們選擇的是iOSOpenDev這個越獄開發平臺工具,該工具集成到Xcode里面,提供了編寫越獄應用插件的各種模版。所以下面就講講如何以正確的姿勢安裝iOSOpenDev。
一般一開始我們會去其官網上下載了一個pkg安裝文件然后點擊安裝,結果一般會安裝失敗,接下來就開始嘗試了各種姿勢。其實那個pkg安裝文件也沒干啥,就是執行了一個iod-setup腳本,好吧于是就手工執行了這個腳本,發現其中下載github上的東西老是失敗,然后翻了個墻,然后就好了。。。
sudo ./iod-setup base
sudo ./iod-setup sdk -sdk iphoneos
執行第二步會出現一個錯誤就是鏈接iOS 9.3的private framework失敗,主要是在9.3的SDK里去掉了private framework。
PrivateFramework directory not found: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk/System/Library/PrivateFrameworks
解決辦法:
1. 在[這里](https://jbdevs.org/sdks/)下載9.2的SDK。
2. 解壓后放到/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs。
3. 你可以把9.2里面的PrivateFrameworks文件夾復制到9.3對應的位置里path/to/iPhoneOS9.3.sdk/System/Library/。
4. 或者是修改/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Info.plist文件,將`MinimumSDKVersion`改為9.2。
5. 再次運行`sudo ./iod-setup sdk -sdk iphoneos`命令。
新建個CaptainHook連上設備編譯一下,就可以發現編譯成功了。
App Hook
在這部分里,我們便開始動手編寫dylib并將其注入到目標應用中。首先打開Xcode,新建一個項目,模版選擇iOSOpenDev里的CaptainHook Tweak,項目名為hook。
刪除hook.mm文件里的模版內容,替換為以下內容:
__attribute__((constructor)) static void entry() {
NSLog(@"hello, world!");
}
Cmd+B
編譯,打開LatestBuild文件夾,得到編譯好的dylib文件。
使用yololib工具對對二進制文件進行dylib的注入。
>./yololib WeChat.app/WeChat hook.dylib
2016-06-16 07:35:06.014 yololib[49519:642763] dylib path @executable_path/hook.dylib
2016-06-16 07:35:06.016 yololib[49519:642763] dylib path @executable_path/hook.dylib
Reading binary: WeChat.app/WeChat
2016-06-16 07:35:06.016 yololib[49519:642763] FAT binary!
2016-06-16 07:35:06.016 yololib[49519:642763] Injecting to arch 9
2016-06-16 07:35:06.016 yololib[49519:642763] Patching mach_header..
2016-06-16 07:35:06.016 yololib[49519:642763] Attaching dylib..
2016-06-16 07:35:06.016 yololib[49519:642763] Injecting to arch 0
2016-06-16 07:35:06.016 yololib[49519:642763] 64bit arch wow
2016-06-16 07:35:06.016 yololib[49519:642763] dylib size wow 56
2016-06-16 07:35:06.017 yololib[49519:642763] mach.ncmds 75
2016-06-16 07:35:06.017 yololib[49519:642763] mach.ncmds 76
2016-06-16 07:35:06.017 yololib[49519:642763] Patching mach_header..
2016-06-16 07:35:06.017 yololib[49519:642763] Attaching dylib..
2016-06-16 07:35:06.017 yololib[49519:642763] size 51
2016-06-16 07:35:06.017 yololib[49519:642763] complete!
注入成功后可以用MachOView程序查看整個MachO文件的結構,比如在Load Commands
這個數據段里,可以看到LC_LOAD_DYLIB
加載動態庫。我們使用MachOView打開,可以看到已經被注入hook.dylib,如圖所示。
別忘了,我們還需要將我們注入的dylib文件放到WeChat.app目錄下。
cp hook.dylib WeChat.app/
接下來就是之前的老套路了,先重簽名然后安裝到設備上。安裝完后,我們用idevicesyslog查看log信息。在命令行中輸入命令:
idevicesyslog
然后打開我們修改過的微信應用,可以看到如下圖的log輸出信息。
可以看到hello, world!
成功輸出,表示我們的dylib的代碼已經能夠執行。
一個簡單的CaptainHook載入Cycript
當然不可能一個NSLog結束了,所以接下來我們將編寫一個dylib來載入Cycript工具方便我們以后調試目標應用。
新建一個CaptainHook,項目名為loadCycript。首先,導入Cycript.framework
,另外還需要導入其依賴的JavaScriptCore.framework
,libsqlite3.0.tbd
和libstdc++.6.0.9.tbd
。
然后,在Build Settings里面的搜索bitcode,將Enable Bitcode
選項設為NO
。
一開始編寫loadCycript.mm文件的內容是這樣的:
#import <Cycript/Cycript.h>
#import <CaptainHook/CaptainHook.h>
#define CYCRIPT_PORT 8888
CHDeclareClass(AppDelegate);
CHDeclareClass(UIApplication);
CHOptimizedMethod2(self, void, AppDelegate, application, UIApplication *, application, didFinishLaunchingWithOptions, NSDictionary *, options)
{
CHSuper2(AppDelegate, application, application, didFinishLaunchingWithOptions, options);
NSLog(@"## Start Cycript ##");
CYListenServer(CYCRIPT_PORT);
}
__attribute__((constructor)) static void entry() {
CHLoadLateClass(AppDelegate);
CHHook2(AppDelegate, application, didFinishLaunchingWithOptions);
}
上述代碼hook了AppDelegate里的application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)
方法,在該方法里開啟Cycript并綁定到8888端口。
編譯該項目生成dylib文件,注入到微信App中,重簽名后安裝到手機里,然后在idevicesyslog輸出的日志里看看有沒有我們輸出的信息,結果找了半天,嗯哼,還真沒有。。。
問題出在哪了呢,難道是application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)
這個方法沒有hook成功嗎?好吧,隨后又試了試hookapplicationDidEnterBackground
等方法,結果沒一個成功,然后就一臉懵逼了,此時第一反應就是不會沒有AppDelegate
這個類吧。
沒辦法,只有導出微信的頭文件看看了。
class-dump -H -o header WeChat.app/WeChat
結果一看,還真沒有(°_°)… 通過搜索關鍵字AppDelegate
終于找到了其真身MicroMessengerAppDelegate
,雖說穿了個馬甲,但依然還是它。
該有的方法都有,這樣我就放心了。于是對之前的代碼稍作修改就可以了。
#import <Cycript/Cycript.h>
#import <CaptainHook/CaptainHook.h>
#define CYCRIPT_PORT 8888
CHDeclareClass(UIApplication);
CHDeclareClass(MicroMessengerAppDelegate);
CHOptimizedMethod2(self, void, MicroMessengerAppDelegate, application, UIApplication *, application, didFinishLaunchingWithOptions, NSDictionary *, options)
{
CHSuper2(MicroMessengerAppDelegate, application, application, didFinishLaunchingWithOptions, options);
NSLog(@"## Start Cycript ##");
CYListenServer(CYCRIPT_PORT);
}
CHConstructor {
@autoreleasepool {
CHLoadLateClass(MicroMessengerAppDelegate);
CHHook2(MicroMessengerAppDelegate, application, didFinishLaunchingWithOptions);
}
}
再跑一遍,這會我們就能在log日志里能看到Cycript成功開啟的消息了。
我們用Cycript遠程連上去調試看看,比如說修改發現頁的tableView背景顏色。
注:本文中用到的AppResign重簽名工具,以及編譯好的loadCycript.dylib可以在這里下載。
后續
至于之后該做什么,你想干嘛就干嘛。Cycript在手,天下我有,你可以使用Class-dump工具dunp出應用的頭文件,或者是將二進制文件拖到ida或hopper里面反匯編分析,寫tweak插件,實現各種姿勢搶紅包等等,本文就不討論這些了。
參考鏈接
之前看的都沒記錄,下列都是后來想到才記下來的。
蒸米的iOS冰與火之歌系列:
- http://drops.wooyun.org/author/蒸米
- http://drops.wooyun.org/papers/12803
- http://drops.wooyun.org/papers/13824
微信重簽名相關:
- https://testerhome.com/topics/4558
- http://www.lxweimin.com/p/3f57d51f770a/comments/1757000
- http://iosre.com/t/topic/2966
- http://bbs.iosre.com/t/targets-app/2664/15
iOSOpenDev:
- https://github.com/wzqcongcong/iOSOpenDev
- http://www.aichengxu.com/view/6004431
- http://bbs.iosre.com/t/xcode7-iosopendev-iosopendev-ios9/1963
- http://bbs.iosre.com/t/xcode-7-3-ios-9-3-sdk-theos-private-framework/3200
其它: