文章轉(zhuǎn)載自楊君的小黑屋,對(duì)排版進(jìn)行了一些調(diào)整
前言
本文會(huì)介紹一個(gè)自己寫(xiě)的工具,能夠把第三方iOS應(yīng)用轉(zhuǎn)成動(dòng)態(tài)庫(kù),并加載到自己的App中,文章最后會(huì)以支付寶為例,展示如何調(diào)用其中的C函數(shù)和OC方法。
工具開(kāi)源地址:
https://github.com/tobefuturer/app2dylib
有什么用
為什么要把第三方應(yīng)用轉(zhuǎn)成動(dòng)態(tài)庫(kù)呢?與一般的注入動(dòng)態(tài)庫(kù)+重簽名打包的手段有什么不一樣呢?
好處主要有下面幾點(diǎn):
- 可以直接調(diào)用別人的算法
逆向分析別人的應(yīng)用時(shí),可能會(huì)遇到一些私有算法,如果搞不定的話,直接拿來(lái)用就好。 - 掌控程序的控制權(quán)
程序的主體是自己的App,第三方應(yīng)用的代碼只是以動(dòng)態(tài)庫(kù)的形式加載,主要的控制權(quán)還是在我們自己手里,所以可以直接繞過(guò)應(yīng)用的檢測(cè)代碼(文章最后有關(guān)于這部分攻防的討論)。 - 同個(gè)進(jìn)程內(nèi)加載多個(gè)應(yīng)用
重簽名打包畢竟只能是原來(lái)的應(yīng)用,但是如果是動(dòng)態(tài)庫(kù)的話,可以同時(shí)加載多個(gè)應(yīng)用到進(jìn)程內(nèi)了,比如你想同時(shí)把美圖秀秀和餓了么加載進(jìn)來(lái)也是可以的(秀秀不餓,想想去年大眾點(diǎn)評(píng)那個(gè)APPmixer的軟廣 - -! )。
應(yīng)用和動(dòng)態(tài)庫(kù)的異同
我們要把應(yīng)用轉(zhuǎn)成動(dòng)態(tài)庫(kù),首先要知道這兩者之前有什么相同與不同,有相同的才存在轉(zhuǎn)換的可能,而不同之處就是我們要重點(diǎn)關(guān)注的了。
相同點(diǎn):

可執(zhí)行文件和動(dòng)態(tài)庫(kù)都是標(biāo)準(zhǔn)的 Mach-O 文件格式,兩者的文件頭部結(jié)構(gòu)非常類似,特別是其中的代碼段(TEXT),和數(shù)據(jù)段(DATA)結(jié)構(gòu)完全一致,這也是后面轉(zhuǎn)換工作的基礎(chǔ)。
不同點(diǎn):
不同點(diǎn)就是我們轉(zhuǎn)換工作的重點(diǎn)了,主要有:
- 頭部的文件類型
一個(gè)是 MH_EXECUTE 可執(zhí)行文件, 一個(gè)是 MH_DYLIB 動(dòng)態(tài)庫(kù), 還有各種頭部的Flags,要特別留意下可執(zhí)行文件中Flags部分的 MH_PIE 標(biāo)志,后面再詳細(xì)說(shuō)。
頭部的文件類型不同 - 動(dòng)態(tài)庫(kù)文件中多一個(gè)類型為 LC_ID_DYLIB 的 Load Command, 作用是動(dòng)態(tài)庫(kù)的標(biāo)識(shí)符,一般為文件路徑。路徑可以隨便填,但是這部分必須要有,是codesign的要求。
LC_ID_DYLIB 的 Load Command - 可執(zhí)行文件會(huì)多出一個(gè) PAGEZERO段,動(dòng)態(tài)庫(kù)中沒(méi)有。這個(gè)段開(kāi)始地址為0(NULL指針指向的位置),是一個(gè)不可讀、不可寫(xiě)、不可執(zhí)行的空間,能夠在空指針訪問(wèn)時(shí)拋出異常。這個(gè)段的大小,32位上是0x4000,64位上是4G。這個(gè)段的處理也是轉(zhuǎn)換工作的重點(diǎn)之一,之前有人嘗試轉(zhuǎn)換,不成功就是因?yàn)闆](méi)有處理好 PAGEZERO.
多出的PAGEZERO段
實(shí)現(xiàn)細(xì)節(jié)
修改文件類型
第一步是修改文件的頭部信息,把文件類型從可執(zhí)行文件修改成動(dòng)態(tài)庫(kù),同時(shí)把一些Flags修改好。
這里一個(gè)比較關(guān)鍵的Flag是可執(zhí)行文件中的 MH_PIE 標(biāo)志位,(position-independent executable)。
這個(gè)標(biāo)志位,表明可執(zhí)行文件能夠在內(nèi)存中任意位置正確地運(yùn)行,而不受其絕對(duì)地址影響的特性,這一特性是動(dòng)態(tài)庫(kù)所必須的一個(gè)特性。沒(méi)有這個(gè)標(biāo)志位的可執(zhí)行文件是沒(méi)有辦法轉(zhuǎn)換成動(dòng)態(tài)庫(kù)的。iOS系統(tǒng)中,arm64架構(gòu)下,目前這個(gè)標(biāo)志位是必須的,不然程序無(wú)法運(yùn)行(系統(tǒng)的安全性要求),但是armv7架構(gòu)下,可以沒(méi)有這個(gè)標(biāo)志位,所以支付寶armv7版本的可執(zhí)行文件是不能轉(zhuǎn)成動(dòng)態(tài)庫(kù)的,就是這個(gè)原因。不過(guò)所有的arm64的應(yīng)用都是可以轉(zhuǎn)換的,后面演示時(shí)用的支付寶是arm64架構(gòu)的。
頭部中添加 LC_ID_DYLIB
直接在文件頭部中按照文檔格式插入一個(gè)Load Command,并填入合適的數(shù)據(jù)。這里要注意下插入內(nèi)容的字節(jié)數(shù)必須是8字節(jié)對(duì)齊的。
修改PAGEZERO段
這部分是最重要的一部分,因?yàn)閍rm64上這個(gè)段的大小有4G,直接往內(nèi)存中加載,會(huì)提示沒(méi)有足夠的連續(xù)的地址空間,所以必須要調(diào)整這個(gè)段的大小,而要調(diào)整 PAGEZERO 這個(gè)段的大小, 又會(huì)引起一連串的地址空間的變化,所以不能盲目的直接改,必須結(jié)合dyld的源碼來(lái)對(duì)應(yīng)修改。(注意這里不能直接把 PAGEZERO 這個(gè)段給去掉,也不能直接把大小調(diào)成0,因?yàn)樯婕暗絛yld的rebase操作,詳細(xì)看后面)
1. 所有段的地址都要重新計(jì)算
單純減少 PAGEZERO 段的占用空間,作用不大,因?yàn)閐yld加載動(dòng)態(tài)庫(kù)的時(shí)候,要求是所有的段一起進(jìn)行mmap(詳細(xì)可以查看dyld源碼的ImageLoaderMachO::assignSegmentAddresses函數(shù)),所以必須把接下來(lái)所有的段的地址都重新計(jì)算一次。
同時(shí)要保證,前后兩個(gè)段沒(méi)有地址空間重疊,并且每個(gè)段都是按0x4000對(duì)齊。因?yàn)?PAGEZERO 是所有段中的第一個(gè),所以可以直接把 PAGEZERO 的大小調(diào)整到0x4000,然后后面每一個(gè)段都按順序依次減少同樣大小(0xFFFFC000 = 0x100000000 - 0x4000),同時(shí)能保證每個(gè)段在文件內(nèi)的偏移量不變。
修改前:

