LLDB+debugserver调试第三方应用

前言

本文主要介绍越狱手机通过LLDB、debugserver调试第三方应用,关于LLDB、debugserver的内容,前面在学习中也有提到过:LLDB调试命令、ptrace反调试

一、简介

debugserver就像是远程服务的控制台应用,主要给gdb或者lldb调试,手机设备开启debugserver服务,本地Mac通过LLDB发送指令给debugserver,debugserver在真机调试的时候被安装到手机上,可以在手机上/Developer/usr/bin/debugserver找到。日常正向开发就是Xcode内部的LLDB调起debugserver进程来调试我们自己的App。

二、debugserver命令选项

iOS端调起debugserver

debugserver host:port [program-name program-arg1 program-arg2 …]

选项 含义
-a 进程 将debugserver依附到指定进程,通过PID或可执行文件名称
-d integer 指定等待时间
-i integer 指定等待时间间隔
-l filename 日志文件。将文件名设置为stdout以记录标准输出
-t 使用task ID代替PID
-v 日志模式

三、配置debugserver

debugserver默认只能调试自己开发的应用,调试其他应用会抛异常unable to start the exception thread。默认的debugserver缺少task_for_pid()权限,因此需要给debugserver赋予task_for_pid权限。

LLDB+debugserver调试第三方应用
unable to start the exception thread异常

(一)、方法一 使用ldid赋予权限

(1)Mac端找到debugserver文件
debugserver可以在/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/你越狱手机版本/DeveloperDiskImage.dmg里的usr/bin路径找到,拷贝出来。由于ldid不支持胖二进制文件,因此需要先瘦身:

lipo -thin armv7 debugserver -output ~/debugserver (注意,本文使用iPhone5测试,因此对应armv7,请自行对应架构)

LLDB+debugserver调试第三方应用
image.png

(2)新建一个xml文件

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>com.apple.springboard.debugapplications</key>
        <true/>
        <key>get-task-allow</key>
        <true/>
        <key>task_for_pid-allow</key>
        <true/>
        <key>run-unsigned-code</key>
        <true/>
    </dict>
</plist>
LLDB+debugserver调试第三方应用
xml

(3)赋予权限

ldid -Sxml全路径 debugserver全路径
例如:ldid -S/Users/kinken_yuen/Desktop/ent.xml /Users/kinken_yuen/Desktop/debugserver

LLDB+debugserver调试第三方应用
赋予权限

(4)拷贝配置后的debugserver到手机

注意: 手机上的/Developer目录实际上是只读的,你不能直接将debugserver复制回去,放到别的地方使用,可以是:scp -P 2222 ./debugserver root@127.0.0.1:/usr/bin/debugserver
下面的方法二最后也需要如此操作

(二)、方法二 使用codesign赋予权限

(1)拷贝出debugserver
先将debugserver从手机复制到Mac

scp -P 2222 root@localhost:/Developer/usr/bin/debugserver ./

因为是从当前越狱手机拷贝,因此不需要再瘦身

LLDB+debugserver调试第三方应用
debugserver文件

(1)使用entitlements权限文件签名
新建entitlements.plist,写入内容

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/ PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.springboard.debugapplications</key>
    <true/>
    <key>run-unsigned-code</key>
    <true/>
    <key>get-task-allow</key>
    <true/>
    <key>task_for_pid-allow</key>
    <true/>
</dict> 
</plist>

(3)赋予权限

codesign -s - --entitlements entitlements.plist -f debugserver

LLDB+debugserver调试第三方应用
签名权限

(4)拷贝配置后的debugserver到手机
参考方法一

四、附加到进程

(一)、通过WIFI连接

(1)、手机端开启debugserver

LLDB+debugserver调试第三方应用

(2)、Mac端LLDB连接debugserver

LLDB+debugserver调试第三方应用
进入lldb

process connect connect://设备IP地址:1234(对应于手机开启的端口号)

LLDB+debugserver调试第三方应用
连接debugserver

(二)、通过USB连接

(1)手机端开启debugserver(同上)
(2)Mac端

  • 先做端口转发

iproxy 1234 1234

接着

process connect connect://127.0.0.1:1234
process connect connect://localhost:1234

LLDB+debugserver调试第三方应用
USB连接

五、小结

第三方应用动态调试的方式不止一种,如重签名后用Xcode调试非越狱Cycript调试越狱Cycript调试,以及上面的lldb + debugserver调试。

解析Mach-O文件

一、前言

本文简要解析Mach-O文件格式、结构,主要是自己认识Mach-O文件,学习的一个过程,一些地方可能介绍得不到位,要了解更多有关信息,可以考虑阅读苹果提供的官方文档介绍。

二、什么是Mach-O文件

维基百科简要说明:

Mach-OMach Object文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性,并提升了符号表中信息的访问速度。

Mach-O曾经为大部分基于Mach核心的操作系统所使用。NeXTSTEP,Darwin和Mac OS X等系统使用这种格式作为其原生可执行文件,库和目标代码的格式。而同样使用GNU Mach作为其微内核的GNU Hurd系统则使用ELF而非Mach-O作为其标准的二进制文件格式。

三、Mach-O格式

Mach-O是一个以数据块分组的二进制字节流,这些数据块包含元信息,比如字节顺序、CPU类型、数据块大小等等。
典型的Mach-O文件包含三个区域:
1.Header:保存Mach-O的一些基本信息,包括平台、文件类型、指令数、指令总大小,dyld标记Flags等等。
2.Load Commands:紧跟Header,加载Mach-O文件时会使用这部分数据确定内存分布,对系统内核加载器和动态连接器起指导作用。
3.Data:每个segment的具体数据保存在这里,包含具体的代码、数据等等。

用一张图表示Mach-O

解析Mach-O文件
Mach-O结构图

