MAC、iOS平台可执行文件:Mach-O
APP的生命周期
-
开发生成ipa包的本质过程
- 我们开发完程序,command +B 编译,然后打开文件目录Products,发现有一个app包
- 这个过程其实经历了编译、连接、签名
- 右击选择show in finder,可以找到这个app包
- 然后右击这个包选择显示包内容,我们可以看见上图列表
- Xib被编译成nib文件,storyboard 被编译成storyboardc文件
- 有一个签名文件
- 那么.m的代码被编译成什么了呢? 就是那个黑色的可执行文件
- EverythingSendUser.app 中的EverythingSendUser文件是iOS可执行文件
- 文件的格式是Mach-O(machine object)
- app文件是如何变成ipa文件呢?
- 新建文件夹,命名为Payload
- 右击压缩为zip格式
- 修改文件后缀名为ipa
- 修改ipa文件名为EverythingSendUser
- 这个就是整个压缩成ipa的过程
- 如果你讲一个打包好的ipa文件,后缀名改为zip然后解压,会发现本质就是这样
- 我们开发完程序,command +B 编译,然后打开文件目录Products,发现有一个app包
-
ipa文件是如何安装到手机上呢?
- 方法一:
- 开发者将ipa文件上传到App Store,用户在App Store下载安装
- 方法二:
- 通过pp助手、iFunbox、Xcode等工具安装
- 注意:如果你的开发账号是免费版的,那么安装的APP也就6天的使用时间。
- 方法一:
逆向APP的思路
- 界面分析
- Cycript、Reveal工具分析界面的控件
- 代码分析
- 对Mach-O文件的静态分析
- MachOView、class-dump、Hopper Disassembler、ida等,这些工具分析
- 作用就是大概知道代码怎么写的
- 动态调试
- 对运行中的APP进行代码调试
- debugserver、LLDB
- 将程序跑起来,然后在运行中进行调试
- 代码编写
- 注入代码到APP中
- 必要时还可能需要重新签名、打包ipa
class-dump
- 顾名思义,它的作用就是把Mach-O文件的class信息给dump出来(把类信息给导出来),生成对应的.h头文件(只能生产.h头文件)
- 官方地址:
http://stevenygard.com/projects/class-dump/
- 下载完工具包后双击dmg将class-dump文件复制到Mac的/usr/local/bin目录,这样在终端就能识别class-dump命令了
- cd /usr/local/bin
- open ./
- 然后复制进去就可以了
- MAC上终端指令的原理是什么呢?
- 我们在终端敲一个指令比如
cd
,那么他就会去两个地方去找这个指令- 第一个地方:
/usr/bin目录中去找
,打开这个路径,我们会发现里面后很多命令,其中就有cd命令- 注意: 自从mac10.11之后这个目录就不允许写入内容了,因此只能使用下面的路径了
- 第二个地方:
/usr/local/bin
,这个也有很多命令。- 我们可以看到这个目录下有如下命令:
- pod: cocoapods命令
- jekyll: 博客命令
- 等等我们自己安装的命令
- 第一个地方:
- 我们在终端敲一个指令比如
- 常用格式
class-dump -H Mach-O文件路径 -o 头文件存放目录
- -H表示要生成头文件
- -o用于制定头文件的存放目录
- 使用步骤
- 拿到ipa文件
- 将ipa改为zip,然后解压
- 右击app的显示包内容,将mach-o文件复制到一个文件中
- 然后执行
class-dump -H Mach-O文件路径 -o 头文件存放目录
命令,就可以拿到这个项目中所有类的头文件了,而且这个头文件中会将每个类的所有方法都列表出来。 - 将这个头文件文件夹直接拖到sublime(这个东西自己百度搜搜)中即可以快速查看了。
Hopper
- 代码的编译过程(即OC代码是如何变成Mach-O文件的呢?)
- 首先会编译成汇编
- 然后再编译成机器指令
- 这个机器指令就是Mach-O文件
- Hopper Disassmbler
- Hopper Disassmbler能够将Mach-O文件的机器语言代码反编译成汇编代码、OC伪代码或者Swift伪代码
- 下载地址
- 常用快捷键
- Shift + Option + X
- 找出哪里引用了这个方法
- 使用
- 将mach-O直接拖入到Hopper Disassmbler即可分析出,所有的汇编代码
- 点击工具栏的按钮,可以转换成OC伪代码。
系统的动态库
- 动态库共享缓存(dyld shared cache)
- 从iOS3.1开始,为了提高性能,绝大部分的系统动态库文件都打包存放到了一个缓存文件中(dyld shared cache)
- 缓存文件路径(iPhone逆向后可以查看手机文件):/System/Library/Caches/com.apple.dyld/dyld_shared_cache_armX
- 也就是说系统的动态库都放在上面那个文件中了
- dyld_shared_cache_armX的X代表ARM处理器指令集架构
- armv6
- Phone、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、iPhone7 Plus、iPhone8、iPhone8 Plus、iPhoneX
- iPad5、iPad Air、iPad Air2、iPad Pro、iPad Pro2
- iPad mini with Retina display、iPad mini3、iPad mini4
- iPod Touch6
- 所有指令集原则上都是向下兼容的
- 动态库共享缓存一个非常明显的好处是节省内存
- 现在的ida、Hopper反编译工具都可以识别动态库共享缓存
- 动态库共享缓存的好处
- 这样做的好处就是节省了内存
- 所谓的动态库就是动态链接、动态加载,在内存中只有一份的,大家公用这一份内存。
- 如果把不同的动态库放在不同的文件
- 由于每个动态库都是mach-O文件,并且里面包含了这个文件的一些描述信息
- 因此多个动态库的话每个文件都需要这些描述信息
- 如果吧不同的动态库放在同一个文件
- 这样的话相当于把不同的各个动态库压缩成一个文件,大家公共有一个描述信息等,这样更节省内存。
- 动态库的加载
- 在Mac\iOS中,是使用了/usr/lib/dyld程序来加载动态库
- dyld
- dynamic link editor,动态链接编辑器
- dynamic loader,动态加载器
- dyld源码
- 苹果开源的一些源码
- 从动态库共享缓存抽取动态库 (学逆向时再看)
- 由于所有的动态库都压缩成一个文件(dyld_shared_cache_armX)了,但是如果我只想要UIKit这个动态库的Mach-O呢? 这就需要抽取了
- 可以使用dyld源码中的
launch-cache/dsc_extractor.cpp
这个文件,编译成一个可执行文件 - 编译dsc_extractor.cpp
- clang++ -o dsc_extractor dsc_extractor.cpp
- 使用dsc_extractor命令
- ./dsc_extractor 动态库共享缓存文件的路径 用于存放抽取结果的文件夹
Mach-O
- Mach-O是Mach object的缩写,是Mac\iOS上用于存储程序、库的标准格式
- 属于Mach-O格式的文件类型有
- 可以在xnu源码中,查看到Mach-O格式的详细定义(https://opensource.apple.com/tarballs/xnu/)
- EXTERNAL_HEADERS/mach-o/fat.h
- EXTERNAL_HEADERS/mach-o/loader.h
- 常见的Mach-O文件类型
- MH_OBJECT
- 目标文件(.o)
- 静态库文件(.a),静态库其实就是N个.o合并在一起
- MH_EXECUTE:可执行文件
- .app/xx
- MH_DYLIB:动态库文件
- .dylib
- .framework/xx
- MH_DYLINKER:动态链接编辑器
- /usr/lib/dyld
- MH_DSYM:存储着二进制文件符号信息的文件
- .dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息)
- 符号信息就是指函数名、类名等
- 如何找呢?
- 将当前项目设置为真机模式
- 编译command +B
- 在项目列表Products 的下面找到 XXX.app,右击选择show in finder
- 就能看见一个.dsym文件了
- MH_OBJECT
- 在Xcode中查看target的Mach-O类型
- target - Build settings -> 搜索 mach 就能看见mach-o的类型了。
Universal Binary(通用二进制文件)
- target - Build settings -> 搜索 mach 就能看见mach-o的类型了。
- 通用二进制文件
- 同时适用于多种架构的二进制文件
- 包含了多种不同架构的独立的二进制文件
- 保证支持多种CPU架构
- 因为需要储存多种架构的代码,通用二进制文件通常比单一平台二进制的程序要大
- 由于两种架构有共同的一些资源,所以并不会达到单一版本的两倍之多
- 由于执行过程中,只调用一部分代码,运行起来也不需要额外的内存
- 因为文件比原来的要大,也被称为“胖二进制文件”(Fat Binary)
- 如何查看一个mach-o文件的类型呢?
- 比如查看以个静态库framework中macho的类型
- 展开这个framework,找到对应的macho文件
- 然后复制到一个文件夹,打开终端,cd 文件路径
-
file mach-o文件
,就会显示出该mach-o的类型macdeMacBook-Pro:未命名文件夹 2 mac$ file KASSilentLive KASSilentLive: Mach-O universal binary with 2 architectures: [arm_v7:current ar archive] [arm64] KASSilentLive (for architecture armv7): current ar archive KASSilentLive (for architecture arm64): current ar archive
- 从上面可以看出这个静态库,是个通用二进制文件,支持2种架构,arm_v7/arm64
- 同时还可以通过
lipo -info mach-o文件
命令直接看改文件所支持的架构。
- 这种通用二进制架构如何配置呢?
- 项目的target->build settings ->architecthures
- 这个就是用来设置通用二进制文件支持哪些架构。
- 这个目录下有2个地方可以设置:
- architecthures
- standard -architecthures $(ARCHS_STANDARD)
- 这个是Xcode自己内置(默认)的环境变量,不同Xcode值不一样
- 比如:Xcode 9是armv7、arm64
- 如果觉得Xcode内置的不够,那么可以选择other自己添加,比如添加:armv7s
- valid architecthures
- 这个是干什么呢?
- Xcode生成通用二进制文件会取architecthures跟valid architecthures的交集
- architecthures
查看Mach-O的结构
- 命令行工具
- file:查看Mach-O的文件类型
- 打开终端,
cd Mach-O文件路径
,然后file Mach-O文件
即可,也可以直接file 文件路径
- 打开终端,
- lipo:常用于多架构Mach-O文件的处理
- 查看架构信息:
lipo -info 文件路径
- 直接会打印出该macho文件支持的CPU架构
- 导出某种特定架构:
lipo 文件路径 -thin 架构类型 -output 输出文件路径
- 当这个macho文件支持多种架构,但是我只想导出特定的某个架构,比如armv7
lipo KASSilentLive -thin armv7 -output /Users/mac/Desktop/test/KASSilentLive_armv7
- 这样就把KASSilentLive这个通用二进制文件,导出为单一的KASSilentLive_armv7了
- 合并多种架构:
lipo -create 文件路径1 文件路径2 -output 输出文件路径
- 作用是将多个不同架构类型的macho文件,合并成一个支持多种架构的macho文件
- 比如:
lipo -create /Users/mac/Desktop/test/KASSilentLive_armv7 /Users/mac/Desktop/test/KASSilentLive_armv64 -output /Users/mac/Desktop/test/KASSilentLive_armv7and64
- 查看架构信息:
- otool:查看Mach-O特定部分和段的内容
- 在终端直接输入
otool
回车,就会显示出帮助信息,告诉我们这个命令行都有那些作用
- 在终端直接输入
- file:查看Mach-O的文件类型
- GUI工具
- MachOView
- 通过可视化的形式,显示出Mach-O的信息。
- 下载后运行会有问题
- bug1: 找不到10.9SDK
- TARGET ->Building Settings->architecthures->Base SDK->macOS
-
bug2:
stdlibc++ headers not found
Build Setting->C++ Standard Library->libstdc++ 修改为 Build Setting->C++ Standard Library->libc++
- bug1: 找不到10.9SDK
- 运行起来,然后在顶部工具栏,file->open ->找到macho文件,就可以展示macho的信息了。
Mach-O的基本结构
- 官方描述 https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/MachOTopics/0-Introduction/introduction.html
-
无论任何类型的Mach-O文件他都有共同的结构,一个Mach-O文件包含3个主要区域
- Header
- Mach-O文件类型、目标架构类型等
- Load commands
- 描述文件在虚拟内存中的逻辑结构、布局
- 就是可执行文件加载到内存中的分配布局
- 就是用来描述有哪些段、那些段的大小等信息
- Raw segment data
- 在Load commands中定义的Segment的原始数据
- 就是Load commands中描述的段的实际数据
- Header
dyld和Mach-O
- dyld用于加载以下类型的Mach-O文件
- MH_EXECUTE
- MH_DYLIB
- MH_BUNDLE
- APP的可执行文件、动态库都是由dyld负责加载的
ASLR
- 什么是ASLR
- Address Space Layout Randomization,地址空间布局随机化
- 是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术
- iOS4.3开始引入了ASLR技术
- 未使用ASLR
- 函数代码存放在__TEXT段中
- 全局变量存放在__DATA段中
- 可执行文件的内存地址是0x0
- __PAGEZERO为预留的段
- 代码段(__TEXT)的内存地址
- 就是LC_SEGMENT(__TEXT)中的VM Address(virtual Memory Adress 虚拟内存地址)
- arm64:0x100000000(8个0)
- 非arm64:0x4000(3个0)
- 可以使用size -l -m -x来查看Mach-O的内存分布
- 使用了ASLR
- 一旦使用了ASLR,让程序每次载入的时候,Mach-O的起始地址不是固定的,防止别人能够定位到代码地址。
- LC_SEGMENT(__TEXT)的VM Address
- 固定长度0x100000000
- ASLR随机产生的Offset(偏移)
- 0x5000(大小随机的)
- 也就是可执行文件的内存地址
- 函数的内存地址
- 函数的内存地址(VM Address) = File Offset + ASLR Offset + __PAGEZERO Size
- Hopper、IDA中的地址都是未使用ASLR的VM Address
- 即将一个Mach-O文件导入到Hopper解析后,我们查找到某个函数,然后找到对应地址
- 这个地址仅仅是Mach-O文件的地址,还没有载入到虚拟内存的
- 因此,要找到载入内存后的真正函数地址= File Offset + ASLR Offset + __PAGEZERO Size