修改后:

2. 對(duì)動(dòng)態(tài)庫(kù)進(jìn)行rebase操作
這里的rebase是系統(tǒng)為了解決動(dòng)態(tài)庫(kù)虛擬內(nèi)存地址沖突,在加載動(dòng)態(tài)庫(kù)時(shí)進(jìn)行的基地址重定位操作。
這一步操作是整個(gè)流程里最重要的,因?yàn)榘凑涨懊娴牟僮?,整個(gè)文件地址空間已經(jīng)發(fā)生了變化,如果dyld依然按照原來(lái)的地址進(jìn)行rebase,必然會(huì)失敗。
那么rebase操作需要做哪些工作呢?
相關(guān)的信息儲(chǔ)存在 Mach-O 文件的 LINKEDIT 段中, 并由 LC_DYLD_INFO_ONLY 指定 rebase info 在文件中的偏移量

詳細(xì)的rebase信息:

紅框里那些Pointer的意思是說(shuō),在內(nèi)存地址為 0x367C698 的地方有一個(gè)指針,這個(gè)指針需要進(jìn)行rebase操作, 操作的內(nèi)容就是和前面調(diào)整地址空間一樣,每個(gè)指針減去 0xFFFFC000。

3. 為什么不能直接去掉PAGEZERO這個(gè)段
這個(gè)原因要涉及到文件中rebase信息的儲(chǔ)存格式,上面的圖中,可以看出rebase要處理的是一個(gè)個(gè)指針,但是實(shí)際上這些信息在文件中并不是以指針數(shù)組的形式存在,而是以一連串rebase opcode的形式存在,上面看到的一個(gè)個(gè)指針其實(shí)是 Mach O View 這個(gè)軟件幫我們將opcode整理得到的。

這些opcode中有一種操作比較關(guān)鍵,REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB。

