在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);
}