查看原文
其他

换一个帅一点姿势实现DexHunter

2018-03-28 virjar 看雪学院



DexHunter 需要刷机,感觉太麻烦了,所以用xposed弄了一份。然后大部分解析逻辑都放到了java层,这样也有另一个好处,就是壳总不能修改java正常的api。所以 define class 之类的都可以直接调用 java 层的classloader 来初始化 class。


注意:目前我只在dalvik上面实现,art还没有看。


1. 寻找两个dalvik虚拟机函数,dvmDecodeIndirectRef 和dvmThreadSelf


/**

 * 函数名称表根据4.4的Android版本设置的,不同Android版本映射可能存在差异,可以直接用ida查看维护

 */

void initDvmFunctionTables() {

    void *libVMhandle = dlopen("libdvm.so", RTLD_GLOBAL | RTLD_LAZY);

 

    initDvmFunctionItem("_Z20dvmDecodeIndirectRefP6ThreadP8_jobject",

                        (void **) (&dvmFunctionTables.dvmDecodeIndirectRef), libVMhandle);

    initDvmFunctionItem("_Z13dvmThreadSelfv", (void **) (&dvmFunctionTables.dvmThreadSelf),

                        libVMhandle);

 

 

    dlclose(libVMhandle);

}



2. 定位dex文件


其实我现在还不知道为啥dump内存需要那么麻烦,直接就能在内存中找到啊。看代码


extern "C"

JNIEXPORT jobject JNICALL

Java_com_virjar_xposedhooktool_unshell_Dumper_originDex(JNIEnv *env, jclass type,

                                                        jclass loader) {

    //TODO check & throw exception

    ClassObject *clazz = (ClassObject *) dvmFunctionTables.dvmDecodeIndirectRef(

            dvmFunctionTables.dvmThreadSelf(),

            loader);

    DvmDex *dvm_dex = clazz->pDvmDex;

    return env->NewDirectByteBuffer(dvm_dex->memMap.addr, dvm_dex->memMap.length);

}


这不就直接找到dex文件了嘛。



3. 创建dexFile模型,使用baksmaliAPI


private static DexBackedDexFile createMemoryDexFile(Class loader) {

        ByteBuffer byteBuffer = originDex(loader);

        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);

        byte[] buffer = new byte[byteBuffer.capacity()];

        byteBuffer.get(buffer, 0, byteBuffer.capacity());

 

        if (HeaderItem.verifyMagic(buffer, 0)) {

            return new DexBackedDexFile(Opcodes.forApi(apiLevel()), buffer);

            //a normal dex file

        }

        if (OdexHeaderItem.verifyMagic(buffer, 0)) {

            //this is a odex file

            try {

                ByteArrayInputStream is = new ByteArrayInputStream(buffer);

                DexUtil.verifyOdexHeader(is);

                is.reset();

                byte[] odexBuf = new byte[OdexHeaderItem.ITEM_SIZE];

                ByteStreams.readFully(is, odexBuf);

                int dexOffset = OdexHeaderItem.getDexOffset(odexBuf);

                if (dexOffset > OdexHeaderItem.ITEM_SIZE) {

                    ByteStreams.skipFully(is, dexOffset - OdexHeaderItem.ITEM_SIZE);

                }

                return new DexBackedOdexFile(Opcodes.forApi(Dumper.apiLevel()), odexBuf, ByteStreams.toByteArray(is));

            } catch (IOException e) {

                //while not happen

                throw new RuntimeException(e);

            }

        }

        throw new IllegalStateException("can not find out dex image in vm memory");

    }



4. 使用smali APi rewrite功能,重构Method的指令数据


                    @Nonnull

                    @Override

                    public Method rewrite(@Nonnull final Method value) {

                        if (!(value instanceof DexBackedMethod)) {

                            return super.rewrite(value);

                        }

 

                        Class<?> definingClass;

                        try {

                            definingClass = classLoader.loadClass(value.getDefiningClass());

                        } catch (ClassNotFoundException e) {

                            return super.rewrite(value);

                        }

                        if (definingClass.getClassLoader() != classLoader) {

                            return super.rewrite(value);

                        }

 

                        final Class<?> searchClass = definingClass;

                        final String methodDescriptor = ReferenceUtil.getMethodDescriptor(value);

                        //覆盖 getImplementation

                        return new RewrittenMethod(value) {

 

                            @Nullable

                            @Override

                            public MethodImplementation getImplementation() {

                                ByteBuffer byteBuffer = methodDataWithDescriptor(methodDescriptor, value.getName(), searchClass);

                                if (byteBuffer == null) {

                                    return super.getImplementation();

                                }

                                byteBuffer.order(ByteOrder.LITTLE_ENDIAN);

                                byte[] buffer = new byte[byteBuffer.capacity()];

                                byteBuffer.get(buffer, 0, byteBuffer.capacity());

                                DexBackedMethodImplementation dexBackedMethodImplementation = new DexBackedMethodImplementation(new MethodSegmentDexFile(buffer, dexFile), (DexBackedMethod) value, 0);

                                return rewriters.getMethodImplementationRewriter().rewrite(dexBackedMethodImplementation);

                            }

 

                            @Override

                            public int getAccessFlags() {

                                int accessFlags = getMethodAccessFlagsWithDescriptor(methodDescriptor, value.getName(), searchClass);

                                if (accessFlags < 0) {

                                    //证明没有找到这个方法

                                    return super.getAccessFlags();

                                }

                                return accessFlags;

                            }

                        };

                    }



