Mach-O文件和Facebook的fishhook

1. 概述

我们知道Windows下的文件都是PE文件,同样在OS X和iOS中可执行文件是Mach-o格式的。

Mach-O通常有三部分组成:

  • 头部 (Header): Mach-O文件的架构 比如Mac的 PPC, PPC64, IA-32, x86-64,ios的arm系列.
  • 加载命令(Load commands): .
  • 原始段数据(Raw segment data):可以拥有多个段(segment),每个段可以拥有零个或多个区域(section)。每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间。
    官方给的图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bJUmrbn2-1579222855806)(/20181228135737477_718594401.png)]

Xcode本身包含了command-line tools,命令行工具本身包含了分析和编译Mach-O,相关如下:

  • lipo /usr/bin/lipo 能够分析二进制文件的架构,可以拆分和合并二进制文件。比如查看一个静态库的架构,可以使用lipo -info lib1.a
  • otool /usr/bin/otool 列出Mach-O文件的sections和segments信息,具体使用可以参考otool --help
  • pagestuff /usr/bin/pagestuff 展示每一个组成反射(image)的每个逻辑页面的内容,其中包含了sections的名字和每个page里的符号。这个工具不能在有多个架构的包含映射的二进制文件中运行。
  • symbol table的展示工具,/usr/bin/nm,允许你查看对象文件符号表的内容

2. Mach-O组成

通常一个iOS App应用会安装在/var/mobile/Applications,系统的原生App会安装在/Applications目录下,大部分情况下,xxx.app/xxx文件并不是Mach-O格式文件,由于现在需要支持不同CPU架构的iOS设备,所以我们编译打包出来的执行文件是一个Universal Binary格式文件(通用二进制文件,也称胖二进制文件),实际上Universal Binary只不过将支持不同架构的Mach-O打包在一起,再在文件起始位置加上Fat Header来说明所包含的Mach-O文件支持的架构和偏移地址信息。

例如携程app:

file CTRIP_WIRELESS
CTRIP_WIRELESS: Mach-O universal binary with 2 architectures
CTRIP_WIRELESS (for architecture i386): Mach-O executable i386
CTRIP_WIRELESS (for architecture x86_64):   Mach-O 64-bit executable x86_64

上面显示程序支持i386和x86_64架构,胖二进制文件定义在 /usr/include/mach-o/fat.h,我们查看一下源码:

#ifndef _MACH_O_FAT_H_
#define _MACH_O_FAT_H_

#include <stdint.h>
#include <mach/machine.h>
#include <architecture/byte_order.h>

//magic数字,例如通常判断png文件格式,还有快速求平方根的0x5f3759df(https://zh.wikipedia.org/wiki/%E5%B9%B3%E6%96%B9%E6%A0%B9%E5%80%92%E6%95%B0%E9%80%9F%E7%AE%97%E6%B3%95)
#define FAT_MAGIC   0xcafebabe
#define FAT_CIGAM   0xbebafeca  /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC or FAT_MAGIC_64 */
    uint32_t    nfat_arch;  /* number of structs that follow */
};

struct fat_arch {
    cpu_type_t  cputype;    /* cpu specifier (int) */
    cpu_subtype_t   cpusubtype; /* machine specifier (int) */
    uint32_t    offset;     /* file offset to this object file */
    uint32_t    size;       /* size of this object file */
    uint32_t    align;      /* alignment as a power of 2 */
};

/*
 * The support for the 64-bit fat file format described here is a work in
 * progress and not yet fully supported in all the Apple Developer Tools.
 *
 * When a slice is greater than 4mb or an offset to a slice is greater than 4mb
 * then the 64-bit fat file format is used.
 */
#define FAT_MAGIC_64    0xcafebabf
#define FAT_CIGAM_64    0xbfbafeca  /* NXSwapLong(FAT_MAGIC_64) */

