iOS 逆向学习之四(初识 Mach-O)

什么是 Mach-O 文件?

Mach-O 是 Mach object 的缩写,是 Mac\iOS 上用来存储程序、库的标准格式

Mach-O 文件类型

  • 可以点击下载xnu 源码,在源码中的 EXTERNAL_HEADERS/mach-o/loader.h 文件中,我们可以看到 Mach-O 格式的所有文件类型

null

xun 是苹果 MacOS\iOS 等操作系统的内核

  • 常见的 Mach-O 文件类型
Mach-O 类型 示例文件
MH_OBJECT 目标文件(.o)
静态库文件(.a)注:
MH_EXECUTE 可执行文件,存放 App 的所有源码信息,在.app/xx
MH_DYLIB 动态库文件.dylib 或者 .framework/xx
MH_DYLINKER 动态链接编辑器,也就是之前所说的 /usr/lib/dyld 工具
MH_DSYM 此文件中存储这二进制文件符号信息,在开发中,我们经常使用此文件来分析 App 的崩溃信息

Mach-O 的基本结构

可以点击官网查看 Mach-O 的介绍。

Mach-O 组成

Mach-O 由 3 个部分组成

  • Header,包含文件类型、目标架构类型等等
  • Load commands, 是描述文件在虚拟内存中的逻辑结构和布局,相当于一份目录索引
  • Raw segment data, 在 Load commands 中所定义的 Segment,在这里都能找到原始数据。

Raw segment data 存放了所有的原始数据,而 Load commands 相当于 Raw segment data 的索引目录

null

窥探 Mach-O 的结构

常用工具

  • 命令行工具,通过 file 命令查看 Mach-O 文件的基本信息
file 文件路径
复制代码
  • otool,查看 Mach-O 特定部分和段的内容
#查看Mach-O文件的header信息
otool -h 文件路径

#查看Mach-O文件的load commands信息
otool -l 文件路径
复制代码

更多使用方法,终端输入 otool -help 查看

  • lipo,用来处理多架构 Mach-O 文件,常用命令如下
#查看架构信息
lipo -info 文件路径

#导出某种类型的架构
lipo 文件路径 -thin 架构类型 -output 输出文件路径

#合并多种架构类型
lipo 文件路径1 文件路径2 -output 输出文件路径
复制代码

Universal Binary(通用二进制文件)

通用二进制文件就是同时适用于多种架构的二进制文件,它包含了多种不同架构的独立的二进制文件,它有以下特点

  • 因为需要存储多种架构的代码,所以通用二进制文件要比单架构二进制文件要大
  • 因为两种种架构之间可以共用一些资源,所以两种架构的通用二进制文件大小不会达到单一架构版本的两倍。
  • 运行过程中只会调用其中的部分代码,所以运行起来不会占用额外的内存
  • 通用二进制文件通常也被称为“胖二进制文件(Fat binary)”

dyld 和 Mach-O

dyld 是 iOS 中用来加载可执行文件、动态库的工具,其实它本身也是一个 Mach-O 文件。

什么是 dyld?
  • dyld 动态加载器(又叫做动态链接编辑器)
  • dyld 的源码可以点击此处下载
dyld 的作用。

dyld 可以用来加载以下三种类型的 Mach-O 文件

  • MH_EXECUTE
  • MH_DYLIB
  • MH_BUNDLE

通过查看 dyld 的源码可以看到加载文件时的类型校验

null

从编码到 App 安装到手机

想要了解 Mach-O 文件,首先要了解从编写代码,开发 App 到 App 打包并安装到手机上的整个过程。

  • 首先我们编写完成代码之后,会通过 LLVM 编译器预处理我们的代码,比如将宏放在指定的位置
  • 预处理结束之后,LLVM 会对代码进行词法分析和语法分析,生成 AST。AST 是抽象语法树,主要用来进行快速遍历,实现静态代码检查的功能。
  • AST 会生成 IR,IR 是一种更加接近机器码的语言,通过 IR 可以生成不同平台的机器码。对于 iOS 平台,IR 生成的可执行文件就是 Mach-O.
  • 然后通过链接器将符号和地址绑定在一起,并且将项目中的多个 Mach-O 文件合并成一个 Mach-O 文件。
  • 最后通过签名等操作生成.app 文件,然后对.app 文件进行压缩就生成了我们可以安装的 ipa 包。
  • 当然,ipa 包的安装途径有两种:
    • 通过开发者账号上传到 App Store,然后在 App Store 上下载安装。
    • 通过 PP 助手、iFunBox、Xcode 等工具来安装

逆向 App,我们需要做哪些工作?

初步了解了什么是 Mach-O 文件,以及 App 从开发到安装的过程, 我们就可以来学习如何逆向一款 App

  • 界面分析 通过之前的学习,我们已经可以使用 Cycript 和 Reveal 对 App 的界面进行分析
  • 代码分析 iOS 开发中,所有的代码最后都会经过编译生成 Mach-o 文件,所以我们需要对 Mach-O 文件进行静态分析