(一)、Header
数据结构

/*
 * The 32-bit mach header appears at the very beginning of the object file for
 * 32-bit architectures.
 */
struct mach_header {
    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 */
};

/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC    0xfeedface  /* the mach magic number */
#define MH_CIGAM    0xcefaedfe  /* NXSwapInt(MH_MAGIC) */

/*
 * The 64-bit mach header appears at the very beginning of object files for
 * 64-bit architectures.
 */
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) */

根据定义与注释,得到以下解释

名称 含义
magic Mach-O魔数,FAT:0xcafebabeARMv7:0xfeedface,ARM64:0xfeedfacf
cputype、cpusubtype CPU架构及子版本
filetype MH_EXECUTABLE(可执行二进制文件)、MH_OBJECT(目标文件)、MH_DYLIB(动态库),有11种宏定义类型,具体可查看源码
ncmds 加载命令的数量
sizeofcmds 所有加载命令的大小
flags dyld加载需要的一些标记,有28种宏定义,具体看源码,其中MH_PIE表示启用ASLR地址空间布局随机化
reserved 64位保留字段

使用MachOView查看某可执行文件:

解析Mach-O文件
Mach-O Header

(二)、Load Commands
数据结构:

/*
 * The load commands directly follow the mach_header.  The total size of all
 * of the commands is given by the sizeofcmds field in the mach_header.  All
 * load commands must have as their first two fields cmd and cmdsize.  The cmd
 * field is filled in with a constant for that command type.  Each command type
 * has a structure specifically for it.  The cmdsize field is the size in bytes
 * of the particular load command structure plus anything that follows it that
 * is a part of the load command (i.e. section structures, strings, etc.).  To
 * advance to the next load command the cmdsize can be added to the offset or
 * pointer of the current load command.  The cmdsize for 32-bit architectures
 * MUST be a multiple of 4 bytes and for 64-bit architectures MUST be a multiple
 * of 8 bytes (these are forever the maximum alignment of any load commands).
 * The padded bytes must be zero.  All tables in the object file must also
 * follow these rules so the file can be memory mapped.  Otherwise the pointers
 * to these tables will not work well or at all on some machines.  With all
 * padding zeroed like objects will compare byte for byte.
 */
struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

注释的大概意思:
load_commands紧跟mach_header,load_commands展开后的数目与总大小已经在mach_header有记录,所有加载指令都是以cmd、cmdsize起头。cmd字段用该命令类型的常量表示,有专门的结构;cmdsize字段以字节为单位,主要记录偏移量让load command指针进入下一条加载指令,32位架构的cmdsize是以4字节的倍数,64位结构的cmdsize是以8字节的倍数(加载指令永远是这样对齐),不够用0填充字节。文件中的所有表都遵循这样的规则,这样就可以被映射到内存,否则的话指针不能很好地指向。


使用MachOView查看Load Commands区:

解析Mach-O文件
Load Commands

Load Commands下常见的加载指令:

指令 含义
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 代码签名信息

LC_SEGMENT_64段数据结构(说明附在注释部分):

/*
 * 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;        /* Load Command类型 */
    uint32_t    cmdsize;    /*包含的所有section结构体的大小 */
    char        segname[16];    /* 段名 */
    uint64_t    vmaddr;     /* 映射到虚拟地址的偏移 */
    uint64_t    vmsize;     /* 映射到虚拟地址的大小 */
    uint64_t    fileoff;    /* 相对于当前架构文件的偏移 */
    uint64_t    filesize;   /* 文件大小 */
    vm_prot_t   maxprot;    /* 段页面的最高内存保护 */
    vm_prot_t   initprot;   /* 初始内存保护 */
    uint32_t    nsects;     /* 包含的section数 */
    uint32_t    flags;      /* 段页面标志 */
};

该数据结构的段主要有以下4种:

含义
_PAGEZERO 空指针陷阱段,映射到虚拟内存空间第一页,捕捉对NULL指针的引用
_TEXT 代码段、只读数据段
_DATA 读取和写入数据段
_LINKEDIT dyld需要使用的信息,包括重定位、绑定、懒加载信息等

(三)、Data
Load Commands区域下来接着就是DATA区域,展开Load Commands下的LC_SEGMENT_64可以看到多个Section64,各个Section的具体信息可以在Load Commands紧接着的部分查看,它们是一一对应的:

解析Mach-O文件
DATA区域

section的数据结构如下:

struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* 节名 */
    char        segname[16];    /* 所属段名 */
    uint64_t    addr;       /* 映射到虚拟地址的偏移 */
    uint64_t    size;       /* 节的大小 */
    uint32_t    offset;     /* 节在当前架构文件中的偏移 */
    uint32_t    align;      /* 节的字节对齐大小n,2^n */
    uint32_t    reloff;     /* 重定位入口的文件偏移 */
    uint32_t    nreloc;     /* 重定位入口个数 */
    uint32_t    flags;      /* 节的类型和属性*/
    uint32_t    reserved1;  /* reserved (for offset or index) */
    uint32_t    reserved2;  /* reserved (for count or sizeof) */
    uint32_t    reserved3;  /* 保留位,以上两同理 */
};

section节已经是最小的分类,大部分内容集中在__TEXT,__DATA这两段中,部分内容如下:

__TEXT节 含义
__text 程序可执行代码区域
__stubs 间接符号存根,用于跳转到懒加载指针表
__stubs_helper 懒加载符号加载辅助函数
__cstring 只读的C字符串,包含OC的部分字符串和属性名
…… ……
__DATA 含义
__nl_symbol_ptr 非懒加载指针表,dyld加载时立即绑定值
__la_symbol_ptr 懒加载指针表,第1次调用才绑定值
__got 非懒加载全局指针表
__mod_init_func constructor函数
__cfstring OC字符串
…… ……

