先來看看大概流程
加固俯瞰
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<File> 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<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//呼叫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<IOException> suppressedExceptions = new ArrayList<IOException>();
//把所有存進來的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<Throwable> 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<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
dexFile.loadClassBinaryName()
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> 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作為真正的應用入口?
…
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/46314.html
標籤:其他
