在apk安全上,最基本的是通過混淆來對apk進行保護,但這只是加大了對源碼的閱讀難度,並不能真正的保護你的源碼,反編譯是可以輕鬆拿到apk的源碼的,我們可以通過將非核心的dex文件暴露來達到保護核心dex文件的目的;
加固的整體思想如下圖
準備工作
處理存放apk的文件夾
/**
* 準備工作
*/
//存儲源核心apk中的解壓後的文件
File tempFileApk = new File("app/source/apk/temp");
if (tempFileApk.exists()) {
File[]files = tempFileApk.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();//先清空文件夾
}
}
}
//存儲殼arr中的解壓後的文件
File tempFileAar = new File("app/source/aar/temp");
if (tempFileAar.exists()) {
File[]files = tempFileAar.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();//先清空文件夾
}
}
}
第一步 處理原始apk 加密核心dex
/**
* 第一步 處理原始apk 加密dex
*
*/
AES.init(AES.DEFAULT_PWD);
//這樣一個最簡單的AES加解密就完成了,
// 但有一個缺點,密碼的長度必須爲128位,也就是16個字節,否則會報錯;
//解壓apk
File apkFile = new File("app/source/apk/app-debug.apk");
File newApkFile = new File(apkFile.getParent() + File.separator + "temp");
if(!newApkFile.exists()) {
newApkFile.mkdirs();
}
File mainDexFile = AES.encryptAPKFile(apkFile,newApkFile);
if (newApkFile.isDirectory()) {
File[] listFiles = newApkFile.listFiles();
for (File file : listFiles) {
if (file.isFile()) {
if (file.getName().endsWith(".dex")) {
String name = file.getName();
System.out.println(" name :"+name);
int cursor = name.indexOf(".dex");
String newName = file.getParent()+ File.separator + name.substring(0, cursor) + "_" + ".dex";
System.out.println("newName:"+newName);
file.renameTo(new File(newName));
}
}
}
}
第二步 處理aar 獲得殼dex
/**
* 第二步 處理aar 獲得殼dex
*/
File aarFile = new File("app/source/aar/protectapp-debug.aar");
File aarDex = Dx.jar2Dex(aarFile);
// aarData = Utils.getBytes(aarDex); //將dex文件讀到byte 數組
File tempMainDex = new File(newApkFile.getPath() + File.separator + "classes.dex");
if (!tempMainDex.exists()) {
tempMainDex.createNewFile();
}
System.out.println("MyMain" + tempMainDex.getAbsolutePath());
FileOutputStream fos = new FileOutputStream(tempMainDex);
byte[] fbytes = Utils.getBytes(aarDex);
fos.write(fbytes);
fos.flush();
fos.close();
第3步 打包簽名
/**
* 第3步 打包簽名
*/
File unsignedApk = new File("app/result/apk-unsigned.apk");
unsignedApk.getParentFile().mkdirs();
// File disFile = new File(apkFile.getAbsolutePath() + File.separator+ "temp");
Zip.zip(newApkFile, unsignedApk);
//不用插件就不能自動使用原apk的簽名...
File signedApk = new File("app/result/apk-signed.apk");
Signature.signature(unsignedApk, signedApk);
上面的就實現了apk對核心dex的加密後重新打包簽名了,加密後的apk組成結構如下
其中classes_.dex就是核心dex,無法通過反編譯工具直接反編譯查看,當然殼classes.dex是可以反編譯查看的,接下來介紹一下,代碼中使用到的Runtime 調用cmd命令進行轉dex和簽名的使用方法
(1)使用dx命令轉dex
注意使用sdk自帶的工具dx.bat 目前存在的目錄已經更改,添加path環境變量 E:\Users\Sdk\build-tools\28.0.3,使用JAVA代碼調用cmd命令如下
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cmd.exe /C dx --dex --output=" + aarDex.getAbsolutePath() + " " +
classes_jar.getAbsolutePath());
System.out.println("dxCommand 1 " );
try {
process.waitFor();
System.out.println("dxCommand 2 " );
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("dxCommand 3 " );
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("dx run failed");
}
process.destroy();
System.out.println("process: process.destroy() " );
(2)使用java代碼調用window下的cmd命令,代碼如下
String cmd[] = {"cmd.exe", "/C ","jarsigner", "-sigalg", "MD5withRSA",
"-digestalg", "SHA1",
"-keystore", "D:\\Documents\\ReinforceApk\\app\\shunplus.jks",
"-storepass", "123456",
"-keypass", "123456",
"-signedjar", signedApk.getAbsolutePath(),
unsignedApk.getAbsolutePath(),
"shun"};
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("簽名執行失敗");
}
System.out.println("finish signed");
process.destroy();
下面奉上Github項目地址處理項目加密代碼
項目代碼地址核心apk及解密代碼
通過上面的代碼加密後還需要一個解密的過程
在殼的ShellApplication的attachBaseContext方法中,此方法運行之前核心dex還處於加密狀態,需要解密後,通過ClassLoader將dex加載到虛擬機中,程序才能正常運行;具體代碼如下:
解密過程
AES.init(getPassword());
File apkFile = new File(getApplicationInfo().sourceDir);
//data/data/包名/files/fake_apk/
File unZipFile = getDir("fake_apk", MODE_PRIVATE);
File app = new File(unZipFile, "app");
if (!app.exists()) {
Zip.unZip(apkFile, app);
File[] files = app.listFiles();
for (File file : files) {
String name = file.getName();
if (name.equals("classes.dex")) {
} else if (name.endsWith(".dex")) {
try {
byte[] bytes = getBytes(file);
FileOutputStream fos = new FileOutputStream(file);
byte[] decrypt = AES.decrypt(bytes);
// fos.write(bytes);
fos.write(decrypt);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
List list = new ArrayList<>();
Log.d("FAKE", Arrays.toString(app.listFiles()));
for (File file : app.listFiles()) {
if (file.getName().endsWith(".dex")) {
list.add(file);
}
}
Log.d("FAKE", list.toString());
try {
V19.install(getClassLoader(), list, unZipFile);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
加載dex文件方法
private static final class V19 {
private V19() {
}
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory) throws IllegalArgumentException,
IllegalAccessException, NoSuchFieldException, InvocationTargetException,
NoSuchMethodException {
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
Log.d(TAG, "Build.VERSION.SDK_INT " + Build.VERSION.SDK_INT);
if (Build.VERSION.SDK_INT >= 23) {
expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
} else {
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
}
if (suppressedExceptions.size() > 0) {
Iterator suppressedExceptionsField = suppressedExceptions.iterator();
while (suppressedExceptionsField.hasNext()) {
IOException dexElementsSuppressedExceptions = (IOException)
suppressedExceptionsField.next();
Log.w("MultiDex", "Exception in makeDexElement",
dexElementsSuppressedExceptions);
}
Field suppressedExceptionsField1 = findField(loader,
"dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[])
suppressedExceptionsField1.get(loader));
if (dexElementsSuppressedExceptions1 == null) {
dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions
.toArray(new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions1.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions1, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
dexElementsSuppressedExceptions1 = combined;
}
suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
}
}
private static Object[] makeDexElements(Object dexPathList,
ArrayList<File> files, File
optimizedDirectory,
ArrayList<IOException> suppressedExceptions) throws
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = findMethod(dexPathList, "makeDexElements", new
Class[]{ArrayList.class, File.class, ArrayList.class});
return ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files,
optimizedDirectory, suppressedExceptions}));
}
}
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makePathElements}.
*/
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makePathElements;
try {
makePathElements = findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
try {
makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
try {
Log.e(TAG, "NoSuchMethodException: try use v19 instead");
return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}