前言
本文會介紹一個自己寫的工具剑刑,能夠把第三方iOS應(yīng)用轉(zhuǎn)成動態(tài)庫媳纬,并加載到自己的App中,文章最后會以支付寶為例施掏,展示如何調(diào)用其中的C函數(shù)和OC方法钮惠。
工具開源地址:
https://github.com/tobefuturer/app2dylib
有什么用
為什么要把第三方應(yīng)用轉(zhuǎn)成動態(tài)庫呢?與一般的注入動態(tài)庫+重簽名打包的手段有什么不一樣呢七芭?
好處主要有下面幾點:
-
可以直接調(diào)用別人的算法
逆向分析別人的應(yīng)用時素挽,可能會遇到一些私有算法,如果搞不定的話狸驳,直接拿來用就好预明。
-
掌控程序的控制權(quán)
程序的主體是自己的App,第三方應(yīng)用的代碼只是以動態(tài)庫的形式加載耙箍,主要的控制權(quán)還是在我們自己手里撰糠,所以可以直接繞過應(yīng)用的檢測代碼(文章最后有關(guān)于這部分攻防的討論)。
-
同個進程內(nèi)加載多個應(yīng)用
重簽名打包畢竟只能是原來的應(yīng)用辩昆,但是如果是動態(tài)庫的話阅酪,可以同時加載多個應(yīng)用到進程內(nèi)了,比如你想同時把美圖秀秀和餓了么加載進來也是可以的(秀秀不餓汁针,想想去年大眾點評那個APPmixer的軟廣 - -! )术辐。
應(yīng)用和動態(tài)庫的異同
我們要把應(yīng)用轉(zhuǎn)成動態(tài)庫,首先要知道這兩者之前有什么相同與不同施无,有相同的才存在轉(zhuǎn)換的可能辉词,而不同之處就是我們要重點關(guān)注的了。
相同點:
可執(zhí)行文件和動態(tài)庫都是標(biāo)準的 Mach-O 文件格式猾骡,兩者的文件頭部結(jié)構(gòu)非常類似较屿,特別是其中的代碼段(TEXT),和數(shù)據(jù)段(DATA)結(jié)構(gòu)完全一致,這也是后面轉(zhuǎn)換工作的基礎(chǔ)卓练。
不同點
不同點就是我們轉(zhuǎn)換工作的重點了,主要有:
頭部的文件類型
一個是 MH_EXECUTE 可執(zhí)行文件购啄, 一個是 MH_DYLIB 動態(tài)庫襟企, 還有各種頭部的Flags,要特別留意下可執(zhí)行文件中Flags部分的 MH_PIE 標(biāo)志狮含,后面再詳細說顽悼。
[圖片上傳中...(image-6d38ae-1543815911164-2)]-
動態(tài)庫文件中多一個類型為 LC_ID_DYLIB 的 Load Command, 作用是動態(tài)庫的標(biāo)識符曼振,一般為文件路徑。路徑可以隨便填蔚龙,但是這部分必須要有冰评,是codesign的要求。
image -
可執(zhí)行文件會多出一個 PAGEZERO段木羹,動態(tài)庫中沒有甲雅。這個段開始地址為0(NULL指針指向的位置),是一個不可讀坑填、不可寫抛人、不可執(zhí)行的空間,能夠在空指針訪問時拋出異常脐瑰。這個段的大小妖枚,32位上是0x4000,64位上是4G苍在。這個段的處理也是轉(zhuǎn)換工作的重點之一绝页,之前有人嘗試轉(zhuǎn)換,不成功就是因為沒有處理好 PAGEZERO.
image
實現(xiàn)細節(jié)
修改文件類型
第一步是修改文件的頭部信息寂恬,把文件類型從可執(zhí)行文件修改成動態(tài)庫续誉,同時把一些Flags修改好。
這里一個比較關(guān)鍵的Flag是可執(zhí)行文件中的 MH_PIE 標(biāo)志位掠剑,(position-independent executable)屈芜。
這個標(biāo)志位,表明可執(zhí)行文件能夠在內(nèi)存中任意位置正確地運行朴译,而不受其絕對地址影響的特性井佑,這一特性是動態(tài)庫所必須的一個特性。沒有這個標(biāo)志位的可執(zhí)行文件是沒有辦法轉(zhuǎn)換成動態(tài)庫的眠寿。iOS系統(tǒng)中躬翁,arm64架構(gòu)下,目前這個標(biāo)志位是必須的盯拱,不然程序無法運行(系統(tǒng)的安全性要求)盒发,但是armv7架構(gòu)下,可以沒有這個標(biāo)志位狡逢,所以支付寶armv7版本的可執(zhí)行文件是不能轉(zhuǎn)成動態(tài)庫的宁舰,就是這個原因。不過所有的arm64的應(yīng)用都是可以轉(zhuǎn)換的奢浑,后面演示時用的支付寶是arm64架構(gòu)的蛮艰。
頭部中添加 LC_ID_DYLIB
直接在文件頭部中按照文檔格式插入一個Load Command,并填入合適的數(shù)據(jù)雀彼。這里要注意下插入內(nèi)容的字節(jié)數(shù)必須是8字節(jié)對齊的壤蚜。
修改PAGEZERO段
這部分是最重要的一部分即寡,因為arm64上這個段的大小有4G,直接往內(nèi)存中加載袜刷,會提示沒有足夠的連續(xù)的地址空間聪富,所以必須要調(diào)整這個段的大小,而要調(diào)整 PAGEZERO 這個段的大小, 又會引起一連串的地址空間的變化著蟹,所以不能盲目的直接改墩蔓,必須結(jié)合dyld的源碼來對應(yīng)修改。(注意這里不能直接把 PAGEZERO 這個段給去掉草则,也不能直接把大小調(diào)成0钢拧,因為涉及到dyld的rebase操作,詳細看后面)
1. 所有段的地址都要重新計算
單純減少 PAGEZERO 段的占用空間炕横,作用不大源内,因為dyld加載動態(tài)庫的時候,要求是所有的段一起進行mmap(詳細可以查看dyld源碼的ImageLoaderMachO::assignSegmentAddresses函數(shù))份殿,所以必須把接下來所有的段的地址都重新計算一次膜钓。
同時要保證,前后兩個段沒有地址空間重疊卿嘲,并且每個段都是按0x4000對齊颂斜。因為 PAGEZERO 是所有段中的第一個,所以可以直接把 PAGEZERO 的大小調(diào)整到0x4000,然后后面每一個段都按順序依次減少同樣大小(0xFFFFC000 = 0x100000000 - 0x4000),同時能保證每個段在文件內(nèi)的偏移量不變章喉。
修改前:
修改后:
2. 對動態(tài)庫進行rebase操作
這里的rebase是系統(tǒng)為了解決動態(tài)庫虛擬內(nèi)存地址沖突,在加載動態(tài)庫時進行的基地址重定位操作司蔬。
這一步操作是整個流程里最重要的,因為按照前面的操作姨蝴,整個文件地址空間已經(jīng)發(fā)生了變化俊啼,如果dyld依然按照原來的地址進行rebase,必然會失敗左医。
那么rebase操作需要做哪些工作呢授帕?
相關(guān)的信息儲存在 Mach-O 文件的 LINKEDIT 段中, 并由 LC_DYLD_INFO_ONLY 指定 rebase info 在文件中的偏移量
詳細的rebase信息:
紅框里那些Pointer的意思是說,在內(nèi)存地址為 0x367C698 的地方有一個指針浮梢,這個指針需要進行rebase操作, 操作的內(nèi)容就是和前面調(diào)整地址空間一樣跛十,每個指針減去 0xFFFFC000。
3. 為什么不能直接去掉PAGEZERO這個段
這個原因要涉及到文件中rebase信息的儲存格式秕硝,上面的圖中偶器,可以看出rebase要處理的是一個個指針,但是實際上這些信息在文件中并不是以指針數(shù)組的形式存在,而是以一連串rebase opcode的形式存在屏轰,上面看到的一個個指針其實是 Mach O View 這個軟件幫我們將opcode整理得到的。
[圖片上傳失敗...(image-193e8b-1543815911165)]
這些opcode中有一種操作比較關(guān)鍵憋飞,REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB霎苗。
這個opcode的意思是, 接下去需要調(diào)整文件的中的第2個段,就是圖中segment(2)所表示的含義榛做。
所以說唁盏,如果把PAGEZERO這個段給去掉了,文件中各個段的序號也就都錯位了检眯,與rebase中的信息就對應(yīng)不上了厘擂。
而且把這個段大小改為0,也是不行的锰瘸,因為dyld在加載的過程中刽严,會重新自動過濾掉大小為0的段,也會導(dǎo)致同樣的段序號錯位的問題避凝。(有興趣的同學(xué)可以看下dyld的源碼舞萄,在ImageLoaderMachO類的構(gòu)造函數(shù)里)
這就是為什么必須要保留PAGEZERO這個段,同時大小不能為0管削。
修改符號表
正常的線上應(yīng)用是不存在符號表的,但是如果你之前用了我的另一個工具 restore-symbol 來恢復(fù)符號表的話崎弃,這個地方自然也需要做一些處理艇炎,處理方法同rebase類似,減去0xFFFFC000.
不過有一些符號需要單獨過濾,比如這個:
這個radr://5614542是個什么神奇的符號呢,google就能發(fā)現(xiàn)咒精,念茜的twitter上提過這個奇葩的符號鞋屈。(女神果然是女神, 棒~ ??)
實際效果
工具開源在github上权旷,用法:
1.下載源碼編譯:
git clone --recursive https://github.com/tobefuturer/app2dylib.git
cd app2dylib && make
./app2dylib
2.把支付寶arm64砸殼坤邪,然后提取可執(zhí)行文件,用上面的工具把支付寶的可執(zhí)行文件轉(zhuǎn)成動態(tài)庫
./app2dylib /tmp/AlipayWallet -o /tmp/libAlipayApp.dylib
3.用 Xcode 新建工程,并把新生成的dylib拖進去,調(diào)整好各項設(shè)置.
Run Script里的代碼(目的是為了對dylib進行簽名)
cd ${BUILT_PRODUCTS_DIR}
cd ${FULL_PRODUCT_NAME}
/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none libAlipayApp.dylib
4.怎么調(diào)用動態(tài)庫里的方法呢蹦肴?
為方便大家嘗試,這里選兩個分析起來比較簡單的函數(shù)調(diào)用演示給大家。
一個是OC的方法 +[aluSecurity rsaEncryptText:pubKey:]
, 可以直接用oc運行時調(diào)用。
另一個是C的函數(shù) int base64_encode(char * output, int * output_length, char * input, int input_length)
這個需要先確定 base64_encode 這個C函數(shù)的函數(shù)簽名和在dylib中的偏移地址(我這邊的9.9.3版本是0xa798e4)侦镇,可以用ida分析得到闹炉。
運行結(jié)果:
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;
};
//運行時 直接調(diào)用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加載在內(nèi)存中的地址
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);
/** 根據(jù)偏移算出函數(shù)地址, 然后調(diào)用*/
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函數(shù)里swizzle了UI層的一些方法,會導(dǎo)致crash概说,如果想干掉那些+load方法的話碧注,看下面。
關(guān)于繞過檢測代碼
文章開頭的簡介中有提到席怪,以動態(tài)庫的形式加載应闯,能夠繞過應(yīng)用的檢測代碼,這說法不完全挂捻,因為如果把檢測代碼寫在類的+load方法里或者mod_init_func函數(shù)( 全局靜態(tài)變量的構(gòu)造函數(shù)和__attribute__((constructor))
指定的函數(shù) )里碉纺,在dylib加載的時候也是可以得到調(diào)用的。
那么也就衍生出兩種配搭的對抗方案:
i)越獄機
+load方法的調(diào)用是在libobjc.dylib中的call_load_methods函數(shù), mod_init_func函數(shù)的調(diào)用是在dyld中的doModInitFunctions函數(shù)骨田,可以直接用CydiaSubstrate inline hook掉這兩個函數(shù)耿导,而且動態(tài)庫是由我們自己加載的,所以可以控制hook和加載dylib的時序态贤。
ii) 非越獄機
非越獄機上舱呻,沒有辦法inline hook,但是可以利用_dyld_register_func_for_add_image 這個函數(shù)注冊回調(diào)悠汽,這個回調(diào)是發(fā)生在動態(tài)庫加載到內(nèi)存后箱吕,+load方法和mod_init_func函數(shù)調(diào)用前,所以可以在這個回調(diào)里把+load方法改名柿冲,把mod_init_func段改名等等茬高,也就可以使得各種檢測函數(shù)沒法調(diào)用了。
總之假抄,主要的控制權(quán)還是在我們手中怎栽。
工具開源地址
https://github.com/tobefuturer/app2dylib
測試環(huán)境:
iPhone 6Plus 、iOS 9.3.1 宿饱、arm64
支付寶9.9.3
實際使用過程中熏瞄,可能會遇到各種奇葩問題,可以去github上提issue谬以,或者email(tobefuturer@gmail.com)强饮,提問時請描述清楚遇到的問題和已經(jīng)嘗試過的解決方法。
參考鏈接&致謝
- dyld的源碼:https://opensource.apple.com/source/dyld/
- 感謝狗哥的iOS逆向群里 @Ouroboros蛉签, @Misty胡陪,@張總 三位大神的激烈討論,還有幫我砸支付寶殼的 @{}
- 順便推廣下iOS逆向的論壇 http://iosre.com/
原文地址:http://blog.imjun.net/posts/convert-iOS-app-to-dynamic-library/