推荐序

本文作者杨君,他之前投稿过一篇安全方面的文章:iOS 符号表恢复 & 逆向支付宝,这次他在国庆期间又发来一篇大作。本文介绍的黑科技非常牛逼,可以将别人的 iOS 应用转成动态库。

看完本文,我在想:如果把支付宝、淘宝、京东、当当、一号店都集中到一个 App 中,不知是怎样的体验?至少这种事情在技术上是可以做到的了。

作者介绍:杨君,中山大学计算机系研究生,iOS 开发者,擅长领域 iOS 安全和逆向工程,个人博客:http://blog.imjun.net 。

前言

本文会介绍一个自己写的工具,能够把第三方 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 的要求。
  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。

修改符号表

正常的线上应用是不存在符号表的,但是如果你之前用了我的另一个工具 restroe-symbol 来恢复符号表的话,这个地方自然也需要做一些处理,处理方法同 rebase 类似,减去 0xFFFFC000.

不过有一些符号需要单独过滤,比如这个:


这个 radr://5614542 是个什么神奇的符号呢,google 就能发现,念茜的 twitter 上提过这个奇葩的符号。(女神果然是女神 , 棒~