[轉載]黑科技:把第三方 iOS 應用轉成動態(tài)庫

前言

本文會介紹一個自己寫的工具,能夠把第三方iOS應用轉成動態(tài)庫,并加載到自己的App中,文章最后會以支付寶為例,展示如何調用其中的C函數(shù)和OC方法。

工具開源地址:
https://github.com/tobefuturer/app2dylib

有什么用

為什么要把第三方應用轉成動態(tài)庫呢?與一般的注入動態(tài)庫+重簽名打包的手段有什么不一樣呢?

好處主要有下面幾點:

  1. 可以直接調用別人的算法

    逆向分析別人的應用時,可能會遇到一些私有算法,如果搞不定的話,直接拿來用就好。

  2. 掌控程序的控制權

    程序的主體是自己的App,第三方應用的代碼只是以動態(tài)庫的形式加載,主要的控制權還是在我們自己手里,所以可以直接繞過應用的檢測代碼(文章最后有關于這部分攻防的討論)。

  3. 同個進程內加載多個應用

    重簽名打包畢竟只能是原來的應用,但是如果是動態(tài)庫的話,可以同時加載多個應用到進程內了,比如你想同時把美圖秀秀和餓了么加載進來也是可以的(秀秀不餓,想想去年大眾點評那個APPmixer的軟廣 - -! )。

應用和動態(tài)庫的異同

我們要把應用轉成動態(tài)庫,首先要知道這兩者之前有什么相同與不同,有相同的才存在轉換的可能,而不同之處就是我們要重點關注的了。

相同點:

![p://upload-images.jianshu.io/upload_images/8702968-b2484b42e3e7e035.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

可執(zhí)行文件和動態(tài)庫都是標準的 Mach-O 文件格式,兩者的文件頭部結構非常類似,特別是其中的代碼段(TEXT),和數(shù)據(jù)段(DATA)結構完全一致,這也是后面轉換工作的基礎。

不同點

不同點就是我們轉換工作的重點了,主要有:

  1. 頭部的文件類型
    一個是 MH_EXECUTE 可執(zhí)行文件, 一個是 MH_DYLIB 動態(tài)庫, 還有各種頭部的Flags,要特別留意下可執(zhí)行文件中Flags部分的 MH_PIE 標志,后面再詳細說。
    [圖片上傳中...(image-6d38ae-1543815911164-2)]

  2. 動態(tài)庫文件中多一個類型為 LC_ID_DYLIB 的 Load Command, 作用是動態(tài)庫的標識符,一般為文件路徑。路徑可以隨便填,但是這部分必須要有,是codesign的要求。


    image
  3. 可執(zhí)行文件會多出一個 PAGEZERO段,動態(tài)庫中沒有。這個段開始地址為0(NULL指針指向的位置),是一個不可讀、不可寫、不可執(zhí)行的空間,能夠在空指針訪問時拋出異常。這個段的大小,32位上是0x4000,64位上是4G。這個段的處理也是轉換工作的重點之一,之前有人嘗試轉換,不成功就是因為沒有處理好 PAGEZERO.


    image

實現(xiàn)細節(jié)

修改文件類型

第一步是修改文件的頭部信息,把文件類型從可執(zhí)行文件修改成動態(tài)庫,同時把一些Flags修改好。

這里一個比較關鍵的Flag是可執(zhí)行文件中的 MH_PIE 標志位,(position-independent executable)。

這個標志位,表明可執(zhí)行文件能夠在內存中任意位置正確地運行,而不受其絕對地址影響的特性,這一特性是動態(tài)庫所必須的一個特性。沒有這個標志位的可執(zhí)行文件是沒有辦法轉換成動態(tài)庫的。iOS系統(tǒng)中,arm64架構下,目前這個標志位是必須的,不然程序無法運行(系統(tǒng)的安全性要求),但是armv7架構下,可以沒有這個標志位,所以支付寶armv7版本的可執(zhí)行文件是不能轉成動態(tài)庫的,就是這個原因。不過所有的arm64的應用都是可以轉換的,后面演示時用的支付寶是arm64架構的。

頭部中添加 LC_ID_DYLIB

直接在文件頭部中按照文檔格式插入一個Load Command,并填入合適的數(shù)據(jù)。這里要注意下插入內容的字節(jié)數(shù)必須是8字節(jié)對齊的。

修改PAGEZERO段

這部分是最重要的一部分,因為arm64上這個段的大小有4G,直接往內存中加載,會提示沒有足夠的連續(xù)的地址空間,所以必須要調整這個段的大小,而要調整 PAGEZERO 這個段的大小, 又會引起一連串的地址空間的變化,所以不能盲目的直接改,必須結合dyld的源碼來對應修改。(注意這里不能直接把 PAGEZERO 這個段給去掉,也不能直接把大小調成0,因為涉及到dyld的rebase操作,詳細看后面)

1. 所有段的地址都要重新計算

單純減少 PAGEZERO 段的占用空間,作用不大,因為dyld加載動態(tài)庫的時候,要求是所有的段一起進行mmap(詳細可以查看dyld源碼的ImageLoaderMachO::assignSegmentAddresses函數(shù)),所以必須把接下來所有的段的地址都重新計算一次。

同時要保證,前后兩個段沒有地址空間重疊,并且每個段都是按0x4000對齊。因為 PAGEZERO 是所有段中的第一個,所以可以直接把 PAGEZERO 的大小調整到0x4000,然后后面每一個段都按順序依次減少同樣大小(0xFFFFC000 = 0x100000000 - 0x4000),同時能保證每個段在文件內的偏移量不變。

修改前:

image

修改后:

image

2. 對動態(tài)庫進行rebase操作

這里的rebase是系統(tǒng)為了解決動態(tài)庫虛擬內存地址沖突,在加載動態(tài)庫時進行的基地址重定位操作。

這一步操作是整個流程里最重要的,因為按照前面的操作,整個文件地址空間已經發(fā)生了變化,如果dyld依然按照原來的地址進行rebase,必然會失敗。

那么rebase操作需要做哪些工作呢?

相關的信息儲存在 Mach-O 文件的 LINKEDIT 段中, 并由 LC_DYLD_INFO_ONLY 指定 rebase info 在文件中的偏移量

image

詳細的rebase信息:

image

紅框里那些Pointer的意思是說,在內存地址為 0x367C698 的地方有一個指針,這個指針需要進行rebase操作, 操作的內容就是和前面調整地址空間一樣,每個指針減去 0xFFFFC000。

image

3. 為什么不能直接去掉PAGEZERO這個段

這個原因要涉及到文件中rebase信息的儲存格式,上面的圖中,可以看出rebase要處理的是一個個指針,但是實際上這些信息在文件中并不是以指針數(shù)組的形式存在,而是以一連串rebase opcode的形式存在,上面看到的一個個指針其實是 Mach O View 這個軟件幫我們將opcode整理得到的。

[圖片上傳失敗...(image-193e8b-1543815911165)]

這些opcode中有一種操作比較關鍵,REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB。

image

這個opcode的意思是, 接下去需要調整文件的中的第2個段,就是圖中segment(2)所表示的含義。

所以說,如果把PAGEZERO這個段給去掉了,文件中各個段的序號也就都錯位了,與rebase中的信息就對應不上了。
而且把這個段大小改為0,也是不行的,因為dyld在加載的過程中,會重新自動過濾掉大小為0的段,也會導致同樣的段序號錯位的問題。(有興趣的同學可以看下dyld的源碼,在ImageLoaderMachO類的構造函數(shù)里)
這就是為什么必須要保留PAGEZERO這個段,同時大小不能為0。

修改符號表

正常的線上應用是不存在符號表的,但是如果你之前用了我的另一個工具 restore-symbol 來恢復符號表的話,這個地方自然也需要做一些處理,處理方法同rebase類似,減去0xFFFFC000.

不過有一些符號需要單獨過濾,比如這個:

image

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

image

實際效果

工具開源在github上,用法:

1.下載源碼編譯:

git clone --recursive https://github.com/tobefuturer/app2dylib.git

cd app2dylib && make

./app2dylib

2.把支付寶arm64砸殼,然后提取可執(zhí)行文件,用上面的工具把支付寶的可執(zhí)行文件轉成動態(tài)庫

./app2dylib /tmp/AlipayWallet -o /tmp/libAlipayApp.dylib

3.用 Xcode 新建工程,并把新生成的dylib拖進去,調整好各項設置.


image

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.怎么調用動態(tài)庫里的方法呢?

為方便大家嘗試,這里選兩個分析起來比較簡單的函數(shù)調用演示給大家。

一個是OC的方法 +[aluSecurity rsaEncryptText:pubKey:], 可以直接用oc運行時調用。

另一個是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分析得到。

運行結果:


image

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);

