ADVMP 三代壳(vmp加固)原理分析(加壳流程)

发布时间 2023-04-14 12:33:04作者: 明月照江江

开源项目地址

https://github.com/chago/ADVMP

vmp 加固可以说时各大加固厂商的拳头产品了,这个开源项目虽然不是十分完善,让我们可以一览vmp加固的原理,是十分好的学习资源

vmp 全称: virtual machine protect , 本质是将原来smali对应的代码转化为自定义的代码,然后通过自定义的解释器进行解释和执行
ADVMP 实现了 基本计算相关指令的解释和执行,而一些调用 ,引用 framework 相关api的部分没有实现,但也可以一窥究竟了
源码目录说明
AdvmpTest:测试用的项目。
base:Java项目。里面是一些工具类代码。
control-centre:Java项目。控制加固流程。
separator:Java项目。抽离方法指令,然后将抽离的指令按照自定义格式输出,并同时输出C文件。
template/jni:C代码。里面包含了解释器的代码。
ycformat:自定义的文件格式,用于保存抽取出来指令等数据。

加壳流程分析

control-centre 的 EntryPoint 是加固流程的入口

public static void main(String[] args) {
        log.info("------ 进入控制中心 ------");
        try {
           ......

            ControlCentre controlCentre = new ControlCentre(opt);
            log.info("开始加固。");
            if (controlCentre.shell()) {
                //log.info
            }

        } catch (ParseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        log.info("------ 离开控制中心 ------");
    }

主要是执行controlCentre.shell() 这里是加壳主流程

    public boolean shell() {
        boolean bRet = false;
        try {
            // 1. 找到所谓的第一个类,比如Application 或者MainActivity
            TypeDescription classDesc = AndroidManifestHelper.findFirstClass(new File(mApkUnpackDir, "AndroidManifest.xml"));
            //2. 找到第一个类的clinit方法,在当中插入System.loadLibrary指令
            InstructionInsert01 instructionInsert01 = new InstructionInsert01(new File(mApkUnpackDir, "classes.dex"), classDesc);
            instructionInsert01.insert();

            // 3. 运行抽离器。将codeItem 抽出,转换,打包 生成yc文件
            runSeparator();

            // 4. 从template目录中拷贝jni文件。
            copyJniFiles();

            // 5. 更新jni文件的内容。
            updateJniFiles();

            // 6. 编译native代码。
            buildNative();

            // 7. 将libs目录重命名为lib。
            mOpt.libDir = new File(mOpt.jniDir.getParentFile(), "lib");
            new File(mOpt.jniDir.getParentFile(), "libs").renameTo(mOpt.libDir);

            // 8. 移动yc文件。
            File assetsDir = new File(mApkUnpackDir, "assets");
            if (!assetsDir.exists()) {
                assetsDir.mkdir();
            }
            File newYcFile = new File(assetsDir, "classes.yc");
            Files.move(mOpt.outYcFile.toPath(), newYcFile.toPath());

            // 9. 移动classes.dex文件。
            Utils.copyFile(new File(mOpt.outYcFile.getParent(), "classes.dex").getAbsolutePath(), new File(mApkUnpackDir, "classes.dex").getAbsolutePath());
            // 10. 拷贝lib目录。
            Utils.copyFolder(mOpt.libDir.getAbsolutePath(), mApkUnpackDir.getAbsolutePath() + File.separator + "lib");

            // 11. 打包
            String name = mOpt.apkFile.getName();
            name = name.substring(0, name.lastIndexOf('.'));
            File outApkFile = new File(mOpt.outDir, name + ".shelled.apk");
            ZipHelper.doZip(mApkUnpackDir.getAbsolutePath(), outApkFile.getAbsolutePath());

            bRet = true;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bRet;
    }

看一下比较核心的 3.运行抽离器

    private boolean runSeparator() throws IOException {
        SeparatorOption opt = new SeparatorOption();
        opt.dexFile = new File(mApkUnpackDir, "classes.dex");
        File outDir = new File(mOpt.workspace, "separator");
        opt.outDexFile = new File(outDir, "classes.dex");
        opt.outYcFile = mOpt.outYcFile = new File(outDir, "classes.yc");
        opt.outCPFile = mOpt.outYcCPFile = new File(outDir, "advmp_separator.cpp");

        Separator separator = new Separator(opt);
        return separator.run();
    }

核心流程:是为了生成classes.ycadvmp_separator.cpp
classes.yc 是一个按格式规则写入的文件,类似之前的二代壳,但这里多了一部指令替换,逆向人员拿到这个文件写入也反编译不出来,(还需要拿到对照表将指令还原才可以)
advmp_separator.cpp 是一个生成的cpp模板代码文件(可以看出壳的本质还是借助生成CPP,然后加入到native源码中打包生成so完成的)

看一下 separator.run()
    public boolean run() {
        boolean bRet = false;
        // 1. 重新生成dex(重要)。
        DexFile newDexFile = mDexRewriter.rewriteDexFile(mDexFile);
        try {
            // 2. 将新dex输出到文件。
            DexFileFactory.writeDexFile(mOpt.outDexFile.getAbsolutePath(), newDexFile);

            // 3.写Yc文件。
            writeYcFile();

            // 4.写C文件。
            writeCFile();

            bRet = true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bRet;
    }

这个mDexRewriter 就很精髓,利用的是dexlib2的能力,对dex中的方法进行了重写

 @Nonnull
        @Override
        public Rewriter<Method> getMethodRewriter(Rewriters rewriters) {
            return new MethodRewriter(rewriters) {
                @Nonnull
                @Override
                public Method rewrite(@Nonnull Method value) {
                    if (mConfigHelper.isValid(value)) {
                        mSeparatedMethod.add(value);
                        // 抽取代码。
                        YcFormat.SeparatorData separatorData = new YcFormat.SeparatorData();
                        separatorData.methodIndex = mSeparatorData.size();
                        separatorData.accessFlag = value.getAccessFlags();
                        separatorData.paramSize = value.getParameters().size();
                        separatorData.registerSize = value.getImplementation().getRegisterCount();

                        separatorData.paramShortDesc = new StringItem();
                        separatorData.paramShortDesc.str = MethodHelper.genParamsShortDesc(value).getBytes();
                        separatorData.paramShortDesc.size = separatorData.paramShortDesc.str.length;

                        separatorData.insts = MethodHelper.getInstructions((DexBackedMethod) value);
                        separatorData.instSize = separatorData.insts.length;
                        separatorData.size = 4 + 4 + 4 + 4 + 4 + separatorData.paramShortDesc.size + 4 + (separatorData.instSize * 2) + 4;
                        mSeparatorData.add(separatorData);

                        // 下面这么做的目的是要把方法的name删除,否则生成的dex安装的时候会有这个错误:INSTALL_FAILED_DEXOPT。
                        List<? extends MethodParameter> oldParams = value.getParameters();
                        List<ImmutableMethodParameter> newParams = new ArrayList<>();
                        for (MethodParameter mp : oldParams) {
                            newParams.add(new ImmutableMethodParameter(mp.getType(), mp.getAnnotations(), null));
                        }

                        // 生成一个新的方法。
                        return new ImmutableMethod(value.getDefiningClass(), value.getName(), newParams, value.getReturnType(), value.getAccessFlags() | AccessFlags.NATIVE.getValue(), value.getAnnotations(), null);
                    }

                    return super.rewrite(value);
                }
            };
        }

重写过程中生成了YcFormat(用于生成Yc文件)和mSeparatedMethod(一个Method对象列表)
最后返回了一个空方法
new ImmutableMethod(value.getDefiningClass(), value.getName(), newParams, value.getReturnType(), value.getAccessFlags() | AccessFlags.NATIVE.getValue(), value.getAnnotations(), null);
实现dex中方法代码的抽离

然后调用writeYcFile() 对YcFormat对象进行解析和写入文件(和二代壳类似)
然后调用 writeCFile() (关键)
    private void writeCFile() throws IOException {
        SeparatorCWriter separatorCWriter = new SeparatorCWriter(mOpt.outCPFile, mSeparatedMethod);
        separatorCWriter.write();
    }

separatorCWriter.write

    public void write() throws IOException {
        try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(mOutFile))) {
            int index = 0;
            for (Method method : mSeparatedMethod) {
                String definingClass = method.getDefiningClass();
                if (classes.containsKey(definingClass)) {
                    classes.get(definingClass).add(method);
                } else {
                    List<Method> ms = new ArrayList<>();
                    ms.add(method);
                    classes.put(definingClass, ms);
                }

                writeMethod(index, method, fileWriter);
                index++;
            }

            write_registerNatives(fileWriter);

            fileWriter.write("void registerFunctions(JNIEnv* env) {");
            fileWriter.newLine();
            for (String registerNativesName : registerNativesNames) {
                fileWriter.write(String.format("if (!%s(env)) { MY_LOG_ERROR(\"register method fail.\"); return; }", registerNativesName));
                fileWriter.newLine();
            }
            fileWriter.newLine();
            fileWriter.write("}");
            fileWriter.newLine();
        }
    }

这里就是想解析mSeparatedMethod(方法列表),生成一个动态注册的模板代码

private void writeMethod(int index, Method method, BufferedWriter fileWriter) throws IOException {
        StringBuffer sb = new StringBuffer();
        sb.append(MethodHelper.genTypeInNative(method));
        sb.append(" ");
        sb.append(method.getName());
        sb.append(" (");
        sb.append(MethodHelper.genParamTypeListInNative(method));
        sb.append(") {");
        fileWriter.write(sb.toString());
        fileWriter.newLine();

        sb.delete(0, sb.length());
        sb.append("jvalue result = BWdvmInterpretPortable(gAdvmp.ycFile->GetSeparatorData(");
        sb.append(index);
        sb.append("), env, thiz");

        List<? extends CharSequence> params = method.getParameterTypes();
        for (int i = 0; i < params.size(); i++) {
            sb.append(", ");
            sb.append(MethodHelper.paramNames[i]);
        }
        sb.append(");");
        fileWriter.write(sb.toString());
        fileWriter.newLine();

        sb.delete(0, sb.length());
        sb.append("return ");
        char cType = method.getReturnType().charAt(0);
        switch (cType) {
            case 'Z':
                sb.append("result.z");
                break;
            case 'B':
                sb.append("result.b");
                break;
            case 'S':
                sb.append("result.s");
                break;
            case 'C':
                sb.append("result.c");
                break;
            case 'I':
                sb.append("result.i");
                break;
            case 'J':
                sb.append("result.j");
                break;
            case 'F':
                sb.append("result.f");
                break;
            case 'D':
                sb.append("result.d");
                break;
            case 'L':
                sb.append("result.l");
                break;
            case '[':
                sb.append("result.l");
                break;
        }
        sb.append(";}");
        fileWriter.write(sb.toString());
        fileWriter.newLine();
    }

每个java侧对应的native方法,都由BWdvmInterpretPortable进行转发执行,这个方法十分关键,会转发给自定义解释器进行执行

到这里 抽取步骤就完成了,

然后是构建生成so的步骤,即ControlCenter shell的后续
            // 从template目录中拷贝jni文件。
            copyJniFiles();

            // 更新jni文件的内容。
            updateJniFiles();

            // 编译native代码。
            buildNative();

copyJniFiles和buildNative都是常规操作, 关键是updateJniFiles,这里有对模板代码进一步的更新

    private void updateJniFiles() throws IOException {
        File file;
        File tmpFile;
        StringBuffer sb = new StringBuffer();

        // 更新avmp.cpp文件中的内容。
        try (BufferedReader reader = new BufferedReader(new FileReader(mOpt.outYcCPFile))) {
            String line = null;
            while (null != (line = reader.readLine())) {
                sb.append(line);
                sb.append(System.getProperty("line.separator"));
            }
        }

        file = new File(mOpt.jniDir.getAbsolutePath() + File.separator + "advmpc" + File.separator + "avmp.cpp");
        tmpFile = new File(mOpt.jniDir.getAbsolutePath() + File.separator + "advmpc" + File.separator + "avmp.cpp" + ".tmp");
        try (BufferedReader reader = new BufferedReader(new FileReader(file));
             BufferedWriter writer = new BufferedWriter(new FileWriter(tmpFile))) {
            String line = null;
            while (null != (line = reader.readLine())) {
                if ("#ifdef _AVMP_DEBUG_".equals(line)) {
                    writer.write("#if 0");
                    writer.newLine();
                } else if ("//+${replaceAll}".equals(line)) {
                    writer.write(sb.toString());
                } else {
                    writer.write(line);
                    writer.newLine();
                }
            }
        }
        file.delete();
        tmpFile.renameTo(file);
        sb.delete(0, sb.length());
    }

这里是将advmp_separator.cpp 的代码和advmp.cpp 的代码合并,生成新的advmp.cpp,看一下编译选项
template/jni/Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := advmp

LOCAL_SRC_FILES := ioapi.c \
				   unzip.c \
				   Globals.cpp \
				   avmp.cpp \   # 我们生成的代码文件
				   BitConvert.cpp \
				   InterpC.cpp \
				   io.cpp \
				   Utils.cpp \
				   YcFile.cpp 

LOCAL_SRC_FILES += DexOpcodes.cpp \
				   Exception.cpp

LOCAL_LDLIBS := -llog -lz

include $(BUILD_SHARED_LIBRARY)

最后构建生成 advmp.so

最后调用ZipHelper.doZip 重新打包成apk,完成(没有重新签名),由用户自行签名