5. 使用 dalvik dvmFindXXXMethodByDescriptor 功能


寻找对应的 method 对象,进而扣出真实的指令,这里指令长度计算,借用了 DexHunter 的代码


extern "C"

JNIEXPORT jobject JNICALL

Java_com_virjar_xposedhooktool_unshell_Dumper_methodDataWithDescriptor(JNIEnv *env, jclass type,

                                                                       jstring methodDescriptor_,

                                                                       jstring methodName_,

                                                                       jclass searchClass) {

    const char *methodDescriptor = env->GetStringUTFChars(methodDescriptor_, 0);

    const char *methodName = env->GetStringUTFChars(methodName_, 0);

    ClassObject *clazz = (ClassObject *) dvmFunctionTables.dvmDecodeIndirectRef(

            dvmFunctionTables.dvmThreadSelf(),

            searchClass);

 

    jobject ret = NULL;

    Method *method = dvmFindDirectMethodByDescriptor(clazz, methodName, methodDescriptor);

    if (method == NULL) {

        method = dvmFindVirtualMethodByDescriptor(clazz, methodName, methodDescriptor);

    }

    if (method == NULL) {

        goto tail;

    }

 

    //check for native

    uint32_t ac = (method->accessFlags) & accessFlagsMask;

    if (method->insns == NULL || ac & ACC_NATIVE) {

        goto tail;

    }

 

    //why 16

    // 2 byte for registersSize

    // 2 byte for insSize

    // 2 byte for outsSize

    // 2 byte for triesSize

    // 4 byte for debugInfoOff

    // 4 byte for insnsSize

    // and then ,the insns address

    DexCode *code = (DexCode *) ((const u1 *) method->insns - 16);

    uint8_t *item = (uint8_t *) code;

    int code_item_len = 0;

    if (code->triesSize) {

        const u1 *handler_data = dexGetCatchHandlerData(code);

        const u1 **phandler = &handler_data;

        uint8_t *tail = codeitem_end(phandler);

        code_item_len = (int) (tail - item);

    } else {

        //正确的DexCode的大小

        code_item_len = 16 + code->insnsSize * 2;

    }

 

    ret = env->NewDirectByteBuffer(item, code_item_len);

 

    tail:

    env->ReleaseStringUTFChars(methodDescriptor_, methodDescriptor);

    env->ReleaseStringUTFChars(methodName_, methodName);

    return ret;

}



6. binggo,使用smali api,直接解码


/**

     * 将指定loader的smali全部dump到硬盘,请异步执行该函数

     *

     * @param loader loader

     */

    public static void dissembleAllDex(Object loader) {

        Class loaderClass = resolveLoaderClass(loader);

        DexBackedDexFile memoryMethodDexFile = createMemoryDexFile(loaderClass);

        File dumpDir = resolveDumpDir(memoryMethodDexFile);

        DexFile reWritedDexFile = rewrite(memoryMethodDexFile, loaderClass.getClassLoader());

        XposedBridge.log("脱壳目录:" + dumpDir.getAbsolutePath());

        int jobs = Runtime.getRuntime().availableProcessors();

        if (jobs > 6) {

            jobs = 6;

        }

        if (memoryMethodDexFile instanceof DexBackedOdexFile) {

            baksmaliOptions.inlineResolver = InlineMethodResolver

                    .createInlineMethodResolver(((DexBackedOdexFile) memoryMethodDexFile).getOdexVersion());

        }

        Log.i("weijia", "开始进行脱壳");

        if (Baksmali.disassembleDexFile(reWritedDexFile, dumpDir, jobs, baksmaliOptions)) {

            Log.i("weijia", "脱壳完成,但是存在错误");

        } else {

            Log.i("weijia", "脱壳成功,请在" + dumpDir + "中查看smali文件");

        }

        Toast.makeText(SharedObject.context, "脱壳完成,请在" + dumpDir + "中查看smali文件", Toast.LENGTH_LONG).show();

    }


7.binggo,使用smali API,输出dex文件


/**

     * 将对应class对应的dex文件的二进制dump出来

     *

     * @param loader 该dex文件定义的任何一个class,或者class定义的object

     * @return 一个byteBuffer,包含了二进制数据

     */

    public static ByteBuffer dumpDex(Object loader) {

        Class loaderClass = resolveLoaderClass(loader);

        DexBackedDexFile memoryDexFile = createMemoryDexFile(loaderClass);

        byte[] buf = (byte[]) XposedHelpers.getObjectField(memoryDexFile, "buf");

 

        DexFile dexFile = rewrite(memoryDexFile, loaderClass.getClassLoader());

        final DexBuilder builder = new DexBuilder(Opcodes.forApi(apiLevel()));

        MemoryDataStore memoryDataStore = new MemoryDataStore(buf.length);

        for (ClassDef classDef : dexFile.getClasses()) {

            try {

                buildClassDef(classDef, builder);

            } catch (Exception e) {

                Log.i("weijia", "error when define class:" + classDef.getType() + " skipped for rebuild it");

            }

        }

        try {

            builder.writeTo(memoryDataStore);

        } catch (IOException ioe) {

            //the memory writer,no ioe happend

            throw new RuntimeException(ioe);

        }

        return ByteBuffer.wrap(memoryDataStore.getData());

    }



7. 如何调用


XposedHelpers.findAndHookConstructor(Activity.class, new XC_MethodHook() {

            @Override

            protected void afterHookedMethod(MethodHookParam param) throws Throwable {

                Object activity = param.thisObject;

                if (activity == null) {

                    return;

                }

                XposedBridge.log("hook class " + activity.getClass());

                if (StringUtils.equalsIgnoreCase(activity.getClass().getName(), "com.xxx.xxx.MainActivity")) {

                    Dumper.dumpDex(activity);

                }

            }

        });



8. 对了你需要移植dalvik的dexlib模块代码,还有/vm/oo包下面的代码,到你的jni环境下。要不然没有dex的相关数据结构,也没有ClassObject的数据结构。


然后,不愿意放整个工程,不要求代码。论文放出来,还是自己实现一遍才能有收获。


其他:


相比原始DexHunter的优点


1. 定位dex更加精准,DexHunter用过filename来确定当前dex是不是需要处理,很容易被加壳平台识别到这个特征。而且每次都要在放Android系统push一些文件,比较麻烦。我这个,直接通过class对象寻找,想找按个找那个。


2. 并发,可能没有写过Linux c语言程序,看那个pthread哪里看不懂。DexHunter为了防止多次重复处理同一个dex文件,写了比较复杂的加锁逻辑。这个放到java层,很简单实现吧。


3. 多dex,如果一个apk多个dex都需要处理。DexHunter不好处理,因为他输出就是whole.dex


4. dvmDefineClass失败,就算正常情况,一个classLoader下面的class,也不是全部可以正常load成功。DexHunter的流程是删除这些bad class,我还可以尽可能的使用原始dex数据进行解码。


5. Dalvik_dalvik_system_DexFile_defineClassNative被替换,这可能导致defineClassNative函数不被调用,这样DexHunter无法拦截到。我用classLoader.loadeClass,java标准接口,这个他永远没法替换。


6.其他数据结构加密调整,如果未来不光光是method的数据变化了。由于使用baksmali建立的模型,任何数据结构都很容易重构替换,毕竟是java,抽象封装很好用。


7.脱壳时机,脱壳输出控制更加方便。提供的是api,你调用就脱壳不调用就不脱壳。脱壳结果是java的二进制流,你想怎么编码、加密、转储都很方便。java层的api太多了。


8.可移植性,是一个普通的xposed项目,任何一个有xposed环境的Android机器都可以(当然现在还没有实现art,不过理论上没问题)。也不需要编译系统镜像。等几天再把xposed包装一下,免root脱壳。


当然,我对dex文件格式,并不是非常熟,反正没有DexHunter玩儿的那么溜,所以只有多用别人的api了。





本文由看雪论坛 virjar 原创

转载请注明来自看雪社区



往期热门阅读:



点击阅读原文/read,

更多干货等着你~

扫描二维码关注我们,更多干货等你来拿!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存