文章目錄
- 前言
- 加載Activity遇到的問題
- APK的啟動程序
- 替換ClassLoader流程
- 獲取ActivityThread類物件
- 獲取AppBindData類物件mBoundApplication
- 獲取LoadedApk類物件info
- 獲取info物件中的ClassLoader
- 設計傀儡dex檔案
- 手工加固APK
- 代碼實作APK加固
- 實作步驟
- 總結
前言
動態加載dex之后,我們會想說,能不能將整個程式的dex都進行動態加載,如果將加載的dex事先加密,加載前解密,這樣就完成了對程式完整的解密了,但這里面遇到一個問題,那就是Android中很多組件其實是事先在清單檔案中注冊過的,我們需要在不多修改清單檔案的前提下,完成對藏匿在資源中加密的dex檔案,完成了這個程序,也就完成了apk的加固
那做到在不過多修改清單檔案的前提下,完成對藏匿在資源中加密的dex檔案,我們將分為以下幾步完成:
- 完成對已注冊的Activity的加載
- 找尋apk啟動時最開始啟動的代碼,插入自己的代碼
- 設計傀儡dex檔案,啟動apk之后,將傀儡dex代碼替換為目標dex代碼
接下來就是按照這個思路,開始加載Activity
加載Activity遇到的問題
我們已經學會了如何動態加載一個類,那動態加載一個Activity呢?為了能讓Activity免除資源困擾,我們在資源中先創建一個Activity類,資源同樣也建立好,
先看Activity類代碼
package com.example.apkdemo2;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity2 extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
}
}
再看資源檔案
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity2">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第二個Activity"
></TextView>
</LinearLayout>
接著將這個Activity反編譯后再轉成dex檔案

然后將生成的dex放到assets目錄下,

