1. 程式人生 > >Apk原始碼的加固(加殼)原理解析和實現

Apk原始碼的加固(加殼)原理解析和實現

好久沒寫部落格了,要深刻檢討下!

前言:
在Android中沒有經過加密的Apk給人的感覺就是在裸奔,通過apktool,dex2jar,AndroidKill等各式各樣的反編譯工具就可以輕鬆的獲取其smail程式碼,如這個叫SourceProject的helloworld程式被apktool反編譯後,對於懂smail語法的逆向工程師來說就一覽無餘了。破解與反破解是相對的,所以我們儘可能的給自己的Apk多穿點衣服。
這裡寫圖片描述

原理解析

首先我們先來看下Apk加殼的步驟:
這裡寫圖片描述

  • 源Apk:需要加殼的Apk
  • 加密的Apk:源Apk經過加密演算法加密後的Apk
  • 加殼程式Apk:是有解密源Apk和動態載入啟動源Apk的外殼

首先我們拿到需要加殼的源Apk,通過加密演算法加密源Apk然後與加殼Apk的dex檔案組合成新的Dex檔案,然後將加殼程式Apk的Dex檔案替換成新的Dex,生成新的Apk重新簽名。
我們先來看下Dex檔案的結構:
這裡寫圖片描述

  • Magic
    Magic數是為了方便虛擬機器識別目標檔案是否是合格的Dex檔案,在Dex檔案中magic的值固定值
  • checksum
    檔案校驗碼 ,使用alder32 演算法校驗檔案除去 maigc ,checksum 外餘下的所有檔案區域 ,用於檢查檔案錯誤
  • signature
    使用 SHA-1 演算法 hash 除去 magic ,checksum 和 signature 外餘下的所有檔案區域 ,用於唯一識別本檔案 。
  • file_size
    當前Dex 檔案的大小 。

所以我們在將Dex與加密演算法加密後的Apk合併生成新的Dex後需要修改新Dex檔案的這三個值,為了方便從新Dex中獲得加密的Apk,我們需要知道加密的Apk的大小,為了方便以後獲得,我們將其大小放置在新Dex的後四位,新生成的Dex檔案結構:
這裡寫圖片描述
生成新Dex後,將加殼程式Apk的Dex檔案替換,重新簽名後加殼的Apk即完成了。如果覺得步驟理清了,我們來看下其具體的實現。

具體實現:

這過程一共要建立三個專案。
首先我們先建立一個需要加密的Apk專案,結構非常簡單隻有幾個Log的列印,結構
這裡寫圖片描述
SourceApplication.java

package com.jju.yuxin.sourceproject;
import android.app.Application;
import android.util.Log;

public class SourceApplication extends Application {
    private static final String TAG=SourceApplication.class.getSimpleName();
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG,"-------------onCreate");
    }
}

MainActivity.java

package com.jju.yuxin.sourceproject;

import android.app.Activity;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends Activity {
    private static final String TAG=MainActivity.class.getSimpleName();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv_content = new TextView(this);
        tv_content.setText("I am Source Apk");
        tv_content.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View arg0) {
                Intent intent = new Intent(MainActivity.this, SubActivity.class);
                startActivity(intent);
            }});
        setContentView(tv_content);
        Log.i(TAG, "onCreate:app:"+getApplicationContext());
    }
}

SubActivity.java

package com.jju.yuxin.sourceproject;
import android.app.Activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class SubActivity extends Activity {
    private static final String TAG=SubActivity.class.getSimpleName();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv_content = new TextView(this);
        tv_content.setText("I am SubActivity");
        setContentView(tv_content);
        Log.i(TAG, "SubActivity:app:"+getApplicationContext());
    }
}

然後將其打包生成Apk。
第二個專案是一個JAVA專案用於將源Apk加密,併合並加殼程式Dex與加密後的源Apk。在貼出這個程式碼時我們先不用管加殼程式Dex從何而來,假設已經有了這樣一個檔案。我們來看下具體實現:

