性能優化專題七--Apk加固之Dex文件的加密與解密

1、我們一般的apk是這樣的,直接打開apk是可以看到裏面的源碼的,如下圖所示

這樣對大家來說就不安全,我們所要做的就是把我們的apk中的dex文件加密,讓別人就算拿到我們的apk裏面啥也看不到。如下圖所示,點擊加密之後的apk,裏面啥也沒有,一個代碼都看不到,這就是我們要做的:

2、具體怎麼做呢?請看下圖介紹

上圖中左邊是我們未加密的apk打開之後顯示的文件結構,我們需要一個proxy代理application  以及一個 加密所用的Tools ,用tools來給左邊的兩個dex進行加密,然後在把加密之後的文件和左邊其它的文件一起打包成apk 這樣別人就看不到我們的源碼了,但是新的文件的dex文件用戶是無法運行的,這個時候,我們就需要一個proxy 的application,把我們新打包的apk所有內容交給代理application,然後讓代理application去和Android系統對接,因爲系統可以加載dex文件的,所以這個時候,我們的代理application要做的就是把拿到的加密的apk中的dex文件進行解密,然後在交給系統處理。

3、我們要先了解自己寫的代碼是如何被加載到內存的,可以在MainActivity中用以下代碼查看:

Log.d("wwy",getClassLoader().toString());    經過運行發現打印信息爲:dalvik.system.PathClassLoader  

我們在系統源碼中查找這個PathClassLoader ,查看父類 BaseDexClassLoader:

我們通過Android系統源碼找到  PathClassLoader 類 ------>  BaseDexClassLoader  ----->  findClass()方法  -----> pathList.findClass() 方法    ,接着查看 pathList 類中都是什麼,發現這個類中的方法 就是 findClass(String name,List<Throwable> suppressed) ;它通過循環遍歷DexFile ,其實這就是獲取dex文件,其實系統所有的dex文件都是要經過這裏處理的

所以關鍵來了,既然所有的dex文件都是通過這裏處理,也就是 dexElements  , 它本身就是一個數組,我們只需要通過反射拿到這個數組,接着我們通過代碼把我們自己的解密之後的dex文件放到我們定義的一個數組中,然後把我們的數組和系統的dex文件數組合併在一起,交給系統去處理。

這裏我們在看系統的dex文件數組 dexElements 是怎麼初始化的呢?通過查看系統源碼,發現如下,

那麼我們通過反射拿到這個方法,就可以拿到系統的dex文件數組了,然後再把我們的數據和系統合併後在賦給系統裏面的dexElements 這樣就把dexElements更新後,這樣我們的dex文件就加載到android裏面運行了。

好了具體的思路上面就介紹到這裏,下面開始實現這個功能。

4、我們新建一個android project ,然後,在項目裏面Create New Module 選擇 Android Library   ,這就是給我們代理的application ,命名爲:proxy_core ;然後還需要一個加密工具,在項目中 Create New Module 選擇 Java Library  這個java library不需要我們運行時處理,它直接在我們編譯的時候就進行加密,這就是爲什麼創建的是java library ,這命名爲:proxy_tools  建好之後,記得把這個proxy_tools 添加到項目中,如下:

添加編譯完之後,在app的build.gradle中會自動如下所示:

好了,下面就開始寫代碼了!

1、代碼中需要用到幾個類,AES加解密類,Zip壓縮解壓類等工具類

首先我先proxy_core代理module下寫一個代理application ,然後繼承至Application,代碼目錄結構請看:

接着把我們這個代理的application加到我們最常寫的配置文件中AndroidManifest.xml 中,我們是不是每個App都有一個application,然後把它配置到AndroidManifest.xml中,這裏唯一不同的是,不是把我們項目中的那個application寫到AndroidManifest.xml中,而是把我們在代理的寫上。然後把我們app自己用到的application也加上,自己的application寫在meta-data中,另一個meta-data按照下面的寫就行,寫法和位置如下

這個是我們自己項目用到的初始化application,上面的代理只是處理代理操作的。