struct fat_arch_64 {
    cpu_type_t  cputype;    /* cpu specifier (int) */
    cpu_subtype_t   cpusubtype; /* machine specifier (int) */
    uint64_t    offset;     /* file offset to this object file */
    uint64_t    size;       /* size of this object file */
    uint32_t    align;      /* alignment as a power of 2 */
    uint32_t    reserved;   /* reserved */
};

#endif /* _MACH_O_FAT_H_ */

结构体struct fat_header:

  • magic字段就是我们常说的魔数(例如通常判断png文件格式,还有快速求平方根的0x5f3759df),加载器通过这个魔数值来判断这是什么样的文件,胖二进制文件的魔数值是0xcafebabe;
  • nfat_arch字段是指当前的胖二进制文件包含了多少个不同架构的Mach-O文件;
    fat_header后会跟着fat_arch,有多少个不同架构的Mach-O文件,就有多少个fat_arch,用于说明对应Mach-O文件大小、支持的CPU架构、偏移地址等;

2.1. 头部(Header)

我们可以使用 otool(1) 来观察可执行文件的头部 – 规定了这个文件是什么,以及文件是如何被加载的。通过 -h 可以打印出头信息:
例如使用otool命令可以查看Mach-O文件的头信息,头信息就是Mach-O文件的第一部分,我们在第一部分介绍Mach-o概述已经介绍。

otool -v -h FishHookDemo

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EwHfiOdw-1579222855807)(/20181228143237049_2115550526.png)]

头信息的结构可以在 /usr/include/mach-o/loader.h中找到:

/*
 * 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 */
};

我们依次介绍这个头信息:

  1. magic,可以看到文件中的内容最开始部分,是以 cafe babe开头的
    对于一个 二进制文件 来讲,每个类型都可以在文件最初几个字节来标识出来,即“魔数”。不同类型的 二进制文件,都有自己独特的"魔数"。
    OS X上,可执行文件的标识有这样几个魔数(不同的魔数代表不同的可执行文件类型)
    是mach-o文件的魔数,0xfeedface代表的是32位,0xfeedfacf代表64位,cafebabe是跨处理器架构的通用格式,#!代表的是脚本文件。
  2. cputype和cupsubtype代表的是cpu的类型和其子类型,图上的例子是模拟器程序,cpu结构是x86_64,如果直接查看ipa,可以看到cpu是arm,subtype是armv7,arm64等
  3. 接着是filetype,2,代表可执行的文件 #define MH_EXECUTE 0×2
  4. ncmds 指的是加载命令(load commands)的数量,例子中一共65个,编号0-64
  5. sizeofcmds 表示23个load commands的总字节大小, load commands区域是紧接着header区域的。
  6. 最后个flags,例子中是0×00200085,可以按文档分析之。
    也可以借助UE程序MachOView,MachOView是Mac上查看Mach-O结构的工具,如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fg3iwMkb-1579222855808)(/20181228145747890_170947103.png)]

2.2. 加载命令(Load commands)

load commmand直接跟在 header 部分的后面,结构定义如下

struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

这些加载命令在Mach-O文件加载解析时,被内核加载器或者动态链接器调用,指导如何设置加载对应的二进制数据段,加载命令的种类有很多种,在<mach-o/loader.h>头文件有简单的注释。
具体可以使用命令进行查看:

otool -v -l FishHookDemo | open -f

2.3. 段数据(Segments)

Segments包含了很多segment,每一个segment定义了一些Mach-O文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了虚拟内存中。每个段都有不同的功能,一般包括:

  1. __PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;
  2. __TEXT: 包含了执行代码以及其他只读数据。 为了让内核将它 直接从可执行文件映射到共享内存, 静态连接器设置该段的虚拟内存权限为不允许写。当这个段被映射到内存后,可以被所有进程共享。(这主要用在frameworks, bundles和共享库等程序中,也可以为同一个可执行文件的多个进程拷贝使用)
  3. __DATA: 包含了程序数据,该段可写;
  4. __OBJC: Objective-C运行时支持库;
  5. __LINKEDIT: 含有为动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等。