四、小结

了解Mach-O可以帮助我们理解dyld的加载Mach-O的过程以及与Mach-O相关的读取或操作,如fishhook、文件内偏移地址等。

五、参考

Dynamic Linking of Imported Functions in Mach-O
mach-o格式分析
《iOS应用逆向与安全》

iOS应用程序启动之dyld加载流程(浅识)

一、程序加载

正向开发中,我们平时编写的程序的入口函数都是main.m里面的main函数,所以很多时候都会以为程序就是从这开始执行。其实main函数之前就有一系列的事情发生,比如+load方法与constructor构造函数就是在main函数之前执行的。

二、dyld、dyld_shared_cache简介

程序启动运行时会依赖很多系统动态库,而系统动态库会通过dyld(动态加载器)(/usr/lib/dyld)加载到内存中,最开始系统内核读取程序可执行文件的Header段信息做一些准备工作,之后就会将工作交给dyld。由于不止一个程序需要使用系统动态库,所以不可能在每个程序加载时都去加载所有的系统动态库,为了优化程序启动速度和利用动态库缓存,苹果从iOS3.1之后,将所有系统库(私有与公有)编译成一个大的缓存文件,这就是dyld_shared_cache,该缓存文件存在iOS系统下的/System/Library/Caches/com.apple.dyld/目录下

三、dyld加载流程

(一)、从新建Demo工程简单入手

创建一个新的iOS App工程,新建一个自定义类,并且在+load方法内下断点,同时也在main方法内下断点,运行工程,接着查看函数调用栈。

iOS应用程序启动之dyld加载流程(浅识)
+load断点
iOS应用程序启动之dyld加载流程(浅识)
main断点
iOS应用程序启动之dyld加载流程(浅识)
查看函数调用栈

从左侧函数调用栈可以看到首先调用的是dyld的__dyld_start函数,我们查看dyld源码(我是对比着433.5版本的dyld2以及635.2版本的dyld3看),搜索__dyld_start,可以在dyldStartup.s文件内找到__dyld_start的汇编实现。

iOS应用程序启动之dyld加载流程(浅识)
__dyld_start

往下查看,__dyld_start内部调用了dyldbootstrap::start()方法,然后再调用dyld的main函数

iOS应用程序启动之dyld加载流程(浅识)
调用dyld的main函数

转到dyld.cpp查看dyld的main函数,注意此main函数不是我们程序的main,而是dyld这个可执行文件的入口main函数,我们全局搜索_main,找到函数实现,如下:

iOS应用程序启动之dyld加载流程(浅识)
dyld的main实现

函数注释部分:dyld的入口,系统内核(XNU)初始化好寄存器后会加载dyld并且跳到__dyld_start函数并且调用该(main)函数

main函数内部大体做了以下操作:

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
    ......
    uintptr_t result = 0;
    //保存传入的可执行文件的头部(是一个struct macho_header结构体),后面根据头部访问信息
    sMainExecutableMachHeader = mainExecutableMH;
    ......
    //根据可执行文件头部,参数等设置上下文信息
    setContext(mainExecutableMH, argc, argv, envp, apple);

    // Pickup the pointer to the exec path.
    //获取可执行文件路径
    sExecPath = _simple_getenv(apple, "executable_path");

    // <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
    if (!sExecPath) sExecPath = apple[0];
    //将相对路径转换成绝对路径
    if ( sExecPath[0] != '/' ) {
        // have relative path, use cwd to make absolute
        char cwdbuff[MAXPATHLEN];
        if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
            // maybe use static buffer to avoid calling malloc so early...
            char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
            strcpy(s, cwdbuff);
            strcat(s, "/");
            strcat(s, sExecPath);
            sExecPath = s;
        }
    }

    // Remember short name of process for later logging
    //获取可执行文件的名字
    sExecShortName = ::strrchr(sExecPath, '/');
    if ( sExecShortName != NULL )
        ++sExecShortName;
    else
        sExecShortName = sExecPath;
    //配置进程是否受限
    configureProcessRestrictions(mainExecutableMH);
    ......
    {
        //检查设置环境变量
        checkEnvironmentVariables(envp);
        //如果DYLD_FALLBACK为nil,将其设置为默认值
        defaultUninitializedFallbackPaths(envp);
    }
    ......
    //如果设置了DYLD_PRINT_OPTS环境变量,则打印参数
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    //如果设置了DYLD_PRINT_ENV环境变量,则打印环境变量
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    //根据Mach-O头部获取当前运行架构信息
    getHostInfo(mainExecutableMH, mainExecutableSlide);

    // load shared cache
    //检查共享缓存是否开启,iOS中必须开启
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
#if TARGET_IPHONE_SIMULATOR
    // <HACK> until <rdar://30773711> is fixed
    gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
    // </HACK>
#endif
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
        //检查共享缓存是否映射到了共享区域
        mapSharedCache();
    }
    ......
    

    // instantiate ImageLoader for main executable
    //加载可执行文件并生成一个ImageLoader实例对象
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
    gLinkContext.mainExecutable = sMainExecutable;
    gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

    ......

        // Now that shared cache is loaded, setup an versioned dylib overrides
    #if SUPPORT_VERSIONED_PATHS
        //检查库的版本是否有更新,有则覆盖原有的
        checkVersionedPaths();
    #endif
    ......
        // load any inserted libraries
        //加载所有DYLD_INSERT_LIBRARIES指定的库
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
        // record count of inserted libraries so that a flat search will look at 
        // inserted libraries, then main, then others.
        sInsertedDylibCount = sAllImages.size()-1;

        // link main executable
        //链接主程序
        gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
        if ( mainExcutableAlreadyRebased ) {
            // previous link() on main executable has already adjusted its internal pointers for ASLR
            // work around that by rebasing by inverse amount
            sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
        }
