iOS安全防护

393 阅读8分钟

一、容器类

通过方法交换,在自定义的方法内添加校验逻辑

二、Unrecognized Selector

对runtime消息转发流程中的第二阶段进行方法交换

- (id)forwardingTargetForSelector:(SEL)aSelector {
}

自定义方法中做未实现的方法异常上报和对应的处理

- (id)zidingyiForwardingTargetForSelector:(SEL)aSelector {
    BOOL aBool = [self respondsToSelector:@selector(selector)];
    NSMethodSignature *signature = [self methodSignatureForSelector:selector];
    /*实现消息转发则正常流转*/
    if (aBool || signature) {
        return [self zidingyiForwardingTargetForSelector:selector];
    } else {
        /*添加方法实现,并返回*/
        zidingyi_reportBugForUnRecoginzeSelector(self.class, selector);
        BMSafeKitObj *safeKitObj = [[BMSafeKitObj alloc]init];
        [safeKitObj addAnyFunc:selector];
        return safeKitObj;
    }
}

动态添加方法实现

- (void)addAnyFunc:(SEL)selector {

    NSString *selectorStr = NSStringFromSelector(selector);

    NSMutableString *m_selectorSr = [selectorStr mutableCopy];

    int count = (int)[m_selectorSr replaceOccurrencesOfString:@":" withString:@"_" options:NSCaseInsensitiveSearch range:NSMakeRange(0, selectorStr.length)];

    NSMutableString *encodeStr = [NSMutableString stringWithFormat:@"%s@:",@encode(int)];

    for (int i = 0; i < count; i++) {
        [encodeStr appendString:@"@"];
    }

    const char *funcTypeEncodeing = [encodeStr UTF8String];

    class_addMethod([BMSafeKitObj class], selector, (IMP)bmSafeKitGodIMP, funcTypeEncodeing);

}

方法实现函数

int bmSafeKitGodIMP(id target, SEL selector, ...) {

    return 0;
}

三、KVO

常见崩溃:

  • 多次移除同一个观察者
  • 未移除观察者removeObserver,dealloc 时崩溃

崩溃)

hook:自定义方法和系统方法进行交换,通过中间代理的方式的给中间代理添加监听, 定义中间代理,同时给NSOBject添加分类获取中间代理对象

- (BMKVOProxy *)kvoProxy {
    id proxy = objc_getAssociatedObject(self, _cmd);
    if (!proxy) {
        proxy = [BMKVOProxy new];
        objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return proxy;
}

hook 添加监听方法,将中间设置成监听者 同时保存keyPath和中间代理对象的映射关系

- (void)bmProxy_addObserverWithProxyItem:(BMKVOProxyItem *)proxyItem didAddBlock:(dispatch_block_t)didAddBlock {
    pthread_mutex_lock(&(_mutex));

    NSMutableSet *set = self.proxyItemMap[proxyItem.keyPath];

    //添加新的监听
    if (!set) {
        set = [NSMutableSet set];
       [self.proxyItemMap setObject:set forKey:proxyItem.keyPath];
    }

    [set addObject:proxyItem];

    pthread_mutex_unlock(&(_mutex));
    didAddBlock();
}

中间代理对象响应obser方法,内部通过keyPath找到代理对象, 将消息转发给真正监听者

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    pthread_mutex_lock(&(_mutex));
    
    __block BMKVOProxyItem *item = nil;
    NSSet *set = self.proxyItemMap[keyPath];
    [set enumerateObjectsUsingBlock:^(BMKVOProxyItem *obj, BOOL * _Nonnull stop) {
        if (object == obj.observed && self == [obj.observer kvoProxy]) {
            *stop = YES;
            item = obj;
        }
    }];

    pthread_mutex_unlock(&(_mutex));
    if (item == nil || item.observer == nil) {
        //观察者被提前释放
        return;
    }

    //判断当前观察者是否实现了方法observeValueForKeyPath:ofObject:change:context
    SEL selector = @selector(observeValueForKeyPath:ofObject:change:context:);
    if([item.observer respondsToSelector:selector]) {
        //分发事件
        [item.observer observeValueForKeyPath:keyPath ofObject:object change:change context:item.context];
    } else {
        [self captureKVOException:@"未实现observeValueForKeyPath" observed:item.observed observer:item.observer keyPath:keyPath];
    }
}

四、野指针防护与监测

方案:hook系统dealloc方法,将要释放的对象和当前线程堆栈进行缓存缓存(缓存有最大阈值,防止内存占用过大),通知修改当前对象的isa指针指向自定义类,通过消息转发的方式防止崩溃,当出现野指针访问时候,自定义类未实现相应方法,所以会走消息转发到自定义处理方法

- (NSMethodSignature *)methodSignatureForSelector: (SEL)sel{
    return [self.originClass instanceMethodSignatureForSelector:sel];
}

