一.什么是Mach-O
Mach是一个由卡内基梅隆大学开发的用于支持操作系统研究的操作系统内核,Mach-O是Mach Object的缩写,是一种用于可执行文件、目标代码、动态库、内核转储的文件格式, 是a.out
的替代。
二.Mach-O的类型
ios15系统下,Mach-O有12中定义
/usr/include/mach-o/Loader.h
*
* Constants for the filetype field of the mach_header
*/
#define MH_OBJECT 0x1 /* relocatable object file */ 可重定位目标文件:目标文件.o 静态库文件.a
#define MH_EXECUTE 0x2 /* demand paged executable file */ 可执行文件:注意:不是.app文件,因为.app文件其实是个文件夹,.app里面的和项目名称同名的黑黑的才是可执行文件
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */ 动态库文件:.dylib .framework/xx文件
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static
linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug
sections */ dsym文件,debugSymbols 调试符号表
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
#define MH_FILESET 0xc /* a file composed of other Mach-Os to
be run in the same userspace sharing
a single linkedit. */
- 验证下我桌面的2个文件
file Desktop/unuseOCclass
Desktop/unuseOCclass: Mach-O 64-bit executable arm64
file Desktop/unuseOCclass.app
Desktop/unuseOCclass.app: directory
打印结果来看,只知道我的文件是个Mach-O文件,不是特别详细,
- 我们用MachOView看下
0x2, MH_EXECUTE和上面对上了😁,说明我这个Mach-O的类型是个可执行文件
二.Mach-O的格式
3.1 设计图
百度搜到的,一目明了- Mach-O分为3个区域,Header & Load Commands & Data
Header: 记录Mach-O的基本信息,例如平台、文件类型、指令数、指令总大小
Load Commands: 紧跟Header,加载Mach-O文件时会使用这部分数据确定内存分布,对系统内核加载器和动态连接器起指导作用
Data: 每个segment的具体数据保存在这里,包含具体的代码、数据等等 - 一个Mach-O拥有一个Header,一个Header里面包含了多个Segment command,每个Segment command又拥有多个Section Data.
3.2 自己验证下
自己找一个可执行文件(ipa包里面和工程名一样的黑乎乎的东西)拖进MachOview里面,看起来和设计图是能对应上的。
3.3 Header
/usr/include/mach-o/Loader.h
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
//翻译:64位的mach-o的header会出现在64位架构的最前面,也就是说Header就是在Mach-O的最前面,和前面看到的设计图是一致的
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
//魔数
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
//flags,太多了只贴2个
/* Constants for the flags field of the mach_header */
#define MH_NOUNDEFS 0x1 /* the object file has no undefined references*/
#define MH_INCRLINK 0x2 /* the object file is the output of an incremental link against a base file and can't be link edited again */
字段 | 解释 |
---|---|
magic | 魔数:魔数与架构一一对应,一个魔数对应一个架构。以前有32位和64位架构的混合体,FAT:0xcafebabe,ARMv7:0xfeedface, ARM64:0xfeedfacf,但是iphone5s后就全部使用arm64了,所以现在所有的iphone都是arm64架构,也就是说它们的mach-o的魔数都是0xfeedfacf |
cuptype、cpusubtype | cpu架构及子版本 |
filetype | 文件类型,咱们上面说的12中mach-o类中中的1种 |
ncmds | load commands的数量 |
sizeofcmds | load commands的大小 |
flags | dyld加载需要的一些标记,对应上面的 flags,太多了只贴2个 |
reserved | 保留字段 |
- 上MachOView, 发现和上面的字段一一对应
所以呢,你会发现MachOView其实就是读取了咱们拖进去的Mach-O的信息然后展示出来
3.4 Load commands
- load_command是个结构体,它的作用是记录总的Segment command的数量以及大小,和Header中的数据保持一致.
struct load_command {
uint32_t cmd; /* type of load command */ load command的类型
uint32_t cmdsize; /* total size of command in bytes */ 记录偏移量,让load command的指针加载下一个command
};
-
Segment Command的类型有很多种,总量为22个
类型 | 解释 |
---|---|
LC_SEGMENT_64 | 定义一段(Segment),加载后被映射到进程的内存空间中,包括里面的节(Section) |
LC_DYLD_INFO_ONLY | 记录有关链接的信息,包括在__LINKEDIT中动态链接的相关信息的具体偏移与大小(重定位,绑定,弱绑定,懒加载绑定,导出信息等),ONLY表示该指令是程序运行所必需的。 |
LC_SYMTAB | 定义符号表和字符串表,链接文件时被dyld使用,也用于调试器映射符号到源文件。符号表定义的本地符号仅用于调试,而已定义和未定义的external符号被链接器使用 |
LC_DYSYMTAB | 将符号表中给出符号的额外信息提供给dyld |
LC_LOAD_DYLINKER | dyld的默认路径 |
LC_UUID | Mach-O唯一ID |
LC_VERSION_MIN_IPHONES | 系统要求的最低版本 |
LC_SOURCE_VERSION | 构建二进制文件的源代码版本号 |
LC_MAIN | 应用程序入口,dyld的_main函数获取该地址,然后跳转 |
LC_ENCRYPTION_INFO_64 | 文件加密标志,加密内容偏移和大小 |
LC_LOAD_DYLIB | 依赖的动态库,含动态库名,版本号等信息 |
LC_RPATH | @rpath搜索路径 |
LC_DATA_IN_CODE | 定义在代码段内的非指令的表 |
LC_CODE_SIGNATURE | 代码签名信息 |
- segment_command_64对应每种不同类型的Segment command
/*
* The 64-bit segment load command indicates that a part of this file is to be
* mapped into a 64-bit task's address space. If the 64-bit segment has
* sections then section_64 structures directly follow the 64-bit segment
* command and their size is reflected in cmdsize.
*/
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */ 对应section的起始地址 。映射到虚拟地址的偏移
uint64_t vmsize; /* memory size of this segment */映射到虚拟地址的大小
uint64_t fileoff; /* file offset of this segment */对应于当前架构文件的偏移(注意:是当前架构文件,不是整个FAT文件)
uint64_t filesize; /* amount to map from the file */文件的大小
vm_prot_t maxprot; /* maximum VM protection */段页面的最高内存保护
vm_prot_t initprot; /* initial VM protection */初始内存保护
uint32_t nsects; /* number of sections in segment */包含section 的个数
uint32_t flags; /* flags */
};
一般只有下面这4个Segment command使用
类型 | 解释 |
---|---|
_PAGEZERO | 空指针陷阱段,映射到虚拟内存空间第一页,捕捉对NULL指针的引用 |
_TEXT | 代码段、只读数据段 |
_DATA | 读取和写入数据段 |
_DATA_CONST | 常量数据的段 |
_LINKEDIT | dyld需要使用的信息,包括重定位、绑定、懒加载信息等 |
3.5 Section Data
- section data作为最小的单位,是真正存储信息的地方
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */重定位入口的个数。
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
- 看下主要的Section Data
Segment _TEXT
name | des |
---|---|
text | 代码区 |
__stubs | 间接符号存根,跳转到懒加载指针表 |
__stub_helper | 帮助解决懒加载符号加载的辅助函数 |
__objc_methname | 方法名 |
__objc_classname | 类名 |
__objc_methtype | 方法签名 |
__cstring | 只读的C风格字符串,包含0C的部分字符串和属性名 |
Segment _DATA
name | des |
---|---|
__nl_symboLptr | 非懒加载指针表,在dyld加载时会立即绑定值 |
__la_symbol_ptr | 懒加载指针表,第1次调用时才会绑定值 |
__got | 非懒加载全局指针表 |
__objc_classrefs | 被引用的类列表 |
__modinit_func | constructor函数 |
__mod_term_func | destructor函数 |
__cfstring | OC字符串 |
__objc_classlist | 程序中类的列表 |
__objc_nlclslist | 程序中自己实现了+load方法的类 |
__objc_protolist | 协议的列表 |
四. 用代码解析Mach-O
4.1 Image
- 要将我们写的代码运行起来,除了Mach-O中的可执行文件,还需要很多系统的库。我们将可执行文件和系统库都看成一个个Image(映像)
- 在LLDB中使用
image list
可以查看所有的image
- 有关image的操作,都定义在<mach-o/dyld.h>
#import <mach-o/dyld.h>
extern const struct mach_header* _NSGetMachExecuteHeader();
//函数返回当前进程中加载的映像的数量, 和image list命令结果一致
uint32_t imagesCount = _dyld_image_count();//397
//函数返回mach-O的头
const struct mach_header* header = _dyld_get_image_header(0);//0x0000000104c50000
//函数返回mach-O的头 _NSGetMachExecuteHeader没有被声明,所以需要extern
const struct mach_header* header1 = _NSGetMachExecuteHeader();//0x0000000104c50000
//获取基地址 ALSR的base值
intptr_t baseAddress = _dyld_get_image_vmaddr_slide(0);//0x002d0000
// 和image list命令结果一致
(lldb) image list -o -f
[ 0] 0x00000000002d0000 /Users/LYK/Library/Developer/Xcode/DerivedData/unuseOCclass/Build/Products/Debug-iphoneos/unuseOCclass.app/unuseOCclass
//函数返回image的名称
///private/var/containers/Bundle/Application/x/x.app/x
const char* imageName = _dyld_get_image_name(0);
//使用static修饰函数,表明该函数只在本文件内使用
static void addImageFunc(const struct mach_header* mh, intptr_t vmaddr_slide) {
// printf("addImageFunc");
}
static void removeImageFunc(const struct mach_header* mh, intptr_t vmaddr_slide) {
printf("removeImageFunc");
}
//监听所有加载的image
//1. 当一个新的image即将添加进来 但未初始化时 会回调该函数
//2. 调用_dyld_register_func_for_add_image函数,系统已经加载了某些image,则会分别对这些已经加载的image进行回调
_dyld_register_func_for_add_image(&addImageFunc);
//监听image的卸载
_dyld_register_func_for_remove_image(&removeImageFunc);
//返回一个image的版本号,如果找不到return -1
int32_t libVersion = NSVersionOfRunTimeLibrary("libSystem");
//返回当前可执行文件的路径名
//和_dyld_get_image_name(0); 一样
//和[NSBundle mainBundle].executablePath; 一样
char executablePathBuffer[256];
uint32_t bufferSize = sizeof(executablePathBuffer)/sizeof(char);
int pathRetInt = _NSGetExecutablePath(executablePathBuffer, &bufferSize);
if (pathRetInt == 0) {
printf("%s", executablePathBuffer);
} else {
printf("获取路径失败");
}
//注册当前线程结束时的回调函数 懒得看了...
//void _tlv_atexit(void (*termFunc)(void* objAddr), void* objAddr)
4.2 Segment command & Section Data
- 有关Segment command 和 Section Data的操作,都定义在<mach-o/getsect.h>
- 获取某个Section Data
//getsectdata 获取进程中可执行程序映像的某个段中某个节的数据指针和尺寸。
//传入segment command中的section data, 返回该section data的地址和大小
intptr_t slide = _dyld_get_image_vmaddr_slide(1);//1 还是 0, 我有疑问
unsigned long size = 0;
char *dataAddress = getsectdata("__TEXT", "__text", &size);//size = 2348 dataAddress=4294990268
char *realAddress = dataAddress + slide;
//获取进程加载的库的segname段和sectname节的数据指针和尺寸。
char *getsectdatafromFramework(const char *FrameworkName, const char *segname, const char *sectname, unsigned long *size);
和MachOView的结果一致
- 获去Segment command和Section Data的边界
get_end
获取当前可执行文件最后一个段的数据后面开始的地址
static unsigned long self_get_end() {
unsigned long end = 0;
//Header
const struct mach_header_64 *header = _NSGetMachExecuteHeader();
//Segment command
struct segment_command_64 *segment = header + 1;
for (int i = 0; i < header->ncmds; i ++) {
printf("%s\n", segment->segname);
if (segment->cmd != LC_SEGMENT_64) {
break;
}
end = segment->vmaddr + segment->vmsize;
segment += 1;
}
return end;
}
unsigned long end = get_end(); //4295065600
unsigned long self_end = self_get_end(); //计算的不对
其实就是截图中__LIKEDIT Segment的4295016448 + 49152 = 4295065600
get_etext
获取当前可执行文件 TEXT segement的 text section的结尾地址
static unsigned long self_get_etext() {
const struct section_64 *sec = getsectbyname("__TEXT","__text");
return sec->addr + sec->size;
}
unsigned long etext = get_etext(); //4294992540
unsigned long self_etext = self_get_etext();//4294992540
用MachOView验证下, 4294989916+2624 = 4294992540, 结果一致
图片.png
get_edata
获取当前可执行文件 DATA segement的 data section的结尾地址(同上)
static unsigned long self_get_edata() {
const struct section_64 *sec = getsectbyname("__DATA", "__data");
return sec->addr + sec->size;
}
unsigned long edata = get_edata(); //4295007008
unsigned long self_edata = self_get_edata();//4295007008
- 根据段名找到它的头指针
const struct segment_command_64 *mhp = getsegbyname("__TEXT");
- 根据段名和节名找到节的头指针
const struct section_64 *sectionHeader = getsectbyname("__TEXT", "__text");
- 根据Header & 段名 找到段的数据
const struct mach_header_64 *header = _NSGetMachExecuteHeader();
unsigned long sssssize = 0;// segment(段) 的 vmsize
uint8_t *segData = getsegmentdata(header, "__TEXT", &sssssize);
4.3 dlfcn
这是一个地址与符号查询的库,定义在"dlfcn.h"下
- dladdr方法,传入一个地址和一个dl结构体。如果地址能找到对应的符号信息则返回非0,否则返回0
extern int dladdr(const void *, Dl_info *);
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */地址归属的映像库文件名称
void *dli_fbase; /* Base address of shared object */地址归属的库在内存中的基地址
const char *dli_sname; /* Name of nearest symbol */离地址最近的符号名称
void *dli_saddr; /* Address of nearest symbol */离地址最近的符号名称的开始地址
} Dl_info;
- demo 找到
objc_msgsend
函数所在的位置
#import "dlfcn.h"
#import <objc/message.h>
#import <mach-o/loader.h>
Dl_info info;
memset(&info, 0, sizeof(info)); //填充干净的数据
int ret = dladdr(objc_msgSend, &info);
if (ret != 0) {
printf("%s\n", info.dli_fname); ///usr/lib/libobjc.A.dylib
printf("%p\n", info.dli_fbase); //0x19e2fe000
printf("%s\n", info.dli_sname); //objc_msgSend
printf("%p\n", info.dli_saddr); //0x19e303960
struct mach_header_64 *pheader = (struct mach_header_64*)info.dli_fbase;//0x19e2fe000
printf("success");
} else {
printf("Problem retrieving program information for %s: %s\n", funcName, dlerror());
}
- dlopen用法
待学习 - 检测某个函数有没有被第三方库hook,原理是看这个函数有没有超出可执行文件的__TEXT段
#import <mach-o/dyld.h>
#import <mach-o/getsect.h>
BOOL checkMethodBeHooked(Class class, SEL selector)
{
//你也可以借助runtime中的C函数来获取方法的实现地址
IMP imp = [class instanceMethodForSelector:selector];
if (imp == NULL)
return NO;
//计算出可执行程序的slide值。
intptr_t pmh = (intptr_t)_NSGetMachExecuteHeader();
intptr_t slide = 0;
#ifdef __LP64__
const struct segment_command_64 *psegment = getsegbyname("__TEXT");
#else
const struct segment_command *psegment = getsegbyname("__TEXT");
#endif
intptr_t slide = pmh - psegment->vmaddr
unsigned long startpos = (unsigned long) pmh;
unsigned long endpos = get_end() + slide;
unsigned long imppos = (unsigned long)imp;
return (imppos < startpos) || (imppos > endpos);
}
1
参考
otool工具的使用
https://www.codeproject.com/Articles/187181/Dynamic-Linking-of-Imported-Functions-in-Mach-O
https://www.jianshu.com/p/7c87e115492d
https://www.jianshu.com/p/3b83193ff851
网友评论