#endif
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        sMainExecutable->setNeverUnloadRecursive();
        if ( sMainExecutable->forceFlat() ) {
            gLinkContext.bindFlat = true;
            gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
        }

        // link any inserted libraries
        //链接所有插入的动态库
        // do this after linking main executable so that any dylibs pulled in by inserted 
        // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
                image->setNeverUnloadRecursive();
            }
            // only INSERTED libraries can interpose
            // register interposing info after all inserted libraries are bound so chaining works
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                //注册符号插入
                image->registerInterposing(gLinkContext);
            }
        }

        // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
        for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
            ImageLoader* image = sAllImages[i];
            if ( image->inSharedCache() )
                continue;
            image->registerInterposing(gLinkContext);
        }
    #if SUPPORT_ACCELERATE_TABLES
        if ( (sAllCacheImagesProxy != NULL) && ImageLoader::haveInterposingTuples() ) {
            // Accelerator tables cannot be used with implicit interposing, so relaunch with accelerator tables disabled
            ImageLoader::clearInterposingTuples();
            // unmap all loaded dylibs (but not main executable)
            for (long i=1; i < sAllImages.size(); ++i) {
                ImageLoader* image = sAllImages[i];
                if ( image == sMainExecutable )
                    continue;
                if ( image == sAllCacheImagesProxy )
                    continue;
                image->setCanUnload();
                ImageLoader::deleteImage(image);
            }
            // note: we don't need to worry about inserted images because if DYLD_INSERT_LIBRARIES was set we would not be using the accelerator table
            sAllImages.clear();
            sImageRoots.clear();
            sImageFilesNeedingTermination.clear();
            sImageFilesNeedingDOFUnregistration.clear();
            sAddImageCallbacks.clear();
            sRemoveImageCallbacks.clear();
            sAddLoadImageCallbacks.clear();
            sDisableAcceleratorTables = true;
            sAllCacheImagesProxy = NULL;
            sMappedRangesStart = NULL;
            mainExcutableAlreadyRebased = true;
            gLinkContext.linkingMainExecutable = false;
            resetAllImages();
            goto reloadAllImages;
        }
    #endif

        // apply interposing to initial set of images
        for(int i=0; i < sImageRoots.size(); ++i) {
            //应用符号插入
            sImageRoots[i]->applyInterposing(gLinkContext);
        }
        ImageLoader::applyInterposingToDyldCache(gLinkContext);
        gLinkContext.linkingMainExecutable = false;

        // Bind and notify for the main executable now that interposing has been registered
        uint64_t bindMainExecutableStartTime = mach_absolute_time();
        sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
        uint64_t bindMainExecutableEndTime = mach_absolute_time();
        ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
        gLinkContext.notifyBatch(dyld_image_state_bound, false);

        // Bind and notify for the inserted images now interposing has been registered
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
            }
        }
        
        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        //弱符号绑定
        sMainExecutable->weakBind(gLinkContext);
        ......
#if SUPPORT_OLD_CRT_INITIALIZATION
        // Old way is to run initializers via a callback from crt1.o
        if ( ! gRunInitializersOldWay ) 
            initializeMainExecutable(); 
    #else
        // run all initializers
        //执行初始化方法
        initializeMainExecutable(); 
    #endif
        // notify any montoring proccesses that this process is about to enter main()
        if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
            dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
        }
        notifyMonitoringDyldMain();

        // find entry point for main executable
        //寻找目标可执行文件入口并执行
        result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
        if ( result != 0 ) {
            // main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
            if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
                *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
            else
                halt("libdyld.dylib support not present for LC_MAIN");
        }
        else {
            // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
            result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
            *startGlue = 0;
        }
#if __has_feature(ptrauth_calls)
        // start() calls the result pointer as a function pointer so we need to sign it.
        result = (uintptr_t)__builtin_ptrauth_sign_unauthenticated((void*)result, 0, 0);
#endif
    }
    catch(const char* message) {
        syncAllImages();
        halt(message);
    }
    catch(...) {
        dyld::log("dyld: launch failedn");
    }

    CRSetCrashLogMessage("dyld2 mode");

    if (sSkipMain) {
        if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
            dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
        }
        result = (uintptr_t)&fake_main;
        *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
    }
    
    return result;
}

将dyld的_main函数内部流程拆分,大概有以下:

  • 1. 设置上下文信息,配置进程是否受限
  • 2. 配置环境变量,获取当前运行架构
  • 3. 检查共享缓存是否映射到共享区域
  • 4. 加载可执行文件,生成ImageLoader实例对象
  • 5. 加载所有插入的库
  • 6. 链接主程序
  • 7. 链接所有插入的库,执行符号替换
  • 8. 执行初始化方法
  • 9. 寻找主程序入口

(二)、分步认识加载流程

1.设置上下文信息,配置进程是否受限

调用setContext,传入Mach-O头部,以及_main的一些参数,设置上下文。接着调用configureProcessRestrictions,跟进查看,主要看iOS平台的一段,将EncVarMode环境变量类型的Mode设置为不同(默认是envNone(受限模式,忽略环境变量)),当设置了get_task_allow权限以及开发内核时会将sEnvMode设置为envAll,但只要将get_task_allow设置了uid或gid,sEnvMode就会设置为受限模式。dyld3下该段的实现代码有了变化,暂时没有具体学习研究。

 uint32_t flags;
#if TARGET_IPHONE_SIMULATOR
    sEnvMode = envAll;
    gLinkContext.requireCodeSignature = true;