一般的段又会按不同的功能划分为几个区(section),即段所有字母大小,加两个下横线作为前缀,而区则为小写,同样加两个下横线作为前缀

下面列出段中可能包含的section:

__TEXT段:
__text, __cstring, __picsymbol_stub, __symbol_stub, __const, __litera14, __litera18;

__DATA段:

__data, __la_symbol_ptr, __nl_symbol_ptr, __dyld, __const, __mod_init_func, __mod_term_func, __bss, __commom;

__IMPORT
__jump_table, __pointers;

其中__TEXT段中的__text是实际上的代码部分;__DATA段的__data是实际的初始数据,更加详细的说明见这里
可以通过otool –s查看某segment的某个section。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gso6j6BT-1579222855809)(/20181228151722512_1922324082.png)]

由于 -s __TEXT __text 很常见,otool 对其设置了一个缩写 -t 。我们还可以通过添加 -v 来查看反汇编代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8hGQZSfZ-1579222855809)(/20181228151846668_1756860706.png)]

3. Mach-O作用

3.1. Xcode中配置LinkMap

LinkMap文件是Xcode产生可执行文件(Mach-O)的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置,如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sgiXKxzo-1579222855810)(/20181228152151635_1805223139.png)]

编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File
位于~/Library/Developer/Xcode/DerivedData/FishHookDemo-fhqbawdslnrnlzhditxvcntioyad/Build/Intermediates.noindex/FishHookDemo.build/Debug-iphoneos/FishHookDemo.build/FishHookDemo-LinkMap-normal-arm64.txt

LinkMap里展示了整个可执行文件的全貌,分为三段,分别是:

  • 以# Object files:为分割标志,列出所有.o目标文件的信息(包括静态链接库.a里的),
  • 以# Sections:为分割标志,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值),字段的含义在Mach-o中已详细介绍。
  • 以# Symbols:为分割标志,列出具体的按每个文件列出每个对应字段的位置和占用空间,例如:
# 2. Symbols:
# 3. Address    Size        File  Name
0x100005D4C 0x00000054  [  1] -[ViewController viewDidLoad]
0x100005DA0 0x00000048  [  1] _rebind_one_method
0x100005DE8 0x00000088  [  1] _myLog
0x100005E70 0x0000008C  [  1] -[ViewController touchesBegan:withEvent:]
0x100005EFC 0x000000A4  [  2] _main
0x100005FA0 0x000000E0  [  3] _prepend_rebindings
0x100006080 0x000002F8  [  3] _rebind_symbols_for_image
0x100006378 0x000000DC  [  3] _rebind_symbols
0x100006454 0x00000038  [  3] __rebind_symbols_for_image
0x10000648C 0x000002AC  [  3] _perform_rebinding_with_section
0x100006738 0x00000088  [  4] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x1000067C0 0x0000004C  [  4] -[AppDelegate applicationWillResignActive:]
0x10000680C 0x0000004C  [  4] -[AppDelegate applicationDidEnterBackground:]
0x100006858 0x0000004C  [  4] -[AppDelegate applicationWillEnterForeground:]
0x1000068A4 0x0000004C  [  4] -[AppDelegate applicationDidBecomeActive:]
0x1000068F0 0x0000004C  [  4] -[AppDelegate applicationWillTerminate:]
0x10000693C 0x0000002C  [  4] -[AppDelegate window]
0x100006968 0x0000004C  [  4] -[AppDelegate setWindow:]
0x1000069B4 0x00000044  [  4] -[AppDelegate .cxx_destruct]
0x1000069F8 0x0000000C  [  5] _NSLog

同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。
例如第1行代表[ViewController viewDidLoad]方法占用了5*16+4=84byte大小。
根据上面符号文件的分析,我们可以写一个脚本统计我们程序的每一个静态库和framwork以及每一个实现文件的大小,有便于我们分析程序文件大小,为代码优化,减少二进制包大小提供了优化方向。我写了一个脚本,代码如下:

