iOS逆向实战--018:HOOK原理

HOOK概述

HOOK,中文译为“挂钩”或“钩子”。在iOS逆向中是指改变程序运行流程的一种技术。通过HOOK可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。所以在学习过程中,我们重点要了解其原理,这样能够对恶意代码进行有效的防护

HOOK示意图

iOSHOOK技术的几种方式:

  • Method Swizzle:主要用于OC方法,利用OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的
  • fishhookFacebook提供的一个动态修改链接MachO文件的工具。利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针,达到对C函数进行HOOK的目的
  • Cydia Substrate:原名为Mobile Substrate,它的主要作用是针对OC方法、C函数以及函数地址进行HOOK操作。当然它并不是仅仅针对iOS而设计的,安卓一样可以使用。官方地址
Method Swizzle

利用OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。

OC中,SELIMP之间的关系,就好像一本书的“目录

  • SEL是方法编号,就像“标题”一样
  • IMP是方法实现的真实地址,就像“页码”一样
  • 它们是一一对应的关系

Runtime提供了交换两个SELIMP对应关系的函数

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
   OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

通过这个函数交换两个SELIMP对应关系的技术,我们称之为Method Swizzle(方法欺骗)

多种HOOK方式

  • class_addMethod方式:让原始方法可以被调用,不至于因为找不到SEL而崩溃
  • class_replaceMethod方式:直接给原始的方法替换IMP
  • method_setImplementation方式:直接重新赋值新的IMP
Cydia Substrate

MobileHooker

顾名思义用于HOOK。它定义一系列的宏和函数,底层调用objcruntimefishhook来替换系统或者目标应用的函数

其中有两个函数:

  • MSHookMessageEx:主要作用于Objective-C方法
void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result) 
  • MSHookFunction:主要作用于CC++函数,Logos语法的%hook就是对以下函数做了一层封装
void MSHookFunction(void function, void* replacement, void** p_original)

MobileLoader

MobileLoader用于加载第三方dylib在运行的应用程序中。启动时MobileLoader会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序

safe mode

破解程序本质是dylib,寄生在别人进程里。 系统进程一旦出错,可能导致整个进程崩溃,崩溃后就会造成iOS瘫痪。所以Cydia Substrate引入了安全模式,在安全模式下所有基于Cydia Substratede的三方dylib都会被禁用,便于查错与修复

fishHook

获取代码

git clone https://github.com/facebook/fishhook.git

关键函数

FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
  • rebindings:存放rebinding结构体的数组,可以同时交换多个函数
  • rebindings_nel:数组的长度

rebinding结构体

struct rebinding {
 const char *name;
 void *replacement;
 void **replaced;
};
  • name:需要HOOK的函数名称,C字符串
  • replacement:新函数的地址
  • replaced:原始函数地址的指针

案例1:

HOOK系统NSLog函数

打开ViewController.m文件,定义新函数

void my_NSLog(NSString *format, ...) {    
   format = [format stringByAppendingString:@"~~~🍺🍺🍺🍺🍺"];
   sys_NSLog(format);
}

定义函数指针,用于保存NSLog系统函数地址

static void (*sys_NSLog)(NSString *format, ...);

viewDidLoad方法中,写入以下代码:

- (void)viewDidLoad {
   [super viewDidLoad];

   struct rebinding reb;
   reb.name="NSLog";
   reb.replacement=my_NSLog;
   reb.replaced=(void *)&sys_NSLog;
   
   struct rebinding rebs[] = { reb };
   rebind_symbols(rebs, 1);
}

添加touchesBegan方法

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
   NSLog(@"hahaha");
}

真机运行项目,点击屏幕,输出以下内容:

fishhookDemo[2072:399007] hahaha~~~🍺🍺🍺🍺🍺

fishHook可以HOOK我们的C函数,但是我们知道函数是静态的,也就是说在编译的时候,编译器就知道了它的实现地址,这也是为什么C函数只写函数声明调用时会报错。那么为什么fishHook还能够改变C函数的调用呢?难道函数也有动态的特性存在?

案例2:

HOOK自定义函数

打开ViewController.m文件,定义自定义函数

void test(NSString *format, ...){
   NSLog(@"hahaha");
}

定义新函数

void my_test(NSString *format, ...){
   format = [format stringByAppendingString:@"~~~🍺🍺🍺🍺🍺"];
   sys_test(format);
}

定义函数指针,用于保存test自定义函数地址

static void (*sys_test)(NSString *format, ...);

viewDidLoad方法中,写入以下代码:

- (void)viewDidLoad {
   [super viewDidLoad];

   struct rebinding reb;
   reb.name="test";
   reb.replacement=my_test;
   reb.replaced=(void *)&sys_test;
   
   struct rebinding rebs[] = { reb };
   rebind_symbols(rebs, 1);
}

添加touchesBegan方法

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
   test(@"hahaha");
}

真机运行项目,点击屏幕,输出以下内容:

fishhookDemo[2117:410009] hahaha

案例2中,HOOK自定义test函数并没有效果。NSLog是系统函数,而test是当前MachO中的函数,它们之间有什么区别?

fishHook原理探究

一般来说,静态语言通过地址直接访问,例如案例2中的test函数。但对于NSLog系统函数来说,它属于外部调用函数,编译时并不能找到它的真实地址

NSLog存储在Fundation框架中,App运行在不同系统版本的不同设备上,每个设备中NSLog的函数地址都各不相同

再有,当App启动时,会涉及ASLR(地址空间配置随机加载),导致程序运行它的虚拟内存地址都不一样

这些原因,都会造成外部函数的真实地址,在编译时期是无法确定的

那外部函数是如何被调用的?

App启动时,dyld读取主程序MachO文件,会加载共享缓存中的系统库,将用到的函数真实地址告诉MachO

对于MachO中的代码段来说,它是只读的。在运行时,无法直接修改外部函数的真实地址

对于上述情况,苹果采用PIC技术(位置独立代码),在MachO调用外部函数时,在可读可写的数据段,定义符号,占8字节,用来存放外部函数的地址。编译时,暂存占位地址。运行时,dyld将符号绑定真实函数地址。对于代码段来说,并没有任何改变

故此,外部调用函数,并不是直接地址访问,而是通过符号找到地址。这跟OCSELIMP的对应关系非常相似。这种机制,可以让开发者动态HOOK外部调用函数

OC中动态改变SELIMP的对应关系,对于外部调用函数,动态改变的是符号和地址的对应关系,上述操作称为:符号表重绑定

案例1

查看NSLog加载前的占位地址

查看MachO文件

  • NSLog函数,存储在懒加载符号表中。dyld加载MachO时,绑定非懒加载符号和弱引用符号,而懒加载符号,则是在首次使用时动态绑定

打开ViewController.m文件,修改viewDidLoad代码

- (void)viewDidLoad {
   [super viewDidLoad];
   
   NSLog(@"begin");

   struct rebinding reb;
   reb.name="NSLog";
   reb.replacement=my_NSLog;
   reb.replaced=(void *)&sys_NSLog;

   struct rebinding rebs[] = { reb };
   rebind_symbols(rebs, 1);
   
   NSLog(@"end");
}

beginend设置两处断点,真机运行项目

使用image list命令,找到程序虚拟内存的首地址

  • 首地址:0x102c48000
  • ASLR0x2c48000

查看NSLogMachO中的偏移地址和占位地址

  • 偏移地址:0xC000
  • 占位地址:0x1000064C0

lldb中,使用程序首地址 + 偏移地址,找到NSLog加载前的占位地址

  • lldb中,获取的NSLog占位地址为0x102c4e4c0,和MachO中看到的0x1000064C0并不一样

因为MachO中的占位地址,还要加上程序启动时的ASLR随机偏移地址

  • 占位地址 + ASLR = 0x102c4e4c0,和lldb中读取出的地址一致

案例2

查看NSLog加载后的真实地址

承接案例1,断点向下执行

lldb中,使用程序首地址 + 偏移地址,找到NSLog加载后的真实地址

  • 之前的占位地址0x102c4e4c0,被真实地址0x19d9327f0替换

使用dis -s 0x19d9327f0命令,读取地址中的代码