我們自己的MyApplication裏面目前啥也沒寫,這個使我們項目中用於初始化的,這裏先不寫東西。

這裏開始寫代理了,在ProxyApplication 中:

public class ProxyApplication extends Application {
 
    //定義好的加密後的文件的存放路徑
    private String app_name;
    private String app_version;
 
    /**
     * ActivityThread創建Application之後調用的第一個方法
     * 可以在這個方法中進行解密,同時把dex交給Android去加載
     * @param base
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //獲取用戶填入的metaData
        getMetaData();
 
        //得到當前apk文件
        File apkFile = new File(getApplicationInfo().sourceDir);
 
        //把apk解壓  這個目錄中的內容需要root權限才能使用
        File versionDir = getDir(app_name+"_" + app_version,MODE_PRIVATE);
 
        File appDir = new File(versionDir,"app");
        File dexDir = new File(appDir,"dexDir");
 
        //得到我們需要加載的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[] decypt = AES.decrypt(bytes);
                        //寫到指定的目錄
                        FileOutputStream fos = new FileOutputStream(file);
                        fos.write(decypt);
                        fos.flush();
                        fos.close();
 
                        dexFiles.add(file);
 
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }else {
            for (File file:dexDir.listFiles()){
                dexFiles.add(file);
            }
        }
 
        try {
            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、反射到初始化makePathElements的方法
        Method makeDexElements = Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
 
        ArrayList<IOException> suppressedException = new ArrayList<>();
        Object[] addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedException);
 
        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();
        }
    }
 
    /**
     * 開始替換application
     */
    @Override
    public void onCreate() {
        super.onCreate();
        try {
            bindRealApplication();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
 
    /**
     * 讓代碼走入if的第三段中
     * @return
     */
    @Override
    public String getPackageName() {
        if (!TextUtils.isEmpty(app_name)){
            return "";
        }
        return super.getPackageName();
    }
 
    @Override
    public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
        if (TextUtils.isEmpty(app_name)){
            return super.createPackageContext(packageName, flags);
        }
        try {
            bindRealApplication();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return delegate;
 
    }
 
    boolean isBindReal;
    Application delegate;
    //下面主要是通過反射系統源碼的內容,然後進行處理,把我們的內容加進去處理
    private void bindRealApplication() throws Exception{
        if (isBindReal){
            return;
        }
        if (TextUtils.isEmpty(app_name)){
            return;
        }
        //得到attchBaseContext(context) 傳入的上下文 ContextImpl
        Context baseContext = getBaseContext();
        //創建用戶真實的application  (MyApplication)
        Class<?> delegateClass = null;
        delegateClass = Class.forName(app_name);
 
        delegate = (Application) delegateClass.newInstance();
 
        //得到attch()方法
        Method attach = Application.class.getDeclaredMethod("attach",Context.class);
        attach.setAccessible(true);
        attach.invoke(delegate,baseContext);
 
        //獲取ContextImpl ----> ,mOuterContext(app);  通過Application的attachBaseContext回調參數獲取
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        //獲取mOuterContext屬性
        Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
        mOuterContextField.setAccessible(true);
        mOuterContextField.set(baseContext,delegate);
 
        //ActivityThread  ----> mAllApplication(ArrayList)  ContextImpl的mMainThread屬性
        Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
        mMainThreadField.setAccessible(true);
        Object mMainThread = mMainThreadField.get(baseContext);
 
        //ActivityThread  ----->  mInitialApplication       ContextImpl的mMainThread屬性
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
        mInitialApplicationField.setAccessible(true);
        mInitialApplicationField.set(mMainThread,delegate);
 
        //ActivityThread ------>  mAllApplications(ArrayList)   ContextImpl的mMainThread屬性
        Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
        mAllApplicationsField.setAccessible(true);
        ArrayList<Application> mApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
        mApplications.remove(this);
        mApplications.add(delegate);
 
        //LoadedApk ----->  mApplicaion             ContextImpl的mPackageInfo屬性
        Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object mPackageInfo = mPackageInfoField.get(baseContext);
 
 
        Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
        Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
        mApplicationField.setAccessible(true);
        mApplicationField.set(mPackageInfo,delegate);
 
        //修改ApplicationInfo  className  LoadedApk
        Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
        mApplicationInfoField.setAccessible(true);
        ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
        mApplicationInfo.className = app_name;
 
 
        delegate.onCreate();
        isBindReal = true;
    }
}

2、下面在proxy_tools中寫一個Main類,和一個main方法,直接運行處理,代碼如下:

public class Main {
 
    public static void main(String[] args) throws Exception{
 
        /**
         * 1、製作只包含解密代碼的dex文件
         */
        File aarFile = new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
        File aarTemp = new File("proxy_tools/temp");
        Zip.unZip(aarFile,aarTemp);
 
        File classesDex = new File(aarTemp,"classes.dex");
        File classesJar = new File(aarTemp,"classes.jar");
        //dx --dex --output out.dex in.jar     E:\AndroidSdk\Sdk\build-tools\23.0.3
        Process process = Runtime.getRuntime().exec("cmd /c dx --dex --output " + classesDex.getAbsolutePath()
         + " " + classesJar.getAbsolutePath());
        process.waitFor();
        if (process.exitValue() != 0){
            throw new RuntimeException("dex error");
        }
 
        /**
         * 2、加密apk中所有的dex文件
         */
        File apkFile = new File("app/build/outputs/apk/debug/app-debug.apk");
        File apkTemp = new File("app/build/outputs/apk/debug/temp");
        Zip.unZip(apkFile,apkTemp);
        //只要dex文件拿出來加密
        File[] dexFiles = apkTemp.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File file, String s) {
                return s.endsWith(".dex");
            }
        });
        //AES加密
        AES.init(AES.DEFAULT_PWD);
        for (File dexFile:dexFiles) {
            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();
        }
        /**
         * 3、把dex放入apk解壓目錄,重新壓成apk文件
         */
        classesDex.renameTo(new File(apkTemp,"classes.dex"));
        File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
        Zip.zip(apkTemp,unSignedApk);
        /**
         * 4、對其和簽名,最後生成簽名apk
         */
        //        zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
        File alignedApk=new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
        process= Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 "+unSignedApk.getAbsolutePath()
                +" "+alignedApk.getAbsolutePath());
