浅析iOS Mach-O文件的懒加载和非懒加载

举报
dosomething 发表于 2023/08/25 17:23:32 2023/08/25
【摘要】 什么是Mach-O文件    Mach-O是Mach Object的英文缩写,是一种文件格式,用于描述苹果操作系统下的可执行程序、动态库的组成;和Win下的PE、Linux下的ELF属于同一类文件。Mach-O文件的组成架构    Mach-O的组成分为3个部分        Header:描述整个Mach-O的类型、cpu类型、要加载的命令等        Load Commonds:描述...

什么是Mach-O文件

    Mach-O是Mach Object的英文缩写,是一种文件格式,用于描述苹果操作系统下的可执行程序、动态库等;和Win下的PE、Linux下的ELF属于同一类文件。

Mach-O文件的组成架构

mach-o-overview.png


    Mach-O的组成分为3个部分

        Header:描述Mach-O的类型、cpu类型、要加载的命令等

        Load Commonds:描述各个Section数据的分布、程序加载到内存中的位置及辅助描述信息

            此区包含多个Segment,每个Segment包括多个Section

        Data:实际的数据部分

  我们用一张图来描述上述的分布是如何加载到内存中的:

        首先我们需要简单熟悉下苹果的空间布局随机化技术ASLR,Address Space Layout Randomization(这里只做简单介绍,不做深究),简单理解就是程序加载到内存的位置不固定,加载时系统会生成一个随机基地址,MachO文件会在此基础上进行展开,也就是一个随机锚点,这样可以有效增加黑客的内存注入的难度

91bbd26013d14efd964d438db8fc634a.png

        从上图可以看出,Mach-O中的描述信息对可执行代码在内存中的分布至关重要,规划了可执行程序的内存架构

            1、在Mach-O运行时,操作系统根据Section Header定义的Section Data在Mach-O文件中的位置(offset)和尺寸(size),将相应的Section Data数据加载到Section Header指定的内存(address)偏移处。

            2、Seaction Header指定的address是相对于ALSLR地址的

            3、Mach-O文件中有多个Section Data区,每个区根据功用代表的不同的数据类型和读写权限,下图详细说明

        Load Comonad下有

            Segment(__PAGEZERO):用于指定空指针的内存位置

            Segment(__TEXT):程序运行的代码,只读区

                Section(__text):代码指令

                Section(__stubs):动态库的函数存根,函数锚点,存放__la_symbol_ptr中的地址

                Section(__stub_helper):辅助定位动态库函数地址

            Segment(__DATA):程序运行时数存储数据的部分,可读可写区

                Section(__got):存放动态库中的全局变量,加载到内存前是空数据,用于非懒加载

                Section(__la_symbol_ptr):懒加载动态库中的函数地址,加载到内存前存放的是__stub_helper中的地址

                Section(__data):存放当前Mach-O已初始化的全局变量

                Section(__bss):存放当前Mach-O未初始化的全局变量和静态局部变量

            Section(SYMTAB):存放符号表

            Section(DYSYMTAB):存放间接符号表

            Section(STRTAB):存放字符串表

为什么要进行懒加载和非懒加载

    通过对Mach-O文件的分析,我们基本上可以知道文件在内存中的分布,Section Data的数据并不能直接运行,不同的Section Data存放不同的数据,需要在运行前进行加载整合,此整合过程就是加载过程。

    对动态库中的数据,根据其数据类型,分为了懒加载和非懒加载

        懒加载:用于动态库中函数地址的加载

        非懒加载:用于动态库中的全局变量的加载

    下面以一个简单的例子说明懒加载和非懒加载的加载过程

#import <Cocoa/Cocoa.h>

extern NSString *const helloworld;  // 引用动态库中的变量

int main(int argc, const char *argv[])
{
    NSLog(@"%@",helloworld);
    NSLog(@"%@",helloworld);
}

    非懒加载过程

        1、Xcode开启汇编debug

            2、添加BreakPoint

            3、运行程序

            4、x86下面的rip寄存下相当于arm下的pc寄存器,可以看出当前%rip为0x10cb63611

                计算%rip+0x11a68=0x10cb63611+0x11a68=0x10cb75079,根据8字节对齐方式,实际使用的内存地址是0x10cb75080

                执行lldb的,memory read 0x10cb75080,结果是0x0111232bb8就是上图所展示的实际helloworld的存储地址,那么0x10cb75080是什么地址呢

        5、执行lldb的,image lookup -a 0x10cb75080,相对于ASLR的地址为0x1000018080,下面已经明确了在__got中,同时偏移为120个字节

        6、__got的首地址为0x180008,首地址+120的偏移=0x180008+0x78=0x18080,刚好指向_helloworld符号。

            细心的同学就发现了,Data列中全部为00000000000,为什么Value中可以知道符号为helloworld呢

        7、这里MachOView主动做了加载,正常情况下这里确实为0,MachOView怎么查到此位置就是helloworld的呢?

            上面我们知道了helloworld相对于__got首地址的偏移是120个字节,每个__got项占用8个字节(刚好是一个64位指针的大小)

