前言
本文會介紹一個自己寫的工具,能夠把第三方iOS應用轉成動態庫,並載入到自己的App中,文章最後會以支付寶為例,展示如何呼叫其中的C函式和OC方法。
工具開源地址:
https://github.com/tobefuturer/app2dylib
有什麼用
為什麼要把第三方應用轉成動態庫呢?與一般的注入動態庫+重簽名打包的手段有什麼不一樣呢?
好處主要有下面幾點:
- 可以直接呼叫別人的演算法
逆向分析別人的應用時,可能會遇到一些私有演算法,如果搞不定的話,直接拿來用就好。
- 掌控程式的控制權
程式的主體是自己的App,第三方應用的程式碼只是以動態庫的形式載入,主要的控制權還是在我們自己手裡,所以可以直接繞過應用的檢測程式碼(文章最後有關於這部分攻防的討論)。
- 同個程式內載入多個應用
重簽名打包畢竟只能是原來的應用,但是如果是動態庫的話,可以同時載入多個應用到程式內了,比如你想同時把美圖秀秀和餓了麼載入進來也是可以的(秀秀不餓,想想去年大眾點評那個APPmixer的軟廣 – -! )。
應用和動態庫的異同
我們要把應用轉成動態庫,首先要知道這兩者之前有什麼相同與不同,有相同的才存在轉換的可能,而不同之處就是我們要重點關注的了。
相同點:
可執行檔案和動態庫都是標準的 Mach-O 檔案格式,兩者的檔案頭部結構非常類似,特別是其中的程式碼段(TEXT),和資料段(DATA)結構完全一致,這也是後面轉換工作的基礎。
不同點
不同點就是我們轉換工作的重點了,主要有:
- 頭部的檔案型別
一個是 MH_EXECUTE 可執行檔案, 一個是 MH_DYLIB 動態庫, 還有各種頭部的Flags,要特別留意下可執行檔案中Flags部分的 MH_PIE 標誌,後面再詳細說。
- 動態庫檔案中多一個型別為 LC_ID_DYLIB 的 Load Command, 作用是動態庫的識別符號,一般為檔案路徑。路徑可以隨便填,但是這部分必須要有,是codesign的要求。
- 可執行檔案會多出一個 PAGEZERO段,動態庫中沒有。這個段開始地址為0(NULL指標指向的位置),是一個不可讀、不可寫、不可執行的空間,能夠在空指標訪問時丟擲異常。這個段的大小,32位上是0x4000,64位上是4G。這個段的處理也是轉換工作的重點之一,之前有人嘗試轉換,不成功就是因為沒有處理好 PAGEZERO.
實現細節
修改檔案型別
第一步是修改檔案的頭部資訊,把檔案型別從可執行檔案修改成動態庫,同時把一些Flags修改好。
這裡一個比較關鍵的Flag是可執行檔案中的 MH_PIE 標誌位,(position-independent executable)。
這個標誌位,表明可執行檔案能夠在記憶體中任意位置正確地執行,而不受其絕對地址影響的特性,這一特性是動態庫所必須的一個特性。沒有這個標誌位的可執行檔案是沒有辦法轉換成動態庫的。iOS系統中,arm64架構下,目前這個標誌位是必須的,不然程式無法執行(系統的安全性要求),但是armv7架構下,可以沒有這個標誌位,所以支付寶armv7版本的可執行檔案是不能轉成動態庫的,就是這個原因。不過所有的arm64的應用都是可以轉換的,後面演示時用的支付寶是arm64架構的。
頭部中新增 LC_ID_DYLIB
直接在檔案頭部中按照文件格式插入一個Load Command,並填入合適的資料。這裡要注意下插入內容的位元組數必須是8位元組對齊的。
修改PAGEZERO段
這部分是最重要的一部分,因為arm64上這個段的大小有4G,直接往記憶體中載入,會提示沒有足夠的連續的地址空間,所以必須要調整這個段的大小,而要調整 PAGEZERO 這個段的大小, 又會引起一連串的地址空間的變化,所以不能盲目的直接改,必須結合dyld的原始碼來對應修改。(注意這裡不能直接把 PAGEZERO 這個段給去掉,也不能直接把大小調成0,因為涉及到dyld的rebase操作,詳細看後面)
1. 所有段的地址都要重新計算
單純減少 PAGEZERO 段的佔用空間,作用不大,因為dyld載入動態庫的時候,要求是所有的段一起進行mmap(詳細可以檢視dyld原始碼的ImageLoaderMachO::assignSegmentAddresses函式),所以必須把接下來所有的段的地址都重新計算一次。
同時要保證,前後兩個段沒有地址空間重疊,並且每個段都是按0x4000對齊。因為 PAGEZERO 是所有段中的第一個,所以可以直接把 PAGEZERO 的大小調整到0x4000,然後後面每一個段都按順序依次減少同樣大小(0xFFFFC000 = 0x100000000 – 0x4000),同時能保證每個段在檔案內的偏移量不變。
修改前:
修改後:
2. 對動態庫進行rebase操作
這裡的rebase是系統為了解決動態庫虛擬記憶體地址衝突,在載入動態庫時進行的基地址重定位操作。
這一步操作是整個流程裡最重要的,因為按照前面的操作,整個檔案地址空間已經發生了變化,如果dyld依然按照原來的地址進行rebase,必然會失敗。
那麼rebase操作需要做哪些工作呢?
相關的資訊儲存在 Mach-O 檔案的 LINKEDIT 段中, 並由 LC_DYLD_INFO_ONLY 指定 rebase info 在檔案中的偏移量
詳細的rebase資訊:
紅框裡那些Pointer的意思是說,在記憶體地址為 0x367C698 的地方有一個指標,這個指標需要進行rebase操作, 操作的內容就是和前面調整地址空間一樣,每個指標減去 0xFFFFC000。
3. 為什麼不能直接去掉PAGEZERO這個段
這個原因要涉及到檔案中rebase資訊的儲存格式,上面的圖中,可以看出rebase要處理的是一個個指標,但是實際上這些資訊在檔案中並不是以指標陣列的形式存在,而是以一連串rebase opcode的形式存在,上面看到的一個個指標其實是 Mach O View 這個軟體幫我們將opcode整理得到的。
這些opcode中有一種操作比較關鍵,REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB。
這個opcode的意思是, 接下去需要調整檔案的中的第2個段,就是圖中segment(2)所表示的含義。
所以說,如果把PAGEZERO這個段給去掉了,檔案中各個段的序號也就都錯位了,與rebase中的資訊就對應不上了。
而且把這個段大小改為0,也是不行的,因為dyld在載入的過程中,會重新自動過濾掉大小為0的段,也會導致同樣的段序號錯位的問題。(有興趣的同學可以看下dyld的原始碼,在ImageLoaderMachO類的建構函式裡)
這就是為什麼必須要保留PAGEZERO這個段,同時大小不能為0。
修改符號表
正常的線上應用是不存在符號表的,但是如果你之前用了我的另一個工具 restore-symbol 來恢復符號表的話,這個地方自然也需要做一些處理,處理方法同rebase類似,減去0xFFFFC000.
不過有一些符號需要單獨過濾,比如這個:
這個radr://5614542是個什麼神奇的符號呢,google就能發現,念茜的twitter上提過這個奇葩的符號。(女神果然是女神, 棒~ ?)
實際效果
工具開源在github上,用法:
1.下載原始碼編譯:
1 2 3 |
git clone --recursive https://github.com/tobefuturer/app2dylib.git cd app2dylib && make ./app2dylib |
2.把支付寶arm64砸殼,然後提取可執行檔案,用上面的工具把支付寶的可執行檔案轉成動態庫
1 |
./app2dylib /tmp/AlipayWallet -o /tmp/libAlipayApp.dylib |
3.用 Xcode 新建工程,並把新生成的dylib拖進去,調整好各項設定.
Run Script裡的程式碼(目的是為了對dylib進行簽名)
1 2 3 |
cd ${BUILT_PRODUCTS_DIR} cd ${FULL_PRODUCT_NAME} /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none libAlipayApp.dylib |
4.怎麼呼叫動態庫裡的方法呢?
為方便大家嘗試,這裡選兩個分析起來比較簡單的函式呼叫演示給大家。
一個是OC的方法 +[aluSecurity rsaEncryptText:pubKey:]
, 可以直接用oc執行時呼叫。
另一個是C的函式 int base64_encode(char * output, int * output_length, char * input, int input_length)
這個需要先確定 base64_encode 這個C函式的函式簽名和在dylib中的偏移地址(我這邊的9.9.3版本是0xa798e4),可以用ida分析得到。
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 28 29 30 31 32 33 34 35 36 37 38 39 |
#import <UIKit/UIKit.h> #import <dlfcn.h> #import <mach/mach.h> #import <mach-o/loader.h> #import <mach-o/dyld.h> #import <objc/runtime.h> int main(int argc, char * argv[]) { NSLog(@"\n===Start===\n"); NSString * dylibName = @"libAlipayApp"; NSString * path = [[NSBundle mainBundle] pathForResource:dylibName ofType:@"dylib"]; if (dlopen(path.UTF8String, RTLD_NOW) == NULL){ NSLog(@"dlopen failed ,error %s", dlerror()); return 0; }; //執行時 直接呼叫oc方法 NSString * plain = @"alipay"; NSString * pubkey = @"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZ6i9VNEGEaZaYE7XffA9XRj15cp/ZKhHYY43EEva8LIhCWi29EREaF4JjZVMwFpUAfrL+9gpA7NMQmaMRHbrz1KHe2Ho4HpUhEac8M9zUbNvaDKSlhx0lq/15TQP+57oQbfJ9oKKd+he4Yd6jpBI3UtGmwJyN/T1S0DQ0aXR8OQIDAQAB"; NSString * cipher = [NSClassFromString(@"aluSecurity") performSelector:NSSelectorFromString(@"rsaEncryptText:pubKey:") withObject:plain withObject:pubkey]; NSLog(@"\n-----------call oc method---------\n明文:%@\n密文: %@\n-----------------------------------", plain,cipher); //確認dylib載入在記憶體中的地址 uint64_t slide = 0; for (int i = 0; i < _dyld_image_count(); i ++) if ([[NSString stringWithUTF8String:_dyld_get_image_name(i)] isEqualToString:path]) slide = _dyld_get_image_vmaddr_slide(i); assert(slide != 0); typedef int (*BASE64_ENCODE_FUNC_TYPE) (char * output, int * output_size , char * input, int input_length); /** 根據偏移算出函式地址, 然後呼叫*/ long long base64_encode_offset_in_dylib = 0xa798e4; BASE64_ENCODE_FUNC_TYPE base64_encode = (BASE64_ENCODE_FUNC_TYPE)(slide + base64_encode_offset_in_dylib); char output[1000] = {0}; int length = 1000; char * input = "alipay"; base64_encode(output, & length, input, (int)strlen(input)); NSLog(@"\n-----------call c function---------\nbase64: %s -> %s\n-----------------------------------", input, output); } |
ps:示例程式碼中,我刻意除掉了介面部分的程式碼,因為支付寶的+load函式裡swizzle了UI層的一些方法,會導致crash,如果想幹掉那些+load方法的話,看下面。
關於繞過檢測程式碼
文章開頭的簡介中有提到,以動態庫的形式載入,能夠繞過應用的檢測程式碼,這說法不完全,因為如果把檢測程式碼寫在類的+load方法裡或者mod_init_func函式( 全域性靜態變數的建構函式和__attribute__((constructor))
指定的函式 )裡,在dylib載入的時候也是可以得到呼叫的。
那麼也就衍生出兩種配搭的對抗方案:
i)越獄機
+load方法的呼叫是在libobjc.dylib中的call_load_methods函式, mod_init_func函式的呼叫是在dyld中的doModInitFunctions函式,可以直接用CydiaSubstrate inline hook掉這兩個函式,而且動態庫是由我們自己載入的,所以可以控制hook和載入dylib的時序。
ii) 非越獄機
非越獄機上,沒有辦法inline hook,但是可以利用_dyld_register_func_for_add_image 這個函式註冊回撥,這個回撥是發生在動態庫載入到記憶體後,+load方法和mod_init_func函式呼叫前,所以可以在這個回撥裡把+load方法改名,把mod_init_func段改名等等,也就可以使得各種檢測函式沒法呼叫了。
總之,主要的控制權還是在我們手中。
工具開源地址
https://github.com/tobefuturer/app2dylib
測試環境:
iPhone 6Plus 、iOS 9.3.1 、arm64
支付寶9.9.3
實際使用過程中,可能會遇到各種奇葩問題,可以去github上提issue,或者email(tobefuturer@gmail.com),提問時請描述清楚遇到的問題和已經嘗試過的解決方法。
參考連結&致謝
- dyld的原始碼:https://opensource.apple.com/source/dyld/
- 感謝狗哥的iOS逆向群裡 @Ouroboros, @Misty,@張總 三位大神的激烈討論,還有幫我砸支付寶殼的 @{}
- 順便推廣下iOS逆向的論壇 http://iosre.com/