Android APK加固-內存加載dex
分析DexClassLoader的構造方法
查看源碼可以到AndroidXref網站查看
查看代碼發現,DexClassLoader調用了父類BaseDexClassLoader構造
點擊父類名稱,繼續觀察父類源碼
發現構造有個核心功能DexPathList,繼續查看
觀察發現,DexPathList構造中,有一個方法makeDexElements像是創建了一個元素集,
跟入
函數判斷后綴名是否是dex,如果是就調用LoadDexFile的方法
這是它另外定義的后綴名
繼續觀察LoadDexFile
發現optimizedDirectory參數被optimizedPathFor方法轉為了一個路徑,加載dex文件的方法是DexFile類中的loadDex方法,繼續跟入
發現DexFile的loadDex返回了DexFile對象,參數分別為源路徑和輸出路徑
繼續查看DexFile的構造
發現DexFile類的構造方法中又有個方法openDexFile和加載Dex文件有關聯。繼續查看
發現openDexFIle返回了個native方法。其實現部分是在C++中。
繼續跟入
總結一下:
1.DexClassLoader調用了父類BaseDexClassLoader構造,發現父類構造有個核心功能DexPathList,
2.核心功能DexPathList構造中,有一個方法makeDexElements像是創建了一個元素集,
3.makeDexElements這個函數判斷后綴名是否是dex,如果是就調用LoadDexFile的方法,
4.在LoadDexFile方法中optimizedDirectory參數被optimizedPathFor方法轉為了一個路徑,而加載dex文件的方法是DexFile類中的loadDex方法,
5.DexFile的loadDex返回了DexFile對象,對象參數分別為源路徑和輸出路徑,
6.跟入發現DexFile類的構造方法中又有個方法openDexFile和加載Dex文件有關聯。
7.發現openDexFIle返回了個native方法。其實現部分是在C++中。
簡述:
makDexELements判斷了4種文件類型,dex/jar/zip/apk,所以android中能夠動態加載的構造方法中,就這四種。
DexClassLoader跟到最后發現最核心功能是openDexFile,native層的,傳遞文件字節碼,返回值是一個虛擬機的cookie值(java層)(C++層是pDexOrJar的指針)
分析DexClassLoader的loadClass方法
由於我們使用的是DexClassLoader,繼承自BaseDexClassLoader,而查看findClass方法在ClassLoader的源碼是必須要實現的,所以應該看BaseDexClassLoader的重寫方法。
發現其中有查找類的方法。調用findClass的pathList對象是在BaseDexClassLoader構造中創建的。
繼續分析DexPathList的findClass方法
發現返回類的對象代碼,是DexFile中的loadClassBinaryName
loadClassBinaryName方法中調用了defineClass,其中參數是名稱,類加載器,cookie值,cookie值是DexFile中的openDexFile方法的返回值。
defineClass方法是個native方法
流程:
1.ClassLoader.loadClass 方法
2.BaseDexClassLoader.findClass方法
3.DexPathList.findClass方法
4.DexFile.loadClassBinaryName方法
5.DexFile.defineClass,Native方法
與DexClassLoad的構造結合起來,可以看到,DexFile這個類是加載類的關鍵,在DexClassLoader的構造方法中,最后調用的openDexFIle方法,返回dalvik虛擬機中的一個cookie值,這個值正是loadClasss方法最后調用的defineClass的參數。
編寫自己的DexClassLoader
思路:
0.創建一個DexClassLoader的子類
1.創建構造方法,加入參數byte[]
2.使用反射調用openDexFile,獲取mCookie
3.重寫loadClass,使用反射調用defineClass
1.因為想要一個帶有傳入參數byte[]的DexClassLoader,所以新創建一個DexClassLoader子類MyDexClassLoader,創建構造方法加入參數byte[]。
public MyDexClassLoader(byte bytes[],
String dexPath,
String optimizedDirectory,
String librarySearchPath,
ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
createDexClassLoader(bytes,parent);
}
2.使用反射調用openDexFile,獲取mCookie。
為了方便,封裝了一個方法,實現調用openDexFile的邏輯,這個邏輯就是創建自己的DexClassLoader的邏輯。另外定義了兩個變量,,存放cookie和parentclassLoadr。
因為openDexFile方法是在DexFile類里面,所以代碼的邏輯應該是先獲取DexFile類,再獲取openDexFile,然后調用。
private ClassLoader mClassLoader;
private int mCookie;
private void createDexClassLoader(byte[] bytes, ClassLoader parent) {
// android 4.1 DexFile.openDexFile(byte[])
mClassLoader = parent;
try {
// 1. 獲取 DexFile 類類型
Class clz = Class.forName("dalvik.system.DexFile");
// 2. 獲取 openDexFile 方法對象
Method method = clz.getDeclaredMethod("openDexFile",byte[].class);
// 3. 調用方法,返回 cookie
method.setAccessible(true);
mCookie = (int) method.invoke(null,new Object[]{bytes});
} catch (Exception e) {
e.printStackTrace();
}
}
3.重寫loadClass
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// android 4.1 DexFile.defineClass(String name, ClassLoader loader, int cookie)
Class c = null;
try {
// 獲取加載的類信息
Class dexFile = Class.forName("dalvik.system.DexFile");
// 獲取靜態方法
Method method = dexFile.getDeclaredMethod("defineClass", String.class, ClassLoader.class, int.class);
method.setAccessible(true);
// 調用
c = (Class)method.invoke(null,name, mClassLoader, mCookie);
return c;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.loadClass(name);
}
完整的MyDexClassLoader.java
package com.bluelesson.mydexclassloader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
import dalvik.system.DexFile;
public class MyDexClassLoader extends DexClassLoader {
public MyDexClassLoader(byte bytes[],
String dexPath,
String optimizedDirectory,
String librarySearchPath,
ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
createDexClassLoader(bytes,parent);
}
private ClassLoader mClassLoader;
private int mCookie;
private void createDexClassLoader(byte[] bytes, ClassLoader parent) {
// android 4.1 DexFile.openDexFile(byte[])
mClassLoader = parent;
try {
// 1. 獲取 DexFile 類類型
Class clz = Class.forName("dalvik.system.DexFile");
// 2. 獲取 openDexFile 方法對象
Method method = clz.getDeclaredMethod("openDexFile",byte[].class);
// 3. 調用方法,返回 cookie
method.setAccessible(true);
mCookie = (int) method.invoke(null,new Object[]{bytes});
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// android 4.1 DexFile.defineClass(String name, ClassLoader loader, int cookie)
Class c = null;
try {
// 獲取加載的類信息
Class dexFile = Class.forName("dalvik.system.DexFile");
// 獲取靜態方法
Method method = dexFile.getDeclaredMethod("defineClass", String.class, ClassLoader.class, int.class);
method.setAccessible(true);
// 調用
c = (Class)method.invoke(null,name, mClassLoader, mCookie);
return c;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.loadClass(name);
}
}
使用MyDexClassLoader
為了測試方便,回顧之前的例子,動態加載Activity是比較簡單,我們可以把原先動態加載activity例子中,創建ClassLoader改為創建自己的MyDexClassLoader,加載Activity時,調用自己的loadClass。
首先新建一個簡單的activity,然后編譯代碼,將Main2Activity生成的calss文件轉為dex文件。然后將文件復制到項目中的assets目錄中,名為m2a.dex
先整理下思路,因為是內存中加載dex,所以把assets目錄中的dex文件讀取到byte數組中即可,然后創建自己的MyDexClassLoader,在獲取類型。步驟如下:
1.讀取文件,返回數組地址
2.創建dex文件的類加載器,返回DexClassLoader對象
3.使用loadClass獲取加載的類信息
4.創建Intent,啟動Activity
根據思路開始寫代碼。
封裝asset目錄讀取文件的方法
byte[] getdexFromAssets(String dexName){
// 獲取assets目錄管理器
AssetManager as = getAssets();
// 合成路徑
String path = getFilesDir() + File.separator + dexName;
Log.i(TAG, path);
try {
// 創建文件流
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 打開文件
InputStream is = as.open(dexName);
// 循環讀取文件,拷貝到對應路徑
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
return null;
}
然后就可以用我們的MyDexLoader載入讀取到的數據,並運行它。
1.讀取文件到內存
2.用自己的MyDexLoader加載
3.加載載入的m2a .dex里面的類
4.替換ClassLoader
5.啟動
public void btnClick(View view) {
// 1. 獲取dex字節數組
byte bytes[] = getdexFromAssets("m2a.dex");
// 2. 加載dex,返回dexClassLoader對象
MyDexClassLoader dex = new MyDexClassLoader(bytes,getPackageCodePath(),
getCacheDir().toString(),null,getClassLoader()
);
// 3. 加載類
Class clz = null;
try {
clz = dex.loadClass("com.bluelesson.mydexclassloader.Main2Activity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 4. 替換ClassLoader
replaceClassLoader1(dex);
// 5. 啟動activity
startActivity(new Intent(this,clz));
}
完整的MainActivity.java
package com.bluelesson.mydexclassloader;
import android.content.Intent;
import android.content.res.AssetManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "15pb-log";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void btnClick(View view) {
// 1. 獲取dex字節數組
byte bytes[] = getdexFromAssets("m2a.dex");
// 2. 加載dex,返回dexClassLoader對象
MyDexClassLoader dex = new MyDexClassLoader(bytes,getPackageCodePath(),
getCacheDir().toString(),null,getClassLoader()
);
// 3. 加載類
Class clz = null;
try {
clz = dex.loadClass("com.bluelesson.mydexclassloader.Main2Activity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 4. 替換ClassLoader
replaceClassLoader1(dex);
// 5. 啟動activity
startActivity(new Intent(this,clz));
}
byte[] getdexFromAssets(String dexName){
// 獲取assets目錄管理器
AssetManager as = getAssets();
// 合成路徑
String path = getFilesDir() + File.separator + dexName;
Log.i(TAG, path);
try {
// 創建文件流
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 打開文件
InputStream is = as.open(dexName);
// 循環讀取文件,拷貝到對應路徑
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
return null;
}
public void replaceClassLoader1(DexClassLoader dexClassLoader){
try {
// 1. 獲取ActivityThead類對象
// android.app.ActivityThread
// 1.1 獲取類類型
Class clzActivityThead = Class.forName("android.app.ActivityThread");
// 1.2 獲取類方法
Method currentActivityThread = clzActivityThead.getMethod("currentActivityThread",new Class[]{});
// 1.3 調用方法
currentActivityThread.setAccessible(true);
Object objActivityThread = currentActivityThread.invoke(null);
// 2. 通過類對象獲取成員變量mBoundApplication
//clzActivityThead.getDeclaredField()
Field field = clzActivityThead.getDeclaredField("mBoundApplication");
// AppBindData
field.setAccessible(true);
Object data = field.get(objActivityThread);
// 3. 獲取mBoundApplication對象中的成員變量info
// 3.1 獲取 AppBindData 類類型
Class clzAppBindData = Class.forName("android.app.ActivityThread$AppBindData");
// 3.2 獲取成員變量info
Field field1 = clzAppBindData.getDeclaredField("info");
// 3.3 獲取對應的值
//LoadedApk
field1.setAccessible(true);
Object info = field1.get(data);
// 4. 獲取info對象中的mClassLoader
// 4.1 獲取 LoadedApk 類型
Class clzLoadedApk = Class.forName("android.app.LoadedApk");
// 4.2 獲取成員變量 mClassLoader
Field field2 = clzLoadedApk.getDeclaredField("mClassLoader");
field2.setAccessible(true);
// 5. 替換ClassLoader
field2.set(info,dexClassLoader);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在4.4的版本運行后提示錯誤:沒有這樣的方法,換成4.1進行測試即可。
小結
這個實驗表面上看像是在assets文件夾里還存在着m2a.dex文件,但,這整個過程是,先加載m2a.dex到內存,(加載m2a. dex只是為了要讓內存中存在dex數據)然后再用MyDexClassLoader讀出來。也就是說,只要內存中用其他方法存在dex數據,用MyDexClassLoader是可以直接讀取數據,assets文件夾里面就可以沒有dex文件。達到從內存中動態加載dex加固APK的目的。