120/8  = 15

            18080所处内存地址相对首项是第15(从0开始算)项

        8、__got header中flag下的Reserved1(Indirect Sys Index),相对于简介符号表的位置为68

        9、Reserved1 + __got中的位置 = 68 + 15 = 83,得到间接符号表中的位置

间接符号表基地址(0x29888)+ 单项字节数4 * 项数(83)=0x29888+4*83= 0x29DD4

                        _helloworld对应的Data列为00007F0,此位置为符号表中的第00007F0项目

        10、符号表的基地址为0x00021738,单项size为16个字节

符号表基地址(0x00021738)+ 单项Size(16) * 第0x00007F0项 = 0x00021738 + 0x10 * 0x00007F0 = 0x00029638

                0x00029638对应的符号表项中,字符串表的String Table Index为0x0000B20

        11、字符串表的基地址为0x00029B00

    字符串表基地址(0x00029B00)+ 字符串表的Index(0x0000B20) =  0x2A620

                就是_hellworld的字符串

        总结:经过那么多的步骤,从__got区,经过间接符号表、符号表、字符串表,最终获取到变量的符号名称helloworld,但是helloworld变量所对应的内容(字符串)还是存在于动态库中,因内容过多,此处不做细致分析,另起文章再析。

         对上述过程进行汇总,得到如下图,更简单明晰

    懒加载过程

        1、Xcode开启汇编debug(同非懒加载)

        2、添加BreakPoint

        3、运行程序

        4、添加汇编断点,NSLog函数的地址都是0x10ab00d82

        4、查看0x10ab00d82对应的内存地址,在__TEXT段下,__stubs区,相对__stubs的基地址偏移为18,

            stubs的基地址(0x00011D70)+ 18(10进制) = 0x00011D82,就是NSLog的位置,但是Data列对应的数据是FF2548630000,并不像是函数的地址,所以此处应该也是MachOView自己计算的

        5、FF2548630000是什么呢,我们看下lldb中0x10ab00d82中存的数据是按照直接读取的顺序刚好是FF2548630000

        6、加汇编断点,并走进去,这里的数据jmpq  *0x6348(%rip),就是FF2548630000的值,也就是stubs中存的都是汇编的16进制表示

        7、rip寄存器前面介绍了(和pc寄存器一样都是指令寄存器),rip存的数据就是当前指令0x10ab00d82 

            *0x6348(%rip) = *(0x10ab00d82 + 0x6348)= *(0x10AB070CA),根据地址8字节对齐,0x10AB070CA需转换为0x10AB070D0

             学过C的都知道,*代表的是解引用,因此此代码就代表的是从0x10AB070D0内存取值,在进行分析内存中取值的内容前,先确定0x10AB070D0内存到底在何处。

           0x10AB070D0在Mach-O的位置是__DATA段,__la_symbol_ptr区,偏移为24

            __la_symbol_ptr区的基地址为0x00180B8 + 24(10进制) = 0x180D0,DATA列就是0x10AB070D0所存的值0x100011F1E

        8、0x100011F1E位于__stub_helper区,可以看到只有2行汇编

            pushq $0x5f

            jmpq 0x100011f04

        0x100011f04位于__stub_helper区的第一行,也就是所有的函数符号寻找都是在通过偏移(如0x5f)传入dyld_stub_binder函数来寻址的,dyld_stub_binder的寻址方式有点复杂,这里不做细致分析,另起文章介绍

        9、在首次寻址结束后,dyld_stub_binder会将寻到的函数地址存入到__la_symbol_ptr对应的DATA中,后面再调用NSLog,__la_symbol_ptr中存入的就是正常的函数地址,不再是__stub_helper中的位置,不需要再寻址了(感兴趣的可以分析下fishhook的源码,fishhook利用的就是懒加载的过程,直接改写__la_symbol_ptr中对应的DATA为被hook的函数地址)。

        总结:对懒加载的过程进行如下简介

            1、初始时__la_symbol_ptr的内容都是指向__stub_helper区

            2、函数调用时,代码中存储的地址是stub区的地址

            3、动态库函数定位过程,通过stub->la_symbol_ptr->stub_helper逐区定位函数地址,并最终通过dyld_stub_binder 寻找到函数地址,并写入__la_symbol_ptr区

            4、本Mach-O文件中的函数地址不需要懒加载,函数地址已存在__text区中

            5、后面再调用同一个函数,直接经过stub->la_symbol_ptr即可获取到函数地址


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。