#/!usr/bin/python
## -*- coding: UTF-8 -*-
#
#使用简介:python linkmap.py XXX-LinkMap-normal-xxxarch.txt 或者 python linkmap.py XXX-LinkMap-normal-xxxarch.txt -g
#使用参数-g会统计每个模块.o的统计大小
#
__author__ = "zmjios"
__date__ = "2016-07-27"

import os
import re
import shutil
import sys

class SymbolModel:
    file = ""
    size = 0

def verify_linkmapfile(args):
    if len(sys.argv) < 2:
        print("请输入linkMap文件")
        return False
    
    path = args[1]

    if not os.path.isfile(path):
        print("请输入文件")
        return False

    file = open(path)
    content = file.read()
    file.close()

    #查找是否存在# Object files:
    if content.find("# Object files:") == -1:
        print("输入linkmap文件非法")
        return False
    #查找是否存在# Sections:
    if content.find("# Sections:") == -1:
        print("输入linkmap文件非法")
        return False
    #查找是否存在# Symbols:
    if content.find("# Symbols:") == -1:
        print("输入linkmap文件非法")
        return False

    return True 

def symbolMapFromContent():
    symbolMap = {}
    reachFiles = False
    reachSections = False
    reachSymblos = False
    file = open(sys.argv[1])
    for line in file.readlines():
        if line.startswith("#"):
            if line.startswith("# Object files:"):
                reachFiles = True
            if line.startswith("# Sections:"):
                reachSections = True
            if line.startswith("# Symbols:"):
                reachSymblos = True
        else:
            if reachFiles == True and reachSections == False and reachSymblos == False:
                #查找 files 列表,找到所有.o文件
                location = line.find("]")
                if location != -1:
                    key = line[:location+1]
                    if  symbolMap.get(key) is not None:
                        continue
                    symbol = SymbolModel()
                    symbol.file = line[location + 1:]
                    symbolMap[key] = symbol
            elif reachFiles == True and reachSections == True and reachSymblos == True:
                #'\t'分割成三部分,分别对应的是Address,Size和 File  Name
                symbolsArray = line.split('\t')
                if len(symbolsArray) == 3:
                    fileKeyAndName = symbolsArray[2]
                    #16进制转10进制
                    size = int(symbolsArray[1],16)
                    location = fileKeyAndName.find(']')
                    if location != -1:
                        key = fileKeyAndName[:location + 1]
                        symbol = symbolMap.get(key)
                        if symbol is not None:
                            symbol.size = symbol.size + size
    file.close()
                            
    return symbolMap
    
def sortSymbol(symbolList):
     return sorted(symbolList, key=lambda s: s.size,reverse = True)

def buildResultWithSymbols(symbols):
    results = ["文件大小\t文件名称\r\n"]
    totalSize = 0
    for symbol in symbols:
        results.append(calSymbol(symbol))
        totalSize += symbol.size
    results.append("总大小: %.2fM" % (totalSize/1024.0/1024.0))
    return results

def buildCombinationResultWithSymbols(symbols):
    #统计不同模块大小
    results = ["库大小\t库名称\r\n"]
    totalSize = 0
    combinationMap = {}
    
    for symbol in symbols:
        names = symbol.file.split('/')
        name = names[len(names) - 1].strip('\n')
        location = name.find("(")
        if name.endswith(")") and location != -1:
            component = name[:location]
            combinationSymbol = combinationMap.get(component)
            if combinationSymbol is None:
                combinationSymbol = SymbolModel()
                combinationMap[component] = combinationSymbol

            combinationSymbol.file = component
            combinationSymbol.size = combinationSymbol.size + symbol.size
        else:
            #symbol可能来自app本身的目标文件或者系统的动态库
            combinationMap[symbol.file] = symbol
    sortedSymbols = sortSymbol(combinationMap.values())

    for symbol in sortedSymbols:
        results.append(calSymbol(symbol))
        totalSize += symbol.size
    results.append("总大小: %.2fM" % (totalSize/1024.0/1024.0))

    return results

