iOS Hook 框架 AnyMethodLog与Aspects分析

2,962 阅读5分钟

最近研究了一下iOS平台上几个hook框架的hook方案,写文记录一下分析的过程

现有hook框架

  1. AnyMethodLog
  2. Aspects

AnyMethodLog hook 方案分析

Hook 代码

//替换方法
BOOL qhd_replaceMethod(Class cls, SEL originSelector, char *returnType) {
    Method originMethod = class_getInstanceMethod(cls, originSelector);
    if (originMethod == nil) {
        return NO;
    }
    const char *originTypes = method_getTypeEncoding(originMethod);
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    if (qhd_isStructType(returnType)) {
        //Reference JSPatch:
        //In some cases that returns struct, we should use the '_stret' API:
        //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
        //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
        NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:originTypes];
        if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
            msgForwardIMP = (IMP)_objc_msgForward_stret;
        }
    }
#endif
    
    IMP originIMP = method_getImplementation(originMethod);
    
    if (originIMP == nil || originIMP == msgForwardIMP) {
        return NO;
    }
    
    //把原方法的IMP换成_objc_msgForward,使之触发forwardInvocation方法
    class_replaceMethod(cls, originSelector, msgForwardIMP, originTypes);
    
    //把方法forwardInvocation的IMP换成qhd_forwardInvocation
    class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)qhd_forwardInvocation, "v@:@");
    
    //创建一个新方法,IMP就是原方法的原来的IMP,那么只要在qhd_forwardInvocation调用新方法即可
    SEL newSelecotr = qhd_createNewSelector(originSelector);
    BOOL isAdd = class_addMethod(cls, newSelecotr, originIMP, originTypes);
    if (!isAdd) {
        DEV_LOG(@"class_addMethod fail");
    }
    
    return YES;
}

阐述一下具体的过程:

  1. 如何让方法每次都走_objc_msgForward呢?把原来的 sel的IMP改成_objc_msgForward.

  2. 这时我们需要保存原来的 IMP 然后hook forwardInvocation ... 换成自己的实现,调用原来的IMP和新增的代码

从代码很明显的可以看出,这是利用OC的消息转发机制,选择了合适的时机,进行打桩。 相较于传统的Swizzle方法,这种方法打主桩,是有可行性的。 并且在ForwardInvocation: 处理,虽然相较其余两个转发机制调用的方法的消耗大,但是更灵活一些,最切合问题。

Aspects

Aspects 的代码我看的比较仔细,相对于AnyMethodLog, Aspects 对Hook的处理更成熟,各种情况都做了考虑,这里来重点分析下。

相较于AnyMethodLog, Aspects 不仅可以hook类,也可以对实例进行hook, 粒度更小,适用的场景更加多样化。

这是Aspects Hook 的代码,可以看到对实例和类的处理是不同的。

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
	Class statedClass = self.class;
	Class baseClass = object_getClass(self);
	NSString *className = NSStringFromClass(baseClass);

    // Already subclassed
	if ([className hasSuffix:AspectsSubclassSuffix]) {
		return baseClass;

        // We swizzle a class object, not a single object.
	}else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
        // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
    }else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

    // Default case. Create dynamic subclass.
	const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
	Class subclass = objc_getClass(subclassName);

	if (subclass == nil) {
		subclass = objc_allocateClassPair(baseClass, subclassName, 0);
		if (subclass == nil) {
            NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
            AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
            return nil;
        }

		aspect_swizzleForwardInvocation(subclass);
		aspect_hookedGetClass(subclass, statedClass);
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
		objc_registerClassPair(subclass);
	}

	object_setClass(self, subclass);
	return subclass;
}

先看看对实例的处理

		subclass = objc_allocateClassPair(baseClass, subclassName, 0);
		aspect_swizzleForwardInvocation(subclass);
		aspect_hookedGetClass(subclass, statedClass);
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
		objc_registerClassPair(subclass);
	    object_setClass(self, subclass);

熟悉kvo原理的同学,一眼就应该看明白了,这是做了什么事情。 这里可谓是相当巧妙的避免了父类和子类实例hook相同的IMP可能导致的循环调用问题。(下一部分会说明如何避免的)

对类的hook和AnyMethodLog十分类似。就不再多阐述了。网上相关介绍 使用 forwardInvocation+hook类 的资料很多。

Aspects和AnyMethodLog都是利用了forwardInvocation进行处理,这是一致的。

现行Hook方案的问题

自己经常hook的同学可能会发现,在hook时,会出现调用循环的问题。

无论是AnyMethodLog 和 Aspects 都无法同时hook 父类和子类的同一个方法到一个相同的IMP上。为什么呢?

思考一下为什么会出现循环调用? 那必定是,调用方又被调用者调用了一次,在iOS Hook 中,如果我们hook 了 父类和子类的同一个方法,让他们拥有相同的实现,就会出现这种问题。

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现 假设我们现在对UIView、UIButton都Hook了initWithFrame:这个方法,在调用[[UIView alloc] initWithFrame:]和[[UIButton alloc] initWithFrame:]都会定向到C函数qhd_forwardInvocation中,在UIView调用的时候没问题。但是在UIButton调用的时候,由于其内部实现获取了super initWithFrame:,就产生了循环定向的问题。

Aspects 中,Hook 之前,是要对能否hook 进行检查了,对于类,有严格的限制,对于实例则没有限制。

类为什么要限制,上面已经阐释了,那么实例为什么可以呢?

这就是 实例Hook 实现方式所产生的结果。

来理一下实例hook怎么实现的:

  1. 生成子类
  2. hook 子类的forwardInvocation(这是一系列操作,不过这个尤为重要)
  3. 对实例的类信息进行伪装

如果我们有 ClassA 的 实例 a, SubClassA 的 实例 suba. 对他们进行hook viewdidload 方法, 那么会生成两个子类,我们记为prefix_ClassA, prefix_SubClassA,我们对forwardInvocation IMP的替换,实际上是在这两个类上进行的。

当方法调用时: suba -> forwardInvocation(我们替换的IMP) ->self viewdidload(SubClassA 的IMP) -> super viewdidload(ClassA的实现) 这显然不会导致循环的问题。

如果是真正的消息转发响应的处理,有兴趣的同学可以看一下。

https://github.com/steipete/Aspects/blob/master/Aspects.m#L508

JSPatch 的方法替换也是利用了 forwardInvocation进行处理。

如果有错误,希望指出,共同学习

我的博客

如果各位同学对文章有什么疑问或者工作之中遇到一些小问题都可以在群里找到我或者其他群友交流讨论,期待你的加入哟~

Emmmmm..由于微信群人数过百导致不可以扫码入群,所以请扫描上面的小姐姐二维码,加完微信好友后回复“iOS”验证身份即会被邀请入群。