iOS逆向--MachoO文件

3,319 阅读10分钟

我们先创建一个目录:cd到该目录中,然后通过vi命令创建一个.c文件

vi test.c

在test.c文件中输入下面内容:

#include<stdio.h>
int main(){
printf("123");
return 1;
}

然后通过clang命令进行编译、链接

clang -c test.h

我们会发现在目录下多一个.o文件,这个.o文件是个目标文件,我们再通过file命令查看一下这个文件的属性

file test.o

我们发现文件是下面属性:

test.o: Mach-O 64-bit object x86_64
  • Mach-O:文件是Mach-O文件
  • 64-bit:文件是64位的
  • object:是个object文件
  • x86_64:是x86架构下的64位的文件

这个文件其实就是通过编译得到的目标文件

我们再通过clang对.o文件进行链接,链接之后得到的就是可执行文件

clang test.o

我们发现目录下得到一个a.out文件,这个文件就是可执行文件,我们再通过file命令看一下这个文件是什么属性

file test.o

得到下面提示

a.out: Mach-O 64-bit executable x86_64

跟.o一样是个mach-O文件,不过文件类型是executable,而executable代表就是可执行文件

我们发现.o和.out都是Mach-O文件,说明Mach-O不只是指可执行文件,它只是一个文件格式

那么.o和.out文件有啥区别呢?

其实我们知道,我们一个项目有很多.h和.m文件, 我们再在目录下创建一个test1文件,里面定义一个test1函数

#include<stdio.h>
void test1(){
printf("test1");
return;
}

在test.c文件中定义一下test1函数,然后调用一下test1函数

#include<stdio.h>

void test1();

int main(){
    test1();
    printf("123");
    return 1;
}

然后再通过clang命令对两个文件都进行编译一下,得到两个.o文件

clang -c test.c test1.c

再对两个.o文件进行链接得到demo可执行文件

clang -o demo test.o test1.o

然后执行demo

./demo

发现有打印

test1123

相当于test1也执行了,这是因为链接时候会把两个文件链接在一起,所以就算没有头文件,test文件中也能执行test1函数

类似于其他.a、.dylib、.framework、dyld、dsym也都是Mach-O文件,我们可以通过find命令进行查找电脑中的这些文件,例如查找电脑中的.a文件

find /-name "*.a"

可执行文件

我们主要对可执行文件进行分析,,在XCode项目的Build setting 里面的Mach-O,里面我们发现输出的是executable,可执行文件

当我们编译的时候选中的是我们的真机iPhone XS Max,那么编译的时候是什么架构的呢,编译一下,我们找到.app文件,然后找到里面的可执行文件,查看一下这个可执行文件的架构类型

file ZJJDEMO.app/WeChat

我们发现是arm64架构的

ZJJAllModule_Example: Mach-O 64-bit executable arm64

当我们把编译版本换成release版本时候,再次查看可执行版本,发现还是arm64版本,我们再把适配的系统类型转成iOS10.0,再次编译,查看

iOS11以上的的只适合ARM64位的

v7 v7s都是32位架构的

arm64 arm64e都是64位架构的

两个架构版本合成的可执行文件叫做通用二进制可执行文件

通用二进制文件

通用二进制文件是苹果公司提出的一种程序代码,能适用多种架构的二进制文件,我们可以将结合了多种架构的可执行文件

通过MachOView打开,打开后就是下面界面

其中问号那个是arm64e架构

我们可以通过lipo指令对可执行文件进行拆分,我们解释一下命令:

  • lipo -info MachO文件:查看machO文件有几种架构
  • lipo MachO文件 -thin 架构 -output 输出文件路径:拆分出某种架构
  • lipo -create MachO1 MachO2 -output 输出文件路径:合并多种架构

下面我们来操作一下,首先我们拿到多种架构的MachO文件,首先看一下这个MachO文件有哪几种架构:

lipo -info machO

输出:

Architectures in the fat file: machO are: armv7 armv7s arm64 arm64e

