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 */
};
我们依次介绍这个头信息:
- magic,可以看到文件中的内容最开始部分,是以 cafe babe开头的
对于一个 二进制文件 来讲,每个类型都可以在文件最初几个字节来标识出来,即“魔数”。不同类型的 二进制文件,都有自己独特的"魔数"。
OS X上,可执行文件的标识有这样几个魔数(不同的魔数代表不同的可执行文件类型)
是mach-o文件的魔数,0xfeedface代表的是32位,0xfeedfacf代表64位,cafebabe是跨处理器架构的通用格式,#!代表的是脚本文件。 - cputype和cupsubtype代表的是cpu的类型和其子类型,图上的例子是模拟器程序,cpu结构是x86_64,如果直接查看ipa,可以看到cpu是arm,subtype是armv7,arm64等
- 接着是filetype,2,代表可执行的文件 #define MH_EXECUTE 0×2
- ncmds 指的是加载命令(load commands)的数量,例子中一共65个,编号0-64
- sizeofcmds 表示23个load commands的总字节大小, load commands区域是紧接着header区域的。
- 最后个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文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了虚拟内存中。每个段都有不同的功能,一般包括:
__PAGEZERO
: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;__TEXT
: 包含了执行代码以及其他只读数据。 为了让内核将它 直接从可执行文件映射到共享内存, 静态连接器设置该段的虚拟内存权限为不允许写。当这个段被映射到内存后,可以被所有进程共享。(这主要用在frameworks, bundles和共享库等程序中,也可以为同一个可执行文件的多个进程拷贝使用)__DATA
: 包含了程序数据,该段可写;__OBJC
: Objective-C运行时支持库;__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进行支付操作。