#elif __IPHONE_OS_VERSION_MIN_REQUIRED
    sEnvMode = envNone;
    gLinkContext.requireCodeSignature = true;
    if ( csops(0, CS_OPS_STATUS, &flags, sizeof(flags)) != -1 ) {
        if ( flags & CS_ENFORCEMENT ) {
            if ( flags & CS_GET_TASK_ALLOW ) {
                // Xcode built app for Debug allowed to use DYLD_* variables
                sEnvMode = envAll;
            }
            else {
                // Development kernel can use DYLD_PRINT_* variables on any FairPlay encrypted app
                uint32_t secureValue = 0;
                size_t   secureValueSize = sizeof(secureValue);
                if ( (sysctlbyname("kern.secure_kernel", &secureValue, &secureValueSize, NULL, 0) == 0) && (secureValue == 0) && isFairPlayEncrypted(mainExecutableMH) ) {
                    sEnvMode = envPrintOnly;
                }
            }
        }
        else {
            // Development kernel can run unsigned code
            sEnvMode = envAll;
            gLinkContext.requireCodeSignature = false;
        }
    }
    if ( issetugid() ) {
        sEnvMode = envNone;
    }
2.配置环境变量,获取当前运行架构

调用checkEnvironmentVariables,如果allowEnvVarsPathallowEnvVarsPrint为空,直接跳过,否则调用processDyldEnvironmentVariable处理并设置环境变量,如下:

static void checkEnvironmentVariables(const char* envp[])
{
    if ( !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsPrint )
        return;
    const char** p;
    for(p = envp; *p != NULL; p++) {
        const char* keyEqualsValue = *p;
        if ( strncmp(keyEqualsValue, "DYLD_", 5) == 0 ) {
            const char* equals = strchr(keyEqualsValue, '=');
            if ( equals != NULL ) {
                strlcat(sLoadingCrashMessage, "n", sizeof(sLoadingCrashMessage));
                strlcat(sLoadingCrashMessage, keyEqualsValue, sizeof(sLoadingCrashMessage));
                const char* value = &equals[1];
                const size_t keyLen = equals-keyEqualsValue;
                char key[keyLen+1];
                strncpy(key, keyEqualsValue, keyLen);
                key[keyLen] = '';
                if ( (strncmp(key, "DYLD_PRINT_", 11) == 0) && !gLinkContext.allowEnvVarsPrint )
                    continue;
                processDyldEnvironmentVariable(key, value, NULL);
            }
        }
        else if ( strncmp(keyEqualsValue, "LD_LIBRARY_PATH=", 16) == 0 ) {
            const char* path = &keyEqualsValue[16];
            sEnv.LD_LIBRARY_PATH = parseColonList(path, NULL);
        }
    }

#if SUPPORT_LC_DYLD_ENVIRONMENT
    checkLoadCommandEnvironmentVariables();
#endif // SUPPORT_LC_DYLD_ENVIRONMENT   
    
#if SUPPORT_ROOT_PATH
    // <rdar://problem/11281064> DYLD_IMAGE_SUFFIX and DYLD_ROOT_PATH cannot be used together
    if ( (gLinkContext.imageSuffix != NULL && *gLinkContext.imageSuffix != NULL) && (gLinkContext.rootPaths != NULL) ) {
        dyld::warn("Ignoring DYLD_IMAGE_SUFFIX because DYLD_ROOT_PATH is used.n");
        gLinkContext.imageSuffix = NULL; // this leaks allocations from parseColonList
    }
#endif
}

返回_main函数,往下一点查看,该段主要做的是,如果设置了这两个环境变量参数,则在App启动时,打印相关参数、环境变量信息。

    //如果设置了DYLD_PRINT_OPTS环境变量,则打印参数
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    //如果设置了DYLD_PRINT_ENV环境变量,则打印环境变量
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);

我在前面的Demo工程下加入这两个参数,运行打印了许多信息,其中包括沙盒目录,DYLD_INSERT_LIBRARIES、进程状态空间等,结果如下:

iOS应用程序启动之dyld加载流程(浅识)
添加参数
iOS应用程序启动之dyld加载流程(浅识)
启动输出参数

继续返回_main函数,查看getHostInfo调用,这步主要是从Mach-O头部获取当前运行架构的信息,如下:

static void getHostInfo(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if CPU_SUBTYPES_SUPPORTED
#if __ARM_ARCH_7K__
    sHostCPU        = CPU_TYPE_ARM;
    sHostCPUsubtype = CPU_SUBTYPE_ARM_V7K;
#elif __ARM_ARCH_7A__
    sHostCPU        = CPU_TYPE_ARM;
    sHostCPUsubtype = CPU_SUBTYPE_ARM_V7;
#elif __ARM_ARCH_6K__
    sHostCPU        = CPU_TYPE_ARM;
    sHostCPUsubtype = CPU_SUBTYPE_ARM_V6;
#elif __ARM_ARCH_7F__
    sHostCPU        = CPU_TYPE_ARM;
    sHostCPUsubtype = CPU_SUBTYPE_ARM_V7F;
#elif __ARM_ARCH_7S__
    sHostCPU        = CPU_TYPE_ARM;
    sHostCPUsubtype = CPU_SUBTYPE_ARM_V7S;
#elif __ARM64_ARCH_8_32__
    sHostCPU        = CPU_TYPE_ARM64_32;
    sHostCPUsubtype = CPU_SUBTYPE_ARM64_32_V8;
#elif __arm64e__
    sHostCPU        = CPU_TYPE_ARM64;
    sHostCPUsubtype = CPU_SUBTYPE_ARM64_E;
#elif __arm64__
    sHostCPU        = CPU_TYPE_ARM64;
    sHostCPUsubtype = CPU_SUBTYPE_ARM64_V8;
#else
    struct host_basic_info info;
    mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
    mach_port_t hostPort = mach_host_self();
    kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count);
    if ( result != KERN_SUCCESS )
        throw "host_info() failed";
    sHostCPU        = info.cpu_type;
    sHostCPUsubtype = info.cpu_subtype;
    mach_port_deallocate(mach_task_self(), hostPort);
  #if __x86_64__
      // host_info returns CPU_TYPE_I386 even for x86_64.  Override that here so that
      // we don't need to mask the cpu type later.
      sHostCPU = CPU_TYPE_X86_64;
    #if !TARGET_IPHONE_SIMULATOR
      sHaswell = (sHostCPUsubtype == CPU_SUBTYPE_X86_64_H);
      // <rdar://problem/18528074> x86_64h: Fall back to the x86_64 slice if an app requires GC.
      if ( sHaswell ) {
        if ( isGCProgram(mainExecutableMH, mainExecutableSlide) ) {
            // When running a GC program on a haswell machine, don't use and 'h slices
            sHostCPUsubtype = CPU_SUBTYPE_X86_64_ALL;
            sHaswell = false;
            gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
        }
      }
    #endif
  #endif
