Android Apk 加固之Dex文件 方案NDK 版本

https://blog.csdn.net/I123456789T/article/details/91562328

這篇文章介紹了 基本的加固流程,但有一個明顯的問題就是 解密是 java 代碼實現,存在的問題就是key可以被找到,很容易被破解;

一、寫一下基礎流程:

參考文檔:https://developer.android.google.cn/studio/build/multidex.html#keep

 

看到這張圖其實,還是很好理解的,就是我們把需要加固的apk,外部包裝一層殼,而這個殼的作用是爲了解密源apk的

二、加固仍存在一些問題:

1、解密殼不能加密,解密都是 java 代碼實現,存在的問題就是key可以被找到,很容易被破解;
2、解密之後的apk源程序放在指定目錄的話,還是存在被破解的風險,因爲這種落地方式解密,是很容易獲取解密之後的apk的
3、在解密得到源程序apk,然後再用DexClassLoader進行加載,這裏相當於兩次把apk加載到內存中,第一次是解密的時候,第二次是加載apk的時候,那麼這效率就會大大降低了

三、解決方案:

第一個問題,使用NDK 解決;使用NDK 解密,本文使用的RC4 算法,處理較快,如果安全性高也可以使用AES,demo中都已加入;

第二個問題,加載後可以刪除掉,但每個加載時間會長;

第三個問題暫時無解決;5.0前是可以解決見:Android中內存加載dex https://blog.csdn.net/zzx410527/article/details/51673908

四、主要使用的一個API DexClassLoader

DexClassLoader當然也是一種ClassLoader,但本身屬於顧名思義是用來加載Dex文件的,是安卓系統獨有的一種類加載器。
基礎概念

在此之前可以稍微回顧下ClassLoader的相關基礎:

    ClassLoader是用來加載class文件的,它負責將*.class加載爲內在中的Class對象
    加載機制爲“雙親委派”,即能交給父類加載器去加載的,絕不自行加載
 

使用方法

 只需要清楚其構造方法的參數意義就可以。

DexClassLoader (String dexPath, 
                String optimizedDirectory, 
                String librarySearchPath, 
                ClassLoader parent)
參數 含義
dexPath 包含dex文件的jar包或apk文件路徑
optimizedDirectory 釋放目錄,可以理解爲緩存目錄,必須爲應用私有目錄,不能爲空
librarySearchPath native庫的路徑,可爲空
parent 父類加載器

 

五、 加殼的具體流程:

1.創建一個空的demo進行,

2.然後在項目中添加一個代理module(解密,和系統源碼交互功能)和tools工具加密Java library 的module ;

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{
                        
// 使用 ndk rc4 解密**************************************
Utils.native_rc4_de(file.getAbsolutePath(),file.getAbsolutePath());
                        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;
    }
}

 這裏我換成了 rc4解密;這裏也可以判斷一下,如果目標文件不存在再做這些操作,可以提高性能,但風險增加。最安全的方式是加載完成,直接刪除;

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");
        }
        process.destroy();

        /**
         * 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);
            byte[] encrypt =RC4.RC4Base(bytes,RC4.ACCESSKEY);
            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 processAlign = Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 "+unSignedApk.getAbsolutePath()
                +" "+alignedApk.getAbsolutePath());
//        System.out.println("signedApkprocess : 11111" + "  :----->  " +unSignedApk.getAbsolutePath() + "\n" +  alignedApk.getAbsolutePath());

        processAlign.waitFor( 10,TimeUnit.SECONDS);
//        if(process.exitValue()!=0){
//            throw new RuntimeException("dex error");
//        }
        processAlign.destroy();

//        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 processsign= 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());
        processsign.waitFor();
        if(processsign.exitValue()!=0){
            throw new RuntimeException("dex error");
        }
        processsign.destroy();
        System.out.println("excute successful");
    }

}

這裏加密 我換成了RC4;

我們在寫好前面的之後,直接運行這個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,

使用 java 工具類,需要配置一下環境:

 

1)、配置電腦的環境變量:

如你的 Android   compileSdkVersion 28  請將 D:\AndroidSDK\build-tools\28.0.2 這個路徑加入然後我就把這個路勁配置到用戶變量中 path 中;重新 啓動 Android Studio;

 最後會生成簽名後的 apk

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


 本文是在https://blog.csdn.net/I123456789T/article/details/91819275 基本上 改進了一些;

1.加密,2.process.waitFor(); 運行問題,見:https://blog.csdn.net/q610098308/article/details/105197814

2.簽名問題見:https://blog.csdn.net/q610098308/article/details/105138228

參數 博客:https://blog.csdn.net/zzx410527/article/details/51673908 不過之個 Android 5.0後就不能再用了。

https://blog.csdn.net/I123456789T/article/details/91819275

 

Demo 下載

Android Apk 加固之Dex文件 完善篇 InMemoryDexClassLoader 之內存加載dex 見

https://blog.csdn.net/q610098308/article/details/105246355

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