APK加壳【1】初步方案实现详解
來源與原理
本文是嘗試對CSDN大牛 Jack_Jia 的博客 Android APK加殼技術方案【2】 進行實現的過程記錄,該文介紹了一種對源程序APK加殼的思路并提供了對應的源碼。
所謂加殼,就是通過給目標APK加一層保護程序,把需要保護的內容加密、隱藏起來,來防止反編譯的一種方法。說到底我們要做的是這樣一個事情,首先把要加殼的APK用自己的加密算法加個密(實驗過程中這步可以省掉),然后藏在另一個APK中(就是殼工程)發布出去,這樣防止破解者直接拿到源程序的APK去反編譯。不好處理的是還需要殼工程在各種版本的Android系統里運行時,要把源程序解密出來還要跟直接裝源程序有同樣的運行效果才行。如何實現原文都已經寫清楚了:
方案
整個方案里面涉及到三種角色:
所以檢查這個方案需要有個DEMO APK、有個加密工具JAVA工程DexShellTool 和一個Android殼工程UnShell,最后加殼后的APK實際上是殼工程編譯出的、并且把其中的dex文件替換為經過加密工具處理生成的新dex、最后重新打包簽名的APK。
源程序
源程序其實沒什么好講的,最好是有個帶有服務、廣播、網絡操作什么的基礎功能比較全面的示例程序,這樣測試可行性更加有說服力一些。
加密工具
加密工具其實原文中給出的很容易看懂,因為沒有涉及到加密算法,所以不到兩百行。基本做了這樣一件事:把源程序加密之后接到殼工程的dex文件尾,然后修改dex文件的文件長度、校驗和什么的。這種隱藏方式略詭異。
殼工程
殼工程既是殼又要有解殼功能,原文只給了兩個類,實際上也只需要這兩個類。ProxyApplication里有解殼與反射實現動態加載源程序的代碼邏輯、RefInvoke則是反射工具。許多童鞋表示反射不好理解,一開始我也是這么覺得。不過經過一行行注釋下來、對比系統源碼,其實也沒有多難。這里要說,靜下心來分析,不到三百行的代碼,能有多復雜呢?
protected void attachBaseContext(Context base) {super.attachBaseContext(base);Log.d(TAG, "attachBaseContext hello world~");try {File odex = this.getDir("payload_odex", MODE_PRIVATE);File libs = this.getDir("payload_lib", MODE_PRIVATE);odexPath = odex.getAbsolutePath();libPath = libs.getAbsolutePath();apkFileName = odex.getAbsolutePath() + "/payload.apk";File dexFile = new File(apkFileName);if (!dexFile.exists())dexFile.createNewFile();// 讀取程序classes.dex文件byte[] dexdata = this.readDexFileFromApk();// 分離出解殼后的apk文件已用于動態加載this.splitPayLoadFromDex(dexdata);// 配置動態加載環境Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread",new Class[] {}, new Object[] {});String packageName = this.getPackageName();HashMap mPackages = (HashMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mPackages");WeakReference wr = (WeakReference) mPackages.get(packageName);//換Loader操作 動態加載如被加密又裝換回來的apk文件DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,libPath, (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", wr.get(), "mClassLoader"));RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",wr.get(), dLoader);} catch (Exception e) {e.printStackTrace();}}public void onCreate() {Log.d(TAG, "on create hello world~");// 如果源應用配置有Appliction對象,則替換為源應用Applicaiton,以便不影響源程序邏輯。String appClassName = null;try {ApplicationInfo ai = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA);Bundle bundle = ai.metaData;if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {appClassName = bundle.getString("APPLICATION_CLASS_NAME");} else {return;}} catch (NameNotFoundException e) {e.printStackTrace();}Log.d(TAG, "the app aplication name is " + appClassName);/*** 調用靜態方法android.app.ActivityThread.currentActivityThread* 獲取當前activity所在的線程對象*/Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread",new Class[] {}, new Object[] {});/*** 獲取currentActivityThread中的mBoundApplication屬性對象,該對象是一個* AppBindData類對象,該類是ActivityThread的一個內部類*/Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mBoundApplication");/*** 獲取mBoundApplication中的info屬性,info 是 LoadedApk類對象*/Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication,"info");/*** loadedApkInfo對象的mApplication屬性置為null*/RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",loadedApkInfo, null);/*** 獲取currentActivityThread對象中的mInitialApplication屬性* 這貨是個正牌的 Application*/Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mInitialApplication");/*** 獲取currentActivityThread對象中的mAllApplications屬性* 這貨是 裝Application的列表*/ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread",currentActivityThread, "mAllApplications");//列表對象終于可以直接調用了 remove調了之前獲取的application 抹去記錄的樣子mAllApplications.remove(oldApplication);/*** 獲取前面得到LoadedApk對象中的mApplicationInfo屬性,是個ApplicationInfo對象*/ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.LoadedApk", loadedApkInfo,"mApplicationInfo");/*** 獲取前面得到AppBindData對象中的appInfo屬性,也是個ApplicationInfo對象*/ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData",mBoundApplication, "appInfo");//把這兩個對象的className屬性設置為從meta-data中獲取的被加密apk的application路徑appinfo_In_LoadedApk.className = appClassName;appinfo_In_AppBindData.className = appClassName;/*** 調用LoadedApk中的makeApplication 方法 造一個application* 前面改過路徑了 */Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo,new Class[] { boolean.class, Instrumentation.class },new Object[] { false, null });RefInvoke.setFieldOjbect("android.app.ActivityThread","mInitialApplication", currentActivityThread, app);HashMap mProviderMap = (HashMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mProviderMap");Iterator it = mProviderMap.values().iterator();while (it.hasNext()) {Object providerClientRecord = it.next();Object localProvider = RefInvoke.getFieldOjbect("android.app.ActivityThread$ProviderClientRecord",providerClientRecord, "mLocalProvider");RefInvoke.setFieldOjbect("android.content.ContentProvider","mContext", localProvider, app);}if(null == app){Log.e(TAG, "application get is null !");}else{app.onCreate();}}輔助源碼看實際上還是很好理解的,不多說。
順手推薦個android在線源碼瀏覽網址 http://androidxref.com/
為了方便后續調試代碼,弄了個shell腳本,同時也可以基本解釋整個加殼的流程:
注意事項
很明顯,這貨是需要一個文件路徑的,這意味著如果直接使用該類,就必須要有個解密好的文件老老實實的躺在存儲器上,這樣一來無論你放在什么地方、該文件存在的時間有多短,破解者都有可能繞過殼、直接拿到解密的文件,這明顯不科學;
其實原文所述的方案是加殼的一個基本思路,具體要預防反編譯實現起來肯定不會如此簡略、加殼也只是預防破解的各路招式之一。但還是要感謝大牛的蕓蕓分享,使我輩菜鳥有了一條入門之路。
原文地址: http://taoyuanxiaoqi.com/2015/01/12/apkshell1/
總結
以上是生活随笔為你收集整理的APK加壳【1】初步方案实现详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dex注入实现详解
- 下一篇: APK加壳【2】内存加载dex实现详解