黑科技:把第三方 iOS 应用转成动态库
gpkp2016
8年前
<p>前言</p> <p>本文会介绍一个自己写的工具,能够把第三方iOS应用转成动态库,并加载到自己的App中,文章最后会以支付宝为例,展示如何调用其中的C函数和OC方法。</p> <h2>有什么用</h2> <p>为什么要把第三方应用转成动态库呢?与一般的注入动态库+重签名打包的手段有什么不一样呢?</p> <p>好处主要有下面几点:</p> <ol> <li> <p>可以直接调用别人的算法</p> <p>逆向分析别人的应用时,可能会遇到一些私有算法,如果搞不定的话,直接拿来用就好。</p> </li> <li> <p>掌控程序的控制权</p> <p>程序的主体是自己的App,第三方应用的代码只是以动态库的形式加载,主要的控制权还是在我们自己手里,所以可以直接绕过应用的检测代码(文章最后有关于这部分攻防的讨论)。</p> </li> <li> <p>同个进程内加载多个应用</p> <p>重签名打包毕竟只能是原来的应用,但是如果是动态库的话,可以同时加载多个应用到进程内了,比如你想同时把美图秀秀和饿了么加载进来也是可以的(秀秀不饿,想想去年大众点评那个APPmixer的软广 – -! )。</p> </li> </ol> <h2>应用和动态库的异同</h2> <p>我们要把应用转成动态库,首先要知道这两者之前有什么相同与不同,有相同的才存在转换的可能,而不同之处就是我们要重点关注的了。</p> <h3>相同点:</h3> <p><img src="https://simg.open-open.com/show/b0e4c1961f12f51001266a51a1cac8cb.jpg"></p> <p>可执行文件和动态库都是标准的 Mach-O 文件格式,两者的文件头部结构非常类似,特别是其中的代码段(TEXT),和数据段(DATA)结构完全一致,这也是后面转换工作的基础。</p> <h3>不同点</h3> <p>不同点就是我们转换工作的重点了,主要有:</p> <ol> <li>头部的文件类型<br> 一个是 MH_EXECUTE 可执行文件, 一个是 MH_DYLIB 动态库, 还有各种头部的Flags,要特别留意下可执行文件中Flags部分的 MH_PIE 标志,后面再详细说。<br> <img src="https://simg.open-open.com/show/05852dc5ce06488be3ac5c91d4fa1680.jpg"></li> <li>动态库文件中多一个类型为 LC_ID_DYLIB 的 Load Command, 作用是动态库的标识符,一般为文件路径。路径可以随便填,但是这部分必须要有,是codesign的要求。<br> <img src="https://simg.open-open.com/show/40b12db736bc87af36f01f161f7aeab1.jpg"></li> <li>可执行文件会多出一个 PAGEZERO段,动态库中没有。这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。这个段的大小,32位上是0x4000,64位上是4G。这个段的处理也是转换工作的重点之一,之前有人尝试转换,不成功就是因为没有处理好 PAGEZERO.<br> <img src="https://simg.open-open.com/show/2460bbd59099de92c8dc1238ae53b791.jpg"></li> </ol> <h2>实现细节</h2> <h3>修改文件类型</h3> <p>第一步是修改文件的头部信息,把文件类型从可执行文件修改成动态库,同时把一些Flags修改好。</p> <p>这里一个比较关键的Flag是可执行文件中的 MH_PIE 标志位,(position-independent executable)。</p> <p>这个标志位,表明可执行文件能够在内存中任意位置正确地运行,而不受其绝对地址影响的特性,这一特性是动态库所必须的一个特性。没有这个标志位的可执行文件是没有办法转换成动态库的。iOS系统中,arm64架构下,目前这个标志位是必须的,不然程序无法运行(系统的安全性要求),但是armv7架构下,可以没有这个标志位,所以支付宝armv7版本的可执行文件是不能转成动态库的,就是这个原因。不过所有的arm64的应用都是可以转换的,后面演示时用的支付宝是arm64架构的。</p> <h3>头部中添加 LC_ID_DYLIB</h3> <p>直接在文件头部中按照文档格式插入一个Load Command,并填入合适的数据。这里要注意下插入内容的字节数必须是8字节对齐的。</p> <h3>修改PAGEZERO段</h3> <p>这部分是最重要的一部分,因为arm64上这个段的大小有4G,直接往内存中加载,会提示没有足够的连续的地址空间,所以必须要调整这个段的大小,而要调整 PAGEZERO 这个段的大小, 又会引起一连串的地址空间的变化,所以不能盲目的直接改,必须结合dyld的源码来对应修改。(注意这里不能直接把 PAGEZERO 这个段给去掉,也不能直接把大小调成0,因为涉及到dyld的rebase操作,详细看后面)</p> <p>1. 所有段的地址都要重新计算</p> <p>单纯减少 PAGEZERO 段的占用空间,作用不大,因为dyld加载动态库的时候,要求是所有的段一起进行mmap(详细可以查看dyld源码的ImageLoaderMachO::assignSegmentAddresses函数),所以必须把接下来所有的段的地址都重新计算一次。</p> <p>同时要保证,前后两个段没有地址空间重叠,并且每个段都是按0x4000对齐。因为 PAGEZERO 是所有段中的第一个,所以可以直接把 PAGEZERO 的大小调整到0x4000,然后后面每一个段都按顺序依次减少同样大小(0xFFFFC000 = 0x100000000 – 0x4000),同时能保证每个段在文件内的偏移量不变。</p> <p>修改前:</p> <p><img src="https://simg.open-open.com/show/f442acbce104deac1eaec8693e17d626.jpg"></p> <p>修改后:</p> <p><img src="https://simg.open-open.com/show/f93056c3461d5896d1623375c7077f46.jpg"></p> <p>2. 对动态库进行rebase操作</p> <p>这里的rebase是系统为了解决动态库虚拟内存地址冲突,在加载动态库时进行的基地址重定位操作。</p> <p>这一步操作是整个流程里最重要的,因为按照前面的操作,整个文件地址空间已经发生了变化,如果dyld依然按照原来的地址进行rebase,必然会失败。</p> <p>那么rebase操作需要做哪些工作呢?</p> <p>相关的信息储存在 Mach-O 文件的 LINKEDIT 段中, 并由 LC_DYLD_INFO_ONLY 指定 rebase info 在文件中的偏移量</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/93df8480be933ef3f3d03c310c4ab653.jpg"></p> <p>详细的rebase信息:</p> <p><img src="https://simg.open-open.com/show/61d471870af243a82ddad65e9cd33d68.jpg"></p> <p>红框里那些Pointer的意思是说,在内存地址为 0x367C698 的地方有一个指针,这个指针需要进行rebase操作, 操作的内容就是和前面调整地址空间一样,每个指针减去 0xFFFFC000。</p> <p><img src="https://simg.open-open.com/show/52aa9400f60bf60b492d873a09502db6.jpg"></p> <p>3. 为什么不能直接去掉PAGEZERO这个段</p> <p>这个原因要涉及到文件中rebase信息的储存格式,上面的图中,可以看出rebase要处理的是一个个指针,但是实际上这些信息在文件中并不是以指针数组的形式存在,而是以一连串rebase opcode的形式存在,上面看到的一个个指针其实是 Mach O View 这个软件帮我们将opcode整理得到的。</p> <p><img src="https://simg.open-open.com/show/91777db0dc90b3fde66e9983a9ff007f.jpg"></p> <p>这些opcode中有一种操作比较关键,REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB。</p> <p><img src="https://simg.open-open.com/show/dd3b72482eca948e009f92f927d7fb76.jpg"></p> <p>这个opcode的意思是, 接下去需要调整文件的中的第2个段,就是图中segment(2)所表示的含义。</p> <p>所以说,如果把PAGEZERO这个段给去掉了,文件中各个段的序号也就都错位了,与rebase中的信息就对应不上了。</p> <p>而且把这个段大小改为0,也是不行的,因为dyld在加载的过程中,会重新自动过滤掉大小为0的段,也会导致同样的段序号错位的问题。(有兴趣的同学可以看下dyld的源码,在ImageLoaderMachO类的构造函数里)</p> <p>这就是为什么必须要保留PAGEZERO这个段,同时大小不能为0。</p> <h3>修改符号表</h3> <p>正常的线上应用是不存在符号表的,但是如果你之前用了我的另一个工具 restore-symbol 来恢复符号表的话,这个地方自然也需要做一些处理,处理方法同rebase类似,减去0xFFFFC000.</p> <p>不过有一些符号需要单独过滤,比如这个:</p> <p><img src="https://simg.open-open.com/show/12100282c0828d495fc3690a2a9f5e11.jpg"></p> <p>这个radr://5614542是个什么神奇的符号呢,google就能发现,念茜的推ter上提过这个奇葩的符号。(女神果然是女神, 棒~ )</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1f435cd2c6fc3e5ebb1582982527410c.jpg"></p> <h2>实际效果</h2> <p>1.下载源码编译:</p> <pre> <code class="language-objectivec">gitclone --recursive https://github.com/tobefuturer/app2dylib.git cdapp2dylib && make ./app2dylib </code></pre> <p>2.把支付宝arm64砸壳,然后提取可执行文件,用上面的工具把支付宝的可执行文件转成动态库</p> <pre> <code class="language-objectivec">./app2dylib /tmp/AlipayWallet -o /tmp/libAlipayApp.dylib </code></pre> <p>3.用 Xcode 新建工程,并把新生成的dylib拖进去,调整好各项设置.</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/6c29fbd9e6081c259267ba1db6895b54.jpg"></p> <p>Run Script里的代码(目的是为了对dylib进行签名)</p> <pre> <code class="language-objectivec">cd ${BUILT_PRODUCTS_DIR} cd ${FULL_PRODUCT_NAME} /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=nonelibAlipayApp.dylib </code></pre> <p>4.怎么调用动态库里的方法呢?</p> <p>为方便大家尝试,这里选两个分析起来比较简单的函数调用演示给大家。</p> <p>一个是OC的方法 +[aluSecurity rsaEncryptText:pubKey:] , 可以直接用oc运行时调用。</p> <p>另一个是C的函数 int base64_encode(char * output, int * output_length, char * input, int input_length)<br> 这个需要先确定 base64_encode 这个C函数的函数签名和在dylib中的偏移地址(我这边的9.9.3版本是0xa798e4),可以用ida分析得到。</p> <p>运行结果:</p> <p><img src="https://simg.open-open.com/show/177ceb29577fad1de1d54caa9e35ff3b.jpg"></p> <pre> <code class="language-objectivec">#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); } </code></pre> <p>ps:示例代码中,我刻意除掉了界面部分的代码,因为支付宝的+load函数里swizzle了UI层的一些方法,会导致crash,如果想干掉那些+load方法的话,看下面。</p> <h2>关于绕过检测代码</h2> <p>文章开头的简介中有提到,以动态库的形式加载,能够绕过应用的检测代码,这说法不完全,因为如果把检测代码写在类的+load方法里或者mod_init_func函数( 全局静态变量的构造函数和 __attribute__((constructor)) 指定的函数 )里,在dylib加载的时候也是可以得到调用的。</p> <p>那么也就衍生出两种配搭的对抗方案:</p> <p>i)越狱机</p> <p>+load方法的调用是在libobjc.dylib中的call_load_methods函数, mod_init_func函数的调用是在dyld中的doModInitFunctions函数,可以直接用CydiaSubstrate inline hook掉这两个函数,而且动态库是由我们自己加载的,所以可以控制hook和加载dylib的时序。</p> <p>ii) 非越狱机</p> <p>非越狱机上,没有办法inline hook,但是可以利用_dyld_register_func_for_add_image 这个函数注册回调,这个回调是发生在动态库加载到内存后,+load方法和mod_init_func函数调用前,所以可以在这个回调里把+load方法改名,把mod_init_func段改名等等,也就可以使得各种检测函数没法调用了。</p> <p>总之,主要的控制权还是在我们手中。</p> <h2>测试环境:</h2> <p>iPhone 6Plus 、iOS 9.3.1 、arm64</p> <p>支付宝9.9.3</p> <p>实际使用过程中,可能会遇到各种奇葩问题,可以去github上提issue,或者email(tobefuturer@gmail.com),提问时请描述清楚遇到的问题和已经尝试过的解决方法。</p> <h2>参考</h2> <ol> <li>dyld的源码: <a href="/misc/goto?guid=4959729801381601032" rel="nofollow,noindex">https://opensource.apple.com/source/dyld/</a></li> </ol> <p> </p> <p> </p> <p> </p>