Android之Apk加殼

lvxiangan發表於2018-12-11

基於ADT環境開發的的實現,請參考: Android中的Apk的加固(加殼)原理解析和實現 
類載入和dex檔案相關的內容,如:Android動態載入Dex機制解析 

一、什麼是加殼?

加殼是在二進位制的程式中植入一段程式碼,在執行的時候優先取得程式的控制權,做一些額外的工作。大多數病毒就是基於此原理。是應用加固的一種手法對原始二進位制原文進行加密/隱藏/混淆。
殼最本質的功能就是實現載入器。

  • 未加殼前,系統直接執行原dex,即原apk
  • 加殼後,系統執行 殼程式碼--> 脫殼得到原dex --> 執行原dex


Apk加殼:就是通過給目標APK加一層保護程式,把需要保護的內容加密、隱藏起來,來防止反編譯的一種方法。
加殼的原理: 


所以我們在加殼過程中需要三個關鍵物件: 
1、未加密的Apk(即demo.apk) 
2、殼程式Apk(即shell.apk,負責解密apk工作) 
3、加密工具(即java工程。將demo.apk加密和shell.dex合併,得到新的dex)

 

二、下面我們來實現如何加殼:
Step1:打包demo工程:demo.apk
Step2
(先設定解殼/密):打包解殼工程:shell.apk,解壓獲取:shell.dex
Step3
(開始加殼/密:執行java工程,合併shell.dex和demo.apk,得到:classes.dex
step4
(修正簽名、執行):把class.dex放進shell.apk,重新簽名得到:shell_demo.apk
shell_demo.apk就是我們想得到的加殼app!


 

Step1:打包demo工程:demo.apk

注意:這裡包括後續打包,只能使用同一個簽名。
原始碼:https://github.com/lvxiangan/Shell/tree/master/Demo
功能:獲取當前包名,廣播監聽網路狀態變化,Glide框架顯示網路圖片(網路操作+圖片顯示)等。
     

關鍵程式碼

1、MyApplication

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("demo", "apk onCreate:" + this);
    }
}


2、AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="demon.demo">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".ImageActivity" />
    </application></manifest>

 

 

 

 

Step2:打包解殼工程:shell.apk,解壓獲取:shell.dex

這個shell.apk在經過後面替換dex後,就是我們想要得到的東西。

原始碼:https://github.com/lvxiangan/Shell/tree/master/MyUnshell
工程目錄:


通過解壓shell.Apk的方式獲取到dex檔案,  更名為shell.dex。如圖:

 

關鍵程式碼
1、ProxyApplication.java

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;

public class ProxyApplication extends Application {
    private static final String appkey = "APPLICATION_CLASS_NAME";
    private String apkFileName;
    private String odexPath;
    private String libPath;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            // 建立兩個資料夾payload_odex,payload_lib 私有的,可寫的檔案目錄
            File odex = this.getDir("demo_odex", MODE_PRIVATE);
            File libs = this.getDir("demo_lib", MODE_PRIVATE);
            odexPath = odex.getAbsolutePath();
            libPath = libs.getAbsolutePath();

            apkFileName = odexPath + "/shelldemo.apk";
            File dexFile = new File(apkFileName);
            Log.i("demo", "apk size:" + dexFile.length());


