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

先来看看大概流程

加固俯瞰


靠谱的app加固分享(未完成)_第1张图片

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 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加固分享(未完成)_第2张图片
这里可以看到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 suppressedExceptions = new ArrayList();
        //调用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 suppressedExceptions = new ArrayList();
        //把所有存进来的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 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 suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

dexFile.loadClassBinaryName()

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

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List 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加固分享(未完成)_第3张图片上图对应以下代码:

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作为真正的应用入口?

你可能感兴趣的:(apk反编译,android源码,android原理剖析,android,class)