dis -s 0x000000019d9327f0
-------------------------
Foundation`NSLog:
   0x19d9327f0 <+0>:  sub    sp, sp, #0x20             ; =0x20 
   0x19d9327f4 <+4>:  stp    x29, x30, [sp, #0x10]
   0x19d9327f8 <+8>:  add    x29, sp, #0x10            ; =0x10 
   0x19d9327fc <+12>: adrp   x8, 311278
   0x19d932800 <+16>: ldr    x8, [x8, #0xb70]
   0x19d932804 <+20>: ldr    x8, [x8]
   0x19d932808 <+24>: str    x8, [sp, #0x8]
   0x19d93280c <+28>: add    x8, x29, #0x10            ; =0x10 
  • 指向NSLog代码

案例3

查看交换后的函数地址

承接案例2,断点向下执行

lldb中,使用程序首地址 + 偏移地址,找到NSLog交换后的函数地址

  • 原本NSLog的真实地址为0x19d9327f0,交换后变为0x102c4d57c

使用dis -s 0x102c4d57c命令,读取地址中的代码

dis -s 0x102c4d57c
-------------------------
fishhookDemo`my_NSLog:
   0x102c4d57c <+0>:  sub    sp, sp, #0x30             ; =0x30 
   0x102c4d580 <+4>:  stp    x29, x30, [sp, #0x20]
   0x102c4d584 <+8>:  add    x29, sp, #0x20            ; =0x20 
   0x102c4d588 <+12>: sub    x8, x29, #0x8             ; =0x8 
   0x102c4d58c <+16>: mov    x9, #0x0
   0x102c4d590 <+20>: stur   x9, [x29, #-0x8]
   0x102c4d594 <+24>: str    x0, [sp, #0x10]
   0x102c4d598 <+28>: mov    x0, x8

指向my_NSLog代码

由此可见,HOOK外部的C函数,本质就是在修改符号和地址的对应关系

符号绑定的过程
  • Symbol Table:符号表,⽤来保存符号
  • String Table:字符串表,⽤来保存符号的名称
  • Indirect Symbol Table:间接符号表,保存使⽤的外部符号。更准确⼀点就是使⽤的外部动态库的符号。是Symbol Table的子集

代码中的函数名、变量名、方法名,在项目编译后,都会生成一张符号表。符号之间也有差别,分为内部符号和外部符号

内部符号是当前MachO中的符号,而外部符号又称为间接符号,例如:系统库、动态库中的符号

符号按可见性划分,分为全局符号和本地符号

  • 全局符号对整个项目可见
  • 本地符号仅对当前文件可见

案例1:

符号绑定的过程

打开ViewController.m文件,写入以下代码:

#import "ViewController.h"

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   
   NSLog(@"第一次外部函数的调用!");
   NSLog(@"第二次外部函数的调用!");
}

@end

真机运行项目,来到viewDidLoad方法:

  • 两次NSLog的指令都是bl 0x1022ba4d8

使用image list命令,找到程序虚拟内存的首地址

  • 首地址:0x1022b4000

使用NSLog地址 - 首地址计算偏移地址

  • 偏移地址:0x64d8

打开MachO文件,找到偏移地址为0x64d8的位置

  • 处于__TEXT,__stubs
  • __stubs:符号桩,本质上就是一段代码,用于跳转到懒加载符号表中,找到对应符号的值
  • 0x64d8即是NSLog的桩

fishHook原理探究中,多次提到跳转到占位地址,这种说法并不准确。真实的情况是,会跳转到外部符号的桩,本质上就是一段代码

案例2:

查看桩对应的代码

查看MachO文件,找到桩对应的值

  • 1F2003D530D9025800021FD6:以二进制指令的形式存储的代码

单步调试,来到NSLog函数

使用x 0x1022ba4d8命令,读取地址中的值,以二进制形式显示

  • 1F2003D530D9025800021FD6,和MachO中对应的指令一致

有此可见,1F2003D530D9025800021FD6对应的就是上述三句汇编代码

案例3:

三句汇编代码的含义?

断点执行到br x16指令

  • br指令:跳转到x16寄存器存储的地址

查看x16寄存器存储的地址,再减去ASLR偏移地址

  • 得到地址:0x10000658c

查看MachO文件,找到0x10000658c地址

  • 指向懒加载符号表中NSLog符号的值

三句汇编代码的含义:找到懒加载符号表中的地址去执行

案例4:

懒加载符号表中的地址,指向的代码是什么?

MachO中,找到偏移地址0x658c,指向__TEXT,__stubs_helper中的代码

  • b指令,跳转到偏移地址为0x6574的位置执行代码

MachO中,找到偏移地址为0x6574的位置

  • x16寄存器,偏移地址为0x8000
  • br指令,跳转到偏移地址为0x8000位置执行代码

MachO中,找到偏移地址为0x8000的位置

  • 处于非懒加载符号表中
  • 执行dyld_stub_binder函数,用于符号绑定

懒加载符号表中的地址,指向寻找并执行dyld_stub_binder函数的代码

案例5:

dyld_stub_binder也是外部函数,它的地址是如何找到的?

MachO中,可以看到dyld_stub_binder函数的偏移地址为0x8000,但全是0,说明此时还没有值

dyld_stub_binder函数,同样是外部函数,存储在非懒加载表中

dyld加载主程序时,会绑定非懒加载符号和弱引用符号,所以dyld_stub_binder函数的值,在程序启动时被dyld直接绑定

真机运行项目,读取首地址 + 0x8000地址中的值

使用dis -s 0x19c2e6f94命令,读取地址中的代码

libdyld.dylib`dyld_stub_binder:
   0x19c2e6f94 <+0>:  stp    x29, x30, [sp, #-0x10]!
   0x19c2e6f98 <+4>:  mov    x29, sp
   0x19c2e6f9c <+8>:  sub    sp, sp, #0xf0             ; =0xf0 
   0x19c2e6fa0 <+12>: stp    x0, x1, [x29, #-0x10]
   0x19c2e6fa4 <+16>: stp    x2, x3, [x29, #-0x20]
   0x19c2e6fa8 <+20>: stp    x4, x5, [x29, #-0x30]
   0x19c2e6fac <+24>: stp    x6, x7, [x29, #-0x40]
   0x19c2e6fb0 <+28>: stp    x8, x9, [x29, #-0x50]

案例6:

NSLog函数,首次加载和非首次加载的区别

真机运行项目,首次加载NSLog函数

  • 先找到桩里面的代码
  • 找到懒加载符号表中的地址去执行
  • 指向__stubs_helper中的代码
  • 寻找并执行dyld_stub_binder函数
  • 符号表重绑定

再次加载NSLog函数

  • 找到桩里面的代码
  • 找到懒加载符号表中的地址去执行
  • 首次加载NSLog函数,懒加载符号表中的地址,因重绑定而修改
  • 修改后的值,直接指向NSLog函数的真实地址
通过符号找到字符串

fishHook提供的rebinding结构体,其中name为需要HOOK的函数名称

作用:当找到相应的符号,再通过符号找到字符串,然后和name进行字符串比较,如果匹配成功,则替换函数指针

案例1:

通过懒加载符号表中的符号,找到间接符号表中的相同符号,再找到符号对应的值

__la_symbol_ptr懒加载符号表中的符号和顺序,跟间接符号表一致

Dynamic Symbol Table中,找到NSLog的值

案例2:

间接符号表中NSLog的值为0xB8,转为10进制184

184对应的是此符号在符号总表中的角标

Symbol Table中,通过角标找到符号,再找到String Table Index的值

案例3:

符号表中NSLogString Table Index值为0xCE,表示在字符串表中的偏移地址

String Table中,找到首地址

  • 首地址0x11230

通过首地址 + 偏移值 = 0x112FE,找到对应字符串

  • _是函数的开始,.是分隔符 。5F_开始,往后读取_NSLog,遇到分隔符结束
总结

HOOK

  • 钩子,改变程序执行流程的一种技术

Method Swizzle
利用OC运行时的特性,修改SELIMP的对应关系,达到对OC方法HOOK的目的

  • IMP,本质上是函数指针
  • method_exchangeImplementations:交互两个IMP
  • class_replaceMethod:替换某个SELIMP,如果没有该方法,使用class_addMethod添加
  • method_getImplementationmethod_setImplementation:获取和设置某个方法的IMP,很多三方框架都使用这种方式

fishHook

  • Facebook提供的一个工具,利用MachO文件的加载原理,动态修改懒加载和非懒加载两张符号表
  • 可以HOOK系统函数,但是无法HOOK自定义函数

fishHook原理解析

  • 共享缓存:iOS系统有一块特殊的位置,存放公用动态库。即:动态库共享缓存(dyld shared cache
  • PIC技术:由于外部函数调用,在编译时期无法确定内存地址。苹果采用PIC技术(位置独立代码),在MachO文件的DATA段,建立懒加载和非懒加载两张符号表,里面存放执行外部函数的指针

符号绑定过程

  • 外部函数调用,先找到桩里面的代码,__TEXT,__stubs
  • 找到懒加载符号表中的地址去执行

外部函数,首次加载

  • 懒加载符号表中的地址,指向__TEXT,__stubs_helper中的代码
  • 通过代码寻找并执行dyld_stub_binder函数
  • 作用:符号表重绑定

外部函数,非首次加载

  • 懒加载符号表中的地址,因重绑定而修改
  • 修改后的值,直接指向外部函数的真实地址

dyld_stub_binder函数

  • 存储在非懒加载符号表中
  • dyld加载主程序时,符号被dyld直接绑定

通过符号找到字符串

  • fishHook利用Lazy Symbol TableIndirect Symbol TableSymbol TableString Table,通过重绑定修改指针的值达到HOOK目的
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容