#endif
#endif
}
3. 检查共享缓存是否映射到共享区域

首先调用checkSharedRegionDisable检查是否开启共享缓存,在iOS中是必须开启的,接着调用mapSharedCache将共享缓存映射到共享区域,在dyld2源码中mapSharedCache内部先通过shared_region_check_np检查缓存是否已经映射,是则更新sharedCacheSlide和sharedCacheUUID,否则调用openSharedCacheFile打开共享缓存文件(/System/Library/Caches/com.apple.dyld/dyld_shared_cache_x),最后使用shared_region_map_and_slide_up完成映射,代码很多,就不贴出了。在dyld3中该mapSharedCache变得很简短,应该是做了优化。

4. 加载可执行文件,生成ImageLoader实例对象

跳到ImageLoader定义处ImageLoader.h,从它的注释可以看出,它是一个抽象基类,专门用于辅助加载特定可执行文件格式的类,对于程序中需要的依赖库、插入库,会创建一个对应的image对象,对这些image进行链接,调用各image的初始化方法等等,包括对runtime的初始化。

iOS应用程序启动之dyld加载流程(浅识)
ImageLoader

instantiateFromLoadedImage实例化一个ImageLoader对象,内部先判断文件架构是否与当前设备架构兼容,接着调用ImageLoaderMachO::instantiateMainExecutable加载文件生成实例,不断添加image。ImageLoaderMachO::instantiateMainExecutable内部会判断Mach-O是否压缩来使用不同的ImageLoader子类进行初始化。

5. 加载所有插入的库

从上一步Imageloader加载的代码接着往下查看,会发现

if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }

该段的作用是遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载,通过该环境变量我们可以注入自定义的一些动态库代码loadInsertedDylib内部会从DYLD_ROOT_PATH、LD_LIBRARY_PATH、DYLD_FRAMEWORK_PATH等路径查找dylib并且检查代码签名,无效则直接抛出异常。

6. 链接主程序

内核调用ImageLoader::link函数,内部调用recursiveLoadLibraries递归加载动态库,加载动态库后,对依赖库进行排序,被依赖的排序在前面,接着调用recursiveRebase,rebase就是针对 “mach-o在加载到内存中不是固定的首地址” (苹果的ASLR地址空间随机化)这一现象做数据修正的过程。接下来调用recursiveBindWithAccounting递归绑定符号表。绑定就是将这个二进制调用的外部符号进行绑定的过程。 比如我们objc代码中需要使用到NSObject, 即符号OBJC_CLASS$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起。lazyBinding就是在加载动态库的时候不会立即binding, 当第一次调用这个方法的时候再实施binding。 做到的方法也很简单: 通过dyld_stub_binder 这个符号来做。 lazy binding的方法第一次会调用到dyld_stub_binder, 然后dyld_stub_binder负责找到真实的方法,并且将地址bind到桩上,下一次就不用再bind了。

7. 链接所有插入的库,执行符号替换

对sAllimages内所有加载好的Image(除了主程序的Image外)中的库调用link进行链接,然后调用registerInterposing注册符号替换。

        // link any inserted libraries
        // do this after linking main executable so that any dylibs pulled in by inserted 
        // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
                image->setNeverUnloadRecursive();
            }
            // only INSERTED libraries can interpose
            // register interposing info after all inserted libraries are bound so chaining works
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                image->registerInterposing(gLinkContext);
            }
        }
8. 执行初始化方法
// run all initializers
initializeMainExecutable(); 

initializeMainExecutable执行初始化方法,其中+load和constructor方法就是在这里执行。initializeMainExecutable内部先调用了动态库的初始化方法,后调用主程序的初始化方法。在Imageloader::recursiveInitialization里面调用了如下内容:

context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

全局搜索static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)找到如下代码段:

if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
        uint64_t t0 = mach_absolute_time();
        dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
        (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
        uint64_t t1 = mach_absolute_time();
        uint64_t t2 = mach_absolute_time();
        uint64_t timeInObjC = t1-t0;
        uint64_t emptyTime = (t2-t1)*100;
        if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
            timingInfo->addTime(image->getShortName(), timeInObjC);
        }
    }

此处调用了sNotifyObjCInit(从名称可以知道大概是通知runtime的意思(ObjCInit)),而sNotifyObjCInit是在此处赋值:

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
    // record functions to call
    sNotifyObjCMapped   = mapped;
    sNotifyObjCInit     = init;
    sNotifyObjCUnmapped = unmapped;

    // call 'mapped' function with all images mapped so far
    try {
        notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
    }
    catch (const char* msg) {
        // ignore request to abort during registration
    }

    // <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
    for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
        ImageLoader* image = *it;
        if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
            dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
            (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
        }
    }
}
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}

查看函数定义:

//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.  During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images.  During any later dlopen() call,
// dyld will also call the "mapped" function.  Dyld will call the "init" function when dyld would be called
// initializers in that image.  This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);