這個(gè)opcode的意思是, 接下去需要調(diào)整文件的中的第2個(gè)段,就是圖中segment(2)所表示的含義。
所以說(shuō),如果把PAGEZERO這個(gè)段給去掉了,文件中各個(gè)段的序號(hào)也就都錯(cuò)位了,與rebase中的信息就對(duì)應(yīng)不上了。
而且把這個(gè)段大小改為0,也是不行的,因?yàn)閐yld在加載的過(guò)程中,會(huì)重新自動(dòng)過(guò)濾掉大小為0的段,也會(huì)導(dǎo)致同樣的段序號(hào)錯(cuò)位的問(wèn)題。(有興趣的同學(xué)可以看下dyld的源碼,在ImageLoaderMachO類的構(gòu)造函數(shù)里)
這就是為什么必須要保留PAGEZERO這個(gè)段,同時(shí)大小不能為0。
修改符號(hào)表
正常的線上應(yīng)用是不存在符號(hào)表的,但是如果你之前用了我的另一個(gè)工具 restore-symbol來(lái)恢復(fù)符號(hào)表的話,這個(gè)地方自然也需要做一些處理,處理方法同rebase類似,減去0xFFFFC000.
不過(guò)有一些符號(hào)需要單獨(dú)過(guò)濾,比如這個(gè):

這個(gè)radr://5614542是個(gè)什么神奇的符號(hào)呢,google就能發(fā)現(xiàn),念茜的twitter上提過(guò)這個(gè)奇葩的符號(hào)。(女神果然是女神, 棒~(yú) ??)

實(shí)際效果
工具開(kāi)源在github上,用法:
1. 下載源碼編譯:
git clone --recursive https://github.com/tobefuturer/app2dylib.git
cd app2dylib && make
./app2dylib
2. 把支付寶arm64砸殼,然后提取可執(zhí)行文件,用上面的工具把支付寶的可執(zhí)行文件轉(zhuǎn)成動(dòng)態(tài)庫(kù)
./app2dylib /tmp/AlipayWallet -o /tmp/libAlipayApp.dylib
3. 用 Xcode 新建工程,并把新生成的dylib拖進(jìn)去,調(diào)整好各項(xiàng)設(shè)置.

Run Script里的代碼(目的是為了對(duì)dylib進(jìn)行簽名)
cd ${BUILT_PRODUCTS_DIR}
cd ${FULL_PRODUCT_NAME}
/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none libAlipayApp.dylib
4. 怎么調(diào)用動(dòng)態(tài)庫(kù)里的方法呢?
為方便大家嘗試,這里選兩個(gè)分析起來(lái)比較簡(jiǎn)單的函數(shù)調(diào)用演示給大家。
一個(gè)是OC的方法 +[aluSecurity rsaEncryptText:pubKey:], 可以直接用oc運(yùn)行時(shí)調(diào)用。
另一個(gè)是C的函數(shù) int base64_encode(char * output, int * output_length, char * input, int input_length)
這個(gè)需要先確定 base64_encode 這個(gè)C函數(shù)的函數(shù)簽名和在dylib中的偏移地址(我這邊的9.9.3版本是0xa798e4),可以用ida分析得到。
運(yùn)行結(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;
};
//運(yùn)行時(shí) 直接調(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);
//確認(rèn)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:示例代碼中,我刻意除掉了界面部分的代碼,因?yàn)橹Ц秾毜?load函數(shù)里swizzle了UI層的一些方法,會(huì)導(dǎo)致crash,如果想干掉那些+load方法的話,看下面。
關(guān)于繞過(guò)檢測(cè)代碼
文章開(kāi)頭的簡(jiǎn)介中有提到,以動(dòng)態(tài)庫(kù)的形式加載,能夠繞過(guò)應(yīng)用的檢測(cè)代碼,這說(shuō)法不完全,因?yàn)槿绻褭z測(cè)代碼寫(xiě)在類的+load方法里或者mod_init_func函數(shù)( 全局靜態(tài)變量的構(gòu)造函數(shù)和attribute((constructor))指定的函數(shù) )里,在dylib加載的時(shí)候也是可以得到調(diào)用的。
那么也就衍生出兩種配搭的對(duì)抗方案:
i)越獄機(jī)
+load方法的調(diào)用是在libobjc.dylib中的call_load_methods函數(shù), mod_init_func函數(shù)的調(diào)用是在dyld中的doModInitFunctions函數(shù),可以直接用CydiaSubstrate inline hook掉這兩個(gè)函數(shù),而且動(dòng)態(tài)庫(kù)是由我們自己加載的,所以可以控制hook和加載dylib的時(shí)序。
ii) 非越獄機(jī)
非越獄機(jī)上,沒(méi)有辦法inline hook,但是可以利用_dyld_register_func_for_add_image 這個(gè)函數(shù)注冊(cè)回調(diào),這個(gè)回調(diào)是發(fā)生在動(dòng)態(tài)庫(kù)加載到內(nèi)存后,+load方法和mod_init_func函數(shù)調(diào)用前,所以可以在這個(gè)回調(diào)里把+load方法改名,把mod_init_func段改名等等,也就可以使得各種檢測(cè)函數(shù)沒(méi)法調(diào)用了。
總之,主要的控制權(quán)還是在我們手中。
測(cè)試環(huán)境:
iPhone 6Plus 、iOS 9.3.1 、arm64
支付寶9.9.3
參考鏈接&致謝
- dyld的源碼:https://opensource.apple.com/source/dyld/
- iOS逆向的論壇 http://iosre.com/


