《iOS底层原理文章汇总》
上一篇文章iOS-逆向11-代码注入得知,要想动态注入,必须要修改MachO文件,通过工具yololib使其中能增加一行自定义的动态库的路径,才能动态注入自己的代码,从而达到hook的目的,MachO文件的结构是什么样的呢?
1.MachO文件
Mach-O其实是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式, 类似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)
2.MachO文件格式
Mach-O为Mach object文件格式的缩写,它是一种用于可执行文件、目标代码、动态库的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性。
属于MachO格式的常见文件
目标文件.o
库文件
.a
.dylib
Framework
可执行文件
dyld
.dsym
File指令
通过 $file 文件路径 查看文件类型
3.目标文件.o
I.单个c文件通过clang(llvm的前端)编译为.o文件
打开Xcode->Cmd+N新建test.c文件,编写main函数
#include <stdio.h>
int main(){
printf("test\n");
return 0;
}
通过clang -o test.c生成test.o文件,通过file指令查看test.o的文件结构属于Mach-O文件,架构为x86-64位处理器
再次执行clang test.o将.o文件编译为可执行文a.out,是可执行文件executable,不再是object文件
也可以通过clang -o test2 test.c一次性将test.c编译生成可执行文件
也可以通过clang -o test3 test.o将.o文件编译生成可执行文件
a.out、test2、test3属于同一个文件,哈希值相等只是文件名不相同,改后缀名后哈希值仍然相等
源文件到可执行文件的中间产物是.o文件
II.项目中会存在多个.c文件,两个.c文件,test.c和test1.c文件,test.c中会调用test1.c中的方法
执行clang -o demo test1.c test.c
./demo输入
test
test1
// test.c
#include <stdio.h>
void test1();
int main(){
printf("test\n");
test1();
return 0;
}
// test1.c
#include <stdio.h>
void test1(){
printf("test1\n");
}
执行
clang -c test1.c test.c
生成.o文件,执行clang -o demo1 test.o test1.o
生成demo1可执行文件但两次文件不一样,因为编译链接顺序发生变化,一次test1.c在前,一次test1.c在前,生成的可执行文件哈希值不相同
通过
objdump --macho -d demo2
查看可执行文件顺序,发现demo2(和demo相同)与demo1的文件顺序不一致,准确的说是text段不一致,demo2中_main函数在前,_test1函数在后,demo中_main函数在后,_test1函数在前demo2如下
demo如下
demo1如下
相当于Xcode工程Build Phases目录下的Compile Sources的文件顺序,文件顺序不一致,编译生成的二进制文件排列不一致
4.库文件
I.以.a文件结尾的动态库静态库文件,查找.a文件,find /usr -name "*.a"
发现libpython3.9.a是动态库可执行文件
发现libx264.a、libSDL2.a、libfdk-aac.a是静态库可执行文件
II.以.dylib结尾的dylib也是MachO文件
III.dyld动态链接器文件,系统内核触发dyld
IV.dsym文件,App打包时生成,edit schemes中修改为release模式下,build编译,App包统计目录下生成Demo.app.dSYM,若遇到崩溃根据堆栈信息,要用此符号文件进行排查分析拿到方法名称
Demo.app.dSYM也是一个包,右键显示包内容,有一个Demo的MachO文件
5.MachO文件架构
MachO-Type类型,Build Settings中查看,生成的文件类型
iOS11.0以上的系统只支持arm64架构,32位的架构在11.0的系统安装不了了,无法适配
若将DeploymentInfo改为支持iOS9.0以上,则会出现armv7和arm64架构
若要添加支持的系统架构,可在Build Settings中Architectures添加架构类型如armv7s,armv7s支持iPhone5和iPhone5c
6.通用二进制文件(Universal binary)
苹果公司提出的一种程序代码。能同时适用多种架构的二进制文件
同一个程序包中同时为多种架构提供最理想的性能。
因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大。
但是 由于两种架构有共通的非执行资源(代码以外的),所以并不会达到单一版本的两倍之多。
而且由于执行中只调用一部分代码,运行起来也不需要额外的内存。
支持armv7s、armv7和arm64文件架构的二进制文件是通用二进制文件
当用hopper打开Demo可执行文件时,会提示是Fat archive,表示是通用二进制文件,选择一种,hopper会分析选择的其中一种
- lipo命令
使用lifo -info 可以查看MachO文件包含的架构
lipo MachO文件 –thin 架构 –output 输出文件路径
使用lipo -create 合并多种架构
$lipo -create MachO1 MachO2 -output 输出文件路径
7.MachO文件结构
Mach-O 的组成结构如图所示包括了
Header 包含该二进制文件的一般信息
字节顺序、架构类型、加载指令的数量等。
使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么
Load commands 一张包含很多内容的表
内容包括区域的位置、符号表、动态符号表等。
Data 通常是对象文件中最大的部分
包含Segement的具体数据
- 通用二进制文件会有多个上述图中的结构,armv7一个,arm64一个,armv7s一个
- 可通过otool查看MachO中的数据,otool指令如下
Usage: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool [-arch arch_type] [-fahlLDtdorSTMRIHGvVcXmqQjCP] [-mcpu=arg] [--version] <object file> ...
-f print the fat headers
-a print the archive header
-h print the mach header
-l print the load commands
-L print shared libraries used
-D print shared library id name
-t print the text section (disassemble with -v)
-x print all text sections (disassemble with -v)
-p <routine name> start dissassemble from routine name
-s <segname> <sectname> print contents of section
-d print the data section
-o print the Objective-C segment
-r print the relocation entries
-S print the table of contents of a library (obsolete)
-T print the table of contents of a dynamic shared library (obsolete)
-M print the module table of a dynamic shared library (obsolete)
-R print the reference table of a dynamic shared library (obsolete)
-I print the indirect symbol table
-H print the two-level hints table (obsolete)
-G print the data in code table
-v print verbosely (symbolically) when possible
-V print disassembled operands symbolically
-c print argument strings of a core file
-X print no leading addresses or headers
-m don't use archive(member) syntax
-B force Thumb disassembly (ARM objects only)
-q use llvm's disassembler (the default)
-Q use otool(1)'s disassembler
-mcpu=arg use `arg' as the cpu for disassembly
-j print opcode bytes
-P print the info plist section as strings
-C print linker optimization hints
--version print the version of /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool
otool -f Demo,查看MachO文件头信息,Demo是通用二进制文件支持三种架构
8.MachOView查看MachO文件
Fat Binary中有三种不同的架构armv7,armv7s,arm64
首先是Fat Header,cputype=12表示ARM架构CPU_TYPE_ARM,
cpusubtype=9(小端模式00000009)表示CPU_SUBTYPE_ARM_V7
cpusubtype=11(小端模式0000000B)表示CPU_SUBTYPE_ARM_V7S
cpusubtype=0(小端模式00000000)表示CPU_SUBTYPE_ARM64_ALL
每种架构的偏移地址Offset和Size相加后会有间隔,是因为分页的原因,iOS中每页的大小为16k,MacOS中每页大小为4K
从Load Commands开始一直到底都是DATA数据段
DATA数据段中Section分为两部分,TEXT和DATA
类名和方法名会有联系,工具classdump会dump出类的名称和方法的列表
Load Commands中能看到加载哪些库
64位环境中PAGEZERO占用4G的空间0xffffffff,后面的所有指令都从0xffffffff00000001开始,目的是和32位指令隔开,插入PAGEZERO后和32位指令不会有重叠,这是PAGEZERO的目的,64位和32位做区分,在内存中执行时和32位完全隔离,是一个空区域分割区,让内存地址加上0xffffffff=4294967296,所有的数据往后移,和32位架构的指令不重复,如果有数据指向PAGEZERO为空,里面是不放数据的
早期的架构armv7和armv7s的PAGEZERO为16384
64位大小的地址
0x12345678a2345678
32位大小的地址
0x12345678
0xffffffff=4G
9.MachO Header
Header的数据结构,可在loader.h文件中查看
filetype文件类型
10.Load Commands
DATA部分由三部分组成,SECTION TEXT代码段,SECTION DATA数据段,LINKEDIT,指明起始位置,偏移位置
ASLR,操作系统为每一个进程分配随机的ASLR,
应用程序加载进内存中时,实际地址=ASLR+Rebase Info Size,在MachO指定后,方便应用程序
加载进内存后调用,编译时期生成的是偏移地址Rebase Info
Offset,表示在整个MachO文件中偏移多少,程序在第一次加载进内存中时根据ASLR+Rebase Info Offset
汇编跳转bl,在编译时期生成的是偏移地址,在文件中偏移多少,运行时期的地址每次都变化,重定向改变的是汇编代码
重定向改变的是汇编代码,地址前的0x1表示PAGEZERO
Binding Info offset和Binding Info Size,外部的符号将地址绑定上去,Weak Binding Info Offset和Weak Binding Info Size弱绑定,Lazy Binding Info offset和Lazy Binding Info Size懒绑定,用到的时候再去绑定,Export Info Offset和Export Info Size对外开放的函数
每一段数据以页为单位
Load Commands和Section之间会有空间,之前动态注入修改成功是因为有空间,才能插入一条
Size of load Commands + 2720 = 0002E734和S
ection64 Text段的地址00032374中间有一段地址隔开,故动态注入时能插入一条新的内容在Load Commands的最底部,若没有空间则无法插入
上篇文章动态注入时,我们看到WeChat可执行文件自己的动态库andromedo的路径@rpath/andromeda.framework/andromeda,@rpath路径在Load Command中已经指明为@executable_path/Frameworks,才能找到动态库andromedo
11.DATA数据段
Load Commands中已经指明了DATA数据段包括三部分内容
TEXT段可以通过工具objdump --macho -d Demo查看到
0x1000065a0都指向开头
Symbol Stubs和Assembly结合起来做符号绑定
外部符号表:调用外部函数,只有在运行那一刻,才能找到
启动时刻就绑定了,应用程序一启动,外部函数和符号表进行绑定
Lazy Symbol Pointers中的函数做绑定时会调用Non-Lazy Symbol Pointers的dyld_stub_binder做绑定,绑定符号的目的,将外部函数的真实地址告诉MachO文件,方便调用,先绑定专门用来绑定的函数,之后用此函数去绑定所有的函数
调用Non-Lazy Symbol Pointers中的dyld_stub_binder去绑定