先来看看大概流程
1、编写加密方法,作为工具方法用于后续的加密和解密准备。
2、编写代理Application(ProxyApplication),作为加固后的apk的伪入口。(ProxyApplication作为伪入口时,需要将加密apk进行解密并重新加载于classLoader中)
3、对需要加密的apk的AndroidManifest文件的Application:name 标签经行更改为ProxyApplication,并用标签声明真正的Application入口和版本号。
4、将1、2步的文件打包成aar包。
5、解压aar包(于aarTemp文件夹),并将解压后的jar文件,编译成dex文件(Entrance.dex)(安卓虚拟机可识别的机器码文件)。
6、解压需要加密的apk(于apkTemp文件夹),遍历解压后的文件夹,取出所有dex文件,用1步中的加密方法对所有dex文件进行加密,并替换原本没加密的dex。
*注:Entrance.dex在aarTemp内,没被加密
7、将aarTemp中的dex文件,复制到apkTemp文件中,并将apkTemp压缩成apk文件。
8、对齐 & 签名(才能正常使用)
附上相关的代码
public class Main {
public static void main(String[] args) {
//第四步:解压arr(包含加密解密工具和ProxyApplication.java)
File aarFile = new File("core/build/outputs/aar/core-debug.aar");
File aarTemp = new File("lib/temp");
Zip.unZip(aarFile, aarTemp);
// 生成classes.dex
File classesJar = new File(aarTemp, "classes.jar");
File classesDex = new File(aarTemp, "classes.dex");
Process process = null;
//dx --dex --output out.dex in.jar
try {
process = Runtime.getRuntime().exec("cmd /c dx --dex --output " + classesDex.getAbsolutePath()
+ " " + classesJar.getAbsolutePath());
process.waitFor();
if (process.exitValue() != 0) {
System.out.println("dex error");
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
//第六步:解压apk
File apkFile = new File("app/build/outputs/apk/debug/app-debug.apk");
File apkTemp = new File("lib/Apktemp");
Zip.unZip(apkFile, apkTemp);
ArrayList dexFiles = new ArrayList<>();
for (File file : apkTemp.listFiles()) {
if (file.getName().endsWith("dex")) {
dexFiles.add(file);
}
}
//加密apk里面的dex
AES.init(AES.DEFAULT_PWD);
for (File dexFile : dexFiles) {
try {
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();
} catch (Exception e) {
e.printStackTrace();
}
}
classesDex.renameTo(new File("lib/Apktemp", "classes.dex"));
File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
//第七步:把apkTemp压缩成unsightApk
try {
Zip.zip(apkTemp, unSignedApk);
} catch (Exception e) {
e.printStackTrace();
}
//第八步:对齐 签名
File alignedApk = new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
try {
process = Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 " + unSignedApk.getAbsolutePath()
+ " " + alignedApk.getAbsolutePath());
process.waitFor();
if (process.exitValue() != 0) {
System.out.println("zipalign error");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
File jks=new File("mykeystore.jks");
try {
process=Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
+" --ks-key-alias key0 --ks-pass pass:11111111 --key-pass pass:11111111 --out "
+signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
process.waitFor();
if(process.exitValue()!=0){
System.out.println("sign error");
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("over ");
}
}
这里详细讲讲ProxyApplication:
探讨1:作为唯一没加密的dex文件内的ProxyApplication如何把加密的dex文件,加载到类加载器(ClassLoader)中?
ProxyApplication三部曲:
1.获得加密apk。
2.解压zip并解密dex文件。
3把新dex文件索引存在类加载器中。
上述过程中,涉及到把dex文件加载到类加载器中,下面简单理解下类加载机制。
前提:android的ClassLoader有两种类型系统类加载器和自定义加载器。
1)BootClassLoader:
安卓系统启动时候会使用BCL来预加载常用类。
2)DexClassLoader
加载dex文件和包含dex文件的压缩包
3)PathClassLoader
加载系统类和应用程序的类
4) InMemoryClassLoader:
androidO新增的,用于加载内存中的dex
·
·ClassLoader是一个抽象类,定义了classloader的主要功能。BootClassLoader是它的内部类
·SecureClassLoader不是ClassLoader的实现类,拓展了ClassLoader的权限方面的功能
·BaseDexClassLoader继承ClassLoader,但是是抽象类,PathClassLoader, DexClassLoader, InMemoryClassLoader都继承它,并各自实现类功能
·双亲委托模式
(讲人话:首先判断该类是否已经加载,如无,不是从自身查找,而是委托到父加载器中找是否有加载目的Class,若无依次向父类递归,直至最顶层ClassLoader类。如果找到了,就直接返回Class,若果没找到就继续依次向下子加载器findClass…)
优点:
1.避免重复加载
2.保护安全性。
(沙雕A建一个 类名为 android.view.View的自定义类,可能造成系统原本的View不可用。但其实还有一层保护,虚拟机把两个类名一致的且被同一个类加载器加载的类,虚拟机才会认为他们是同一个类)
来一个demo打印看看应用的类加载器是什么:
这里可以看到PathClassLoader作为加载器。
ClassLoader的加载过程:
ClassLoader.java
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//找该类是否被加载过了
Class> c = findLoadedClass(name);
if (c == null) {
try {
//先判断父类是否存在
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果不存在就在自层找
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//在委托流程中没找到该类,就会执行该句
c = findClass(name);
}
}
//如果已加载就直接返回
return c;
}
BaseDexClassLoader.java
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
//调用pathList的findClass
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;
}
先看看pathList是什么对象
/**
* Constructs an instance.
*
* dexFile must be an in-memory representation of a full dexFile.
*
* @param dexFiles the array of in-memory dex files containing classes.
* @param parent the parent class loader
*
* @hide
*/
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
// TODO We should support giving this a library search path maybe.
super(parent);
//在构造器内初始化 是一个DexPathList对象
this.pathList = new DexPathList(this, dexFiles);
}
接下来看看DexPathList对象怎么存放已加载的class
/**
* Construct an instance.
*
* @param definingContext the context in which any as-yet unresolved
* classes should be defined
*
* @param dexFiles the bytebuffers containing the dex files that we should load classes from.
*/
public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
...
this.definingContext = definingContext;
// TODO It might be useful to let in-memory dex-paths have native libraries.
this.nativeLibraryDirectories = Collections.emptyList();
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);
ArrayList suppressedExceptions = new ArrayList();
//把所有存进来的dex文件存储在dexElements对象
this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
接下来看看dexElements 是何方神圣!?
这是dexElements的对象声明
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private Element[] dexElements;
重点来了:
/**
* Element of the dex/resource path. Note: should be called DexElement, but apps reflect on
* this.
*/
/*package*/ static class Element {
/**
* A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
* (only when dexFile is null).
*/
private final File path;
private final DexFile dexFile;
private ClassPathURLStreamHandler urlHandler;
private boolean initialized;
/**
* Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
* should be null), or a jar (in which case dexZipPath should denote the zip file).
*/
public Element(DexFile dexFile, File dexZipPath) {
this.dexFile = dexFile;
this.path = dexZipPath;
}
public Element(DexFile dexFile) {
this.dexFile = dexFile;
this.path = null;
}
public Element(File path) {
this.path = path;
this.dexFile = null;
}
....
}
从上面代码可以看到Element存放了dex文件的实例,和对应路径。
回来~从BaseDexClassLoader.findClass()->DexPathList.findClass()
就看看DexPathList.findClass()的实现内容
public Class> findClass(String name, List suppressed) {
//遍历dexElements,findClass()
for (Element element : dexElements) {
/
Class> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
看看element.findClass()
public Class> findClass(String name, ClassLoader definingContext,
List suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
dexFile.loadClassBinaryName()
public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List suppressed) {
Class result = null;
try {
//调用native
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
native方法往下就不再分析。从这波代码分析,找到一个重要转折点dexElements(Element数组),每当找应用程序的类时,都会遍历这个数组,找到目的的dex文件,再得到目的Class。
回到加固
由此,我们把解密的dex文件通过反射合并到这个dexElements对象(Element数组)就完事。
如下图:
上图对应以下代码:
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目录中的内容需要root权限才能用
File versionDir = getDir(app_name+"_"+app_version,MODE_PRIVATE);
File appDir=new File(versionDir,"app");
File dexDir=new File(appDir,"dexDir");
Log.e("ProxyApplication", "attachBaseContext:first "+apkFile.getAbsolutePath() );
Log.e("ProxyApplication", "attachBaseContext:sec "+versionDir.getAbsolutePath() );
//得到我们需要加载的Dex文件
List<File> dexFiles=new ArrayList<>();
//进行解密(最好做MD5文件校验)
if(!dexDir.exists() || dexDir.list().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) throws Exception{
//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);
}
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();
}
}
}
(tinker热修复共同点:加入新dex去dexElements)
探讨2: 初次冷启动ProxyApplication进程时,已经将ProxyApplication作为入口,后续的冷启动如何更替为真正的MyApplication作为真正的应用入口?
…