欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

靠谱的app加固分享(未完成)

程序员文章站 2022-03-23 11:16:25
先来看看大概流程加固俯瞰1、编写加密方法,作为工具方法用于后续的加密和解密准备。2、编写代理Application(ProxyApplication),作为加固后的apk的伪入口。(ProxyApplication作为伪入口时,需要将加密apk进行解密并重新加载于classLoader中)3、将1、2步的文件打包成aar包。4、解压aar包(于aarTemp文件夹),并将解压后的jar文件,编译成dex文件(Entrance.dex)(安卓虚拟机可识别的机器码文件)。5、对需要加密的apk的An...

先来看看大概流程

加固俯瞰


靠谱的app加固分享(未完成)

1、编写加密方法,作为工具方法用于后续的加密和解密准备。

2、编写代理Application(ProxyApplication),作为加固后的apk的伪入口。(ProxyApplication作为伪入口时,需要将加密apk进行解密并重新加载于classLoader中)

3、对需要加密的apk的AndroidManifest文件的Application:name 标签经行更改为ProxyApplication,并用标签声明真正的Application入口和版本号。

4、将1、2步的文件打包成aar包。

5、解压aar包(于aarTemp文件夹),并将解压后的jar文件,编译成dex文件(Entrance.dex)(安卓虚拟机可识别的机器码文件)。

6、解压需要加密的apk(于apkTemp文件夹),遍历解压后的文件夹,取出所有dex文件,用1步中的加密方法对所有dex文件进行加密,并替换原本没加密的dex。
*注:Entrance.dex在aarTemp内,没被加密

7、将aarTemp中的dex文件,复制到apkTemp文件中,并将apkTemp压缩成apk文件。

8、对齐 & 签名(才能正常使用)
附上相关的代码

public class Main {
    public static void main(String[] args) {
        //第四步:解压arr(包含加密解密工具和ProxyApplication.java)
        File aarFile = new File("core/build/outputs/aar/core-debug.aar");
        File aarTemp = new File("lib/temp");
        Zip.unZip(aarFile, aarTemp);
        // 生成classes.dex
        File classesJar = new File(aarTemp, "classes.jar");
        File classesDex = new File(aarTemp, "classes.dex");
        Process process = null;
        //dx --dex --output out.dex in.jar
        try {
            process = Runtime.getRuntime().exec("cmd /c  dx --dex --output " + classesDex.getAbsolutePath()
                    + " " + classesJar.getAbsolutePath());
            process.waitFor();
            if (process.exitValue() != 0) {
                System.out.println("dex error");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //第六步:解压apk
        File apkFile = new File("app/build/outputs/apk/debug/app-debug.apk");
        File apkTemp = new File("lib/Apktemp");
        Zip.unZip(apkFile, apkTemp);
        ArrayList<File> dexFiles = new ArrayList<>();
        for (File file : apkTemp.listFiles()) {
            if (file.getName().endsWith("dex")) {
                dexFiles.add(file);
            }
        }
        //加密apk里面的dex
        AES.init(AES.DEFAULT_PWD);
        for (File dexFile : dexFiles) {
            try {
                byte[] bytes = Utils.getBytes(dexFile);

                byte[] encrypt = AES.encrypt(bytes);
                FileOutputStream fos = new FileOutputStream(new File(apkTemp,
                        "secret-" + dexFile.getName()));
                fos.write(encrypt);
                fos.flush();
                fos.close();
                dexFile.delete();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        classesDex.renameTo(new File("lib/Apktemp", "classes.dex"));
        File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
        //第七步:把apkTemp压缩成unsightApk
        try {
            Zip.zip(apkTemp, unSignedApk);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //第八步:对齐 签名
        File alignedApk = new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
        try {
            process = Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 " + unSignedApk.getAbsolutePath()
                    + " " + alignedApk.getAbsolutePath());

            process.waitFor();
            if (process.exitValue() != 0) {
                System.out.println("zipalign error");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
        File jks=new File("mykeystore.jks");
        try {
            process=Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
                    +" --ks-key-alias key0 --ks-pass pass:11111111 --key-pass pass:11111111 --out "
                    +signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
            process.waitFor();
            if(process.exitValue()!=0){
                System.out.println("sign error");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("over ");
    }
}

这里详细讲讲ProxyApplication:
探讨1:作为唯一没加密的dex文件内的ProxyApplication如何把加密的dex文件,加载到类加载器(ClassLoader)中?
ProxyApplication三部曲:
1.获得加密apk。
2.解压zip并解密dex文件。
3把新dex文件索引存在类加载器中。

上述过程中,涉及到把dex文件加载到类加载器中,下面简单理解下类加载机制。
前提:android的ClassLoader有两种类型系统类加载器和自定义加载器。
1)BootClassLoader:
安卓系统启动时候会使用BCL来预加载常用类。
2)DexClassLoader
加载dex文件和包含dex文件的压缩包
3)PathClassLoader
加载系统类和应用程序的类
4) InMemoryClassLoader
androidO新增的,用于加载内存中的dex
·
·ClassLoader是一个抽象类,定义了classloader的主要功能。BootClassLoader是它的内部类
·SecureClassLoader不是ClassLoader的实现类,拓展了ClassLoader的权限方面的功能
·BaseDexClassLoader继承ClassLoader,但是是抽象类,PathClassLoader, DexClassLoader, InMemoryClassLoader都继承它,并各自实现类功能
·双亲委托模式
(讲人话:首先判断该类是否已经加载,如无,不是从自身查找,而是委托到父加载器中找是否有加载目的Class,若无依次向父类递归,直至最顶层ClassLoader类。如果找到了,就直接返回Class,若果没找到就继续依次向下子加载器findClass…)
优点:
1.避免重复加载
2.保护安全性。
(沙雕A建一个 类名为 android.view.View的自定义类,可能造成系统原本的View不可用。但其实还有一层保护,虚拟机把两个类名一致的且被同一个类加载器加载的类,虚拟机才会认为他们是同一个类)

来一个demo打印看看应用的类加载器是什么:
靠谱的app加固分享(未完成)
这里可以看到PathClassLoader作为加载器。

ClassLoader的加载过程:
ClassLoader.java

  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
        //找该类是否被加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    //先判断父类是否存在
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果不存在就在自层找
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    //在委托流程中没找到该类,就会执行该句
                    c = findClass(name);
                }
            }
            //如果已加载就直接返回
            return c;
    }

BaseDexClassLoader.java

 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //调用pathList的findClass
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

先看看pathList是什么对象

 /**
     * Constructs an instance.
     *
     * dexFile must be an in-memory representation of a full dexFile.
     *
     * @param dexFiles the array of in-memory dex files containing classes.
     * @param parent the parent class loader
     *
     * @hide
     */
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        //在构造器内初始化 是一个DexPathList对象
        this.pathList = new DexPathList(this, dexFiles);
    }

接下来看看DexPathList对象怎么存放已加载的class

 /**
     * Construct an instance.
     *
     * @param definingContext the context in which any as-yet unresolved
     * classes should be defined
     *
     * @param dexFiles the bytebuffers containing the dex files that we should load classes from.
     */
    public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
       ...
        this.definingContext = definingContext;
        // TODO It might be useful to let in-memory dex-paths have native libraries.
        this.nativeLibraryDirectories = Collections.emptyList();
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        //把所有存进来的dex文件存储在dexElements对象
        this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
    }

接下来看看dexElements 是何方神圣!?
这是dexElements的对象声明

  /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

重点来了:

 /**
     * Element of the dex/resource path. Note: should be called DexElement, but apps reflect on
     * this.
     */
    /*package*/ static class Element {
        /**
         * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
         * (only when dexFile is null).
         */
        private final File path;

        private final DexFile dexFile;

        private ClassPathURLStreamHandler urlHandler;
        private boolean initialized;

        /**
         * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
         * should be null), or a jar (in which case dexZipPath should denote the zip file).
         */
        public Element(DexFile dexFile, File dexZipPath) {
            this.dexFile = dexFile;
            this.path = dexZipPath;
        }

        public Element(DexFile dexFile) {
            this.dexFile = dexFile;
            this.path = null;
        }

        public Element(File path) {
          this.path = path;
          this.dexFile = null;
        }
        ....
     }

从上面代码可以看到Element存放了dex文件的实例,和对应路径。

回来~从BaseDexClassLoader.findClass()->DexPathList.findClass()
就看看DexPathList.findClass()的实现内容

public Class<?> findClass(String name, List<Throwable> suppressed) {
//遍历dexElements,findClass()
        for (Element element : dexElements) {
        /
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

看看element.findClass()

 public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

dexFile.loadClassBinaryName()

  public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {

            //调用native
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

native方法往下就不再分析。从这波代码分析,找到一个重要转折点dexElements(Element数组),每当找应用程序的类时,都会遍历这个数组,找到目的的dex文件,再得到目的Class。
回到加固
由此,我们把解密的dex文件通过反射合并到这个dexElements对象(Element数组)就完事。
如下图:
靠谱的app加固分享(未完成)上图对应以下代码:

public class ProxyApplication extends Application {

    //定义好解密后的文件的存放路径
    private String app_name;
    private String app_version;

    /**
     * ActivityThread创建Application之后调用的第一个方法
     * 可以在这个方法中进行解密,同时把dex交给android去加载
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //获取用户填入的metadata
        getMetaData();

        //得到当前加密了的APK文件
        File apkFile=new File(getApplicationInfo().sourceDir);
        //把apk解压   app_name+"_"+app_version目录中的内容需要root权限才能用
        File versionDir = getDir(app_name+"_"+app_version,MODE_PRIVATE);
        File appDir=new File(versionDir,"app");
        File dexDir=new File(appDir,"dexDir");

        Log.e("ProxyApplication", "attachBaseContext:first "+apkFile.getAbsolutePath() );
        Log.e("ProxyApplication", "attachBaseContext:sec "+versionDir.getAbsolutePath() );

        //得到我们需要加载的Dex文件
        List<File> dexFiles=new ArrayList<>();
        //进行解密(最好做MD5文件校验)
        if(!dexDir.exists() || dexDir.list().length==0){
            //把apk解压到appDir
            Zip.unZip(apkFile,appDir);
            //获取目录下所有的文件
            File[] files=appDir.listFiles();
            for (File file : files) {
                String name=file.getName();
                if(name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){
                    try{
                        AES.init(AES.DEFAULT_PWD);
                        //读取文件内容
                        byte[] bytes=Utils.getBytes(file);
                        //解密
                        byte[] decrypt=AES.decrypt(bytes);
                        //写到指定的目录
                        FileOutputStream fos=new FileOutputStream(file);
                        fos.write(decrypt);
                        fos.flush();
                        fos.close();
                        dexFiles.add(file);

                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }else{
            for (File file : dexDir.listFiles()) {
                dexFiles.add(file);
            }
        }

        try{
            //2.把解密后的文件加载到系统
            loadDex(dexFiles,versionDir);
        }catch (Exception e){
            e.printStackTrace();
        }


    }

    private void loadDex(List<File> dexFiles, File versionDir) throws Exception{
        //1.获取pathlist
        Field pathListField = Utils.findField(getClassLoader(), "pathList");
        Object pathList = pathListField.get(getClassLoader());
        //2.获取数组dexElements
        Field dexElementsField=Utils.findField(pathList,"dexElements");
        Object[] dexElements=(Object[])dexElementsField.get(pathList);
        //3.反射到初始化dexElements的方法
        Method makeDexElements=Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);

        //合并数组
        Object[] newElements= (Object[])Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length);
        System.arraycopy(dexElements,0,newElements,0,dexElements.length);
        System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);

        //替换classloader中的element数组
        dexElementsField.set(pathList,newElements);
    }

    private void getMetaData() {
        try{
            ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
                    getPackageName(), PackageManager.GET_META_DATA);
            Bundle metaData=applicationInfo.metaData;
            if(null!=metaData){
                if(metaData.containsKey("app_name")){
                    app_name=metaData.getString("app_name");
                }
                if(metaData.containsKey("app_version")){
                    app_version=metaData.getString("app_version");
                }
            }

        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

(tinker热修复共同点:加入新dex去dexElements)
探讨2: 初次冷启动ProxyApplication进程时,已经将ProxyApplication作为入口,后续的冷启动如何更替为真正的MyApplication作为真正的应用入口?

本文地址:https://blog.csdn.net/qq_37977070/article/details/108495821