黑科技:把第三方 iOS 應用轉成動態庫

gpkp2016 7年前發布 | 10K 次閱讀 動態庫 iOS開發 移動開發

前言

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

有什么用

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

好處主要有下面幾點:

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

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

  2. 掌控程序的控制權

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

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

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

應用和動態庫的異同

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

相同點:

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

不同點

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

  1. 頭部的文件類型
    一個是 MH_EXECUTE 可執行文件, 一個是 MH_DYLIB 動態庫, 還有各種頭部的Flags,要特別留意下可執行文件中Flags部分的 MH_PIE 標志,后面再詳細說。
  2. 動態庫文件中多一個類型為 LC_ID_DYLIB 的 Load Command, 作用是動態庫的標識符,一般為文件路徑。路徑可以隨便填,但是這部分必須要有,是codesign的要求。
  3. 可執行文件會多出一個 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就能發現,念茜的推ter上提過這個奇葩的符號。(女神果然是女神, 棒~ ?)

實際效果

1.下載源碼編譯:

gitclone --recursive https://github.com/tobefuturer/app2dylib.git
cdapp2dylib && make
./app2dylib

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

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

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

Run Script里的代碼(目的是為了對dylib進行簽名)

cd ${BUILT_PRODUCTS_DIR}
cd ${FULL_PRODUCT_NAME}
/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=nonelibAlipayApp.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分析得到。

運行結果:

#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:dylibNameofType:@"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:plainwithObject:pubkey];
    NSLog(@"\n-----------call oc method---------\n明文:%@\n密文: %@\n-----------------------------------", plain,cipher);
    
    //確認dylib加載在內存中的地址
    uint64_tslide = 0;
    for (int i = 0; i <  _dyld_image_count(); i ++)
        if ([[NSStringstringWithUTF8String:_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段改名等等,也就可以使得各種檢測函數沒法調用了。

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

測試環境:

iPhone 6Plus 、iOS 9.3.1 、arm64

支付寶9.9.3

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

參考

  1. dyld的源碼: https://opensource.apple.com/source/dyld/

 

 

 

 本文由用戶 gpkp2016 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!