2019-04-19

另一款免Root使用Xposed的解决方案

作者:administrator 所属分类 - 干货

前段时间,安卓上有人开发出来了热升级修复的方案,从此这个方案不仅用于热升级修复,也用于动态Hook。目前安卓上的Hook方案很多,不过兼容Xposed的方案比较少。在看了 ganyao 大佬的一些文章以后,就开始着手实现对安卓的免Root实现加载Xposed的解决方案。

相信大家都对Windows下的Hook有自己的见解,Windows下的Hook比较简单,特别是WinAPI的一些Hook。而安卓的实现也非常类似。目前来说,安卓每个方法对应着一个 ArtMethod , ArtMethod 保存了该方法是否私有、方法所属类、地址等等。替换这个方法的 ArtMethod ,我们就可以实现Hook或者修改这个方法。但是安卓5.0以后,不再是简单的JIT编译执行。在App运行的时候,是有解释和跳转到编译代码运行等几种模式的。

这里我们再谈安卓的Hook和热修复升级的关系,最开始,手淘是基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术——Dexposed。 但该方案对于底层Dalvik结构过于依赖,最终无法兼容Android 5.0 以后的ART虚拟机,因此作罢。

手机淘宝的基于DeXposed的热升级技术在Android 5.0以后没法使用了,这个时候支付使用了另一种修复技术:AndFix。后面阿里百川使用了一种综合各优点的HotFix方案,这个方案和后来的 Sophix 方案都被广泛使用。

说完热修复升级,我们再回到安卓的Hook。我们需要的安卓Hook跟热升级修复方案有着很多相同的目标:例如底层替换、实时生效、类修复反射注入等等。

这个时候,我们可以使用这些热修复升级方案,取得我们所需要的Art方法信息,然后我们对这些信息进行修改,还原回去即可。 当一个非静态方法被热替换后,再反射调用这个方法,会抛出异常。因此我们只能在替换以后,再重新启动应用,无法实时生效。而类静态方法不受这个限制,我们可以在运行当中动态替换方法。因此不需要结束一整个沙盒而实现动态替换等等。

一个类的加载,必须经历resolve->link->init三个阶段,** 父类/实现接口权限控制检查** 主要发生在link阶段。我们来看代码:

bool dvmLinkClass(ClassObject* claszz) {
...
    if (clazz->status == CLASS_IDX) {
        ...
        if (clazz->interfaceCount > 0) {
            for (i = 0; i < clazz->interfaceCount: i++) {
                assert(interfaceIdxArray[i] != kDexNoIndex);
                clazz->interfaces[i] = dvmResolveClass(clazz, interfaceIdxArray[i], false);
                ...
                /* are we aoolowed to implement this interface?  */
                if  (!dvmCheckClassAccess(clazz, clazz->interfaces[i])) {
                    dvmLinearReadOnly(clazz->classLoader, clazz->interfaces);
                    ALOGW("Interface '%s' is not accessible to '%s' ", clazz->interfaces[i]->descriptor, clazz->descriptor);
                    dvmThrowIllegalAccessError("interface not accessible");
                    goto bail;
                }
            }
        }
    }
    ...
    if (strcmp(class->descriptor, "Ljava/lang/Object;") == 0){
    ...
    } else {
        if (clazz->super == NULL) {
            dvmThrowLinkageError("no superclass defined");
            goto bail;
        } else if (!dvmCheckClassAccess(clazz, clazz->super)) { // 检查父类的访问权限
            ALOGW("Superclass of '%s' (%s) is not accessible", clazz->descriptor, clazz->super->descriptor);
            dvmThrowIllegalAccessError("superclass not accessible");
            goto bail;
        }
    }
}

在上述代码实例上可以看到,一个类的link阶段,会一次对当前类实现的接口和父类进行访问权限检查。接下来看一下dvmCheckClassAccess的具体实现:

bool dvmCheckClassAccess(const ClassObject* accessFrom, const ClassObject* clazz) {
    if (dvmIsPublicClass(clazz)) { // 如果父类是public类,直接return true
        return true;
    return dvmInSamePackage(accessFrom, clazz);
}
 
bool dvmInSamePackage(const ClassObject* class1, const ClassObject* class2) {
    /* quick test for instr-class access */
    if (class1 == class2) {
        return true;
    }
 
    /* class loaders must match */
    if (class1->classLoader != class2->classLoader) { // classLoader不一致,直接return false
        return false;
    }
 
    if (dvmIsArrayClass(class1))
        class1 = class1->elementClass;
    if (dvmIsArrayClass(class2))
        class2 = class2->elementClass;
    
    /* check again */ 
    if (class1 == class2)
        return true; 
 
    int commonLen;
 
    commonLen = strcmpCount(class1->descriptor, class2->descriptor);
    if (strchr(class1->descriptor + commonLen, '/') != NULL || 
        strchr(class2->descriptor + commonLen, '/') != NULL) {
        return false;
    }
 
    return true;
}

我们可以看到如果当前类和实现接口/父类是否public,同时负责加载两者的classLoader不一样的情况下,直接return false.

所以如果此时不进行任何处理的话,那么在类的加载阶段就会报错。
如果补丁类中存在非public类的访问,非public方法/域的调用,那么都会失败。更为致命的是,在Hook加载是检测不出来的,Hook的内容会被正常加载,但是在运行阶段会直接crash。而我们实现Hook的方式就是将App安装进沙盒,然后Hook自己实现Hook外部App。

基于这些限制,我们可以先杀死沙盒里面正在运行的App,然后Hook生效,这个时候我们就可以再运行这个App了。这里我们看一个巧妙的实现:

ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx, bool fromUnverifiedConstant) {
    DvmDex* pDvmDex = referrer -> pDvmDex;
    ClassObject* resClass;
    const char* className;
 
    /*
     * Check the table first -- this gets called from the other "resolve"
     * methods;
    */
    // 提前把patch类加入到pDvmDex.pResClasses数组,resClass结果不为NULL
    resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
 
    if (resClass != NULL) {
        return resClass;
    }
 
    className = dexStringByTypeIdx(pDvmDex->pDexFile, classIdx);
    if (className[0] != '\0' && className[1] == '\0') {
        /* primitive type */
        resClass = dvmFindPrimitiveClass(className[0]);
    } else {
        resClass = dvmFindClassNoInit(className, referrer->classLoader);
    }
 
    if (resClass != NULL) {
        // fromUnverifiedConstant变量设为true,绕过dex一致性校验
        if (!fromUnverifiedConstant && IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)) {
            ClassObject* resClassCheck = resClass;
            if (dvmIsArrayClass(resClassCheck)) {
                resClassCheck = resClassCheck->elementClass;
            }
 
            if (referrer->pDvmDex != resClassCheck->pDvmDex && resClassCheck->ClassLoader != NULL) {
                dvmThrowIllegalAccessError("Class ref in pre-verified class resolved to unexpected", "implementation");
                return NULL;
            }
        }
        // 这里的dvmDexSetResolvedClass与前面的dvmDexGetResolvedClass前后呼应,说白了就是get为null后就去set。
        dvmDexSetResolvedClass(pDvm, classIdx, resClass);
    }
    return resClass;
}

到这里,我们就可以实现Hook了。这里我们使用的是native的内联hook。基于热升级的原理,我们可以这样完成对自己的Inline Hook:

    JumpTrampoline:Hook链表的头节点,一段跳转指令,覆盖原方法前几字节,将会跳转到HookTrampoline。
    HookTrampoline:判断是否需要Hook,如果是,设置r0为Hook方法并跳转到Hook方法,否则跳转到下一个HookTrampoline(多个相似的不同方法可能会共用相同的指令,因此多个方法Hook将形成一个链表结构)。
    OriginalTrampoline:Hook链表的尾节点,用于恢复原方法执行流。
    Hook方法:执行想要的逻辑,修改原方法参数、屏蔽原方法调用(Hook方法通过调用Forward方法来实现原方法调用)。
    Forward方法:一个静态的native方法。没有方法体、也不会被实际调用,如其名仅仅起到Forward的作用,方法EntryPoint将会被TargetTrampoline替换。
    TargetTrampoline:用于执行原方法,设置r0为原方法并恢复原方法执行流。

但是,基于这个实现还有不足,那就是这个方法缺少编译后的机器码。不过我们也有一个解决方案:

Inline模式要求方法必须有编译后的机器代码,而7.0之后默认不会进行AOT编译。
因而必须找到一个能编译方法的方案。幸运的是Android默认的JIT便提供了这样的方法:“jit_compile_method”。
该方法由libart-compile.so导出,可以利用dlsym获取(7.0之后限制了dlsym,改用enhanced_dlsym代替,不仅支持.dynsym(动态符号表)查询,还支持.symtab(符号表)查询)。
值得注意的是,JIT编译会改变线程状态,为了线程保持正确的状态,编译完成后需要恢复线程状态。

下列几种情况下将Hook失败:


    JIT编译失败。
    编译后的指令长度小于JumpTrampoline的长度。
    Native方法(没有实际方法体因此也不能Hook)。

到了这里,我们就大概知道怎么实现Xposed模块的加载了。我们需要一个Xposed的兼容层,这里直接给出例子:

https://github.com/lianglixin/SandVXposed

这个应用基于SandHook制作,能在Android 4.x到Android Q上运行(直到20190418),可以加载一些Xposed模块。

art1.jpg
art2.jpg

从Android N以后,需要禁用虚拟机优化,否则方法被优化以后就没法Hook了。具体的表现是App打开多几次就会没法Hook,我们需要禁用优化解决。
自从Android P以后,谷歌对部分导出函数设置了限制,这些导出函数是隐藏的,如果我们想调用,必须以系统的身份来调用。但是这个方法可能会在未来失效。目前来说临时的解决方法就是伪装成系统来调用这些导出函数,或者反射(个人更倾向前者)。

Demo使用的是沙盒方法,而不是重新打包应用的方法。重新打包应用可能触发应用的签名校验,从而使得应用检测到被修改,直接的后果就是“封号”。

基于这些,我们就可以去解读SandHook和Demo的源码了。Demo使用SandHook实现了Hook,我们可以发挥自己的想象力,做出自己喜欢的模块。

隐藏