我们发现有4种架构。再拆分出armv7架构:

lipo machO -thin armv7 -output macho_armv7

我们发现在当前文件夹中多了一个macho_armv7的可执行文件,并且原来可执行文件是336kb,而拆出来的文件大小是73kb

我们再查看一下这个文件的架构,发现只有armv7架构了

通过这种方式,我们可以把相应架构的可执行文件都分离出来,我们先拆出arm64和armv7的可执行文件

我们在将这两个可执行文件合成一个:

lipo -create macho_arm64 macho_armv7 -output mach_arm64_v7

查看一下mach_arm64_v7的属性:

lipo -info mach_arm64_v7

输出:

Architectures in the fat file: mach_arm64_v7 are: armv7 arm64

说明合并成功了,当然不仅仅是两个,可以多个macho文件合成一个,我们可以通过这种方式来分析可执行文件的单一架构。

合并之后的可执行文件,代码部分是不共用的,每个架构都有自己的独立的代码,但是资源是共用的,但是合并的话会增加一些合并架构的东西,所以合并后的可执行文件可能会比合并的可执行文件的大小和大一点

MachO文件结构

首先我们看一下苹果官方的图片

分三部分:

  • header:包含该二进制文件的一般信息,最开始读取的部分,例如字节顺序、架构类型、加载指令的数量等。类似于一本书的标题。使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么,文件类型是什么
  • load commands:一张包含很多内容的表,对Data的一种描述,类似目录。内容包括区域的位置、符号表、动态符号表等
  • Data:具体数据区,文件最大的部分

例如我们把单一架构的可执行文件通过MachOView打开

里面有这三类,而对于多个架构合成的maco文件我,我们也打开看看:

我们发现会有多种架构并列,而每个架构都有自己但多的head、Load Commands、Data

Header

我们在仔细分析一下head里面数据

  • Magic Number:确定是多少位的,MH_MAGIC:代表32位,MH_MAGIC_64:64位
  • CPU Type:代表arm型号。 0000000C对应值是CPU_TYPE_ARM,是32位的;0100000C对应的值是CPU_TYPE_ARM64,代表是64位的
  • CPU SubType:指出具体架构类型,例如00000009,代表arm_v7;00000000代表arm64;0000000B代表arm_v7s
  • File Type:00000002代表MH_EXECUTE,可执行文件。假如我们打开的是.a文件,这个值就是MH_OBJECT
  • Number of load Commands:代表有多少条加载指令,也就是Load Commands里面有多少条
  • Size of Load Commands:加载文件的大小
  • Flags:标志位,给CPU做标记用,主要标识二进制文件支持的功能,主要是和系统加载、链接有关

其实Header是一个结构体,具体结构如下:

这个我们可以在系统库mach-o里的loader.h文件中看到

关于File Type字段的一些值也可以在这个文件中看到

系统在加载MachO文件的时候,会首先加载Header,这样就知道该MachO文件架构模式。CPU类型。已经加载大小、方式等

Load Commands

我们再看一下Load Commands:

Load Commands的最尾部的地址是

而DATA的最开始的地址是:

说明 在Load Commands和DATA之间有一大块内存地址,这是为什么呢,因为苹果有一个原则就是二进制对齐原则,这样会导致一些内存空间是空余出来的,而且还有扩容原则、读取数据原则,预留的空间是为了使数据的读取更高效,用空间换时间,同时这些地方也可以提供给我们添加动态库。

而Header和Load Commands直接是没有空余的空间

我们再看一下具体结构:

前4个部分,告诉系统文件分为四大块,每块内容

  • Command:Load Command类型
  • Command Size:Load Command大小
  • Segment Name:代表什么段,__TEXT:说明是文字段
  • VM Address:虚拟地址,映射到内存中的地址
  • VM Size:虚拟文件大小
  • File Offset:文件偏移大小
  • File Size:文件大小