/** 根據(jù)偏移算出函數(shù)地址, 然后調用*/

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層的一些方法,會導致crash,如果想干掉那些+load方法的話,看下面。

關于繞過檢測代碼

文章開頭的簡介中有提到,以動態(tài)庫的形式加載,能夠繞過應用的檢測代碼,這說法不完全,因為如果把檢測代碼寫在類的+load方法里或者mod_init_func函數(shù)( 全局靜態(tài)變量的構造函數(shù)和__attribute__((constructor))指定的函數(shù) )里,在dylib加載的時候也是可以得到調用的。

那么也就衍生出兩種配搭的對抗方案:

i)越獄機
+load方法的調用是在libobjc.dylib中的call_load_methods函數(shù), mod_init_func函數(shù)的調用是在dyld中的doModInitFunctions函數(shù),可以直接用CydiaSubstrate inline hook掉這兩個函數(shù),而且動態(tài)庫是由我們自己加載的,所以可以控制hook和加載dylib的時序。

ii) 非越獄機
非越獄機上,沒有辦法inline hook,但是可以利用_dyld_register_func_for_add_image 這個函數(shù)注冊回調,這個回調是發(fā)生在動態(tài)庫加載到內存后,+load方法和mod_init_func函數(shù)調用前,所以可以在這個回調里把+load方法改名,把mod_init_func段改名等等,也就可以使得各種檢測函數(shù)沒法調用了。

總之,主要的控制權還是在我們手中。

工具開源地址

https://github.com/tobefuturer/app2dylib

測試環(huán)境:
iPhone 6Plus 、iOS 9.3.1 、arm64
支付寶9.9.3

實際使用過程中,可能會遇到各種奇葩問題,可以去github上提issue,或者email(tobefuturer@gmail.com),提問時請描述清楚遇到的問題和已經嘗試過的解決方法。

參考鏈接&致謝

  1. dyld的源碼:https://opensource.apple.com/source/dyld/
  2. 感謝狗哥的iOS逆向群里 @Ouroboros, @Misty,@張總 三位大神的激烈討論,還有幫我砸支付寶殼的 @{}
  3. 順便推廣下iOS逆向的論壇 http://iosre.com/

原文地址:http://blog.imjun.net/posts/convert-iOS-app-to-dynamic-library/

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容