apk加固是每一個app發布之前必須要做的事情;如果一個apk沒有加固那么別人就很容易被別人反編譯,看到這其中的原碼,雖然現在有代碼混淆、把業務寫到native層,但是這都是治標不治本。反編譯的技術在更新,那么保護Apk的技術就不能停止,雖然網上有很多加固的第三方框架,但是這加固的原理還是有必要去了解一下的,其實加固有些人認為很高深的技術,其實不然,說的簡單點就是對源Apk的dex進行加密,然后在套上一層殼即可,當然這里還有一些細節需要處理,這就是本文需要介紹的內容了。
加固原理
1.在主工程中添加一個Module這個module就是用來給apk加殼用的。
2.在主工程中添加一個java的中lib庫這個是用來加密主apk的dex、生成加固后的apk
一個apk需要啟動系統會去解壓這個apk去得用dex文件,去解析這些dex文件去啟動改apk,因為要把這個主apk的dex加密來達到不被反編譯的效果,所以這些dex文件已經被加密了,系統去加載這些dex文件肯定是會報錯的。為了能讓apk能正常啟動那就應該要去啟動這個殼的Application。所以得在app的AndroidManifest注冊殼的Application;這樣才能正常的啟動App。
下面的Apk加固的流程圖:
加密的流程
先要生成Module中的aar庫,生成后要解壓該庫得apk的殼dex包,在proxy_tools的java庫中進行加密
生成apk的殼文件(jar轉dex)
File aarFile = new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
File aarTemp = new File("proxy_tools/temp");
Zip.unZip(aarFile, aarTemp);
File classesJar = new File(aarTemp, "classes.jar");
File classesDex = new File(aarTemp, "classes.dex");
Process process = null;
try {
process = Runtime.getRuntime().exec("cmd /c dx --dex --output " + classesDex.getAbsolutePath()
+ " " + classesJar.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (process.exitValue() != 0) {
throw new RuntimeException("dex error");
}
通過這樣的操作就可以把aar中的jar轉換成dex包了,這樣apk的殼dex就可以得到了。
加密apk中的dex文件
//下面加密碼APK中所有的dex文件
File apkFile = new File("app/build/outputs/apk/debug/app-debug.apk");
File apkTemp = new File("app/build/outputs/apk/debug/temp");
//把apk解壓出來
Zip.unZip(apkFile, apkTemp);
//只要dex文件拿出來加密
File dexFiles[] = apkTemp.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});
//進行AES加密
AES.init(AES.DEFAULT_PWD);
for (File dexFile : dexFiles) {
try {
byte[] bytes = Utils.getBytes(dexFile);
byte[] encrypt = AES.encrypt(bytes);
FileOutputStream fileOutputStream = new FileOutputStream(new File(apkTemp,
"secret-" + dexFile.getName()));
fileOutputStream.write(encrypt);
fileOutputStream.flush();
fileOutputStream.close();
dexFile.delete();//刪除沒有加密的dex
} catch (Exception e) {
e.printStackTrace();
}
}
這樣就進行了apk中的dex文件加密,加密完成后需要把殼dex文件放到apk的目錄下生成新apk文件。
重新生成apk文件
//把dex放入apk解壓路徑中,重新生成apk文件
classesDex.renameTo(new File(apkTemp, "classes.dex"));
File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
try {
//生未簽名的apk
Zip.zip(apkTemp, unSignedApk);
} catch (Exception e) {
e.printStackTrace();
}
新的apk已經生成,下面就要簽名對齊
新apk的簽名對齊
//4.簽名對齊
File alignedApk = new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
try {
process = Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 " + unSignedApk.getAbsolutePath()
+ " " + alignedApk.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}
try {
process.waitFor();
} catch (InterruptedException e) {
System.out.println("exception " + e.toString());
e.printStackTrace();
}
簽名對齊后這樣就可以簽名生成可以運行的apk了
簽名生成可以運行的apk文件
File jskFile = new File("proxy_tools/proxy.jks");
System.out.println("開始簽名");
try {
process = Runtime.getRuntime().exec("cmd /c apksigner sign --ks " + jskFile.getAbsolutePath()
+ " --ks-key-alias qwert --ks-pass pass:123456 --key-pass pass:123456 --out "
+ signedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath());
System.out.println("簽名完成");
} catch (IOException e) {
e.printStackTrace();
}
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (process.exitValue() != 0) {
throw new RuntimeException("signed apk error");
}
System.out.println("生成apk成功");
}
到這里加密部分的已經完成,加密完成后就可以進行解密操作了。
解密流程
在解密之前先說一下apk安裝后都會在/data/app/包名-xxx/生成一個叫base.apk的文件,這個是文件是apk中的所有dex文件一個包,所以需要拿到這個base.apk中的dex進行解密。
File apkFile = new File(getApplicationInfo().sourceDir);
Log.d(TAG,"apk的路徑:"+apkFile.getAbsolutePath());
File versionDir = getDir(app_name+"_"+ app_version,MODE_PRIVATE);
File appDir = new File(versionDir,"app");
File dexFile = new File(appDir,"dexDir");
//得用我們的需要加載的dex文件
List<File> dexFiles = new ArrayList<>();
if(MD5.verification(dexFile)){
//把apk解壓出來
Zip.unZip(apkFile,appDir);
//獲取目錄下所有文件
File [] files = appDir.listFiles();
for(File file : files){
String name = file.getName();
Log.d(ProxyApplication.class.getName(),"dexName:"+name);
if(name.endsWith(".dex") &&!TextUtils.equals(name,"classes.dex")){
AES.init(AES.DEFAULT_PWD);
//讀取文件內容
try {
byte[] bytes = Utils.getBytes(file);
//解密
byte[] decrypt = AES.decrypt(bytes);
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(decrypt);
fileOutputStream.flush();
fileOutputStream.close();
dexFiles.add(file);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}else {
for(File file : dexFile.listFiles()){
dexFiles.add(file);
}
}
//加載解密后的dex文件到系統
try {
loadDex(dexFiles,versionDir);
} catch (Exception e) {
e.printStackTrace();
}
這里就已經完成了dex文件的解密,接下來的就應該把解密后的dex文件交給系統處理讓它去運行apk。
把dex交給系統處理
要把已經解密好的dex文件交給系統,那么首先要知道系統是如何的加載dex文件的,只有搞懂系統是如何的加載dex文件的才能把已經解密好的dex文件交給系統;getClassLoader這個方法相信我們都不陌生了,通過查看系統原碼發現這個getClassLoader返回的ClassLoader是具體實現是一個叫PathClassLoader的類,這個類在系統原碼中看到只有構造方法就沒有其它的方法了,那再往它的父類中看,它的父類是一個叫BaseDexClassLoader通過查看里面的方法可以看到有一個findClass的方法去加載這些類。findClass這個類又調用了pathList的里面的findClass方法,那個這個pathList又是什么呢?這個pathList是一個叫DexPathList的類,那再到DexPathList中查看這個findClass方法干了什么事情?下面這個就是DexPathList里面的findClass方法
通過上面的代碼可以看出這個DexPathList中的findClass方法是在遍歷一個叫dexElements的一個數組來生成Class對象;接下就要找這個dexElements數組是在哪里被初始化的呢?通過查看DexPathList這個類的構造方法發現這個dexElements是通過一個叫makePathElements的方法來完成初始化的。
到這里就已經知道系統是如何加載dex文件的,那么已經的解密好的dex文件要放到系統的dexElements中才能被系統加載到,系統是調用makePathElements方法去生成的dexElements數組,那么我們也可以通過反射的去調用這個方法生成一個自已的dexElements數組,再將自已的dexElement數組和系統的dexElements數組合并一個新數組設置到這個DexPathList中。
private void loadDex(List<File> dexFiles,File versionDir) throws Exception{
//通過查看源碼我們可以知道系統存放dex是用一個數組存的:dexElements,而dexElements是DexPathList的成員
Field pathListField = Utils.findField(getClassLoader(),"pathList");
Object pathList = pathListField.get(getClassLoader());
Field dexElementsFieId = Utils.findField(pathList,"dexElements");
//這個是DexPathList里面的原始數據,也就是系統中的dexElements數組
Object dexElements[] = (Object[]) dexElementsFieId.get(pathList);
//構建自已的dexElement
Method makeDexElements = Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
Object[] mCustomDexElements = (Object[]) makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);
//合并數組
Object newElements = Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+mCustomDexElements.length);
System.arraycopy(dexElements,0,newElements,0,dexElements.length);
System.arraycopy(mCustomDexElements,0,newElements,dexElements.length,mCustomDexElements.length);
//替換掉classloader中的elements數組
dexElementsFieId.set(pathList,newElements);
}
通過反射的就把解密的dex文件生成dexElements數組,再和系統的dexElements合并生成一個新數組替換掉DexPathList中的dexElements就達到交給系統處理效果。
總結
apk加固后主dex文件已經是看不到原碼,只能看殼的dex文件中的原碼,這樣就達到了加固的效果,但是這樣做還是不夠完善,因為主App的Application已經是沒有用了,這個問題的解決方法請到apk加固二中查看。
apk加固二http://www.lxweimin.com/p/67feffa9bff3
如果大家在看的時候如果有錯誤的地方歡迎指出,我們共同進步。