Apk通過安卓修改大師加固的內部邏輯揭祕
一、前 言
Android Apk加固的發展已經有一段時間了,相對來說本文要記錄的Android加殼的實現思路是4年的東西了,已經被老鳥玩爛了,Android加固的安全廠商也不會採用這麼粗獷的方式來進行Android Apk的加固處理。早期Android加固聚焦的兩個點主要是在DexClassLoader和Android類載入這兩條程式碼執行流程上去下功夫,後期Android加固會在Android動態庫so的加固處理和Android應用程式的自定義smali位元組碼虛擬機器上下功夫來實現與逆向分析的對抗。安卓修改大師是Android APK加殼技術方案【1】、Android APK加殼技術方案【2】
源程式:需要被加殼保護的Android應用程式的程式碼,可以是Android應用的apk也可以是Android應用的dex檔案和so庫檔案。 加密工具:對源程式進行加密的工具即對Android應用的apk或者Android應用的dex檔案和so庫檔案進行加密的工具,為解殼程式提供源程式的解殼資料,語言和平臺不限,加密處理方式根據加殼的需求來處理。 解殼程式:解密解殼資料並通過DexClassLoader動態載入和執行源程式的程式碼,實現Android解殼程式與Android源程式的無縫替換,Android源程式apk得到執行,Android解殼程式又被稱作外殼程式。
二、Android 源程式的加殼思路
1.解殼資料位於解殼程式檔案尾部
先將Android源程式的Apk使用加密工具進行加密處理得到需要解密的解殼資料,然後將該解殼資料放到Android解殼程式的dex檔案的後面進行隱藏。
加殼源程式工作流程: 1. 加密源程式apk檔案為解殼資料。 2. 把解殼資料寫入解殼程式dex檔案末尾,並在檔案尾部新增解殼資料的長度大小。 3. 修改解殼程式dex頭中checksum、signature 和file_size頭資訊。 4. 修改源程式AndroidMainfest.xml檔案並覆蓋解殼程式AndroidMainfest.xml檔案。
源程式Apk加殼處理後,解殼程式dex檔案的示意圖如下:
解殼程式工作流程: 1.讀取解殼程式dex檔案末尾的資料,獲取解殼資料長度。 2.從解殼程式dex檔案中讀取解殼資料,解密解殼資料,以檔案形式儲存解密資料到xx.apk檔案。 3.獲取到釋放出的源程式xx.apk,解殼程式apk應用通過DexClassLoader動態載入xx.apk進行執行。
2.解殼資料位於解殼程式檔案頭後面
先將Android源程式的Apk使用加密工具進行加密處理得到需要解密的解殼資料,然後將該解殼資料放到Android解殼程式的dex檔案的檔案頭的後面。
源程式Apk加殼處理後,解殼程式dex檔案的示意圖如下:
加殼源程式工作流程: 1. 加密源程式apk檔案為解殼資料。 2. 計算解殼資料長度,新增該資料長度到解殼dex檔案頭的末尾(插入資料的位置為0x70處),並繼續新增解殼資料到檔案頭末尾。 3. 修改解殼程式dex檔案頭中checksum、signature、file_size、header_size、string_ids_off、type_ids_off、proto_ids_off、field_ids_off、method_ids_off、class_defs_off和data_off相關項,分析map_off 資料修改相關的資料偏移量。 4. 修改源程式AndroidMainfest.xml檔案並覆蓋解殼程式AndroidMainfest.xml檔案。
解殼程式工作流程: 1.從解殼程式的dex檔案位置偏移0x70處讀取解殼資料的長度。 2.從解殼程式的dex檔案中讀取解殼資料並解密解殼資料,以檔案形式儲存解密資料到xx.apk。 3.獲取到釋放出的源程式xx.apk,解殼程式apk應用通過DexClassLoader動態載入xx.apk進行執行。
提示: 上面的示意圖和加殼步驟是搬運自Jack_Jia大牛的部落格修改而來,知道自己語言組織能力不行也不獻醜了。儘管現在很多Android加固廠商加密和隱藏源程式apk都不會採取上面的方法,但是對於學習Android的加固拓展思路還是有幫助的。當然了Android加固廠商也不會簡單粗暴的直接加密整個源程式的apk,這樣做效率不高也沒有必要,將源程式的apk進行解包拆分為dex檔案、so庫檔案和資源,然後分別對dex檔案、so庫檔案和資源進行有針對性的加固處理,例如:dex檔案的記憶體載入、so庫檔案的自定義載入、資源的弱加固處理等等,上面提到的Android加殼方法僅僅是一個大致的實現思路,離商業應用還有一段不小的距離。
三、Android 源程式加殼的程式碼實現
Jack_Jia提供了兩種加密隱藏Android源程式的方法,第1種相對來說比較簡單,第2種比較複雜,涉及到Android解殼程式dex檔案的修改地方比較多,因此以第1種對整個Android源程式APK包進行加密隱藏處理的方案來實現。Jack_Jia提供的加殼程式碼是基於Android 2.3的系統程式碼來實現的,如果要該加殼程式碼在Android系統以後版本上執行成功,還需參考相關Android系統原始碼APK執行的流程來進行對應的反射修改。
(1).解殼資料位於解殼程式dex檔案尾部的加殼流程:
1.加密源程式apk為解殼資料,加密源程式apk的演算法需要自己自定義。 2.把解殼資料寫入到解殼程式dex檔案末尾,並在檔案尾部新增解殼資料的長度大小。 3.修正解殼程式dex檔案頭中checksum、signature 和file_size欄位。
對Android源程式apk進行加密隱藏的功能由java程式碼編寫的加密工具DexShellTool來實現,其中[ g:/payload.apk ]為被加密隱藏保護的源程式apk,[ g:/unshell.dex ]為Android解殼程式apk的原dex檔案,[ g:/classes.dex ]為Android解殼程式apk被修改後帶有解殼資料的新dex檔案。
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;// 對加殼的Apk進行加密隱藏處理的工具程式碼public class DexShellTool { public static void main(String[] args) { try { // 開啟被加殼的apk檔案"g:/payload.apk" File payloadSrcFile = new File("g:/payload.apk"); // 開啟加殼apk程式的外殼apk的dex檔案"g:/unshell.dex" File unShellDexFile = new File("g:/unshell.dex"); // 讀取payload.apk檔案的資料內容進行加密處理 byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile)); // 讀取外殼apk的dex檔案"g:/unshell.dex"的資料到位元組陣列中 byte[] unShellDexArray = readFileBytes(unShellDexFile); // 加密後payload.apk檔案的資料長度 int payloadLen = payloadArray.length; // 外殼程式apk的dex檔案"g:/unshell.dex"的資料長度 int unShellDexLen = unShellDexArray.length; // 獲取加密後"g:/payload.apk"檔案隱藏到"g:/unshell.dex"中所需的位元組長度 int totalLen = payloadLen + unShellDexLen +4; // 申請記憶體空間 byte[] newdex = new byte[totalLen]; // 新增解殼程式碼"g:/unshell.dex"到申請記憶體緩衝區中 System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen); // 新增加密後"g:/payload.apk"檔案的解殼資料到申請記憶體緩衝區中 System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen); // 新增加密後"g:/payload.apk"檔案的解殼資料長度到末尾的4位元組 System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4); // 修正DEX file size檔案頭 fixFileSizeHeader(newdex); // 修正DEX SHA1簽名 檔案頭 fixSHA1Header(newdex); //修改DEX CheckSum檔案頭 fixCheckSumHeader(newdex); String str = "g:/classes.dex"; // 建立或者開啟檔案"g:/classes.dex" File file = new File(str); // 刪除已存在舊的檔案 if (!file.exists()) { file.createNewFile(); } // 構建新的加殼的外殼dex檔案"g:/classes.dex" FileOutputStream localFileOutputStream = new FileOutputStream(str); localFileOutputStream.write(newdex); // 重新整理檔案流 localFileOutputStream.flush(); // 關閉檔案即構建檔案完成 localFileOutputStream.close(); } catch (Exception e) { e.printStackTrace(); } } // 直接返回資料,讀者可以新增自己加密方法 private static byte[] encrpt(byte[] srcdata){ // 自定義的加密演算法,對原apk檔案的資料和apk檔案的長度進行加密處理 return srcdata; } // 修正dex檔案頭的校驗碼checksum欄位 private static void fixCheckSumHeader(byte[] dexBytes) { Adler32 adler = new Adler32(); adler.update(dexBytes, 12, dexBytes.length - 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])); } // 修正dex檔案的校驗碼checksum System.arraycopy(recs, 0, dexBytes, 8, 4); System.out.println(Long.toHexString(value)); System.out.println(); } // 將int型資料轉換成位元組陣列的形式存放 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檔案頭存放dex檔案signature(SHA-1簽名)的欄位 private static void fixSHA1Header(byte[] dexBytes) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(dexBytes, 32, dexBytes.length - 32); byte[] newdt = md.digest(); // 修正dex檔案的SHA-1簽名 System.arraycopy(newdt, 0, dexBytes, 12, 20); // 列印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檔案頭存放dex檔案大小的欄位file_size 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])); } // 修正dex檔案的大小 System.arraycopy(refs, 0, dexBytes, 32, 4); } // 讀取檔案的資料內容到位元組陣列中 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(); } } } }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
(2).加殼的Android源程式被Android解殼程式解殼的流程和程式碼實現:
-
Android應用程式由不同的元件構成,Android系統在有需要的時候才會啟動Android程式的元件。因此,解殼程式必須在Android系統啟動元件之前執行,完成對解殼資料的解殼以及解殼後對源程式apk檔案的動態載入,否則會使程式出現載入類失敗的異常。Android開發者都知道Applicaiton做為整個應用的上下文,會被Android系統第一時間呼叫,這也是Android應用開發者程式程式碼的第一執行點。因此,通過對解殼程式AndroidMainfest.xml中的 application 進行配置即繼承實現Application類,就可以實現解殼程式碼第一時間執行,如下圖所示:
-
如何替換回源程式Apk原有的Application? 當在解殼程式Apk的AndroidMainfest.xml檔案配置為解殼程式碼的Application類時,源程式Apk原有的Applicaiton將被替換。為了不影響源程式Apk的程式碼邏輯,我們需要在解殼程式碼執行完成後,替換回源程式Apk原有的Application類物件,我們通過在解殼程式apk的AndroidMainfest.xml檔案中配置源程式apk原有Applicaiton類的資訊 來達到我們的目的,解殼程式Apk要在執行完畢後,通過建立meta-data配置的Application物件並通過反射修改回源程式Apk的原Application。在解殼程式apk的AndroidMainfest.xml檔案中配置源程式apk原有Applicaiton類的資訊如下圖所示,通過反射的修改見後面的實現程式碼:
-
如何通過DexClassLoader實現對apk程式碼的動態載入? 我們知道DexClassLoader載入的類是沒有元件生命週期的,也就是說即使DexClassLoader通過對APK的動態載入完成了對元件類的載入,當系統啟動該元件時,還會出現載入類失敗的異常。為什麼Android元件類被動態載入入Android虛擬機器,但Android系統卻出現載入類失敗呢? 通過檢視Android原始碼知道Android元件類的載入是由另一個ClassLoader來完成的,DexClassLoader和系統元件ClassLoader並不存在關係,系統元件ClassLoader當然找不到由DexClassLoader載入的類,如果把系統元件ClassLoader的parent修改成DexClassLoader,就可以實現對apk程式碼的動態載入。
-
如何使解殼後的源程式apk的資原始檔被程式碼動態引用? 程式碼預設引用的資原始檔在最外層的解殼程式中,因此,我們要增加系統的資源載入路徑來實現對解殼後APK檔案資源的載入,比較簡單的做法是 Android解殼程式在程式碼上只有繼承重寫的Application類而沒有其他的程式碼實現,圖示什麼的使用源程式Apk的圖示;將源程式apk的全部資源拷貝到解殼程式apk中並將源程式apk的activity、service、content provider、broadcast receiver等的宣告描述新增到解殼程式apk的AndroidMainfest.xml檔案中。
源程式Apk的解殼程式碼在解殼程式Apk繼承重寫Application類的ProxyApplication中實現,在ProxyApplication類的attachBaseContext方法中實現對源程式apk(這裡為 payload.apk)的解殼釋放和源程式apk的DexClassLoader載入以及將源程式的DexClassLoader與解殼程式的ClassLoader引用關聯起來,在ProxyApplication類的onCreate方法中通過反射修改替換掉解殼程式apk的Application為源程式apk的Application並呼叫執行。
解殼程式的解殼程式碼 ProxyApplication.java 的實現
package com.example.androidshell;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.util.ArrayList;import java.util.HashMap;import java.util.Iterator;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;import dalvik.system.DexClassLoader;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.pm.PackageManager.NameNotFoundException;import android.os.Bundle;public class ProxyApplication extends Application { private static final String appkey = "APPLICATION_CLASS_NAME"; private String apkFileName; private String odexPath; private String libPath; protected void attachBaseContext(Context base) { super.attachBaseContext(base); try { // 在當前apk目錄下建立資料夾"payload_odex" File odex = this.getDir("payload_odex", MODE_PRIVATE); // 在當前apk目錄下建立資料夾"payload_lib"存放被加密隱藏的apk的so庫檔案 File libs = this.getDir("payload_lib", MODE_PRIVATE); // 獲取檔案的全路徑 odexPath = odex.getAbsolutePath(); libPath = libs.getAbsolutePath(); // 拼接構建檔案路徑字串 apkFileName = odex.getAbsolutePath() + "/payload.apk"; // 建立檔案"payload.apk" File dexFile = new File(apkFileName); // 刪除舊的檔案 if (!dexFile.exists()) dexFile.createNewFile(); // 讀取加殼外殼apk程式的classes.dex檔案資料到位元組陣列中 byte[] dexdata = this.readDexFileFromApk(); // 解密釋放被加密隱藏的apk檔案並獲取該apk的so庫檔案 this.splitPayLoadFromDex(dexdata); // 配置動態載入環境,用以後面獲取當前apk的父ClassLoader Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {}); // 獲取當前外殼apk的包名 String packageName = this.getPackageName(); HashMap mPackages = (HashMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mPackages"); // 獲取當前外殼apk的引用 WeakReference wr = (WeakReference) mPackages.get(packageName); // public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) // 動態載入被加隱藏釋放的原apk程式 DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath, libPath, (ClassLoader)RefInvoke.getFieldOjbect("android.app.LoadedApk", wr.get(), "mClassLoader")); // 將動態載入的加密隱藏釋放的原apk的ClassLoader和當前外殼apk的引用關聯起來 RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader); } catch (Exception e) { e.printStackTrace(); } } // 先執行的attachBaseContext再執行的onCreate函式 public void onCreate() { // 如果源應用配置有Appliction物件,則替換為源應用Applicaiton,以便不影響源程式邏輯。 String appClassName = null; try { // 獲取meta-data元資料即獲取被加密隱藏的原apk的Application // 其實APPLICATION_CLASS_NAME不一定要配置在AndroidMainfest.xml檔案中,寫在程式碼裡也是可以的,只不過通用性不是那麼好。 ApplicationInfo ai = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA); Bundle bundle = ai.metaData; if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) { // 獲取被加密隱藏的原apk的Application appClassName = bundle.getString("APPLICATION_CLASS_NAME"); } else { return; } } catch (NameNotFoundException e) { e.printStackTrace(); } // 呼叫靜態方法android.app.ActivityThread.currentActivityThread 獲取當前activity的執行緒物件 // public static ActivityThread currentActivityThread() { // return sCurrentActivityThread; // } Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {}); // 獲取當前activity的執行緒物件sCurrentActivityThread中mBoundApplication成員變數,該物件是一個AppBindData類物件 Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mBoundApplication"); // 獲取mBoundApplication中的info成員變數,其中info是LoadedApk類物件 Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "info"); // 設定loadedApkInfo物件的mApplication成員變數為null(因為不是通過系統載入apk的方式載入的) RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null); // 獲取currentActivityThread物件sCurrentActivityThread中的mInitialApplication成員變數(存放當前Application) Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication"); // 獲取currentActivityThread物件sCurrentActivityThread中的mAllApplications成員變數(存放Application的列表) ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications"); // 從當前mAllApplications中移除當前(舊的)Application mAllApplications.remove(oldApplication); // 獲取上面得到LoadedApk物件中的mApplicationInfo成員變數,是個ApplicationInfo物件 ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.LoadedApk", loadedApkInfo, "mApplicationInfo"); // 獲取上面得到AppBindData物件中的appInfo成員變數,也是個ApplicationInfo物件 ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "appInfo"); // 把上面兩個物件的classNam成員變數設定為從meta-data中獲取的被隱藏加密的apk的Application路徑 appinfo_In_LoadedApk.className = appClassName; appinfo_In_AppBindData.className = appClassName; // 呼叫LoadedApk中的makeApplication 方法,構造一個新的application Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] { false, null }); // 設定currentActivityThread物件sCurrentActivityThread中的mInitialApplication成員變數的值為新構造的Application RefInvoke.setFieldOjbect("android.app.ActivityThread", "mInitialApplication", currentActivityThread, app); // 獲取當前activity的執行緒物件sCurrentActivityThread中的成員變數mProviderMap的值 HashMap mProviderMap = (HashMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mProviderMap"); // 遍歷該物件mProviderMap Iterator it = mProviderMap.values().iterator(); while (it.hasNext()) { Object providerClientRecord = it.next(); // 獲取ProviderClientRecord類物件的成員變數mLocalProvider Object localProvider = RefInvoke.getFieldOjbect("android.app.ActivityThread$ProviderClientRecord", providerClientRecord, "mLocalProvider"); // 設定mLocalProvider中的成員變數mContext的值為新構建的Application即app RefInvoke.setFieldOjbect("android.content.ContentProvider", "mContext", localProvider, app); } // 新構建Application呼叫onCreate()方法 app.onCreate(); } // 解密釋放被加密隱藏的apk檔案並獲取該apk的so庫檔案 private void splitPayLoadFromDex(byte[] data) throws IOException { // 獲取被加密apk解密後的apk檔案相關的資料(包括長度和原apk檔案的資料) byte[] apkdata = decrypt(data); // 獲取解密後apk檔案相關的資料長度(存放在最後4位元組) int ablen = apkdata.length; // 獲取被加密原apk檔案的資料長度的位元組陣列 byte[] dexlen = new byte[4]; System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4); // 獲取到被加密原apk檔案的int型的資料長度 ByteArrayInputStream bais = new ByteArrayInputStream(dexlen); DataInputStream in = new DataInputStream(bais); int readInt = in.readInt(); // 列印原apk檔案的資料長度 System.out.println(Integer.toHexString(readInt)); // 獲取原apk檔案的資料釋放出被隱藏的apk檔案 byte[] newdex = new byte[readInt]; System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt); // 釋放被隱藏的apk檔案 File file = new File(apkFileName); try { // 釋放被加密隱藏的原apk檔案 FileOutputStream localFileOutputStream = new FileOutputStream(file); localFileOutputStream.write(newdex); localFileOutputStream.close(); } catch (IOException localIOException) { throw new RuntimeException(localIOException); } // 解析被加密的原apk檔案獲取so庫檔案 ZipInputStream localZipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(file))); while (true) { ZipEntry localZipEntry = localZipInputStream.getNextEntry(); if (localZipEntry == null) { localZipInputStream.close(); break; } // 被加密apk的so庫檔案儲存到指定的檔案路徑下 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即當前apk的"classes.dex"檔案到位元組記憶體緩衝區中 private byte[] readDexFileFromApk() throws IOException { ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream(); // 讀取當前外殼apk檔案的資料 ZipInputStream localZipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(this.getApplicationInfo().sourceDir))); while (true) { // 遍歷外殼apk壓縮檔案 ZipEntry localZipEntry = localZipInputStream.getNextEntry(); if (localZipEntry == null) { localZipInputStream.close(); break; } // 判斷是否是加殼外殼apk的"classes.dex"即當前apk的"classes.dex"檔案 if (localZipEntry.getName().equals("classes.dex")) { byte[] arrayOfByte = new byte[1024]; while (true) { int i = localZipInputStream.read(arrayOfByte); if (i == -1) break; // 把讀取的dex檔案的資料寫到到位元組記憶體緩衝區中 dexByteArrayOutputStream.write(arrayOfByte, 0, i); } } localZipInputStream.closeEntry(); } localZipInputStream.close(); return dexByteArrayOutputStream.toByteArray(); } // 直接返回資料,讀者可以新增自己解密方法 // 返回被加密apk解密後的apk檔案的資料,例如:payload.apk private byte[] decrypt(byte[] data) { // 對加密的apk程式的資料進行解密處理 return data; } }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
上面的程式碼中使用到的反射工具類 RefInvoke.java 的實現
package com.example.androidshell;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;// Android的類反射呼叫的工具public class RefInvoke { // 通過類反射呼叫呼叫類的靜態方法 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 (SecurityException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } // 通過類反射呼叫呼叫類的例項方法 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 (SecurityException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } // 通過類反射呼叫獲取類的例項成員變數 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 (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } // 通過類反射呼叫獲取類的靜態成員變數 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 (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } // 通過類反射呼叫設定類的例項成員變數的值 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 (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } // 通過類反射呼叫設定類的靜態成員變數的值 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 (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
四、後 記
本文通過分析安卓修改大師(http://www.apkeditor.cn)的加殼原理文章的整理和學習,一些關鍵地方描述直接照搬Jack_Jia大牛的描述和示意圖,知道自己描述不清楚,同時也參考了 桃園小七 的部落格,一併感謝。這裡僅僅是對Android程式的整體dex加固原理的初步學習,自己可以仿照上面的原理開發安卓修改大師實現的,開發一款能用的Android加固殼還需要考慮很多因素,比如Android系統的相容性、加殼後應用的啟動速度等等。由於每個Android系統版本的原始碼會有所改變,為了相容性的考慮,DexClassLoader的動態載入和源程式Application的替換需要有針對性的進行修改。