一、Dex加殼由來
最近在學(xué)習(xí)apk加密唇礁,在網(wǎng)上看了一篇《Android中的Apk的加固(加殼)原理解析和實(shí)現(xiàn)》勾栗,我發(fā)現(xiàn)原文把整個(gè)apk都寫入到dex文件中,如果apk小還好盏筐,當(dāng)原APK大于200M械姻,客戶端解殼很費(fèi)勁,打開后應(yīng)用就卡住了机断,如果只是把原apk的dex加殼不就很容易解開了嘛楷拳。我不是原創(chuàng),只是按照我自己的思路將大神的加固稍作調(diào)整吏奸,并且將整個(gè)項(xiàng)目整理如下欢揖。
二、Dex結(jié)構(gòu)
如圖所示奋蔚,新的dex由解殼dex她混、dex集合、dex集合描述和描述長(zhǎng)度組成
三泊碑、核心代碼
- 加殼
/**
* 給apk加殼
* @param primaryApkPath 原apk
* @param unShellApkPath 解殼apk
* @param outApkPath 加殼后新APK
* @throws Exception
*/
public static void apkShell(String primaryApkPath,String unShellApkPath,String outApkPath) throws Exception{
if(!FileUtils.isExit(primaryApkPath, unShellApkPath)){
throw new RuntimeException("check params");
}
//解壓原apk
String unPrimaryApkDstPath = primaryApkPath.replace(".apk", "");
ApkToolUtils.decompile(primaryApkPath, unPrimaryApkDstPath);
String primaryManifestPath = unPrimaryApkDstPath + File.separator + "AndroidManifest.xml";
//解壓解殼apk
String unShellApkDstPath = unShellApkPath.replace(".apk", "");
ApkToolUtils.decompile(unShellApkPath, unShellApkDstPath);
String unShellManifestPath = unShellApkDstPath + File.separator + "AndroidManifest.xml";
String unShellDexPath = unShellApkDstPath + File.separator + "classes.dex";
File unShellFile = new File(unShellDexPath);
File unApkDir = new File(unPrimaryApkDstPath);
ArrayList<File> dexArray = new ArrayList<File>();
for(File file : unApkDir.listFiles()){//讀取解殼后的dex
if(file.getName().endsWith(".dex")){
dexArray.add(file);
}
}
String shellDexPath = unPrimaryApkDstPath + File.separator + "classes.dex";
shellDex(dexArray, unShellFile, shellDexPath);//生產(chǎn)新的dex(加殼)
String mateInfPath = unPrimaryApkDstPath + File.separator +"META-INF";//刪除meta-inf坤按,重新簽名后會(huì)生成
FileUtils.delete(mateInfPath);
for(File file : dexArray){//清理多余dex文件
if(file.getName().equals("classes.dex")){
continue;
}
FileUtils.delete(file.getAbsolutePath());
}
String unShellApplicationName = AndroidXmlUtils.readApplicationName(unShellManifestPath);//解殼ApplicationName
String primaryApplicationName = AndroidXmlUtils.readApplicationName(primaryManifestPath);//原applicationName
AndroidXmlUtils.changeApplicationName(primaryManifestPath, unShellApplicationName);//改變?cè)瑼pplicationname為解殼ApplicationName
if(primaryApplicationName != null){//將原ApplicationName寫入mateData中,解殼application中會(huì)讀取并替換應(yīng)用Application
AndroidXmlUtils.addMateData(primaryManifestPath, "APPLICATION_CLASS_NAME", primaryApplicationName);
}
//回編馒过,回編系統(tǒng)最好是linux
ApkToolUtils.compile(unPrimaryApkDstPath,outApkPath);
//v1簽名
SignUtils.V1(outApkPath, SignUtils.getDefaultKeystore());
//清理目錄
FileUtils.delete(unPrimaryApkDstPath);
FileUtils.delete(unShellApkDstPath);
}
加殼工程是一個(gè)java工程腹忽,解壓apk使用了apktool窘奏,apktool這個(gè)工具最好是在linux下使用嘹锁,xml操作使用了W3C java自帶的,不咋個(gè)好用着裹,為了項(xiàng)目簡(jiǎn)單沒用其他的jar包领猾。加殼項(xiàng)目中對(duì)byte數(shù)組的加密使用了aes,也可以用其他方法去實(shí)現(xiàn)骇扇。
- 解殼
/**
* 從殼的dex文件中分離出原來的dex文件
* @param data
* @param primaryDexDir
* @throws IOException
*/
public void splitPrimaryDexFromShellDex(byte[] data, String primaryDexDir) throws IOException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, NoSuchPaddingException {
int shellDexLen = data.length;
byte[] dexFileCommentLenByte = new byte[4];//dex信息長(zhǎng)度
System.arraycopy(data, shellDexLen-4, dexFileCommentLenByte, 0, 4);
ByteArrayInputStream bais = new ByteArrayInputStream(dexFileCommentLenByte);
DataInputStream in = new DataInputStream(bais);
int dexFileCommentLen = in.readInt();
byte[] dexFileCommentByte = new byte[dexFileCommentLen];//dex信息正文
System.arraycopy(data,shellDexLen-4-dexFileCommentLen,dexFileCommentByte,0,dexFileCommentLen);
String dexFileComment = new String(dexFileCommentByte);
LogUtils.d("dex comment:"+dexFileComment);
ArrayList<DexFile> dexFileArrayList = (ArrayList<DexFile>) JSON.parseArray(dexFileComment,DexFile.class);
int currentReadEndIndex = shellDexLen - 4 - dexFileCommentLen;//當(dāng)前已經(jīng)讀取到的內(nèi)容的下標(biāo)
for(int i = dexFileArrayList.size()-1; i>=0; i--){//取出所有的dex,并寫入到payload_dex目錄下
DexFile dexFile = dexFileArrayList.get(i);
byte[] primaryDexData = new byte[dexFile.getDexLength()];
System.arraycopy(data,currentReadEndIndex-dexFile.getDexLength(),primaryDexData,0,dexFile.getDexLength());
primaryDexData = decryAES(primaryDexData);//界面
File primaryDexFile = new File(primaryDexDir,dexFile.getDexName());
if(!primaryDexFile.exists()) primaryDexFile.createNewFile();
FileOutputStream localFileOutputStream = new FileOutputStream(primaryDexFile);
localFileOutputStream.write(primaryDexData);
localFileOutputStream.close();
currentReadEndIndex -= dexFile.getDexLength();
}
}
//代碼片段摔竿,DexClassLoder加載多個(gè)dex
//找到dex并通過DexClassLoader去加載
StringBuffer dexPaths = new StringBuffer();
for(File file:dex.listFiles()){
dexPaths.append(file.getAbsolutePath());
dexPaths.append(File.pathSeparator);
}
dexPaths.delete(dexPaths.length()-1,dexPaths.length());
LogUtils.d(dexPaths.toString());
DexClassLoader classLoader = new DexClassLoader(dexPaths.toString(), odex.getAbsolutePath(),getApplicationInfo().nativeLibraryDir,(ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk", wr.get(), "mClassLoader"));//android4.4后ART會(huì)對(duì)dex做優(yōu)化匠题,第一次加載時(shí)間較長(zhǎng),后面就很快了
將原項(xiàng)目dex從殼dex中獲取出來韭山,然后在onCreate中將dex拼接后使用DexClassLoder加載郁季,nativeLibrary咋們只對(duì)dex做了加殼所以可以直接使用Application的nativeLibraryDir冷溃。
其它核心代碼,application替換這類的年柠,可以在原文中查看凿歼。
四、效果
從左往右分別為原demo工程的apk冗恨,為了實(shí)現(xiàn)多dex加了很多費(fèi)代碼答憔,加殼后的apk,解殼apk掀抹∨巴兀可以看出加殼后項(xiàng)目demo工程的dex被隱藏,顯示的是解殼工程的代碼
五傲武、待優(yōu)化
- 將客戶端的解密放入native層蓉驹;
- 將解密后的dex文件隱藏;
解密后的文件依舊存于應(yīng)用的私有存儲(chǔ)空間中揪利,ROOT了的手機(jī)和模擬器很容易就可以拿到解密后的dex态兴,所以這種加殼方法只是將代碼從apk中隱藏。
如果有好的解決方法疟位,或者好的加殼方法望告知瞻润!
附送整個(gè)項(xiàng)目代碼代碼傳送門