静态分析的工具有 MachOView、class-dump、Hopper Disassembler、ida 等等,后面会一一学习

  • 动态调试 除了静态分析,我们还需要运行目标 App,对 App 进行动态调试

动态调试的工具有 debugserver、LLDB 等等

  • 代码编写、注入 进行完界面分析、代码分析和动态调试之后,我们可以在特定位置注入我们自己写的代码,必要时可以重新签名并且打包 ipa

调试工具

calss-dump

class-dump 的作用就是把 Mach-O 文件的 class 信息给导出来,生成对应的.h 头文件

  • 可以点击官网下载class-dump 工具包
  • 下载完成之后将其中的 class-dump 可执行文件复制到 Mac 上的 /usr/local/bin 目录中,这样在终端就能识别 class-dump 命令了

在 Mac 中,终端执行的所有指令都会去 /usr/bin 目录和 /usr/local/bin 目录下寻找

  • class-dump 的常用命令如下
# -H表示需要生成头文件  -o用于指定头文件的存放目录
class-dump -H Mach-O文件路径 -o 头文件存放目录
复制代码

Hopper Disassmbler

Hopper Disassmbler 可以将 Mach-O 文件的机器语言反编译成汇编代码、OC 伪代码或者是 Swift 伪代码。最常使用的快捷键有

#找出哪里引用了这个方法
Shift + Option + X
复制代码

下载地址:pan.baidu.com/s/1yP_VcBlQ…


静态库和动态库

在 iOS 开发中,有很多功能都是现成可用的,不关你的 App 在用,其它的 App 也在用,比如 UIKit 框架、GUI 框架、I/O、网络等等。这些库都是通过链接器链接到 Mach-O 文件中的。

静态库

静态库是编译时链接的库,需要连接进入 Mach-O 文件中,如果需要更新就必须重新编译一次,无法做到动态加载和更新

动态库

动态库是运行时链接的库。

Mach-O 是文件编译之后的产物,所以动态库并没有参与 Mach-O 文件的编译和链接。所以 Mach-O 文件中没有包含动态库的符号定义,也就是说这些符号会直接显示未定义,但是他们的名字和对应库的路径会被记录下来。在运行时通过 dlopen 和 dlsym 导入动态库时,会根据记录的路径找到对应的库,再通过记录的名字符号找到绑定的地址。

动态库共享缓存(dyld shared cache)

从 iOS3.1 开始,为了提高性能,绝大部分的系统动态库文件都打包存放到了一个缓存文件中(dyld shared cache),缓存路径是 /System/Library/Caches/com.apple.dyld/dyld_shared_cache_armX

dyld_shared_cache_armX 里面的 X 代表 ARM 处理器指令集的架构

ARM 指令集

ARM 指令集(CPU 指令的集合)有以下几种

ARM 指令集 支持的设备
armv6 iPhone、iPhone3G
iPod Touch、iPod Touch2
armv7 iPhone3GS、iPhone4、iPhone4S

iPad、iPad2、iPad3(The New iPad)、iPad mini
iPod、Touch3G、iPod Touch4、iPod Touch5 |
| armv7s | iPhone5、iPhone5C、iPad4 |
| arm64 | iPhone5S、iPhone6、iPhone6 Plus、iPhone6S、iPhone6S Plus
iPhoneSE、iPhone7、iPhone7Plus、iPhone8、iPhone8 Plus、iPhoneX
iPad5、iPad Air、iPad Air2、iPad Pro、iPad Pro2
iPad mini with Retina display、iPad mini3、iPad mini4
iPod Touch6 |

以上所有的指令集都是向下兼容的 为什么要使用动态库共享缓存呢?最大的好处就是节省内存。

从动态库共享缓存抽取动态库

由于动态库共享缓存太大,如果想获取其中某个动态库,例如 UIKit,就需要从动态库共享缓存中抽取对应的动态库

  • 使用 dyld 源码中提供的方式来进行抽取,工具在源码中的 launch-cache/dsc_extractor.cpp 文件中
    • 首先需要去掉源码中的 #if 0 判断
    • 然后使用如下命令编译 dsc_extractor.cpp 文件
    clang++ -o dsc_extractor dsc_extractor.cpp
    复制代码
    

    此处是将 dsc_extractor.cpp 编译生成可执行文件 dsc_extractor

    • 进入执行文件 dsc_extractor 所在目录。通过以下的命令来抽取动态库
    ./dsc_extractor 动态库共享缓存文件的路径 用于存放抽取结果的目录
    复制代码
    

    null

    建议抽取 armv7s 架构的动态库,arm64 抽取时会报以上错误,原因是 dsc_extractor.bundle 不能在 Xcode10 之后使用

    • 抽取完成之后,使用 Hopper Disassmbler 打开想要逆向的动态库,就可以看到动态库中的源码信息。

    null