def calSymbol(symbol):
    size = ""
    if symbol.size / 1024.0 / 1024.0 > 1:
        size = "%.2fM" % (symbol.size / 1024.0 / 1024.0)
    else:
        size = "%.2fK" % (symbol.size / 1024.0)
    names = symbol.file.split('/')
    if len(names) > 0:
        size = "%s\t%s" % (size,names[len(names) - 1])
    return size

def analyzeLinkMap():
    if verify_linkmapfile(sys.argv) == True:
        print("**********正在开始解析*********")
        symbolDic = symbolMapFromContent()
        symbolList = sortSymbol(symbolDic.values())
        if len(sys.argv) >= 3 and sys.argv[2] == "-g":
            results = buildCombinationResultWithSymbols(symbolList)
        else:
            results = buildResultWithSymbols(symbolList)
        for result in results:
            print(result)
        print("***********解析结束***********")


if __name__ == "__main__":
    analyzeLinkMap()

运行脚本./linkmap_analyze.py FishHookDemo-LinkMap-normal-arm64.txt:

➜ ./linkmap_analyze.py FishHookDemo-LinkMap-normal-arm64.txt
**********正在开始解析*********
文件大小    文件名称

8.58K   AppDelegate.o

2.04K   fishhook.o

0.95K   ViewController.o

0.31K   libSystem.tbd

0.22K   libobjc.tbd

0.18K   main.o

0.13K    linker synthesized

0.06K   Foundation.tbd

0.03K   UIKit.tbd

总大小: 0.01M
***********解析结束***********

3.2. 查找无用selector和无用class

WeMobileDev公众号之前介绍了iOS微信安装包瘦身也做了相关介绍。无论是Mach-O或者是linkMap文件,都能做相关操作。具体原理是过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系统API的Protocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。

万能的github已经有人写了相关脚本,有需要可以参考。

3.3. class-dump和越狱相关

class-dump正是利用Mach-O文件导出出Mac或者iOS app的头文件的命令行工具。

4. fishhook实现原理

4.1. 原理分析

fishhook的原理,其实就是根据mach-o的原理,动态的修改mach-o符号表的内容,来实现rebind系统C函数的实现。

使用Mach-O View查找Symbol Table中某个方法的指针步骤:

  • 通过方法名(比如rebind NSlog),在Mach-O文件的__DATA中的__la_symbol_ptr懒加载表中查找到行数(比如在第11行)
    ---->
  • 查找__DATA中的Dynamic Symbol Table表中对应行数(比如上一步中的第11行,就是NSLog在符号表中的位置) ,查找到Data值(比如为0x0000007A,转换成10进制就是122)
    ---->
  • 使用上一步查找到的Data值(比如122),查找__DATA中的Symbol Table 中第122,查找到data的值为0x000000AC
  • 切换到String Table表,查找的String Table第一行的pFile列的值为0x00004F30,使用该值加上一步的偏移值(0x000000AC),得到结果为0x4FDC,就是NSLog符号表的位置

根据上述过程可知,rebind其实就是修改Dynamic Symbol Table中的Data值,来达到修改NSLog方法指针的目的。

原理图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQYJidA8-1579222855810)(/20181228165034087_1408923525.png)]

4.2. 实例分析

跟踪fishhook前后NSLog函数的实现,rebind之前,使用image list查看mach-o文件加载到内存中的地址,地址为0x00000001097b0000

