什么是Dex文件?
classes.dex是apk組成的一部分,包含了能被Dalvik/Art理解的可執(zhí)行文件,類似Windows的exe文件;
APK組成:
1. assets目錄:存放assets目錄下的文件,可以通過(guò)AssetManager對(duì)象獲取-
2. lib目錄:存放所支持的CPU架構(gòu)對(duì)應(yīng)的二進(jìn)制文件(so文件),這些文件用來(lái)各自支持自己CPU架構(gòu)的二進(jìn)制接口(ABI)
3. res目錄:存放res目錄下沒(méi)有被編譯到arsc文件的資源,layout,drawable,mipmap等-
4. META-INF目錄:存放簽名的目錄,
5. classes.dex文件:dvm的可執(zhí)行文件,將R.java,java source Coed,java interface打包成dex文件-
6. resources.arsc文件(資源映射表):res/values目錄下的所有配置內(nèi)容,以及在APK res目錄下文件文件的映射方式
7. Manifest文件:配置文件,同項(xiàng)目中的manifest文件
Dex文件內(nèi)容:

文件頭:記錄了dex文件的一些基本信息, dex文件大小,dex文件頭大小,sha1簽名,checksum校驗(yàn)和,以及大致的數(shù)據(jù)分布
索引區(qū):存放數(shù)據(jù)的偏移量
數(shù)據(jù)區(qū):真實(shí)的數(shù)據(jù)存放在data數(shù)據(jù)區(qū)
APK打包流程:

1. aapt將資源文件打包成R.Java, resource.arsc ,res目錄
2. aidl 將aidl 接口解析成對(duì)應(yīng)的java接口
3. Javac 將源代碼編譯成.class字節(jié)碼
4. dx.bat將class字節(jié)碼轉(zhuǎn)化成dvm字節(jié)碼(dex文件)
5. 打包生成APK
6. JarSignerapk進(jìn)行debug或release簽名
7. zipalign對(duì)其操作,APK包中所有的資源文件距離文件起始偏移為4字節(jié)整數(shù)倍,這樣通過(guò)內(nèi)存映射訪問(wèn)apk文件時(shí)的速度會(huì)更快

上述的操作都是通過(guò)Android SDK自帶的工具來(lái)完成;
Dex加密
下面通過(guò)一個(gè)簡(jiǎn)單的demo描述APK加固的整體流程
加密流程:
- 首先將未加密的APK進(jìn)行解壓,獲取到Dex文件,然后對(duì)dex文件的每一個(gè)字節(jié)進(jìn)行加密(AES),加密完成生成新的dex文件(classes2.dex)
- 下面創(chuàng)建一個(gè)dex殼,通過(guò)對(duì)arr文件(android module 的打包文件)的解壓可以獲取到一個(gè)classes.jar文件,再通過(guò)cmd的命令,將jar轉(zhuǎn)成殼dex
- 將殼dex 和 加密的dex(源dex)一起打包成新的APK,然后再對(duì)APK進(jìn)行簽名;簽名后可以正常安裝,
1. 解壓APK,對(duì)Dex文件加密
-
解壓apk
File apkFile = new File("source/apk/app-debug.apk");
// 解壓apk文件到unzip目錄
File apkUnZipFile = new File("source/apk/unzip");
Zip.unZip(apkFile,apkUnZipFile);
-
將dex文件轉(zhuǎn)為內(nèi)存中的字節(jié)數(shù)組
// 創(chuàng)建dexFile,將dexFile寫(xiě)入內(nèi)存—dexBytes
File dexFile = new File("source/apk/unzip/classes.dex");
RandomAccessFile inputStream = new RandomAccessFile(dexFile,"r");
byte[] dexBytes = new byte[(int) inputStream.length()];
inputStream.readFully(dexBytes);
inputStream.close();
-
AES加密初始化
// AES加密操作初始化
Cipher encoder = Cipher.getInstance("AES/ECB/PKCS5Padding");
Cipher decoder = Cipher.getInstance("AES/ECB/PKCS5Padding");
String key = "abcdefghijklmnop";
byte[] keyBytes = key.getBytes();
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes,"AES");
encoder.init(Cipher.ENCRYPT_MODE,secretKeySpec);
decoder.init(Cipher.DECRYPT_MODE,secretKeySpec);
-
字節(jié)數(shù)組加密
// 對(duì)dex字節(jié)數(shù)組加密
byte[] dexBytesEncrypted = encoder.doFinal(dexBytes);
-
加密后的字節(jié)數(shù)組轉(zhuǎn)為dex文件
// 將加密后的dex字節(jié)數(shù)組寫(xiě)入原來(lái)的文件,
FileOutputStream fos = new FileOutputStream(dexFile);
fos.write(dexBytesEncrypted);
fos.close();
// 將加密后的dex文件改名為classes1.dex(源)
dexFile.renameTo(new File("source/apk/unzip/classes1.dex"));
2. 解壓aar,獲取殼
-
解壓aar文件,獲取classes.jar
// 將arr文件解壓
File arrFile = new File("source/aar/mylibrary-debug.aar");
Zip.unZip(arrFile,new File("source/aar/unzip"));