package com.forceapk;

import java.io.ByteArrayOutputStream;  
import java.io.File;  
import java.io.FileInputStream;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.security.MessageDigest;  
import java.security.NoSuchAlgorithmException;  
import java.util.zip.Adler32;  


public class mymain { 
    public static void main(String[] args) {  
        try {  
            //需要加殼的源APK  ,以二進位制形式讀出,並進行加密處理
            File srcApkFile = new File("force/SourceAPK.apk");   
            System.out.println("apk size:"+srcApkFile.length()); 
            byte[] enSrcApkArray = encrpt(readFileBytes(srcApkFile));

            //需要解殼的dex  以二進位制形式讀出dex  
            File unShellDexFile = new File("force/shelldex.dex");    
            byte[] unShellDexArray = readFileBytes(unShellDexFile);

            //將源APK長度和需要解殼的DEX長度相加並加上存放源APK大小的四位得到總長度
            int enSrcApkLen = enSrcApkArray.length;  
            int unShellDexLen = unShellDexArray.length;  
            int totalLen = enSrcApkLen + unShellDexLen +4;

            //依次將解殼DEX,加密後的源APK,加密後的源APK大小,拼接出新的Dex
            byte[] newdex = new byte[totalLen];  
            System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);
            System.arraycopy(enSrcApkArray, 0, newdex, unShellDexLen, enSrcApkLen);
            System.arraycopy(intToByte(enSrcApkLen), 0, newdex, totalLen-4, 4);


            //修改DEX file size檔案頭  
            fixFileSizeHeader(newdex);  
            //修改DEX SHA1 檔案頭  
            fixSHA1Header(newdex);  
            //修改DEX CheckSum檔案頭  
            fixCheckSumHeader(newdex);  

            //寫出
            String str = "force/classes.dex";  
            File file = new File(str);  
            if (!file.exists()) {  
                file.createNewFile();  
            }  
            FileOutputStream localFileOutputStream = new FileOutputStream(str);  
            localFileOutputStream.write(newdex);  
            localFileOutputStream.flush();  
            localFileOutputStream.close();  


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

    //可以修改成自己的加密方法  
    private static byte[] encrpt(byte[] srcdata){  
        for(int i = 0;i<srcdata.length;i++){  
            srcdata[i] = (byte)(0xFF ^ srcdata[i]);  
        }  
        return srcdata;  
    }  

    /** 
     * 修改dex頭,CheckSum 校驗碼 
     * @param dexBytes 
     */  
    private static void fixCheckSumHeader(byte[] dexBytes) {  
        Adler32 adler = new Adler32();  
        adler.update(dexBytes, 12, dexBytes.length - 12);//從12到檔案末尾計算校驗碼  
        long value = adler.getValue();  
        int va = (int) value;  
        byte[] newcs = intToByte(va);  
        //高位在前,低位在前掉個個  
        byte[] recs = new byte[4];  
        for (int i = 0; i < 4; i++) {  
            recs[i] = newcs[newcs.length - 1 - i];  
            System.out.println(Integer.toHexString(newcs[i]));  
        }  
        System.arraycopy(recs, 0, dexBytes, 8, 4);//效驗碼賦值(8-11)  
        System.out.println(Long.toHexString(value));  
        System.out.println();  
    }  


    /** 
     * int 轉byte[] 
     * @param number 
     * @return 
     */  
    public static byte[] intToByte(int number) {  
        byte[] b = new byte[4];  
        for (int i = 3; i >= 0; i--) {  
            b[i] = (byte) (number % 256);  
            number >>= 8;  
        }  
        return b;  
    }  

    /** 
     * 修改dex頭 sha1值 
     * @param dexBytes 
     * @throws NoSuchAlgorithmException 
     */  
    private static void fixSHA1Header(byte[] dexBytes)  
            throws NoSuchAlgorithmException {  
        MessageDigest md = MessageDigest.getInstance("SHA-1");  
        md.update(dexBytes, 32, dexBytes.length - 32);//從32為到結束計算sha--1  
        byte[] newdt = md.digest();  
        System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)  
        //輸出sha-1值,可有可無  
        String hexstr = "";  
        for (int i = 0; i < newdt.length; i++) {  
            hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)  
                    .substring(1);  
        }  
        System.out.println(hexstr);  
    }  

    /** 
     * 修改dex頭 file_size值 
     * @param dexBytes 
     */  
    private static void fixFileSizeHeader(byte[] dexBytes) {  
        //新檔案長度  
        byte[] newfs = intToByte(dexBytes.length);  
        System.out.println(Integer.toHexString(dexBytes.length));  
        byte[] refs = new byte[4];  
        //高位在前,低位在前掉個個  
        for (int i = 0; i < 4; i++) {  
            refs[i] = newfs[newfs.length - 1 - i];  
            System.out.println(Integer.toHexString(newfs[i]));  
        }  
        System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)  
    }  

    /** 
     * 以二進位制讀出檔案內容 
     * @param file 
     * @return 
     * @throws IOException 
     */  
    private static byte[] readFileBytes(File file) throws IOException {  
        byte[] arrayOfByte = new byte[1024];  
        ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();  
        FileInputStream fis = new FileInputStream(file);  
        while (true) {  
            int i = fis.read(arrayOfByte);  
            if (i != -1) {  
                localByteArrayOutputStream.write(arrayOfByte, 0, i);  
            } else {  
                return localByteArrayOutputStream.toByteArray();  
            }  
        }  
    }  
}  