然后洗掉MainActivity2這個類,
我們在這個專案中去加載Activity,因為當前專案已經有Activity2的資源檔案了,這樣就可以不受資源的困擾,難度會低很多,
接著完成動態加載dex的步驟:
- 拷貝自定義資源中的dex到程式中
- 創建一個DexClassLoader,加載dex
- 呼叫加載dex中的class方法
這個步驟我們之前已經完成了
區別在于第三步,這里需要獲取型別別,設定Intent資訊,啟動Activity,代碼如下:
Class clz=null;
try {
clz=dexClassLoader.loadClass("com.example.apkdemo2.MainActivity2");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Intent intent=new Intent(this,clz);
startActivity(intent);
完成后我們在界面中增加按鈕,然后在按鈕中呼叫上面的方法,

運行程式,點擊按鈕,發現出現了錯誤,報錯資訊如下:
unable to instantiate activity
無法實體化Activity,
我們使用創建了DexClassLoader加載器,加載了我們需要的類,但有一個問題,那就是程式當前的ClassLoader是哪個?答案是PathClassLoader,但是PathClassLoader沒有我們加載的類,
針對這個問題有兩個解決方案
- 直接使用PathClassLoader加載我們需要的類
- 使用我們自己的ClassLoader替換掉PathClassLoader
第一種方法顯然不行,除非我們能先將dex檔案安裝到apk中,既然都能安裝到apk中了,那就不能做到我們想要做的隱藏效果了,所以排除第一種方案
第二種方法,替換掉PathClassLoader,這種方法應該是可行的,因為只有ClassLoader換了,用對應的ClassLoader肯定能加載對應的類
那怎么才能替換掉ClassLoader呢?需要我們進一步分析APK的啟動程序,找到保存ClassLoader的變數,變數所在的類,使用反射修改,
接下來從分析APK的啟動程序開始
APK的啟動程序
關于APK的啟動程序,這里推薦一篇文章:
《Android應用程式啟動程序源代碼分析》https://blog.csdn.net/Luoshengyang/article/details/6689748
這篇文章詳細的分析了Android應用程式的啟動程序,這里就不再贅述,做一個簡單總結
Step 1. Launcher.startActivitySafely
Step 2. Activity.startActivity
Step 3. Activity.startActivityForResult
Step 4. Instrumentation.execStartActivity
Step 5. ActivityManagerProxy.startActivity
Step 6. ActivityManagerService.startActivity
Step 7. ActivityStack.startActivityMayWait
Step 8. ActivityStack.startActivityLocked
Step 9. ActivityStack.startActivityUncheckedLocked
Step 10. Activity.resumeTopActivityLocked
Step 11. ActivityStack.startPausingLocked
Step 12. ApplicationThreadProxy.schedulePauseActivity
Step 13. ApplicationThread.schedulePauseActivity
Step 14. ActivityThread.queueOrSendMessage
Step 15. H.handleMessage
Step 16. ActivityThread.handlePauseActivity
Step 17. ActivityManagerProxy.activityPaused
Step 18. ActivityManagerService.activityPaused
Step 19. ActivityStack.activityPaused
Step 20. ActivityStack.completePauseLocked
Step 21. ActivityStack.resumeTopActivityLokced
Step 22. ActivityStack.startSpecificActivityLocked
Step 23. ActivityManagerService.startProcessLocked
Step 24. ActivityThread.main
Step 25. ActivityManagerProxy.attachApplication
Step 26. ActivityManagerService.attachApplication
Step 27. ActivityManagerService.attachApplicationLocked
Step 28. ActivityStack.realStartActivityLocked
Step 29. ApplicationThreadProxy.scheduleLaunchActivity
Step 30. ApplicationThread.scheduleLaunchActivity
Step 31. ActivityThread.queueOrSendMessage
Step 32. H.handleMessage
Step 33. ActivityThread.handleLaunchActivity
Step 34. ActivityThread.performLaunchActivity
Step 35. MainActivity.onCreate
apk的啟動程序相對比較復雜,我們的分析目的是為了找到PathClassLoader,所以不需要對每一個步驟進行詳細了解,整個啟動程序可以簡化為下面的步驟
Step 2. Activity.startActivity--->CreateProcess
......
Step 24. ActivityThread.main----->入口點
......
Step 35. MainActivity.onCreate--->main函式
用Windows系統來對比參照的話,Step 2可以看作是創建行程,一直到Step 24中間的步驟就等于是創建行程的準備作業,而真正的程式入口則是ActivityThread.main,相當于Windows的程式入口,接著Step 35才是真正執行用戶代碼的地方,相當于是main函式了
然后就可以進入Android原始碼網站http://androidxref.com/,開始分析app啟動程序

另外一種分析的方法就是在onCreate函式下斷,然后查看呼叫堆疊
替換ClassLoader流程
獲取ActivityThread類物件
這個原始碼分析起來還是比較吃力的,這里直接看結論,替換ClassLoader的流程如下

首先獲取ActivityThread型別別,然后呼叫這個類的currentActivityThread方法,目的是為了拿到sCurrentActivityThread物件

private static ActivityThread sCurrentActivityThread;
sCurrentActivityThread是ActivityThread類的類物件,拿到了這個類的類物件,我們就可以操作整個類的資料
獲取AppBindData類物件mBoundApplication

然后.通過類物件獲取成員變數mBoundApplication
獲取LoadedApk類物件info

獲取到了AppBindData的類物件以后,就可以拿到AppBindData的類物件內的成員變數info,也就是LoadedApk類物件
獲取info物件中的ClassLoader

接著就能獲取到LoadedApk類物件內的ClassLoader,最后需要將這一段替換ClassLoader的邏輯轉換成代碼,就解決了這個問題,
完整代碼如下:
//替換app啟動時的classloader為加載的dexclassloader
private void replaceClassLoader(DexClassLoader dexClassLoader) {
try {
//1.獲取ActivityThread類物件
//獲取型別別
Class clzActivityThread=Class.forName("android.app.ActivityThread");
//獲取類方法
Method methodcurrentActivityThread=clzActivityThread.getDeclaredMethod("currentActivityThread");
//呼叫方法
Object objectActivityThread = methodcurrentActivityThread.invoke(null,new Object[]{});
//2.通過類物件獲取成員變數mBoundApplication
//獲取欄位
Field fieldmBoundApplication=clzActivityThread.getDeclaredField("mBoundApplication");
//取消訪問檢查
fieldmBoundApplication.setAccessible(true);
//獲取欄位的值
Object objBoundApplication= fieldmBoundApplication.get(objectActivityThread);
//3.獲取mBoundApplication物件中的成員變數info
//獲取型別別
Class clzAppBindData=Class.forName("android.app.ActivityThread$AppBindData");
//獲取欄位
Field fieldInfo=clzAppBindData.getDeclaredField("info");
fieldInfo.setAccessible(true);
//獲取欄位的值
Object objInfo = fieldInfo.get(objBoundApplication);
//4.獲取info物件中的mClassLoader
//獲取型別別
Class clzLoadedApk=Class.forName("android.app.LoadedApk");
//獲取欄位
Field fieldmClassLoader=clzLoadedApk.getDeclaredField("mClassLoader");
fieldmClassLoader.setAccessible(true);
//設定欄位 替換ClassLoader
fieldmClassLoader.set(objInfo,dexClassLoader);
} catch (Exception e) {
e.printStackTrace();
}
}
設計傀儡dex檔案
經過對APK啟動的分析,可知在APK啟動時在創建MainActivity前,會創建Application物件,呼叫其attachBaseContext方法,以及onCreate方法 ,所以我們設計的傀儡dex只需要先原APK清單檔案中添加Application的節點,創建一個MyApplication類,實作attachBaseContext方法以及onCreate方法,在這兩個方法中初始化原dex檔案,加載dex檔案即可
我們可以在attachBaseContext方法,加載源dex檔案回傳dex加載器,替換系統默認的加載器,然后剩下的交給系統處理即可
先撰寫傀儡Application代碼,繼承自Application,重寫attachBaseContext方法和onCreate方法
package com.example.dummydex;
import android.app.Application;
import android.content.Context;
public class DummyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//code
}
@Override
public void onCreate() {
super.onCreate();
//code
}
}
在attachBaseContext方法中添加加載源dex檔案和替換ClassLoader的代碼
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//拷貝自定義資源中的dex檔案到程式目錄下
String path=CopyDex("src.dex");
//創建一個DexClassLoader 加載dex
DexClassLoader dexClassLoader=GetLoader(path);
//替換ClassLoader
replaceClassLoader(dexClassLoader);
}
代碼中的三個方法和之前的一致,到此DummyApplication類已經寫完,注意需要將DummyApplication類的完整類(帶包名)寫入待加密的清單檔案
使用smali.jar將DummyApplication類編譯成class.dex