- (void)forwardInvocation: (NSInvocation *)invocation{
    [self throwMessageSentExceptionWithSelector: invocation.selector];
}

自定义方法中,获取到当前堆栈信息并上报

主要做了如下内容:

  • 1.需要对那些类监测
    • 获取当前App的类不包含动态库
+ (NSArray *)allClassesInApp{
    const char *imageName = [[NSBundle mainBundle].executablePath UTF8String];
    unsigned int count = 0;
    const char** classes = objc_copyClassNamesForImage(imageName, &count);
    NSMutableSet *set = [NSMutableSet set];
    for (int i = 0 ; i < count; i ++) {
        NSString *className = @(classes[i]);
        [set addObject:className];
    }
    free(classes);
    return set.allObjects;
}

对所有的类使用分类的方式添加监测野指针标识

  • 2.自定义zidingyiDelloc方法对系统方法dealloc进行交换
    • 自定义dealloc方法

- (void)badaccess_dealloc{
    if (self) {
        //这里只能同步,不能切换到子线程,因为在H5中对象释放异步调用会有问题
        //TaggedPointer 需要排除
        if (_objc_isTaggedPointer(self)) {
            return;
        }

        //获取原来的类
        Class oriClass = object_getClass(self);
        //这里获取类名
        const char *name = class_getName(oriClass);
        //获取大小
        uint64_t size = malloc_size(self);

        if (!objc_getAssociatedObject(oriClass, @"jiancebiaoshi")) {
            object_dispose (self);
            return;
        }
        
        if (size > 512) {
            object_dispose(self);
            return;
        }

        //销毁这个对象所持有的其他对象
        objc_destructInstance(self);
     
        //重指isa
        Class BadAccessProxy = NSClassFromString(@"BadAccessProxy");
        object_setClass(self, BadAccessProxy);
        
        //保存原来的类
        objc_setAssociatedObject(self, "BadAccessProxy", oriClass, OBJC_ASSOCIATION_RETAIN);
        
        //加入缓存池
        asycAddPool(self,backtrace);
    }
}
  • 3.缓存池模块
    • 用数组保存释放的对象,并记录大小
    • 使用多个线程实现多读单写的能力

- (instancetype)init {
    self = [super init];
    if (self) {
        // 创建一个并发队列来处理读写操作
        self.queue = dispatch_queue_create("com.example.readwritequeue", DISPATCH_QUEUE_CONCURRENT);
        self.data = [NSMutableArray array]; // 假设我们操作一个数组
    }
    return self;
}

// 读取数据
- (void)readDataWithCompletion:(void (^)(NSArray *data))completion {
    dispatch_async(self.queue, ^{
        // 在读操作中不阻塞,允许多个读操作并发
        completion([self.data copy]);
    });
}

// 写入数据,使用栅栏确保写操作时没有其他读写操作
- (void)writeData:(NSArray *)newData completion:(void (^)(void))completion {
    dispatch_barrier_async(self.queue, ^{
        // 栅栏操作会在所有读操作完成后执行,且会阻塞其他操作,确保数据安全修改
        [self.data removeAllObjects];
        [self.data addObjectsFromArray:newData];
        completion();
    });
}
  • 4.当出现坏内存访问的时候,怎么在消息转发的实现中获取当前线程的调用栈信息并进行符号化 我们需要做如下几件事:

    • 符号化:只有符号化了才能有意义使用命令行的 atos 命令
 atos -o TestApp.app.dSYM/Contents/Resources/DWARF/TestApp -l 0x1042e4000 0x10cd54a10
 -o 后面是你的 dSYM 文件对应的可执行文件路径。
 -l 后面是模块的基地址 0x1042e4000(注意是这行里的 baseAddress,不是崩溃地址)。
 -  0x10cd54a10:是相对基地址的偏移量
 

所以我们需要获取到当前调用栈中的函数地址之后通过作差的方式得到偏移量地址

  • 获取当前线程的调用栈信息:通过获取当前线程的状态能够获取到当前栈帧通过向上遍历得到函数的真实调用地址(基地址+off偏移量)

#if defined(__arm64__) || (__arm__)
    mach_msg_type_number_t state_count = ARM_THREAD_STATE64_COUNT;
    thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&machineContext->__ss, &state_count);
#else

获取当前栈帧的FP
uintptr_t framePtr = kscpu_framePointer(machineContext);  // 

