注:這麼長時間以來本文後續加入了新的內容以及一些修改,由於精力有限,所以不會同步更新到簡書上。請大家移步到本專案的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還是會閃退。
首先,找到應用對應的二進位制檔案,檢視包含哪些架構:
1 2 3 4 |
> 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代表被解密了:
1 2 3 4 5 6 7 8 9 10 11 12 |
> 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和一個分享擴充套件。所以同理,我們還需要依次確認以下二進位制檔案,這裡就跳過了。
1 2 3 |
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
結構體中以陣列的形式返回,並對其結果做了一個刷選,去掉了那些過期的證照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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格式)。
1 |
security cms -D -i "~/Library/MobileDevice/Provisioning Profiles/xxx.mobileprovision" |
獲取開發者簽名證照:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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") } |
以上程式碼相當於:
1 2 3 |
> security find-identity -v -p codesigning 1) 1234567890123456789012345678901234567890 "iPhone Developer: XXX (xxxxxxxxxx)" 2) 1234567890123456789012345678901234567890 "Mac Developer: XXX (xxxxxxxxxx)" |
2. 一些準備工作
建立臨時目錄,makeTempFolder()
方法:
1 2 |
> mktemp -d -t com.eular.test /var/folders/qr/8_n21zhd4f993khcsh_qll000000gp/T/com.eular.test.6aHPpdBZ |
處理不同格式的輸入檔案,包括deb
,ipa
,app
,xcarchive
:
1 2 3 4 |
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目錄裡:
1 |
cp xxx.mobileprovision Payload/xxx.app/embedded.mobileprovision |
根據provisioning檔案匯出entitlements.plist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 } } |
修復可執行檔案的許可權:
1 2 3 |
if let bundleExecutable = getPlistKey(appBundleInfoPlist, keyName: "CFBundleExecutable"){ NSTask().execute(chmodPath, workingDirectory: nil, arguments: ["755", appBundlePath.stringByAppendingPathComponent(bundleExecutable)]) } |
替換所有Info.plist裡的CFBundleIdentifier
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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檔案到手機上:
1 |
./mobiledevice install_app xxx.ipa |
當然,你也可以使用ideviceinstaller
工具。
1 |
./ideviceinstaller -i xxx.ipa |
總之,上述步驟較多,主要集中在前4個步驟上,不建議自己操作,你可以選擇使用圖形介面的iOS App Signer
應用,也可以使用我提供的根據其開原始碼寫的命令列工具,AppResign
,你可以直接下載編譯好的二進位制檔案。
使用方法:
1 |
./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上的東西老是失敗,然後翻了個牆,然後就好了。。。
1 2 |
sudo ./iod-setup base sudo ./iod-setup sdk -sdk iphoneos |
執行第二步會出現一個錯誤就是連結iOS 9.3的private framework失敗,主要是在9.3的SDK裡去掉了private framework。
1 |
PrivateFramework directory not found: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk/System/Library/PrivateFrameworks |
解決辦法:
1 2 3 4 5 |
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檔案裡的模版內容,替換為以下內容:
1 2 3 |
__attribute__((constructor)) static void entry() { NSLog(@"hello, world!"); } |
Cmd+B
編譯,開啟LatestBuild資料夾,得到編譯好的dylib檔案。
使用yololib工具對對二進位制檔案進行dylib的注入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
>./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目錄下。
1 |
cp hook.dylib WeChat.app/ |
接下來就是之前的老套路了,先重簽名然後安裝到裝置上。安裝完後,我們用idevicesyslog檢視log資訊。在命令列中輸入命令:
1 |
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檔案的內容是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#import #import #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
這個類吧。
沒辦法,只有匯出微信的標頭檔案看看了。
1 |
class-dump -H -o header WeChat.app/WeChat |
結果一看,還真沒有(°_°)… 通過搜尋關鍵字AppDelegate
終於找到了其真身MicroMessengerAppDelegate
,雖說穿了個馬甲,但依然還是它。
該有的方法都有,這樣我就放心了。於是對之前的程式碼稍作修改就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#import #import #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.jianshu.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
其它: