换一个帅一点姿势实现DexHunter

https://bbs.pediy.com/thread-225427.htm

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());
    }

8.如何调用

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);
                }
            }
        });

9.对了你需要移植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了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注