我們可以看到程式比較簡單和我們之前描述的一樣,核心部分就在檔案拼接部分和修改三個頭資訊的部分。

            //依次將解殼DEX,加密後的源APK,加密後的源APK大小,拼接出新的Dex
            byte[] newdex = new byte[totalLen];  
            System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);
            System.arraycopy(enSrcApkArray, 0, newdex, unShellDexLen, enSrcApkLen);
            System.arraycopy(intToByte(enSrcApkLen), 0, newdex, totalLen-4, 4);
            //修改DEX file size檔案頭  
            fixFileSizeHeader(newdex);  
            //修改DEX SHA1 檔案頭  
            fixSHA1Header(newdex);  
            //修改DEX CheckSum檔案頭  
            fixCheckSumHeader(newdex);

我們再來看下第三個專案:
加殼程式,這個程式主要負責將在JAVA專案中加密的源Apk獲取及解密,以及動態載入源Apk。專案結構
這裡寫圖片描述
我們看下程式碼:

package com.jju.yuxin.reforceapk;

import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import dalvik.system.DexClassLoader;

/**
 * =============================================================================
 * Copyright (c) 2017 yuxin All rights reserved.
 * Packname com.jju.yuxin.reforceapk
 * Created by yuxin.
 * Created time 2017/6/18 0018 下午 5:03.
 * Version   1.0;
 * Describe :
 * History:
 * ==============================================================================
 */
public class ProxyApplication extends Application{