//        System.out.println("signedApkprocess : 11111" + "  :----->  " +unSignedApk.getAbsolutePath() + "\n" +  alignedApk.getAbsolutePath());
        process.waitFor();
//        if(process.exitValue()!=0){
//            throw new RuntimeException("dex error");
//        }
 
//        apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
//        apksigner sign  --ks jks文件地址 --ks-key-alias 別名 --ks-pass pass:jsk密碼 --key-pass pass:別名密碼 --out  out.apk in.apk
        File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
        File jks=new File("proxy_tools/proxy1.jks");
        process= Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
                +" --ks-key-alias wwy --ks-pass pass:123456 --key-pass pass:123456 --out "
                +signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
        process.waitFor();
        if(process.exitValue()!=0){
            throw new RuntimeException("dex error");
        }
        System.out.println("執行成功");
    }
 
}

我們在寫好前面的之後,直接運行這個main方法,就可以在我們的app -> build->outputs->apk->debug下面看到生成的幾個apk,分別爲 app-debug.apk,  app-unsigned.apk,  app-unsigned-aligned.apk,  app-signed-aligned.apk,最終 app-signed-aligned.apk 纔是我們最後安裝使用的apk。

這裏帶多一句嘴,上面的代理ProxyApplication被我們配置到Mainfest的application 標籤中,這個位置經常是我們配置項目使用的application的,其實不用擔心,代碼中已經處理過了,當代理application處理完之後,會自動把我們配置的app裏面的項目用到的MyApplication 類替換過來,所以項目在第一次運行完之後,正式運行還是以我們自己的MyApplication爲主,大可放心。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章