至此,傀儡dex檔案生成完畢
手工加固APK
有了以上的準備,我們基本上可以手工完成對一個APK的加固,大致步驟:
- 獲取待加固的apk,隨便寫一個hello world
- 使用apktool反編譯apk,修改AndroidManifest.xml,添加DummyApplication類資訊,讓程式運行的第一個類為DummyApplication類
- 將源classes.dex放入assets目錄,修改檔案名為src.dex
- 使用apktool重打包修改后的資訊
- 替換重打包后的classes.dex為傀儡dex
- 為新打包的apk進行簽名
實際操作如下:

java -jar .\apktool.jar d .\test.apk
首先用apktool將需要加固的apk進行反編譯,反編譯后會生成檔案夾

然后修改清單檔案,在application中添加name屬性,類名為dummydex的完整包名+類名

修改完成之后 新建assets檔案夾,將apk原本的dex檔案放到assets檔案夾下,然后修改名稱為src.dex

java -jar .\apktool.jar b .\test -o app.apk
再將apk進行回編譯,如果回編譯的時候報了下面的錯誤
No resource identifier found for attribute ‘compileSdkVersion’ in package ‘android’
那么需要執行一下這條命令
java -jar apktool.jar empty-framework-dir --force
清空一下framework目錄

然后打開壓縮包,將原來的classes.dex洗掉,然后把dummy.dex放到apk里面