当文件读进内存之后我要从文件的offset位置加上文件的大小,

  • LC_SEGMENT_64(_PAGEZERO):虚拟的,内存中其实是不存在的
  • LC_SEGMENT_64(_TEXT):
  • LC_SEGMENT_64(_DATA):
  • LC_SEGMENT_64(_LINKEDIT):
  • LC_DYLD_INFO_ONLY:动态链接相关的信息!因为iOS系统存在共享缓存,例如系统库Foundation框架在iOS系统中所有APP都共用这个库,而且内存中只有这一份,而dyld加载app的时候会将foundation框架中的函数生成一个符号,然后通过这个符号和相应的函数实现进行绑定
  • LC_SYMTAB:符号表
  • LC_DYSYMTAB:动态符号表
  • LC_LOAD_DYLINKER:dyld路径,也是使用谁加载我,iOS中是dyld
  • LC_UUID:文件的唯一标示
  • LC_VERSION_MIN_IPHONEOS:支持的最低操作系统版本
  • LC_SOURCE_VERSION:源代码版本
  • LC_MAIN:主程序入口地址和栈大小
  • LC_ENCRPTION_INFO_64:
  • LC_LOAD_DYLIB(Foundation):依赖库的路径,包括三方库 ...
  • LC_RPATH:
  • LC_FUNCTION_STARTS:函数起始位置表地址
  • LC_DATA_IN_CODE:
  • LC_CODE_SIGNATURE:代码签名

我们看一下TEXT段和DATA段

我们发现__DATA的偏移量就是__TEXT的文件大小

DATA

我们再到DATA部分可以看到DATA部分分为_TEXT和__DATA,而Load Commands部分就是描述DATA部分的数据

TEXT部分 --代码段

  • Section64 (_TEXT,_text):程序代码,hoper重点分析它
  • Section64 (_TEXT,_stubs):动态链接
  • Section64 (_TEXT,_stubs_helper):
  • Section64 (_TEXT,_objc_methname):方法名称
  • Section64 (_TEXT,_objc_classname):类名称
  • Section64 (_TEXT,_objc_methtype):方法类型
  • Section64 (_TEXT,_cstring):静态字符串,也就是字符串常量,刚运行时候字符串就加载进去了
  • Section64 (_TEXT,_unwind_info):

DATA部分 --数据段

  • Section64 (_DATA,_got):非懒加载的符号表
  • Section64 (_DATA,_la_symbol_ptr):懒加载的符号表,只有当调用的时候生成一个符号,然后和函数绑定
  • Section64 (_DATA,_objc_classlist):类列表
  • Section64 (_DATA,_objc_protolist):协议列表
  • Section64 (_DATA,_objc_imageinfo):
  • Section64 (_DATA,_objc_const):常量区
  • Section64 (_DATA,_objc_selrefs):方法
  • Section64 (_DATA,_objc_classrefs):类
  • Section64 (_DATA,_objc_superrefs):
  • Section64 (_DATA,_objc_ivar):
  • Section64 (_DATA,_objc_data):
  • Section64 (_DATA,_data):
  • Dynamic Loader Info
  • Function Starts
  • Symbol Table
  • Data in Code Entries
  • Dynamic Symbol Table
  • String Tabel
  • Code Signature

总结

MachO文件的结构分为Header、Load Commond、DATA三部分,如果将MachO文件当做一本书的话,那么Header就是书的封面,Load Commond就是书的目录,DATA就是书的内容。

因为MachO只是一种文件类型,所以,系统在加载应用程序的时候会加载很多个MachO文件,如果一个MachO文件依赖其他MachO文件,那么就会将依赖的MachO文件加载到自己的Load Commands里面,而程序调用方法也是先找到方法所在的MachO文件在哪个,找到对应的Header,然后在文件的Load Commands里面找方法在的位置,如果方法不再自己文件中就会找该MachO文件所依赖的库,而MachO文件的Load Commands会指引依赖库的位置,直到找到方法的所在