            if (!dexFile.exists()) {
                // 在payload_odex資料夾內,建立payload.apk
                dexFile.createNewFile();
                // 讀取程式classes.dex檔案
                byte[] dexdata = this.readDexFileFromApk();

                // 分離出解殼後的apk檔案已用於動態載入
                this.splitPayLoadFromDex(dexdata);
            }
            // 配置動態載入環境 獲取主執行緒物件 http://blog.csdn.net/myarrow/article/details/14223493
            Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[]{}, new Object[]{});
            String packageName = this.getPackageName();//當前apk的包名
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mPackages");
            WeakReference wr = (WeakReference) mPackages.get(packageName);

            //建立被加殼apk的DexClassLoader物件 載入apk內的類和原生程式碼(c/c++程式碼)
            DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath, libPath, (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", wr.get(), "mClassLoader"));
            //base.getClassLoader(); 是不是就等同於 (ClassLoader) RefInvoke.getFieldOjbect()? 有空驗證下//?

            //把當前程式的DexClassLoader 設定成了被加殼apk的DexClassLoader
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader);

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

    @Override
    public void onCreate() {
        //loadResources(apkFileName);
        Log.i("demo", "onCreate");

        // 如果源應用配置有Appliction物件,則替換為源應用Applicaiton,以便不影響源程式邏輯。
        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("demo", "have no application class name");
                return;
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.i("demo", "error:" + Log.getStackTraceString(e));
        }
        //有值的話呼叫該Applicaiton
        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");
        //把當前程式的mApplication 設定成了null
        RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null);
        Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication");

        ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications");
        mAllApplications.remove(oldApplication);//刪除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");
        appinfo_In_LoadedApk.className = appClassName;
        appinfo_In_AppBindData.className = appClassName;
        Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[]{boolean.class, Instrumentation.class}, new Object[]{false, null});//執行 makeApplication(false,null)
        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("demo", "app:" + app);

        app.onCreate();
    }


    /**
     * 釋放被加殼的apk檔案,so檔案
     *
     * @param
     * @throws IOException
     */
    private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
        int ablen = apkdata.length;
        //取被加殼apk的長度 這裡的長度取值,對應加殼時長度的賦值都可以做些簡化
        byte[] dexlen = new byte[4];
        System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
        ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
        DataInputStream in = new DataInputStream(bais);
        int readInt = in.readInt();
        System.out.println(Integer.toHexString(readInt));
        byte[] newdex = new byte[readInt];
        //把被加殼apk內容拷貝到newdex中
        System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
        //這裡應該加上對於apk的解密操作,若加殼是加密處理的話
        //?

        //對源程式Apk進行解密
        newdex = decrypt(newdex);

        //寫入apk檔案
        File file = new File(apkFileName);
        try {
            FileOutputStream localFileOutputStream = new FileOutputStream(file);
            localFileOutputStream.write(newdex);
            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檔案內容(byte)
     *
     * @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;
            }
            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 AssetManager mAssetManager;//資源管理器
    protected Resources mResources;//資源
    protected Resources.Theme mTheme;//主題


    protected void loadResources(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            Log.i("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;
    }

}