// 遍历栈,最多遍历30层栈帧
for(; i < 30; i++) {
    // 复制当前栈帧内容
    if (bm_mach_copyMem((0 == i) ? (void *)framePtr : frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
        break;  // 如果复制失败,则停止遍历
    }
    
    // 跳过前几个栈帧(例如函数调用链的初期部分)
    if (i >= skippedEntries) {
        // 记录栈帧的返回地址
        context->entys[i-skippedEntries].loadAddress = frame.return_address & 0x0000000FFFFFFFFF;
        
        // 如果没有有效的返回地址或前一层栈帧的 FP 为 0,则停止遍历
        if(context->entys[i-skippedEntries].loadAddress == 0 || frame.previous == 0) {
            break;
        }
        
        // 更新栈帧计数
        context->stackCount = (i - skippedEntries + 1);
    }
}
  • 根据函数地址获取基地址
typedef struct dl_info {
    const char      *dli_fname;     /* 共享对象的路径名 */
    void            *dli_fbase;     /* 共享对象的基地址 */
    const char      *dli_sname;     /* 最接近的符号的名称eg:函数名称 */
    void            *dli_saddr;     /* 最接近的符号的地址 */
} Dl_info;

Dl_info symbols;
if (!ksdl_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(loadAddress), &symbols)) {
    continue;
}

  • 有了基地址计算偏移量
stackFrame.offset = stackFrame.loadAddress - (unsigned long long)symbolsBuffer.dli_fbase;
if (symbolsBuffer.dli_sname && symbolsBuffer.dli_sname > 0) {
    stackFrame.offset =  stackFrame.loadAddress - (unsigned long long)symbolsBuffer.dli_saddr;
}
  • 解释:
  1. stackFrame.loadAddress:

    • 这是栈帧的加载地址(即该栈帧的位置在内存中的地址)。
  2. symbolsBuffer.dli_fbase:

    • 这是 Dl_info 结构体中的字段,表示映像(或共享库)加载到内存中的基础地址,也就是库文件的起始地址。
  3. symbolsBuffer.dli_saddr:

    • 这是 Dl_info 结构体中的字段,表示函数的实际起始地址,也就是该函数的入口地址。
  4. symbolsBuffer.dli_sname:

    • 这是 Dl_info 结构体中的字段,表示符号(函数)的名称。如果这个字段非 NULL,表示符号信息有效。

代码分析:

  1. stackFrame.offset = stackFrame.loadAddress - (unsigned long long)symbolsBuffer.dli_fbase;:

    • 这一行计算栈帧相对于加载地址(即映像的起始地址)的偏移量。dli_fbase 表示映像(或共享库)在内存中的基地址,stackFrame.loadAddress 是栈帧的内存地址。通过这行代码,得到栈帧相对于加载基地址的偏移量。
  2. if (symbolsBuffer.dli_sname && symbolsBuffer.dli_sname > 0):

    • 这里检查符号信息是否有效,即是否有函数的名称。如果 dli_snameNULL,且有有效的名称(即它的地址大于 0),则说明当前栈帧有符号信息,表示该栈帧属于某个函数。
  3. stackFrame.offset = stackFrame.loadAddress - (unsigned long long)symbolsBuffer.dli_saddr;:

    • 如果符号信息有效(即 dli_sname 存在),这行代码计算栈帧相对于该函数入口地址(dli_saddr)的偏移量。这里不再是相对于映像的基地址,而是相对于具体的函数的入口地址计算偏移量。

举例说明:

假设有以下值:

  • stackFrame.loadAddress = 0x1000(栈帧的内存地址)
  • symbolsBuffer.dli_fbase = 0x2000(映像的基地址)
  • symbolsBuffer.dli_saddr = 0x1500(函数的入口地址)
  • symbolsBuffer.dli_sname = "some_function"(函数名有效)

情况 1:符号信息无效(dli_snameNULL

stackFrame.offset = stackFrame.loadAddress - (unsigned long long)symbolsBuffer.dli_fbase;
// 假设此时 dli_sname 是 NULL
  • stackFrame.offset = 0x1000 - 0x2000 = -0x1000
  • 这里,偏移量是相对于映像的基地址计算的,因为 dli_snameNULL,符号信息无效,所以使用的是基地址。

情况 2:符号信息有效(dli_sname 不为 NULL

if (symbolsBuffer.dli_sname && symbolsBuffer.dli_sname > 0) {
    stackFrame.offset = stackFrame.loadAddress - (unsigned long long)symbolsBuffer.dli_saddr;
}
// 假设此时 dli_sname 是有效的
  • stackFrame.offset = 0x1000 - 0x1500 = -0x500
  • 这里,偏移量是相对于函数的入口地址计算的,因为 dli_snameNULL,符号信息有效。所以这次偏移量是从函数的入口地址计算的。

总结:

  • if 外的计算: 如果没有符号信息(即 dli_snameNULL),则计算栈帧相对于映像基地址的偏移量。
  • if 内的计算: 如果符号信息有效(即 dli_snameNULL),则计算栈帧相对于函数入口地址的偏移量。

这样,通过判断是否有符号信息,代码能够决定栈帧的偏移量是从映像基地址开始计算,还是从函数入口地址开始计算。