(lldb) image list
[  0] 21EFFE20-FDB5-33EA-8468-52F928B7ED3E 0x00000001097b0000 /Users/kwok/Library/Developer/Xcode/DerivedData/FishHookDemo-fhqbawdslnrnlzhditxvcntioyad/Build/Products/Debug-iphonesimulator/FishHookDemo.app/FishHookDemo 
[  1] 1780094A-8FE2-3EAA-B4A3-C4CF14BC5196 0x0000000118716000 /usr/lib/dyld 
[  2] C3514384-926E-3813-BF0C-69FFC704E283 0x00000001097bb000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim 
[  3] E5391C7B-0161-33AF-A5A7-1E18DBF9041F 0x0000000109aa4000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation 
[  4] 177A61B3-9E02-3A09-9A98-C1C3C9AB7958 0x000000010a0ca000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libobjc.A.dylib 
[  5] C89C657A-9BD2-3C7D-AD2E-ACF00916BF7D 0x000000010aa01000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libSystem.B.dylib 
[  6] 80ACDA5E-AD72-3857-AF41-395941288C21 0x000000010aa09000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation 
[  7] C9C0B972-5616-3213-96C3-8FD355DE0066 0x000000010af73000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit 
...

通过macoview查找出偏移量 ,为0x00003020
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nDO9YjLF-1579222855811)(/20181228160109112_1737150085.png)]

通过查看内存地址(mach-o地址0x00000001097b0000+ 0x00003020偏移地址),查看NSLog函数指针的地址

(lldb) x 0x00000001097b0000+0x00003020
0x1097b3020: 76 a2 b4 09 01 00 00 00 70 5c b3 09 01 00 00 00  v.......p\......
0x1097b3030: 91 77 99 0d 01 00 00 00 e6 19 7b 09 01 00 00 00  .w........{.....

因为arm和intel都是小端CPU,所以NSLog函数的指针地址为76 a2 b4 09 01 00 00地址对应的内容0x0109b4a276,使用汇编指令dis -s 0x0109b4a276查看函数指针的汇编:

(lldb) dis -s 0x0109b4a276
Foundation`NSLog:
    0x109b4a276 <+0>:  pushq  %rbp
    0x109b4a277 <+1>:  movq   %rsp, %rbp
    0x109b4a27a <+4>:  subq   $0xd0, %rsp
    0x109b4a281 <+11>: testb  %al, %al
    0x109b4a283 <+13>: je     0x109b4a2ab               ; <+53>
    0x109b4a285 <+15>: movaps %xmm0, -0xa0(%rbp)
    0x109b4a28c <+22>: movaps %xmm1, -0x90(%rbp)

所以就成功查看到没有使用fishhook进行rebind之前,NSLog的函数地址,进行hook操作之后,使用同样的操作,查看NSLog函数所指向的指针,会发现变成了rebind之后的函数指针,如下所示:

# 2. 查找函数指针地址
(lldb) x 0x00000001097b0000+0x00003020
0x1097b3020: f0 0d 7b 09 01 00 00 00 70 5c b3 09 01 00 00 00  ..{.....p\......
0x1097b3030: 91 77 99 0d 01 00 00 00 2b 21 5b 0c 01 00 00 00  .w......+![.....

# 3. 使用汇编地址
(lldb) dis -s 0x01097b0df0
FishHookDemo`myLog:
    0x1097b0df0 <+0>:  pushq  %rbp
    0x1097b0df1 <+1>:  movq   %rsp, %rbp
    0x1097b0df4 <+4>:  subq   $0x30, %rsp
    0x1097b0df8 <+8>:  movq   $0x0, -0x8(%rbp)
    0x1097b0e00 <+16>: leaq   -0x8(%rbp), %rax
    0x1097b0e04 <+20>: movq   %rdi, -0x10(%rbp)
    0x1097b0e08 <+24>: movq   %rax, %rdi
    0x1097b0e0b <+27>: movq   -0x10(%rbp), %rsi
(lldb) 

4.3. 开发中如何防止类似fishhook这样动态修改mach-o符号表

因为fishhook的存在,所以在日常开发中,进行安全防护就显得特别重要,比如微信为了防止修改mach-o符号表,在越狱的机器上,就禁用了指纹/FaceId进行支付操作。

5. 参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值