feature: App 逆向防护

This commit is contained in:
杭城小刘
2024-07-15 20:03:01 +08:00
parent 13f7457be9
commit 83fefff66b
109 changed files with 2549 additions and 672 deletions

View File

@@ -75,7 +75,7 @@ Person 类存在3个 BOOL 属性:
上 Demo
<img src="./../assets/BOOLPropertyImplementedByBitOperator.png" style="zoom:25%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BOOLPropertyImplementedByBitOperator.png" style="zoom:25%">
@@ -85,7 +85,7 @@ Person 类存在3个 BOOL 属性:
新方案采用:**结构体的位域能力**,限定单个成员变量所占用的内存。代码如下:
<img src="./../assets/BOOLProptertImpledByStructureBitDomain.png" style="zoom:25%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BOOLProptertImpledByStructureBitDomain.png" style="zoom:25%" />
@@ -97,7 +97,7 @@ Person 类存在3个 BOOL 属性:
虽然上述方式都可以实现存储 Person 类3个属性的目的但是还有第三种方案参考 iOS 系统设计,采用 Union 实现。代码如下
<img src="./../assets/BOOLPropertyImplementedByUnion.png" style="zoom:25%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BOOLPropertyImplementedByUnion.png" style="zoom:25%" />
分析:
@@ -168,7 +168,7 @@ union {
与一个不是3个数之一的数按位与得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值
<img src="./../assets/BitOperateorOnEnumDemo.png" style="zoom:25%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BitOperateorOnEnumDemo.png" style="zoom:25%" />
有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。
@@ -273,7 +273,7 @@ isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class类对象、元类
`0x0000000ffffffff8ULL` 用程序员模式打开计算器
<img src="./../assets/objc-isa-mask.png" style="zoom:30%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-isa-mask.png" style="zoom:30%">
@@ -283,7 +283,7 @@ extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcl
知道结构体可以指定存储大小这个功能后,可以看到 `isa_t` 联合体与 `ISA_MASK` 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象)
<img src="./../assets/objc-isa.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-isa.png" style="zoom:30%" />
@@ -423,7 +423,7 @@ struct class_ro_t {
具体关系整理如下图
<img src="./../assets/runtime-class.png" style="zoom:50%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-class.png" style="zoom:50%" />
@@ -435,7 +435,7 @@ struct class_ro_t {
<img src="./../assets/runtime-class-rw-t.png" style="zoom:40%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-class-rw-t.png" style="zoom:40%" />
比如访问 method 的过程
@@ -453,7 +453,7 @@ struct class_ro_t {
- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容
<img src="./../assets/runtime-class-ro-t.png" style="zoom:50%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-class-ro-t.png" style="zoom:50%" />
@@ -1240,7 +1240,7 @@ v
可以对照下面的表格进行查看:
![](./../assets/runtime-method-encoding.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-method-encoding.png)
```objectivec
- (int)calcuate:(int)baseHeight heigith:(float)height;
@@ -1513,7 +1513,7 @@ bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache.
NSLog(@"%s %p", goodStudentSayBucket._key, goodStudentSayBucket._imp);
```
<img src="./../assets/runtime-method-find.png" style="zoom:25%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-method-find.png" style="zoom:25%">
@@ -1544,7 +1544,7 @@ static inline mask_t cache_hash(cache_key_t key, mask_t mask)
## Runtime - objc_msgSend
<img src="./../assets/MethodCallIsObjcMsgSend.png" style="zoom:20%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MethodCallIsObjcMsgSend.png" style="zoom:20%" />
```c++
Person *p = [[Person alloc] init];
@@ -2117,7 +2117,7 @@ if (imp) goto done;
上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示
<img src="./../assets/runtime-objc_msgSend-messageSend.png" style="zoom:60%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-objc_msgSend-messageSend.png" style="zoom:60%" />
@@ -2262,7 +2262,7 @@ SEL_resolveClassMethod, sel);`
完整流程如下
<img src="./../assets/runtime-objc_msgSend-ResolveMethod.png" style="zoom:40%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-objc_msgSend-ResolveMethod.png" style="zoom:40%" />
@@ -2307,7 +2307,7 @@ Person *person = [[Person alloc] init];
知道 `objc_msgSend` 的流程,我们尝试给它修正下
<img src="./../assets/objc_msgSend_fix1.png" style="zoom:25%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc_msgSend_fix1.png" style="zoom:25%">
方法1增加一个兜底方法然后利用 `class_addMethod` 动态增加方法实现
@@ -2341,7 +2341,7 @@ class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncod
方法3也可以添加 c 语言方法
<img src="./../assets/objc_msgSend_fix2.png" style="zoom:25%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc_msgSend_fix2.png" style="zoom:25%">
c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 `(IMP)cfuntionResolver`,手动添加方法签名。
@@ -2454,7 +2454,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二
![](./../assets/runtime-forwardingFailed.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-forwardingFailed.png)
```c
int __forwarding__(void *frameStackPointer, int isStret) {
@@ -2498,7 +2498,7 @@ int __forwarding__(void *frameStackPointer, int isStret) {
完整流程如下
<img src="./../assets/runtime-forwarding.png" style="zoom:40%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-forwarding.png" style="zoom:40%" />
@@ -2532,13 +2532,13 @@ int __forwarding__(void *frameStackPointer, int isStret) {
Person 类不存在对象方法 makeliving PersonHelper 类存在。
<img src="./../assets/RuntimeCallUnKnownMethod.png" style="zoom:25%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeCallUnKnownMethod.png" style="zoom:25%" />
调用对象不存在的方法,则会抛出错误。同时在 Runtime 动态消息解析阶段,`resolveInstanceMethod` 没有处理对象方法,所以会报错
方法1因为动态消息解析没有处理则会开始走消息转发阶段。消息转发首先会调用 `- (id)forwardingTargetForSelector:(SEL)aSelector` 方法。(如果是对象方法则调用 `- (id)forwardingTargetForSelector:(SEL)aSelector`,如果是类方法则调用 `+ (id)forwardingTargetForSelector:(SEL)aSelector`
<img src="./../assets/RuntimeMethodForwardingDemo1.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingDemo1.png" style="zoom:30%" />
方法2如果消息转发里`forwardingTargetForSelector` 返回了 nil则开始调用方法签名 `methodSignatureForSelector` 方法和 `forwardInvocation` 方法
@@ -2568,13 +2568,13 @@ Person 类不存在对象方法 makeliving PersonHelper 类存在。
注意:`methodSignatureForSelector` 如果返回 nil则 `forwardInvocation` 不会执行
<img src="./../assets/RuntimeMethodForwardingDemo2.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingDemo2.png" style="zoom:30%" />
上述方式不够优雅,针对方法签名的获取太被动,方法改变了,方法签名处需要调整,很麻烦。
<img src="./../assets/RuntimeMethodForwardingWithMethodSignature.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingWithMethodSignature.png" style="zoom:30%" />
@@ -2601,11 +2601,11 @@ Person 类不存在对象方法 makeliving PersonHelper 类存在。
}
```
<img src="./../assets/RuntimeMethodForwardingByClassMethod.png" style="zoom:30%"/>
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingByClassMethod.png" style="zoom:30%"/>
<img src="./../assets/RuntimeMethodForwardingByClassMethod2.png" style="zoom:30%"/>
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingByClassMethod2.png" style="zoom:30%"/>
@@ -2632,7 +2632,7 @@ OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 rece
3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找
<img src="./../assets/MethodCacheLookUpProcess.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MethodCacheLookUpProcess.png" style="zoom:30%" />
先根据 superclass 找到父类然后在父类中也按照上面3点进行递归查找
@@ -2757,7 +2757,7 @@ objc_msgSendSuper(arg, sel_registerName("class"))
我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2`
![](./../assets/runtime-super.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-super.png)
查看 objc4 源代码发现是一段汇编实现。
@@ -2845,7 +2845,7 @@ call - 调用函数
也可以在 Xcode 上可视化面板操作,路径为:菜单栏 `Product -> Perform Action -> Assemble "ViewController.m"`
<img src="./../assets/XcodeAssembleClass.png" style="zoom:30%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeAssembleClass.png" style="zoom:30%">
@@ -2871,7 +2871,7 @@ call - 调用函数
Demo1
<img src="./../assets/RuntimeIsKindOfClassDemo1.png" style="zoom:40%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeIsKindOfClassDemo1.png" style="zoom:40%" />
@@ -2899,7 +2899,7 @@ Demo1
Demo2
<img src="./../assets/RuntimeIsKindOfClassDemo2.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeIsKindOfClassDemo2.png" style="zoom:30%" />
下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass`
@@ -2917,7 +2917,7 @@ Demo2
可以看到 `+(BOOL)isMemberOfClass:(Class)cls` 方法内部就是对当前类获取类对象,然后与传递进来的 cls 判断是否相等。由于是 `[Student isMemberOfClass:[Student class]])` `Student` 类调用类方法 `+isMemberOfClass` 所以类对象的类对象也就是元类对象cls 参数也就是 `[Student class]` 是一个类对象,元类对象等于类对象吗?显然不是
想让判断成立,可以改为 `[Student isMemberOfClass:object_getClass([Student class])]` 或者 `[[Student **class**] isMemberOfClass:object_getClass([Student class])]`
想让判断成立,可以改为 `[Student isMemberOfClass:object_getClass([Student class])]` 或者 `[[Student class] isMemberOfClass:object_getClass([Student class])]`
`+(BOOL)isKindOfClass:(Class)cls` 同理分析。作用是当前类的元类,是否是右边传入对象的元类或者元类的子类。
@@ -2958,7 +2958,7 @@ QA
综合练习:
<img src="./../assets/RuntimeIsKindOfClassUnitDemo.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeIsKindOfClassUnitDemo.png" style="zoom:30%" />
@@ -2966,16 +2966,12 @@ QA
## Runtime 刁钻题
能否向编译后的类添加实例变量class_ro_t 不可以添加。但可以向动态创建的类添加。
### NSObject 的内存布局、isa、对象属性访问原理
> 这道题目设计super 调用的本质、函数栈空间向下增长、runtime 消息调用本质isa、访问对象的成员变量找到 isa约过前面的8字节按照成员变量的大小去找成员
>
> 因为实例对象里存的就是isa + 各个成员变量的值
```objective-c
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@@ -2999,22 +2995,20 @@ QA
程序运行什么结果?
<img src="./../assets/RuntimeQA1.png" style="zoom:30%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeQA1.png" style="zoom:30%">
为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘"
为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘"。我们来分析下:
我们来分析下:
1.**方法调用本质就是寻找 isa 进行消息发送**
第一、**方法调用本质就是寻找 isa 进行消息发送**
```objective-c
Person *person = [[Person alloc] init];
[person sayHi];
```
`[[Person alloc] init]`在内存中分配一块内存,然后 isa 指向这块内存,然后 person 指针,指向结构体,结构体的第一个成员。
`[[Person alloc] init]` 在内存中分配一块内存,然后 isa 指向这块内存,然后 person 指针,指向结构体,结构体的第一个成员。
2.**栈空间数据内存向下生长。第一个变量地址高,其次降低。且每个变量的内存地址是连续的。**
第二、**栈空间数据内存向下生长。第一个变量地址高,其次降低。且每个变量的内存地址是连续的。**
这个流程其实和上面的代码一样的。所以可以正常调用
@@ -3029,9 +3023,9 @@ void test () {
方法内的变量存储在栈上,堆向上增长,栈向下增长。
![](./../assets/runtime-isa-demo.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-isa-demo.png)
3.**实例对象的本质就是一个结构体存储所有成员变量isa 是一个特殊成员变量,其他的成员变量,这里就是 _name`sayHi` 方法内部的 self 就是 obj找成员变量的本质就是找内存地址的过程此时就是偏移8个字节**
第三、**实例对象的本质就是一个结构体存储所有成员变量isa 是一个特殊成员变量,其他的成员变量,这里就是 _name`sayHi` 方法内部的 self 就是 obj找成员变量的本质就是找内存地址的过程,本质就是对象地址 + Offset。此时就是 isa 地址 + 8字节偏移量**
上面代码可以类比类调用方法的流程。 obj 指针指向 Person 这块内存,给类对象发送 `sayHi` 消息也就是通过 obj 指针找到 isa恰好 obj 指针指向的地址就是类对象的类结构体的地址,结构体成员变量第一个就是 isa 指针,结构体的其他成员变量就是类的其他属性,这里也就是 `_name`,所以我们给自定义的指针 `void *p` 调用 sayHi 方法,系统 runtime 在打印 name 的时候,会在 p 附近下8个字节因为 isa 是指针长度为8找 `_name` 属性,此时也就找到了 temp 字符串。
@@ -3046,17 +3040,17 @@ struct Person_IMPL {
再看一个变体1
<img src="./../assets/RuntimeQA2.png" style="zoom:30%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeQA2.png" style="zoom:30%">
打印输出是因为 `*p` 类似 isa 指针。本身占用8字节空间然后访问 `self->_name` 就是 `base + 8 = isa地址 + 8 ` 出的内存就是 name`*p` 是在栈中加8就是向上声明的变量当前情况下 `Address(*p) + 8` 就是 temp 变量。所以输出 `<NSObject: 0x****>`
再看一个变体2
<img src="./../assets/RuntimeQA3.png" style="zoom:30%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeQA3.png" style="zoom:30%">
搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。
分析: 搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。
再强调一句,根据指针寻找成员变量 _name 的过程其实就是根据内存偏移找对象的过程。在变体2中isa 地址就是 class 的地址,所以按照`地址 +8` 的策略,其实前一个局部变量。
@@ -3069,7 +3063,11 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self所以“前一个局部变量” 也就是 selfViewController
![](./../assets/runtime-super-isa-demo.png)
结构体有2个成员变量顺序越高的成员变量地址越高。所以在栈上struct 中第一个成员变量地址更低。在通过 isa 内存偏移的时候,优先找到 self。所以会输出 ViewController
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-super-isa-demo.png)
可能会疑问,知道了 super 调用本质知道了会产生一个局部变量的结构体但是结构体里面2个成员变量找属性的时候isa 下的8个字节会命中哪个
@@ -3079,12 +3077,44 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
<img src="./../assets/RuntimeQA.png" style="zoom: 80%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeQA.png" style="zoom: 80%">
### runtime 对象方法、类方法的查找过程熟悉吗?
下面的代码会 crash 吗?
```objective-c
id rs = [NSObject valueForKey:@"isa"];
NSLog(@"%@", rs);
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeCallValueForKeyOnObject.png" style="zoom:50%" />
不会 Crash。输出的是 NSObject 的元类对象。因为 NSObject 调用的是类方法,先去元类对象的方法列表中查找,发现没有 `valueForKey` 方法。则继续从 NSObject 元类对象的父类对象,也就是 NSObject 的类对象上查找 `valueForKey` 方法。
因为分类 `NSObject(NSKeyValueCoding)` 实现了 `-valueForKey` 方法。所以不会 crash在类对象方法中访问 isa也就是获取类对象的 isa也就是 NSObject 的元类对象。
查看了 objc 源码,会发现很多 NSObject 的基础方法:`+ (id)init`、`- (id)init` 等均有 `+` 、`-` 方法。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSObjectImpleByClassAndInstance.png" style="zoom:30%" />
为什么这么设计?猜测是为了代码的健壮。
- 因为存在继承关系,所有的对象的基类需要有个源头。即使是 NSObject 也是如此。
- 调用对象方法的本质就是根据对象的 isa 找到类对象,然后从方法缓存中去查找方法实现,有就调用,没有就从类对象的方法列表中查找,找到则写入方法缓存并调用,没有则根据 superclass 的类对象继续查找...,一直找到 NSObject 还是找不到,则走 Runtime 消息转发流程。
- 调用类方法的本质是根据对象的 isa 找到类对象,再根据类对象的 isa 找到元类对象,从元类对象的方法方法列表中查找方法实现,如果没有则继续向上查找,直到找到基类的元类对象,也就是 NSObject如果找不到则走消息转发流程
- 但是 Apple 的设计是为了 NSObject 元类对象的父类也要有个东西去接着,于是就让 NSObject 的类对象来充当 。
这也就是为什么 NSObject 子类对象调用 `+` 类方法不 crash 的原因。
## 应用场景
### 统计 App 中未响应的方法。给 NSObject 添加分类 `NSObject+ExceptionHunter.m`,利用 NSProxy 实现
@@ -3121,7 +3151,7 @@ Person *p = [Person new];
object_setClass(p, [Student class]);
```
![](./../assets/runtime-changeisa-demo.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-changeisa-demo.png)
@@ -3150,7 +3180,7 @@ void createClass (void) {
}
```
![](./../assets/runtime-dynamicCreateClass-demo.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-dynamicCreateClass-demo.png)
runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)`
@@ -3229,7 +3259,7 @@ free(properties);
<img src="./../assets/RuntimeDicToObject.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeDicToObject.png" style="zoom:30%" />
不够健壮体现在:
@@ -3369,3 +3399,7 @@ OC 是一门动态性很强的编程语言,允许很多操作推迟到程序
- 无痕埋点
- 热修复
可以认为 oc 中对象上一个指向 CLassObject 地址的变量 `id obj = &ClassObject`
而对象的实例变量 `void *ivar = &obj + offset(N*size)` 。根据 isa 也就是对象的基地址,然后偏移访问 ivar。