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

2,580 阅读8分钟

什么是Mach-O文件?

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

Mach-O文件类型

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

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的索引目录

窥探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的源码可以看到加载文件时的类型校验

从编码到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 动态库共享缓存文件的路径 用于存放抽取结果的目录
    

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

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