前言
apk正常打包后可以通过 反编译工具使用 得到源码,那这么长时间的辛苦不就白费了吗,这就引出一个问题了:怎么保证不让别人不容易拿到源码呢?
当然得是通过加固啦,使用第三方的加固工具 (结尾给大家),但是作为一名热爱学习的程序员,当然得明白其中的原理才好。
app加固原理
- 制作一个壳程序 (功能:解密和加载dex文件)
- 使用加密工具对原apk的dex文件进行加密
- 最后重新打包、对齐、签名
实现
1. 制作壳程序
制作壳程序,壳程序包含两功能解密dex文件和加载dex文件,先说加载dex,
解密dex文件:解压apk包得到dex文件,然后把加密过的dex文件进行解密
那系统又是怎么加载dex文件呢?
Android源码目录\libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
这里调用父类的构造方法
Android源码目录\libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java
看下面这个方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//这里传个名字 和 集合, 就是说把某个类进行加载
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
Android源码目录\libcore\dalvik\src\main\java\dalvik\system\DexPathList.java
public Class findClass(String name, List<Throwable> suppressed) {
//通过遍历dexElements去加载
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
从这个方法中看到,dex是通过遍历dexElements去加载的,可以通过反射dexElements拿到已经加载的dex文件,那我们看dexElements的初始化
//dexElements 初始化
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
............................
}
那我们通过反射调用这个方法把解密后的dex文件通过makePathElements方法反射 加载进来,再和原来的dex合并,那这个app就能运行了。
3. 重新打包、对齐、签名
- 重新打包
把壳程序的dex文件和加密后的文件进行打包 - 对齐
zipalign -v -p 4 input_unsigned.apk output_unsigned.apk
- 签名
apksigner sign --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out out.apk in.apk
代码实现
壳程序
- 加密算法
public class AES { //16字节 public static final String DEFAULT_PWD = "abcdefghijklmnop"; //填充方式 private static final String algorithmStr = "AES/ECB/PKCS5Padding"; private static Cipher encryptCipher; private static Cipher decryptCipher; public static void init(String password) { try { // 生成一个实现指定转换的 Cipher 对象。 encryptCipher = Cipher.getInstance(algorithmStr); decryptCipher = Cipher.getInstance(algorithmStr);// algorithmStr byte[] keyStr = password.getBytes(); SecretKeySpec key = new SecretKeySpec(keyStr, "AES"); encryptCipher.init(Cipher.ENCRYPT_MODE, key); decryptCipher.init(Cipher.DECRYPT_MODE, key); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } } public static byte[] encrypt(byte[] content) { try { byte[] result = encryptCipher.doFinal(content); return result; } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return null; } public static byte[] decrypt(byte[] content) { try { byte[] result = decryptCipher.doFinal(content); return result; } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return null; } }
- 解压和压缩
public class Zip {
private static void deleteFile(File file){
if (file.isDirectory()){
File[] files = file.listFiles();
for (File f: files) {
deleteFile(f);
}
}else{
file.delete();
}
}
/**
* 解压zip文件至dir目录
* @param zip
* @param dir
*/
public static void unZip(File zip, File dir) {
try {
deleteFile(dir);
ZipFile zipFile = new ZipFile(zip);
//zip文件中每一个条目
Enumeration<? extends ZipEntry> entries = zipFile.entries();
//遍历
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
//zip中 文件/目录名
String name = zipEntry.getName();
//原来的签名文件 不需要了
if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
.equals("META-INF/MANIFEST.MF")) {
continue;
}
//空目录不管
if (!zipEntry.isDirectory()) {
File file = new File(dir, name);
//创建目录
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
//写文件
FileOutputStream fos = new FileOutputStream(file);
InputStream is = zipFile.getInputStream(zipEntry);
byte[] buffer = new byte[2048];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
is.close();
fos.close();
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 压缩目录为zip
* @param dir 待压缩目录
* @param zip 输出的zip文件
* @throws Exception
*/
public static void zip(File dir, File zip) throws Exception {
zip.delete();
// 对输出文件做CRC32校验
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(
zip), new CRC32());
ZipOutputStream zos = new ZipOutputStream(cos);
//压缩
compress(dir, zos, "");
zos.flush();
zos.close();
}
/**
* 添加目录/文件 至zip中
* @param srcFile 需要添加的目录/文件
* @param zos zip输出流
* @param basePath 递归子目录时的完整目录 如 lib/x86
* @throws Exception
*/
private static void compress(File srcFile, ZipOutputStream zos,
String basePath) throws Exception {
if (srcFile.isDirectory()) {
File[] files = srcFile.listFiles();
for (File file : files) {
// zip 递归添加目录中的文件
compress(file, zos, basePath + srcFile.getName() + "/");
}
} else {
compressFile(srcFile, zos, basePath);
}
}
private static void compressFile(File file, ZipOutputStream zos, String dir)
throws Exception {
// temp/lib/x86/libdn_ssl.so
String fullName = dir + file.getName();
// 需要去掉temp
String[] fileNames = fullName.split("/");
//正确的文件目录名 (去掉了temp)
StringBuffer sb = new StringBuffer();
if (fileNames.length > 1){
for (int i = 1;i<fileNames.length;++i){
sb.append("/");
sb.append(fileNames[i]);
}
}else{
sb.append("/");
}
//添加一个zip条目
ZipEntry entry = new ZipEntry(sb.substring(1));
zos.putNextEntry(entry);
//读取条目输出到zip中
FileInputStream fis = new FileInputStream(file);
int len;
byte data[] = new byte[2048];
while ((len = fis.read(data, 0, 2048)) != -1) {
zos.write(data, 0, len);
}
fis.close();
zos.closeEntry();
}
}
- 工具类
public class Utils {
/**
* 读取文件
*
* @param file
* @return
* @throws Exception
*/
public static byte[] getBytes(File file) throws Exception {
RandomAccessFile r = new RandomAccessFile(file, "r");
byte[] buffer = new byte[(int) r.length()];
r.readFully(buffer);
r.close();
return buffer;
}
/**
* 反射获得 指定对象(当前-》父类-》父类...)中的 成员属性
*
* @param instance
* @param name
* @return
* @throws NoSuchFieldException
*/
public static Field findField(Object instance, String name) throws NoSuchFieldException {
Class clazz = instance.getClass();
//反射获得
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(name);
//如果无法访问 设置为可访问
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException e) {
//如果找不到往父类找
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}
/**
* 反射获得 指定对象(当前-》父类-》父类...)中的 函数
*
* @param instance
* @param name
* @param parameterTypes
* @return
* @throws NoSuchMethodException
*/
public static Method findMethod(Object instance, String name, Class... parameterTypes)
throws NoSuchMethodException {
Class clazz = instance.getClass();
while (clazz != null) {
try {
Method method = clazz.getDeclaredMethod(name, parameterTypes);
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method;
} catch (NoSuchMethodException e) {
//如果找不到往父类找
clazz = clazz.getSuperclass();
}
}
throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList
(parameterTypes) + " not found in " + instance.getClass());
}
// 所有文件md5总和
private static String fileSum = "";
/**
*
* @param file
* @param suffix
* @return
*/
public static String traverseFolder(File file, String suffix) {
if (file == null) {
throw new NullPointerException("遍历路径为空路径或非法路径");
}
if (file.exists()) { //判断文件或目录是否存在
File[] files = file.listFiles();
if (files.length == 0) { // 文件夹为空
return null;
} else {
for (File f : files) { // 遍历文件夹
if (f.isDirectory()) { // 判断是否是目录
if ((f.getName().endsWith(suffix))) { // 只小羊.dex 结尾的目录 则计算该目录下的文件的md5值
// 递归遍历
traverseFolder(f, suffix);
}
} else {
// 得到文件的md5值
String string = checkMd5(f);
// 将每个文件的md5值相加
fileSum += string;
}
}
}
} else {
return null; // 目录不存在
}
return fileSum; // 返回所有文件md5值字符串之和
}
/**
* 计算文件md5值
* 检验文件生成唯一的md5值 作用:检验文件是否已被修改
*
* @param file 需要检验的文件
* @return 该文件的md5值
*/
private static String checkMd5(File file) {
// 若输入的参数不是一个文件 则抛出异常
if (!file.isFile()) {
throw new NumberFormatException("参数错误!请输入校准文件。");
}
// 定义相关变量
FileInputStream fis = null;
byte[] rb = null;
DigestInputStream digestInputStream = null;
try {
fis = new FileInputStream(file);
MessageDigest md5 = MessageDigest.getInstance("md5");
digestInputStream = new DigestInputStream(fis, md5);
byte[] buffer = new byte[4096];
while (digestInputStream.read(buffer) > 0) ;
md5 = digestInputStream.getMessageDigest();
rb = md5.digest();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < rb.length; i++) {
String a = Integer.toHexString(0XFF & rb[i]);
if (a.length() < 2) {
a = '0' + a;
}
sb.append(a);
}
return sb.toString(); //得到md5值
}
}
- application
public class ProxyApplication extends Application {
//定义好解密后的文件的存放路径
private String app_name;
private String app_version;
/**
* ActivityThread创建Application之后调用的第一个方法
* 可以在这个方法中进行解密,同时把dex交给android去加载
*/
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//获取用户填入的metadata
getMetaData();
//得到当前加密了的APK文件
File apkFile = new File(getApplicationInfo().sourceDir);
//把apk解压 app_name+"_"+app_version目录中的内容需要boot权限才能用
File versionDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);
File appDir = new File(versionDir, "app");
File dexDir = new File(appDir, "dexDir");
//得到我们需要的加载的Dex文件
List<File> dexFiles = new ArrayList<>();
//进行解密(最好做MD5文件校验)
if (!dexDir.exists() || dexDir.listFiles().length == 0) {
//把apk解压到appDir
Zip.unZip(apkFile, appDir);
//获取目录下的所有的文件
File[] files = appDir.listFiles();
for (File file : files) {
String name = file.getName();
if (name.endsWith(".dex") && !TextUtils.equals(name, "classes.dex")) {
try {
AES.init(AES.DEFAULT_PWD);
//读取文件内容
byte[] bytes = Utils.getBytes(file);
//解密
byte[] decrypt = AES.decrypt(bytes);
//写到指定的目录
FileOutputStream fos = new FileOutputStream(file);
fos.write(decrypt);
fos.flush();
fos.close();
dexFiles.add(file);
} catch (Exception e) {
e.printStackTrace();
}
}
}
} else {
for (File file : dexDir.listFiles()) {
dexFiles.add(file);
}
}
try {
//2.把解密后的文件加载到系统
loadDex(dexFiles, versionDir);
} catch (Exception e) {
e.printStackTrace();
}
}
private void loadDex(List<File> dexFiles, File versionDir) {
try {
//1.获取pathlist
Field pathListField = Utils.findField(getClassLoader(), "pathList");
Object pathList = pathListField.get(getClassLoader());
//2.获取数组dexElements
Field dexElementsField=Utils.findField(pathList,"dexElements");
Object[] dexElements=(Object[])dexElementsField.get(pathList);
//3.反射到初始化dexElements的方法
Method makeDexElements=Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);
//合并数组
Object[] newElements= (Object[])Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length);
System.arraycopy(dexElements,0,newElements,0,dexElements.length);
System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);
//替换classloader中的element数组
dexElementsField.set(pathList,newElements);
} catch (Exception e) {
e.printStackTrace();
}
}
private void getMetaData() {
try {
ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
getPackageName(), PackageManager.GET_META_DATA);
Bundle metaData = applicationInfo.metaData;
if (null != metaData) {
if (metaData.containsKey("app_name")) {
app_name = metaData.getString("app_name");
}
if (metaData.containsKey("app_version")) {
app_version = metaData.getString("app_version");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
加固工具
- 加密算法、解压和压缩和工具类 上面的一样,这里就不贴代码了
public class Main {
public static void main(String[] args) throws Exception {
/**
* 1.制作只包含解密代码的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");
//
// //dx --dex --output out.dex in.jar
Process process = Runtime.getRuntime().exec("cmd /c dx --dex --output " + classesDex.getAbsolutePath()
+ " " + classesJar.getAbsolutePath());
process.waitFor();
if (process.exitValue() != 0) {
throw new RuntimeException("dex error");
}
/**
* 2.加密APK中所有的dex文件
*/
File apkFile=new File("app/build/outputs/apk/debug/app-debug.apk");
File apkTemp = new File("app/build/outputs/apk/debug/temp");
//解压
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) {
byte[] bytes = Utils.getBytes(dexFile);
byte[] encrypt = AES.encrypt(bytes);
FileOutputStream fos = new FileOutputStream(new File(apkTemp,
"secret-" + dexFile.getName()));
fos.write(encrypt);
fos.flush();
fos.close();
dexFile.delete();
}
/**
* 3.把dex放入apk解压目录,重新压成apk文件
*/
classesDex.renameTo(new File(apkTemp,"classes.dex"));
File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
Zip.zip(apkTemp,unSignedApk);
//
//
// /**
// * 4.对齐和签名
// */
// zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
File alignedApk = new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
process = Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 " + unSignedApk.getAbsolutePath()
+ " " + alignedApk.getAbsolutePath());
process.waitFor();
// if(process.exitValue()!=0){
// throw new RuntimeException("dex error");
// }
//
//
//// apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
//// apksigner sign --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out out.apk in.apk
File signedApk = new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
File jks = new File("proxy_tools/proxy2.jks");
process = Runtime.getRuntime().exec("cmd /c apksigner sign --ks " + jks.getAbsolutePath()
+ " --ks-key-alias 123 --ks-pass pass:123456 --key-pass pass:123456 --out "
+ signedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath());
process.waitFor();
// if(process.exitValue()!=0){
// throw new RuntimeException("dex error");
// }
System.out.println("执行成功");
}
}