前言
apk正常打包后可以通過 反編譯工具使用 得到源碼米同,那這么長時間的辛苦不就白費了嗎鸯乃,這就引出一個問題了:怎么保證不讓別人不容易拿到源碼呢罕邀?
當(dāng)然得是通過加固啦,使用第三方的加固工具 (結(jié)尾給大家)蛉顽,但是作為一名熱愛學(xué)習(xí)的程序員,當(dāng)然得明白其中的原理才好先较。
app加固原理
- 制作一個殼程序 (功能:解密和加載dex文件)
- 使用加密工具對原apk的dex文件進(jìn)行加密
- 最后重新打包携冤、對齊悼粮、簽名
實現(xiàn)
1. 制作殼程序
制作殼程序,殼程序包含兩功能解密dex文件和加載dex文件曾棕,先說加載dex扣猫,
解密dex文件:解壓apk包得到dex文件,然后把加密過的dex文件進(jìn)行解密
那系統(tǒng)又是怎么加載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);
}
}
這里調(diào)用父類的構(gòu)造方法
Android源碼目錄\libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java
看下面這個方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//這里傳個名字 和 集合申尤, 就是說把某個類進(jìn)行加載
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拿到已經(jīng)加載的dex文件昧穿,那我們看dexElements的初始化
//dexElements 初始化
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
............................
}
那我們通過反射調(diào)用這個方法把解密后的dex文件通過makePathElements方法反射 加載進(jìn)來,再和原來的dex合并橙喘,那這個app就能運(yùn)行了时鸵。
3. 重新打包、對齊厅瞎、簽名
- 重新打包
把殼程序的dex文件和加密后的文件進(jìn)行打包 - 對齊
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
代碼實現(xiàn)
殼程序
- 加密算法
public class AES { //16字節(jié) 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 { // 生成一個實現(xiàn)指定轉(zhuǎn)換的 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);
//創(chuàng)建目錄
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;
}
/**
* 反射獲得 指定對象(當(dāng)前-》父類-》父類...)中的 成員屬性
*
* @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);
//如果無法訪問 設(shè)置為可訪問
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException e) {
//如果找不到往父類找
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}
/**
* 反射獲得 指定對象(當(dāng)前-》父類-》父類...)中的 函數(shù)
*
* @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 結(jié)尾的目錄 則計算該目錄下的文件的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) {
// 若輸入的參數(shù)不是一個文件 則拋出異常
if (!file.isFile()) {
throw new NumberFormatException("參數(shù)錯誤!請輸入校準(zhǔn)文件和簸。");
}
// 定義相關(guān)變量
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創(chuàng)建Application之后調(diào)用的第一個方法
* 可以在這個方法中進(jìn)行解密彭雾,同時把dex交給android去加載
*/
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//獲取用戶填入的metadata
getMetaData();
//得到當(dāng)前加密了的APK文件
File apkFile = new File(getApplicationInfo().sourceDir);
//把a(bǔ)pk解壓 app_name+"_"+app_version目錄中的內(nèi)容需要boot權(quán)限才能用
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<>();
//進(jìn)行解密(最好做MD5文件校驗)
if (!dexDir.exists() || dexDir.listFiles().length == 0) {
//把a(bǔ)pk解壓到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);
//讀取文件內(nèi)容
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.把解密后的文件加載到系統(tǒng)
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.獲取數(shù)組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);
//合并數(shù)組
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數(shù)組
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("執(zhí)行成功");
}
}