-
通過(guò)cmd調(diào)用dx將jar轉(zhuǎn)為dex
// 通過(guò)cmd 調(diào)用 dx 將jar轉(zhuǎn)為dex(殼)
File jarFile = new File("source/aar/unzip/classes.jar");
File desDexFile = new File("source/apk/unzip/classes.dex");
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cmd.exe /C dx --dex --output="
+ desDexFile.getAbsolutePath()
+ " "
+ jarFile.getAbsolutePath());
process.waitFor(); // 等待子進(jìn)程完成
process.destroy();
通過(guò)RunTime啟動(dòng)cmd命令,jvm會(huì)創(chuàng)建一個(gè)子進(jìn)程Process,waitFor()表示當(dāng)前進(jìn)程阻塞直到子進(jìn)程完成;
3. 打包新APK,通過(guò)cmd調(diào)用jarsigner重新簽名
-
打包成新的apk文件
// 殼 和 源 打包成新apk
File unsignedApk = new File("source/result/unsigned.apk");
Zip.zip(apkUnZipFile,unsignedApk);
-
簽名
// 簽名
File signedApk = new File("source/result/signed.apk");
Signature.signature(unsignedApk,signedApk);
public class Signature {
public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
String cmd[] = {"cmd.exe", "/C ","jarsigner", "-sigalg", "MD5withRSA",
"-digestalg", "SHA1",
"-keystore", "C:/Users/allen/.android/debug.keystore",
"-storepass", "android",
"-keypass", "android",
"-signedjar", signedApk.getAbsolutePath(),
unsignedApk.getAbsolutePath(),
"androiddebugkey"};
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start sign");
// BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
// String line;
// while ((line = reader.readLine()) != null)
// System.out.println("tasklist: " + line);
try {
int waitResult = process.waitFor();
System.out.println("waitResult: " + waitResult);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
System.out.println("process.exitValue() " + process.exitValue() );
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len=inputStream.read(buffer)) != -1){
bos.write(buffer,0,len);
}
System.out.println(new String(bos.toByteArray(),"GBK"));
throw new RuntimeException("簽名執(zhí)行失敗");
}
System.out.println("finish signed");
process.destroy();
}
}

殼中是未加密的module代碼,可以直接運(yùn)行,并且負(fù)責(zé)源dex的解密工作
Dex解密(脫殼)
脫殼實(shí)現(xiàn):
脫殼解密過(guò)程一般是在殼Module的Application中進(jìn)行,參考Tinker的脫殼實(shí)現(xiàn):首先將apk進(jìn)行解壓獲取到加密的classes1.dex文件,然后通過(guò)流轉(zhuǎn)成Byte數(shù)組,再進(jìn)行AES解密,解密后重新寫(xiě)回到原來(lái)的classes1.dex;至此,解密過(guò)程完成,下面需要將解密后的dex文件運(yùn)行起來(lái),
在Application中重寫(xiě)attachBaseContext()
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
所有的脫殼,尋找dex中的class,classloader進(jìn)行類加載,都是在attachBaseContext()中完成的(即App運(yùn)行啟動(dòng)時(shí))
根據(jù)指定目錄獲取apk,解壓,尋找源dex,解密
File apkFile = new File(getApplicationInfo().sourceDir);
//data/data/包名/files/fake_apk/
File unZipFile = getDir("fake_apk", MODE_PRIVATE);
File app = new File(unZipFile, "app"); // 根據(jù)指定的目錄找到apk文件
if (!app.exists()) {
Zip.unZip(apkFile, app); // 解壓apk
File[] files = app.listFiles();
for (File file : files) {
String name = file.getName();
if (name.equals("classes.dex")) { // 過(guò)濾殼dex
} else if (name.endsWith(".dex")) { // 選擇源dex 解密
try {
byte[] bytes = getBytes(file);
FileOutputStream fos = new FileOutputStream(file);
byte[] decrypt = AES.decrypt(bytes);
// fos.write(bytes);
fos.write(decrypt); // 將解密后的字節(jié)數(shù)組寫(xiě)回源文件(源dex文件)
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
將所有的dex文件從apk取出,進(jìn)行類加載
List list = new ArrayList<>(); // 解密后的dex文件
Log.d("FAKE", Arrays.toString(app.listFiles()));
for (File file : app.listFiles()) {
if (file.getName().endsWith(".dex")) {
list.add(file);
System.gc();
}
}
對(duì)源dex中加密的類進(jìn)行類加載
ClassLoader原理
先看一下上文類加載的原理,在介紹加固脫殼的類加載思路:
- 通過(guò)反射獲取
classLoader的pathList(DexPathList類) - 再獲取
pathList的DexElements(element[]) - 傳入源dex,通過(guò)反射調(diào)用
DexPathList類的makeDexElements創(chuàng)建新的Element[], - 合并兩個(gè)數(shù)組
- 反射將新的
element[]set給classLoader