并修改為classes.dex,最近將apk打上簽名,整個加固程序就完成了

最后安裝,運行成功,那么整個加固程序就是成功的,

此時我們再用AndroidKiller進行反編譯,MainActivity已經無法找到

工程檔案中只有DummyApplication.smali檔案,到此就完成了整個手工加固的程序
代碼實作APK加固
實作步驟
根據手工加固APK的步驟得出將其轉出代碼的步驟:
- 獲取待加密的apk路徑
- 呼叫apktool,反編譯目標apk
- 修改AndroidManifest.xml添加DummyApplication的資訊
- 將源classes.dex復制到assets目錄,修改檔案名為src.dex
- 呼叫apktool重新打包,生成新的apk
- 修改新的apk中的calsses.dex將其替換為傀儡dex或者將反編譯后的smali代碼替換為傀儡dex的smali代碼
- 為新的apk進行簽名
主要流程代碼如下:
public static void Pack(){
//需要反編譯的apk檔案名
String fileName="test.apk";
//獲取當前路徑
String currentdir=System.getProperty("user.dir");
//構造全路徑
String filepath=currentdir+ File.separator+fileName;
//去掉擴展名
String NoExtenDir=StringsUtils.getFileNameNoEx(filepath);
//運行apktool 反編譯apk
System.err.println("1.反編譯apk...");
CMDUtils.runCMD("java -jar apktool.jar d "+filepath);
System.err.println("1.反編譯apk完成...");
//修改xml檔案
String xmlPath=NoExtenDir+File.separator+"AndroidManifest.xml";
System.err.println("2.修改清單檔案...");
XMLUtils.ChagenApplication(xmlPath);
System.err.println("2.修改清單檔案完成...");
//拷貝源dex到assets目錄
String assetsDir=NoExtenDir+ File.separator+"assets";
System.err.println("3.拷貝源dex到assets目錄...");
FileUtils.copyFileFromZip(filepath,assetsDir);
System.err.println("3.拷貝源dex到assets目錄完成...");
//洗掉smali檔案 拷貝dummydex的smali
String smaliDir=NoExtenDir+ File.separator+"smali";
System.err.println("4.洗掉smali檔案 拷貝dummydex的smali...");
FileUtils.deleteFolder(smaliDir);
String oldDir=currentdir+ File.separator+"dummySmali";
FileUtils.copyFolder(oldDir,smaliDir);
System.err.println("4.洗掉smali檔案 拷貝dummydex的smali完成...");
//重打包
System.err.println("5.重打包apk...");
CMDUtils.runCMD("java -jar apktool.jar b "+NoExtenDir+" -o pack.apk");
System.err.println("5.重打包apk完成...");
//簽名
System.err.println("6.對apk進行簽名...");
String outpath =currentdir+File.separator+"pack.apk";
String signPath =currentdir+File.separator+"pack_sigin.apk";
CMDUtils.runCMD("java -jar signapk.jar testkey.x509.pem testkey.pk8 "+outpath+" "+signPath,"sign");
System.err.println("6.對apk進行簽名完成...");
//收尾作業
System.err.println("7.收尾作業...");
FileUtils.deleteFolder(NoExtenDir);
File file=new File(outpath);
file.delete();
System.err.println("7.收尾作業完成...");
//輸出檔案路徑
System.out.println("8. 已加固檔案:"+signPath);
}
運行效果如圖:

運行完成之后,

APK加固也成功了,完整工程代碼如下:
https://download.csdn.net/download/qq_38474570/23560575?spm=1001.2014.3001.5503
總結
通過回顧思路,寫代碼對APK加固有了一定的認識,在完全實作自動化的那一刻,感嘆程式的魅力,不過這只是一個Demo,還有很多可以完善的地方,比如記憶體加載dex檔案,合并源dex和傀儡dex等等,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/304972.html
標籤:其他
上一篇:現代圖片選擇器(PHPicker)在 SwiftUI 應用
下一篇:Arouter實作界面跳轉和傳參