2.RefInvoke.java

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RefInvoke {
    /**
     * 反射執行類的靜態函式(public)
     *
     * @param class_name  類名
     * @param method_name 函式名
     * @param pareTyple   函式的引數型別
     * @param pareVaules  呼叫函式時傳入的引數
     * @return
     */
    public static Object invokeStaticMethod(String class_name, String method_name, Class[] pareTyple, Object[] pareVaules) {

        try {
            Class obj_class = Class.forName(class_name);
            Method method = obj_class.getMethod(method_name, pareTyple);
            return method.invoke(null, pareVaules);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }

    /**
     * 反射執行類的函式(public)
     *
     * @param class_name
     * @param method_name
     * @param obj
     * @param pareTyple
     * @param pareVaules
     * @return
     */
    public static Object invokeMethod(String class_name, String method_name, Object obj, Class[] pareTyple, Object[] pareVaules) {

        try {
            Class obj_class = Class.forName(class_name);
            Method method = obj_class.getMethod(method_name, pareTyple);
            return method.invoke(obj, pareVaules);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }

    /**
     * 反射得到類的屬性(包括私有和保護)
     *
     * @param class_name
     * @param obj
     * @param filedName
     * @return
     */
    public static Object getFieldOjbect(String class_name, Object obj, String filedName) {
        try {
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(filedName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }

    /**
     * 反射得到類的靜態屬性(包括私有和保護)
     *
     * @param class_name
     * @param filedName
     * @return
     */
    public static Object getStaticFieldOjbect(String class_name, String filedName) {

        try {
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(filedName);
            field.setAccessible(true);
            return field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }

    /**
     * 設定類的屬性(包括私有和保護)
     *
     * @param classname
     * @param filedName
     * @param obj
     * @param filedVaule
     */
    public static void setFieldOjbect(String classname, String filedName, Object obj, Object filedVaule) {
        try {
            Class obj_class = Class.forName(classname);
            Field field = obj_class.getDeclaredField(filedName);
            field.setAccessible(true);
            field.set(obj, filedVaule);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 設定類的靜態屬性(包括私有和保護)
     *
     * @param class_name
     * @param filedName
     * @param filedVaule
     */
    public static void setStaticOjbect(String class_name, String filedName, Object filedVaule) {
        try {
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(filedName);
            field.setAccessible(true);
            field.set(null, filedVaule);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}


3、根據demo的 AndroidManifest.xml ,配置shell工程AndroidManifest.xml
注意:

  • 把demo.apk的AndroidManifest.xml中所有:許可權、元件(activity、service、broadcastreceiver) 複製過來,元件必須使用完整的包名。 
  • 使用meta-data配置 demo.apk 的MyApplication,也要使用完整包名。 

注意對比兩個配置檔案的區別。
解殼工程:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   
package="demon.myunshell">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
       
android:name=".ProxyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
       
<meta-data
            android:name="APPLICATION_CLASS_NAME"
            android:value="demon.demo.MyApplication" />

        <activity android:name="demon.demo.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name="demon.demo.ImageActivity" />
    </application>

</manifest>

demo工程:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   
package="demon.demo">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
android:name=".ImageActivity" />
    </application>

</manifest>


 

Step3:執行java工程,合併shell.dex和demo.apk,得到:classes.dex

原始碼:https://github.com/lvxiangan/Shell/tree/master/DexShellTool
這是一個java工程,目錄結構如下:

工程下新建force資料夾,將demo.apk,shell.dex複製到裡面去,執行如下程式碼,生成新的dex檔案,即classes.dex:


加密合併成功後的classes.dex,大小几乎等於demo.apk + shell.dex。

 

關鍵程式碼:
public class DexShellTool {
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            File payloadSrcFile = new File("force/demo.apk");   //需要加殼的程式
            System.out.println("apk size:"+payloadSrcFile.length());
            File unShellDexFile = new File("force/shell.dex");    //解殼dex
            byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二進位制形式讀出apk,並進行加密處理//對源Apk進行加密操作
            byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二進位制形式讀出dex
            int payloadLen = payloadArray.length;
            int unShellDexLen = unShellDexArray.length;
            int totalLen = payloadLen + unShellDexLen +4;//多出4位元組是存放長度的。
            byte[] newdex = new byte[totalLen]; // 申請了新的長度
            //新增解殼程式碼
            System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷貝dex內容
            //新增加密後的解殼資料
            System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex內容後面拷貝apk的內容
            //新增解殼資料長度
            System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 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();
            }
        }
    }
}

 

step4:把class.dex放進shell.apk,重新簽名得到:shell_demo.apk

素材下載:https://github.com/lvxiangan/Shell/tree/master/Tools

解壓step2的shell.apk

  • 將step3得到的classes.dex替換classes.dex。 
  • 重新簽名

完成後,如下圖:

注意觀察classes.dex的大小,判斷是否複製成功。

 

開始重新簽名:

  • 新建一個Tools資料夾,將前面的簽名檔案,shell.apk複製進去。 
  • 簽名命令太長不好記,我們新建sign.bat檔案,新增如下內容,注意使用該命令系統必須配置Java環境變數,可根據自身情況進行修改,方便下次使用:

jarsigner -verbose -keystore DeMon.jks -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar shelldemo.apk shell.apk key

命令說明:
jarsigner -verbose -keystore 簽名檔案 -storepass 密碼  -keypass alias的密碼 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA  簽名後的檔案 簽名前的apk alias名稱

  •  
  • 雙擊執行sign.bat檔案,成功簽名Tools檔案會新增一個shelldemo.apk,會比shell.apk稍大,大概就是生成的簽名檔案的大小。shelldemo.apk就是成功加殼後的apk,可以安裝執行。

正確簽名後的內容如下:

 

 

 

step5:驗證效果
  

注意對比demo.apk的效果圖,除了標題和包名與不一致外,功能上完全相同,即符合預期。Apk加殼成功!

 

 

 

總結

優點不多述,說說缺點吧: 
1、 Apk體積變大,尤其是res檔案成倍增長。 
2、解殼過程容易被反編譯,最好用C/C++實現
3、第一次安裝啟動需要等待載入時間較長,使用者體驗不好。

 

GitHub地址:
https://github.com/lvxiangan/Shell

改進版:https://github.com/lvxiangan/Android-Shell2
1、解決加殼後執行有兩個app的問題、
2、在一個AS工程管理各個模組,打包輸出時記得選擇切換模組
3、在殼程式實現JNI解密




參考:https://blog.csdn.net/DeMonliuhui/article/details/78269234

相關文章