    private static final String appkey = "APPLICATION_CLASS_NAME";
    private  static final String TAG=ProxyApplication.class.getSimpleName();
    private String srcApkFilePath;
    private String odexPath;
    private String libPath;
    //以下是載入資源
    protected AssetManager mAssetManager;
    protected Resources mResources;
    protected Resources.Theme mTheme;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Log.d(TAG,"----------onCreate");
        try {

            File odex = this.getDir("payload_odex", MODE_PRIVATE);
            File libs = this.getDir("payload_lib", MODE_PRIVATE);
            //用於存放源apk釋放出來的dex
            odexPath = odex.getAbsolutePath();
            //用於存放源Apk用到的so檔案
            libPath = libs.getAbsolutePath();
            //用於存放解密後的apk
            srcApkFilePath = odex.getAbsolutePath() + "/payload.apk";

            File srcApkFile= new File(srcApkFilePath);
            Log.i("demo", "apk size:"+srcApkFile.length());

            //第一次載入
            if (!srcApkFile.exists())
            {
                Log.i("demo", "isFirstLoading");
                srcApkFile.createNewFile();
                //拿到dex檔案
                byte[] dexdata = this.readDexFileFromApk();
                //取出源APK解密後放置在/payload.apk,及其so檔案放置在payload_lib/下
                this.splitPayLoadFromDex(dexdata);
            }

            // 配置動態載入環境
            //反射獲取主執行緒物件,並從中獲取所有已載入的package資訊,並中找到當前的LoadApk物件的弱引用
            Object currentActivityThread = RefInvoke.invokeStaticMethod(
                    "android.app.ActivityThread", "currentActivityThread",
                    new Class[] {}, new Object[] {});
            String packageName = this.getPackageName();
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mPackages");
            WeakReference wr = (WeakReference) mPackages.get(packageName);

            //建立一個新的DexClassLoader用於載入源Apk,
            // 傳入apk路徑,dex釋放路徑,so路徑,及父節點的DexClassLoader使其遵循雙親委託模型
            DexClassLoader dLoader = new DexClassLoader(srcApkFilePath, odexPath,
                    libPath, (ClassLoader) RefInvoke.getFieldOjbect(
                    "android.app.LoadedApk", wr.get(), "mClassLoader"));

            //getClassLoader()等同於 (ClassLoader) RefInvoke.getFieldOjbect()
            //但是為了替換掉父節點我們需要通過反射來獲取並修改其值
            Log.i(TAG,"父classloader:"+(ClassLoader) RefInvoke.getFieldOjbect(
                    "android.app.LoadedApk", wr.get(), "mClassLoader"));
            //將父節點DexClassLoader替換
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
                    wr.get(), dLoader);

            Log.i(TAG,"子classloader:"+dLoader);