_dyld_objc_notify_register函数是是供objc runtime调用的,可以在objc4源码中的_objc_init中找到记录:

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

这几步操作实际上是sNotifyObjCInit调用就是objc中的load_images,而后者会调用所有的+load方法,我们回到新建工程的界面查看函数调用栈,也可以发现确实是这样的调用顺序:

iOS应用程序启动之dyld加载流程(浅识)
函数调用栈

调用context.notifySingle之后,会调用ImageLoaderMachO::doInitialization,内部调用doImageInitImageLoaderMachO::doModInitFunctions,其中ImageLoaderMachO::doModInitFunctions内部调用__mod_init_funcs section,也就是constructor方法

9. 寻找主程序入口

差不多到了_main的末尾,调用getEntryFromLC_MAIN读取Mach-O的LC_MAIN段获取程序的入口地址,也就是我们的main函数入口地址。

四、小结

写到这里差不多已经乱掉,dyld加载过程真是非常复杂,这是自己学习过程的一次简陋笔记,很短时间内码出来,自己也觉得写得不太好,如果日后遇到回来再看看能否改良,如有出错,有请高手指出赐教!最后用一张图简单总结一下流程吧:

iOS应用程序启动之dyld加载流程(浅识)
小结图

五、参考

iOS程序启动->dyld加载->runtime初始化(初识)
DYLD加载Mach-O完整流程
iOS 程序 main 函数之前发生了什么
dylib动态库加载过程分析
dyld加载Mach-O
dyld与ObjC
《iOS应用逆向与安全》–刘培庆

使用Charles抓取iTunes应用商店旧版ipa包

准备

iTunes 12.6.3 (Mac os 10.14 Mojave用不了12.6.3)
Charles 4.2.7 (解压密码:xclient.info)
资源传送门(提取码:p67f)

如果系统安装了高版本iTunes,可以参考Mac 卸载iTunes,安装旧版本iTunes运行的时候会提示iTunes Libraray.itl由高版本创建,可以按住”Option”键双击iTunes,创建资料库。

安装&配置Charles

1.运行Charles后,先安装证书

使用Charles抓取iTunes应用商店旧版ipa包
安装证书

2.信任证书

使用Charles抓取iTunes应用商店旧版ipa包
信任证书

3.重新打开Charles,开启Mac OS全局代理

使用Charles抓取iTunes应用商店旧版ipa包
开启全局代理

开始抓包

  1. 打开iTunes,搜索微信(随便试一下,想抓什么包看自己需求),点击下载
  2. 回到Charles,查看类似https://p2-buy.itunes.apple.com字眼的请求,对其下断点,并且Enable SSL Proxying
    使用Charles抓取iTunes应用商店旧版ipa包
    断点请求、开启SSL代理
  3. 回到iTunes资料库中删除下载的微信,并重新搜索,再次点击下载
  4. Charles会对请求断点,点击两次Execute

    使用Charles抓取iTunes应用商店旧版ipa包
    继续请求
  5. 在新的https://p2-buy.itunes.apple.com 中查找我们需要的版本ID,具体如下图
    使用Charles抓取iTunes应用商店旧版ipa包
    获取历史版本ID
  6. 这里我选择第一个版本ID,也就是3328911(对应微信应该是1.0),版本ID查询
  7. 本次完成后Charles可能会断住,直接点击Execute,然后重复第3步(回到iTunes资料库中删除下载的微信,并重新搜索,再次点击下载),这个时候Charles又会断住,就在这里修改我们想要的版本号,具体如下图
    使用Charles抓取iTunes应用商店旧版ipa包
    修改成需要下载的版本ID

    PS:修改完成之后一直点Execute让请求继续,iTunes会自动开始下载

8.post一下下载的微信1.0

使用Charles抓取iTunes应用商店旧版ipa包
效果

后来又测试了一下,其实是可以先用网页查询自己所需的版本ID,然后在第二次下载请求下载断点处修改Request也可以达到效果,也就是说可以省去从请求数据中查找版本ID的步骤。

fishhook简单使用&符号查找过程&源码分析

一、hook定义

hook:改变程序执行流程的一种技术统称。

二、fishhook简介

它是Facebook提供的一个动态修改链接mach-O文件的工具。利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针达到C函数HOOK的目的。

fishhook

三、使用fishhook修改系统C函数实现

以NSLog函数为例

1.利用一个函数指针保存原NSLog函数的地址
static void (*sys_nslog)(NSString *format,...);

2.替换函数,效果就是调用NSLog时,先执行我们的替换函数,一般都会手动调用父类或原先的实现(根据需求),通过上一步指针的记录来调用。

void myNSLog(NSString *format, ...) {
    format = [format stringByAppendingString:@"hook成功!"];
    sys_nslog(format);
    sys_nslog(@"%s",__func__);
}

3.重新绑定符号

    struct rebinding nslog;
    nslog.name = "NSLog";
    nslog.replacement = myNSLog;
    nslog.replaced = (void *)&sys_nslog;
    
    struct rebinding rebs[1] = {nslog};
    /**
     重新绑定符号

     @param rebindings#> 存放rebingding结构体的数组 description#>
     @param rebindings_nel#> 数组的长度 description#>
     @return return value description
     */
    rebind_symbols(rebs, 1);

Demo


四、符号查找过程梳理

引用fishhook给出的流程图:

fishhook简单使用&amp;符号查找过程&amp;源码分析
符号查找流程

下面以查找printf函数为例

分析用到的Mach-O文件:Mach-O File

1.Find entry with same index in indirect symbol table

第一阶段,从__DATA段的__la_symbol_ptr节开始,寻找索引(用于Dynamic Symbol Table下的Indirect Symbols)。也就是说两个表的索引是一样的printf函数在Lazy Symbol Pointers下为第44条记录,对应在Indirect Symbols下也为第44条记录。

