[转]黑科技:把第三方iOS应用转成动态库

文章转载自杨君的小黑屋,对排版进行了一些调整

前言

本文会介绍一个自己写的工具,能够把第三方iOS应用转成动态库,并加载到自己的App中,文章最后会以支付宝为例,展示如何调用其中的C函数和OC方法。
工具开源地址:
https://github.com/tobefuturer/app2dylib

有什么用

为什么要把第三方应用转成动态库呢?与一般的注入动态库+重签名打包的手段有什么不一样呢?

好处主要有下面几点:

  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的要求。
    LC_ID_DYLIB 的 Load Command
  3. 可执行文件会多出一个 PAGEZERO段,动态库中没有。这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。这个段的大小,32位上是0x4000,64位上是4G。这个段的处理也是转换工作的重点之一,之前有人尝试转换,不成功就是因为没有处理好 PAGEZERO.
    多出的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),同时能保证每个段在文件内的偏移量不变。

修改前:

PAGEZERO段修改前

修改后:

PAGEZERO段修改后

2. 对动态库进行rebase操作

这里的rebase是系统为了解决动态库虚拟内存地址冲突,在加载动态库时进行的基地址重定位操作。

这一步操作是整个流程里最重要的,因为按照前面的操作,整个文件地址空间已经发生了变化,如果dyld依然按照原来的地址进行rebase,必然会失败。

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

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

rebase在文件中的偏移量

详细的rebase信息:


详细的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. 下载源码编译:

git clone --recursive https://github.com/tobefuturer/app2dylib.git
cd app2dylib && 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=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分析得到。

运行结果:

#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段改名等等,也就可以使得各种检测函数没法调用了。

总之,主要的控制权还是在我们手中。
测试环境:
iPhone 6Plus 、iOS 9.3.1 、arm64
支付宝9.9.3

参考链接&致谢

  1. dyld的源码:https://opensource.apple.com/source/dyld/
  2. iOS逆向的论坛 http://iosre.com/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容