            try{
                //嘗試載入源Apk的MainActivity
                Object actObj = dLoader.loadClass("com.jju.yuxin.sourceproject.MainActivity");

                Log.i(TAG, "SrcApk_MainActivity:"+actObj);
            }catch(Exception e){
                Log.i(TAG, "LoadSrcActivityErr:"+Log.getStackTraceString(e));
            }


        } catch (Exception e) {
            Log.i(TAG, "error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }
    }


    public void onCreate() {

            //載入源apk資源
            //loadResources(srcApkFilePath);

            Log.i(TAG, "--------onCreate");
            //獲取配置在清單檔案的源Apk的Application路勁
            String appClassName = null;
            try {
                ApplicationInfo ai = this.getPackageManager()
                        .getApplicationInfo(this.getPackageName(),
                                PackageManager.GET_META_DATA);
                Bundle bundle = ai.metaData;
                if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
                    appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml檔案中的。
                } else {
                    Log.i(TAG, "have no application class name");
                    return;
                }
            } catch (PackageManager.NameNotFoundException e) {
                Log.i(TAG, "error:"+Log.getStackTraceString(e));
                e.printStackTrace();
            }

            //獲取當前殼Apk的ApplicationInfo
            Object currentActivityThread = RefInvoke.invokeStaticMethod(
                    "android.app.ActivityThread", "currentActivityThread",
                    new Class[] {}, new Object[] {});
            Object mBoundApplication = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mBoundApplication");
            Object loadedApkInfo = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread$AppBindData",
                    mBoundApplication, "info");
            //將LoadedApk中的ApplicationInfo設定為null
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
                    loadedApkInfo, null);

            //獲取currentActivityThread中註冊的Application
            Object oldApplication = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mInitialApplication");

            //獲取ActivityThread中所有已註冊的Application,並將當前殼Apk的Application從中移除
            ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
                    .getFieldOjbect("android.app.ActivityThread",
                            currentActivityThread, "mAllApplications");
            mAllApplications.remove(oldApplication);

            ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
                    .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
                            "mApplicationInfo");

            ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
                    .getFieldOjbect("android.app.ActivityThread$AppBindData",
                            mBoundApplication, "appInfo");
            //替換原來的Application
            appinfo_In_LoadedApk.className = appClassName;
            appinfo_In_AppBindData.className = appClassName;

            //註冊Application
            Application app = (Application) RefInvoke.invokeMethod(
                    "android.app.LoadedApk", "makeApplication", loadedApkInfo,
                    new Class[] { boolean.class, Instrumentation.class },
                    new Object[] { false, null });

            //替換ActivityThread中的Application
            RefInvoke.setFieldOjbect("android.app.ActivityThread",
                    "mInitialApplication", currentActivityThread, app);

            ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mProviderMap");

            Iterator it = mProviderMap.values().iterator();
            while (it.hasNext()) {
                Object providerClientRecord = it.next();
                Object localProvider = RefInvoke.getFieldOjbect(
                        "android.app.ActivityThread$ProviderClientRecord",
                        providerClientRecord, "mLocalProvider");
                RefInvoke.setFieldOjbect("android.content.ContentProvider",
                        "mContext", localProvider, app);
            }

            Log.i(TAG, "Srcapp:"+app);

            app.onCreate();

    }


    private void splitPayLoadFromDex(byte[] shelldexdata) throws IOException {
        int sdlen = shelldexdata.length;
        //取被加殼apk的長度
        byte[] dexlen = new byte[4];
        System.arraycopy(shelldexdata, sdlen - 4, dexlen, 0, 4);
        ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
        DataInputStream in = new DataInputStream(bais);
        int readInt = in.readInt();
        Log.d(TAG,"Integer.toHexString(readInt):"+Integer.toHexString(readInt));

        //取出apk
        byte[] ensrcapk = new byte[readInt];
        System.arraycopy(shelldexdata, sdlen - 4 - readInt, ensrcapk, 0, readInt);

        //對源程式Apk進行解密
        byte[]  srcapk = decrypt(ensrcapk);

        //寫入源apk檔案
        File file = new File(srcApkFilePath);
        try {
            FileOutputStream localFileOutputStream = new FileOutputStream(file);
            localFileOutputStream.write(srcapk);
            localFileOutputStream.close();
        } catch (IOException localIOException) {
            throw new RuntimeException(localIOException);
        }

        //分析源apk檔案
        ZipInputStream localZipInputStream = new ZipInputStream(
                new BufferedInputStream(new FileInputStream(file)));

        while (true) {
            ZipEntry localZipEntry = localZipInputStream.getNextEntry();
            if (localZipEntry == null) {
                localZipInputStream.close();
                break;
            }
            //依次取出被加殼apk用到的so檔案,放到 libPath中(data/data/包名/payload_lib)
            String name = localZipEntry.getName();
            if (name.startsWith("lib/") && name.endsWith(".so")) {
                File storeFile = new File(libPath + "/"
                        + name.substring(name.lastIndexOf('/')));
                storeFile.createNewFile();
                FileOutputStream fos = new FileOutputStream(storeFile);
                byte[] arrayOfByte = new byte[1024];
                while (true) {
                    int i = localZipInputStream.read(arrayOfByte);
                    if (i == -1)
                        break;
                    fos.write(arrayOfByte, 0, i);
                }
                fos.flush();
                fos.close();
            }
            localZipInputStream.closeEntry();
        }
        localZipInputStream.close();
    }


    /**
     * 拿到自己apk檔案中的dex檔案
     * @return
     * @throws IOException
     */
    private byte[] readDexFileFromApk() throws IOException {
        ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();

        ZipInputStream localZipInputStream = new ZipInputStream(
                new BufferedInputStream(new FileInputStream(
                        this.getApplicationInfo().sourceDir)));

        while (true) {
            ZipEntry localZipEntry = localZipInputStream.getNextEntry();
            if (localZipEntry == null) {
                localZipInputStream.close();
                break;
            }
            //拿到dex檔案
            if (localZipEntry.getName().equals("classes.dex")) {
                byte[] arrayOfByte = new byte[1024];
                while (true) {
                    int i = localZipInputStream.read(arrayOfByte);
                    if (i == -1)
                        break;
                    dexByteArrayOutputStream.write(arrayOfByte, 0, i);
                }
            }
            localZipInputStream.closeEntry();
        }
        localZipInputStream.close();
        return dexByteArrayOutputStream.toByteArray();
    }


    // //直接返回資料,讀者可以新增自己解密方法
    private byte[] decrypt(byte[] srcdata) {
        for(int i=0;i<srcdata.length;i++){
            srcdata[i] = (byte)(0xFF ^ srcdata[i]);
        }
        return srcdata;
    }


    protected void loadResources(String srcApkPath) {
        //建立一個AssetManager放置源apk的資源
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, srcApkPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            Log.i(TAG, "inject:loadResource error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }
        Resources superRes = super.getResources();
        superRes.getDisplayMetrics();
        superRes.getConfiguration();
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

    @Override
    public AssetManager getAssets() {
        return mAssetManager == null ? super.getAssets() : mAssetManager;
    }

    @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

}

這個檔案比較長我們來依次分析:

            //第一次載入
            if (!srcApkFile.exists())
            {
                Log.i("demo", "isFirstLoading");
                srcApkFile.createNewFile();
                //拿到dex檔案
                byte[] dexdata = this.readDexFileFromApk();
                //取出源APK解密後放置在/payload.apk,及其so檔案放置在payload_lib/下
                this.splitPayLoadFromDex(dexdata);
            }

通過判斷用於存放解密後的源Apk檔案是否存在來判斷是否是第一次載入。第一次載入時通過readDexFileFromApk()來獲取當前Apk的Dex檔案

    /**
     * 拿到自己apk檔案中的dex檔案
     * @return
     * @throws IOException
     */
    private byte[] readDexFileFromApk() throws IOException {
        ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();

        ZipInputStream localZipInputStream = new ZipInputStream(
                new BufferedInputStream(new FileInputStream(
                        this.getApplicationInfo().sourceDir)));

        while (true) {
            ZipEntry localZipEntry = localZipInputStream.getNextEntry();
            if (localZipEntry == null) {
                localZipInputStream.close();
                break;
            }
            //拿到dex檔案
            if (localZipEntry.getName().equals("classes.dex")) {
                byte[] arrayOfByte = new byte[1024];
                while (true) {
                    int i = localZipInputStream.read(arrayOfByte);
                    if (i == -1)
                        break;
                    dexByteArrayOutputStream.write(arrayOfByte, 0, i);
                }
            }
            localZipInputStream.closeEntry();
        }
        localZipInputStream.close();
        return dexByteArrayOutputStream.toByteArray();
    }

然後通過this.splitPayLoadFromDex();將當前Dex分解,從中獲取源Apk並將其解密,以及源Apk的so庫

 private void splitPayLoadFromDex(byte[] shelldexdata) throws IOException {
        int sdlen = shelldexdata.length;
        //取被加殼apk的長度
        byte[] dexlen = new byte[4];
        System.arraycopy(shelldexdata, sdlen - 4, dexlen, 0, 4);
        ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
        DataInputStream in = new DataInputStream(bais);
        int readInt = in.readInt();
        Log.d(TAG,"Integer.toHexString(readInt):"+Integer.toHexString(readInt));

        //取出apk
        byte[] ensrcapk = new byte[readInt];
        System.arraycopy(shelldexdata, sdlen - 4 - readInt, ensrcapk, 0, readInt);

        //對源程式Apk進行解密
        byte[]  srcapk = decrypt(ensrcapk);

        //寫入源apk檔案
        File file = new File(srcApkFilePath);
        try {
            FileOutputStream localFileOutputStream = new FileOutputStream(file);
            localFileOutputStream.write(srcapk);
            localFileOutputStream.close();
        } catch (IOException localIOException) {
            throw new RuntimeException(localIOException);
        }

        //分析源apk檔案
        ZipInputStream localZipInputStream = new ZipInputStream(
                new BufferedInputStream(new FileInputStream(file)));

        while (true) {
            ZipEntry localZipEntry = localZipInputStream.getNextEntry();
            if (localZipEntry == null) {
                localZipInputStream.close();
                break;
            }
            //依次取出被加殼apk用到的so檔案,放到 libPath中(data/data/包名/payload_lib)
            String name = localZipEntry.getName();
            if (name.startsWith("lib/") && name.endsWith(".so")) {
                File storeFile = new File(libPath + "/"
                        + name.substring(name.lastIndexOf('/')));
                storeFile.createNewFile();
                FileOutputStream fos = new FileOutputStream(storeFile);
                byte[] arrayOfByte = new byte[1024];
                while (true) {
                    int i = localZipInputStream.read(arrayOfByte);
                    if (i == -1)
                        break;
                    fos.write(arrayOfByte, 0, i);
                }
                fos.flush();
                fos.close();
            }
            localZipInputStream.closeEntry();
        }
        localZipInputStream.close();
    }

然後通過動態載入機制將加殼程式的ClassLoader替換成他的子ClassLoader這樣確保既能載入自己的Class又能載入源Apk的Class
核心程式碼,如果這段程式碼不是很懂,你可能需要去了解Java反射,Android中Classloader的雙親委託模型,以及動態載入機制

            DexClassLoader dLoader = new DexClassLoader(srcApkFilePath, odexPath,
                    libPath, (ClassLoader) RefInvoke.getFieldOjbect(
                    "android.app.LoadedApk", wr.get(), "mClassLoader"));

然後在Application的onCreate()中替換LoadApk以及ActivityThread中的Application,希望更清楚的明白這點需要了解下Android中Activity以及Application的啟動流程,其中在加殼程式的清單檔案中我們配置了源Apk的相關資訊以便能找到他們

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:name=".ProxyApplication"
        android:theme="@style/AppTheme">
        <meta-data
            android:name="APPLICATION_CLASS_NAME"
            android:value="com.jju.yuxin.sourceproject.SourceApplication"/>

        <activity android:name="com.jju.yuxin.sourceproject.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.jju.yuxin.sourceproject.SubActivity">
        </activity>
    </application>

實現操作流程:
先將源Apk的專案生成Apk,放置到JAVA專案中
這裡寫圖片描述
將加殼程式也生成Apk,通過直接將apk字尾名改成zip的方式獲取到classes.dex,(最好複製一份,這個後面還要用)。將classes.dex改名成shelldex.dex放置到JAVA專案中
這裡寫圖片描述
執行JAVA專案,將生成新的Dex檔案classes.dex,將新生成的classes.dex替換加殼Apk的classes.dex(通過解壓軟體直接拖放進去替換即可)
這裡寫圖片描述
最後cd到apktool的目錄下,使用apktool中的jarsigner對應用重新簽名即可,重新簽名指令

jarsigner -verbose -keystore 簽名檔案路徑 -storepass 密碼 -keypass 密碼 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar 簽名後生成Apk路徑 需要簽名Apk路徑 簽名檔案別名
//例如:jarsigner -verbose -keystore yuxin.jks -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk reforce.apk yuxin

這三個專案的Github地址(專案地址

常見錯誤:

  • ClassNotFoundException:

    這個錯誤主要注意:Class路徑拼寫有沒錯,加密Apk能否正確的轉為源Apk,還有就是ClassLoader有沒用錯

  • Class ref in pre-verified class resolved to unexpected
    implementation:
    這是類被重複載入的錯誤,需要檢查報錯的類是否被別的ClassLoder已經載入過了,我的Activity在繼承android.support.v7.app.AppCompatActivity時候報了這個錯誤,改成Activity就好了,原因還沒去找。