一、前 言
Android Apk加固的發展已經有一段時間了,相對來說本篇博客要記錄的Android加殼的實現思路是4年的東西了,已經被老鳥玩爛了,Android加固的安全廠商也不會採用這麼粗獷的方式來進行Android Apk的加固處理。早期Android加固聚焦的兩個點主要是在DexClassLoader和Android類加載這兩條代碼執行流程上去下功夫,後期Android加固會在Android動態庫so的加固處理和Android應用程序的自定義smali字節碼虛擬機上下功夫來實現與逆向分析的對抗。本篇博客記錄主要學習參考的博文是Android APK加殼技術方案【1】、Android APK加殼技術方案【2】、APK加殼【1】初步方案實現詳解 這3篇。Android加固的關注點主要集中在兩個方面:dex文件的隱藏和dex文件的加載運行,針對Android應用dex文件的加固也有兩種方案即,dex文件的整體加固方案、dex文件的部分加固方案。本篇博客提到的Android Apk的加固屬於dex整理加固的方案。
源程序:需要被加殼保護的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();
}
}
}
}
(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;
}
}
上面的代碼中使用到的反射工具類 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();
}
}
}
四、後 記
本篇博文主要是對Jack_Jia大牛的加殼原理文章的整理和學習,一些關鍵地方描述直接照搬Jack_Jia大牛的描述和示意圖,知道自己描述不清楚,同時也參考了 桃園小七 的博客,一併感謝。這裏僅僅是對Android程序的整體dex加固原理的初步學習,如果要做出一款能用的Android加固殼還需要考慮很多因素,比如Android系統的兼容性、加殼後應用的啓動速度等等。由於每個Android系統版本的源碼會有所改變,爲了兼容性的考慮,DexClassLoader的動態加載和源程序Application的替換需要有針對性的進行修改。