美文网首页
Mach-O扫盲

Mach-O扫盲

作者: 李永开 | 来源:发表于2022-06-26 14:36 被阅读0次

一.什么是Mach-O

Mach是一个由卡内基梅隆大学开发的用于支持操作系统研究的操作系统内核Mach-OMach 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

相关文章

网友评论

      本文标题:Mach-O扫盲

      本文链接:https://www.haomeiwen.com/subject/akcgvrtx.html