fishhook简单使用&amp;符号查找过程&amp;源码分析
Lazy Symbol Pointers
fishhook简单使用&amp;符号查找过程&amp;源码分析
Indirect Symbols

这里自己想了一下,可以用公式计算:

  • printf在Lazy Symbol Pointers的Offset :0x241D8
  • Lazy Symbol Pointers起始Offset : 0x24080
  • Lazy Symbol Pointers两条记录之间相差: 0x8
  • 因此printf索引:(0x241D8 – 0x24080) / 0x8 = 0x2B = 43 (索引从0开始)
  • 切换到Indirect Symbols计算:0x2B * 0x4(两条记录之间相差) + 0x2A4E0(起始Offset) = 0X2A58C(对应Indirect Symbols下的符号__printf)

2.Treat value as index into symbol table array

第二阶段,将Indirect Symbols下的__printf符号对应的Data值换算成10进制,此处为将0x1A5转成10进制的421,跳到Symbols Table下的Symbols,找到第421条记录,对应着__printf,如下:

fishhook简单使用&amp;符号查找过程&amp;源码分析
Symbols

3.Look up string table entry by adding offset from symbol table entry to string table base

第三阶段,将symbol table的符号偏移值(Data段)加上String Table的基址,此处为0x577 + 0x2A6C0 = 0x2AC37,如下:

fishhook简单使用&amp;符号查找过程&amp;源码分析
偏移值
fishhook简单使用&amp;符号查找过程&amp;源码分析
String Table 基址
fishhook简单使用&amp;符号查找过程&amp;源码分析
符号位置处

以上

五、fishhook源码分析

从我们常用的符号重绑定函数rebind_symbols入口开始,prepend_rebindings函数内部分配内存空间将需要重新绑定符号的结构体初始化成一条struct rebindings_entry结构体的链表,通过retval检查是否传入结构体。如果是第一次调用,就注册image添加的回调,否则遍历所有加载的image(模块),如下:

int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
  int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
  if (retval < 0) {
    return retval;
  }
  // If this was the first call, register callback for image additions (which is also invoked for
  // existing images, otherwise, just run on existing images
  if (!_rebindings_head->next) {
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    uint32_t c = _dyld_image_count();
    for (uint32_t i = 0; i < c; i++) {
      _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
    }
  }
  return retval;
}

最后,都会调用rebind_symbols_for_image函数对某个模块中的符号表指针进行替换。rebind_symbols_for_image内部获取__LINKEDIT、符号表、间接跳转表、字符串表在内存中的真实位置,然后调用perform_rebingding_with_section分别对懒加载表和非懒加载表进行替换,如下:

static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
  Dl_info info;
  if (dladdr(header, &info) == 0) {
    return;
  }

  segment_command_t *cur_seg_cmd;
  segment_command_t *linkedit_segment = NULL;
  struct symtab_command* symtab_cmd = NULL;
  struct dysymtab_command* dysymtab_cmd = NULL;

  uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    //获取__LINKEDIT
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
      if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
        linkedit_segment = cur_seg_cmd;
      }
    //获取符号表
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
      symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    //获取动态符号表
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
      dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
  }

  if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
      !dysymtab_cmd->nindirectsyms) {
    return;
  }

  // Find base symbol/string table addresses
  //就是获取machoheader
  uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
  //符号表在内存的位置
  nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  //字符串表在内存的位置
  char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);

  //动态符号表间接跳转表在内存的位置
  // Get indirect symbol table (array of uint32_t indices into symbol table)
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

  cur = (uintptr_t)header + sizeof(mach_header_t);
  //遍历Load Commands下的每个加载指令
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
      if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
          strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
        continue;
      }
      //分别绑定懒加载和非懒加载表(__DATA段)
      for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
        section_t *sect =
          (section_t *)(cur + sizeof(segment_command_t)) + j;
        if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
        if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
      }
    }
  }
}

关键函数perform_rebinding_with_section,参数传进了结构体数组,内存中节、ASLR、符号表、字符串表、间接符号表的地址,主要工作如下:

  • 1.获取懒加载表或非懒加载表在间接符号表中的位置
  • 2.遍历section,找到对应位置在间接跳转表中所对应的符号表下标
  • 3.根据符号表的下标,获取符号表中对应的符号字符串
  • 4.遍历rebindings链表,判断符号是否匹配或是否已被替换,如果没有,则进行替换操作
    如下:
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
                                           section_t *section,
                                           intptr_t slide,
                                           nlist_t *symtab,
                                           char *strtab,
                                           uint32_t *indirect_symtab) {
  //在间接跳转符号表中的偏移
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
  //找到对应的section
  void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
  for (uint i = 0; i < section->size / sizeof(void *); i++) {
    //从间接符号表中获取在符号表中的索引
    uint32_t symtab_index = indirect_symbol_indices[i];
    if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
        symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
      continue;
    }
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
    char *symbol_name = strtab + strtab_offset;
    printf("%sn",symbol_name);
    if (strnlen(symbol_name, 2) < 2) {
      continue;
    }
    struct rebindings_entry *cur = rebindings;
    while (cur) {
      for (uint j = 0; j < cur->rebindings_nel; j++) {
        if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
          if (cur->rebindings[j].replaced != NULL &&
              indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
            *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
          }
          indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
          goto symbol_loop;
        }
      }
      cur = cur->next;
    }
  symbol_loop:;
  }
}

六、小结

了解fishhook的原理同时以及联想到dyld的加载流程可以加深对Mach-O文件的认识,,符号查找过程主要跟那几个表有关系,分别是Lazy Symbol PointersIndirect SymbolsSymbolsString Table。笔记过程大部分是个人理解,可能有些地方可能写得不太清晰,有错误请指出。

七、参考

fishhook源码分析
《iOS应用逆向与安全》