前言
一、InlineHook概述
inlineHook(内联钩子)
:所谓InlineHook
就是直接修改目标函数
的头部代码
,让它跳转
到我们自定义的函数
里面执行我们的代码,从而达到Hook
的目的。这种Hook技术一般用在静态语言
的HOOK
上面。
Inline Hook
就是在运行的流程中插入跳转指令
来抢夺运行流程的一个方法。大体分为三步
- 将
原函数
的前 N 个字节搬运
到Hook 函数
的前 N 个字节; - 然后将
原函数
的前 N 个字节填充
跳转到 Hook 函数的指令; - 在
Hook
函数末尾几个字节填充
跳转回原函数 +N 的跳转指令;
之前的文章14-Hook原理(一)fishHook中也有介绍InlineHook
。
二、Dobby框架
Dobby
是一个全平台的InlineHook
框架,详情可查看 官方文档。
2.1 Dobby框架搭建
要学习Dobby
,当然是先大框架
- 首先clone工程
#depth用于指定克隆深度,为1即表示只克隆最近一次commit.
git clone https://github.com/jmpews/Dobby.git --depth=1
- 由于
Dobby
是跨平台的,所以项目并不是一个Xcode工程
,要使用cmake
将这个工程编译成为Xcode
工程。相关指令如下
// 进入`Dobby目录`,创建一个文件夹`build_for_ios_arm64`
cd Dobby && mkdir build_for_ios_arm64 && cd build_for_ios_arm64
// 然后`cmake`编译生成XCode工程
cmake .. -G Xcode \
-DCMAKE_TOOLCHAIN_FILE=cmake/ios.toolchain.cmake \
-DPLATFORM=OS64 -DARCHS="arm64" -DCMAKE_SYSTEM_PROCESSOR=arm64 \
-DENABLE_BITCODE=0 -DENABLE_ARC=0 -DENABLE_VISIBILITY=1 -DDEPLOYMENT_TARGET=9.3 \
-DDynamicBinaryInstrument=ON -DNearBranch=ON -DPlugin.SymbolResolver=ON -DPlugin.Darwin.HideLibrary=ON -DPlugin.Darwin.ObjectiveC=ON
命令执行完成后,工程应该是这样
- 编译
Xcode工程
我们生成动态库DobbyX.framework
2.2 项目演示
Dobby
的使用
- 新建一个工程
DoddyDemo
,导入DobbyX.framework
导⼊DobbyX.framework
到⼯程,如果遇到Bitcode
问题,两种解决方式
- 关闭当前⼯程
DoddyDemo
的Bitcode
- 编译
DobbyX.framework
时,开启Bitcode
Bitcode
的设置
- 打开
ViewController.m
文件,写入以下代码
- 定义将要
被HOOK
的静态函数
int sum(int a,int b){
return a + b;
}
- 定义函数指针,⽤于保存
被替换函数
的地址
static int (*sum_p)(int a,int b);
- 定义
新函数
,⽤此函数替换将要HOOK的函数,该函数的返回值及参数
必须⼀致
int mySum(int a,int b) {
NSLog(@"Sum:%d,",sum_p(a,b));
return a - b;
}
- 调用
DobbyHook
进行函数的Hook
int DobbyHook(void *address, void *replace_call, void **origin_call);
Dobby
的核心的函数
参数名 | 释义 |
---|---|
address |
需要HOOK的函数地址 |
replace_call |
新函数地址 |
origin_call |
保留原始函数的指针的地址 |
在viewDidLoad
中进行Hook
- (void)viewDidLoad {
[super viewDidLoad];
DobbyHook((void *)sum, mySum, (void *)&sum_p);
}
- 在
touchesBegan
中,调用sum函数
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"Sum:%d",sum(10, 20));
}
- 真机运行项目,点击屏幕
sum函数
HOOK成功!
三、Dobby Hook原理
接下来,我们来研究下Dobby
Hook的原理。
- 在上述案例中的
touchesBegan
方法上设置断点
- 打开汇编,真机运行项目,点击屏幕,进入
touchesBegan
方法
step into,进入sum函数
我们发现
-
拉伸栈空间
的代码没有了 -
前三句
的代码被替换了
-
单步
调试,向下执行3步
。通过br x17
指令,跳转到mySum
函数
看到第一句代码,就是拉伸栈空间
的代码。然后br x17
,会回到sum
函数
接着就是执行完sum函数的逻辑,最后在结尾恢复栈平衡
。
- 当
sum函数
执行ret指令
,返回mySum函数
,执行后续代码
综上所述
静态函数
的HOOK,并没有在原始函数中增加代码,而是将拉伸栈空间的三句代码
进行了替换
- 当调用
原始函数
,才会拉伸栈平衡
。然后在原始函数
的代码中,恢复栈平衡
案例修改
我们再看修改下案例 mySum
函数中,不调用
原始函数
int mySum(int a,int b) {
// NSLog(@"Sum:%d,",sum_p(a,b));
return a - b;
}
真机运行项目,查看sum函数
的汇编
上图中,代码并没有变化。
接着进入mySum
函数
跳转到指定地址的代码blr x18
没有了。
当mySum
函数执行ret指令
,直接返回到touchesBegan
此时sum
函数的原始代码都不会被执行
。这种情况
不会
拉伸栈空间,sum
函数的原始代码不会
被执行,所以也不会恢复栈平衡
。
2.1 Hook函数地址
在逆向开发中,一般的应用App会剥离
符号表,所以我们无法获得符号名称
,那么久没法Hook方法名称,智能Hook地址
。
但是,应用每次启动时,ASLR
偏移地址都不一样
,所以不能直接
Hook固定的地址。正确的做法
先找到函数在MachO中的
偏移地址
,加上PAGEZERO
的0x100000000
,再加上本次启动的ASLR偏移地址
案例演示
继续使用上述案例,找到sum
函数的地址
函数实现地址是 0x104221bcc
。
然后使用image list
,找到主程序的基地址
基地址是 0x10421c000
。
那么函数在MachO中的偏移地址 函数实现地址 - 主程序基地址
0x104221bcc - 0x10421c000 = 0x5BCC
偏移地址是 0x5BCC
。在MachO文件中,查看该偏移地址
上图显而易见,对应的正是sum函数
的汇编代码。
对函数地址
进行Hook
- 打开
ViewController.m
文件,写入以下代码
#import "ViewController.h"
#import
#import
@implementation ViewController
int sum(int a,int b){
return a + b;
}
static uintptr_t sumP = 0x5BCC + 0x100000000;
- (void)viewDidLoad {
[super viewDidLoad];
sumP += _dyld_get_image_vmaddr_slide(0);
DobbyHook((void *)sumP, mySum, (void *)&sum_p);
}
static int (*sum_p)(int a,int b);
int mySum(int a,int b) {
NSLog(@"Sum:%d,",sum_p(a,b));
return a - b;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"Sum:%d",sum(10, 20));
}
@end
⚠️注意:因为修改了代码,需要重新编译,所以,
sum
函数在Mach-O中的偏移地址
会发生改变
。
- 重新编译,查看Mach-O中的
sum函数
的偏移地址
偏移地址
是 0x5B34
,那么修改代码
static uintptr_t sumP = 0x5B34 + 0x100000000;
- 真机运行项目,点击屏幕
果然,Hook成功!
2.2 Dobby注入
最后,我们来看看使用Dobby
,如何代码注入?
- 创建
FuncDemo
项目,打开ViewController.m
文件,写入以下代码
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"Sum:%d",sum(10,15));
}
int sum(int a,int b){
return a + b;
}
@end
- 真机运行项目,使用
函数实现地址 - 主程序基地址
,计算出sum函数
在MachO中的偏移地址
计算出偏移地址 0x5D68
⚠️注意:因为真实场景中会
剥离符号
,所以需要再build setting中设置
剥离符号
后真机运行,通过暂停
然后lldb中输入image list获取基地址
计算出sum函数
的地址 0x1000dc000 + 0x5D68 = 0x1000E1D68
。
- 对地址
0x1000E1D68
设置断点
能断点成功,说明sum函数
的实现地址没有改变。
对FuncDemo.app
进行代码注入
- 搭建
HookDemo
项目,将yololib、appSign.sh、DobbyX.framework
,拷贝到项目根目录
其中,脚本appSign.sh
# ${SRCROOT} 它是工程文件所在的目录
TEMP_PATH="${SRCROOT}/Temp"
#资源文件夹,我们提前在工程目录下新建一个APP文件夹,里面放ipa包
ASSETS_PATH="${SRCROOT}/APP"
# 拿到临时的APP的路径
TEMP_APP_PATH=$(set -- "${ASSETS_PATH}/"*.app;echo "$1")
#目标ipa包路径
#TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"
#TEMP_APP_PATH=$(set -- "${ASSETS_PATH}/"*.app;echo "$1")
#TEMP_APP_PATH="${ASSETS_PATH}/*.app"
#清空Temp文件夹
#rm -rf "${SRCROOT}/Temp"
#mkdir -p "${SRCROOT}/Temp"
#----------------------------------------
# 1. 解压IPA到Temp下
#unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
# 拿到解压的临时的APP的路径
#TEMP_APP_PATH=$(set -- "${ASSETS_PATH}/"*.app;echo "$1")
#TEMP_APP_PATH=$(echo "${ASSETS_PATH}/"*.app)
echo "TEMP_APP_PATH路径:$TEMP_APP_PATH"
#echo "Tempapp路径:$TEMP_APP_PATH/"
# echo "路径是:$TEMP_APP_PATH"
#----------------------------------------
# 2. 将解压出来的.app拷贝进入工程下
# BUILT_PRODUCTS_DIR 工程生成的APP包的路径
# TARGET_NAME target名称
TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
echo "app路径:$TARGET_APP_PATH"
rm -rf $TARGET_APP_PATH
mkdir -p $TARGET_APP_PATH
cp -rf $TEMP_APP_PATH/ $TARGET_APP_PATH
#----------------------------------------
# 3. 删除extension和WatchAPP.个人证书没法签名Extention
rm -rf "$TARGET_APP_PATH/PlugIns"
rm -rf "$TARGET_APP_PATH/Watch"
#----------------------------------------
# 4. 更新info.plist文件 CFBundleIdentifier
# 设置:"Set : KEY Value" "目标文件路径"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"
#----------------------------------------
# 5. 给MachO文件上执行权限
# 拿到MachO文件的路径
APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
#上可执行权限
chmod +x "$TARGET_APP_PATH/$APP_BINARY"
#----------------------------------------
# 6. 重签名第三方 FrameWorks
TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
then
for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
do
#签名
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
done
fi
#注入
./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/Hook.framework/Hook"
- 在项目根目录,创建
App目录
,将FuncDemo.app
拷贝至App目录
- 在
HookDemo
主工程中添加target,就是注入的动态库,命名HOOK
,在HOOK
动态库中,创建Inject
类
- 在
HookDemo
主工程中,拖入DobbyX.framework
,勾选HookDemo
- 在
HookDemo
中,找到Embed Framewords
,添加DobbyX.framework
然后在Hook.framework
Target配置
- 打开
Inject.m
文件,写入以下代码
#import "Inject.h"
#import
#import
@implementation Inject
static uintptr_t sumP = 0x5D68 + 0x100000000;
+(void)load{
sumP += _dyld_get_image_vmaddr_slide(0);
DobbyHook((void *)sumP, mySum, (void *)&sum_p);
}
static int (*sum_p)(int a,int b);
int mySum(int a,int b) {
NSLog(@"Sum:%d,",sum_p(a,b));
return a - b;
}
@end
- 真机运行项目,点击屏幕
FuncDemo[11452:2162229] Sum:25,
FuncDemo[11452:2162229] Sum:-5
总结
-
Dobby
原理 运行时对目标函数的汇编代码替换,修改的是内存中MachO的代码段
-
Dobby
替换汇编代码时,对原始函数的调用,会影响栈的拉伸和平衡
- 在真实Hook场景中,我们
拿不到符号名称
,只能对地址进行HOOK - Hook地址时,需要加上
PAGEZERO
和ASLR