殼實現原理 & 方案概述
- 殼本身也是一個APP程序,只不過它的作用只是用來解密原APP程序,然後加載。
- 殼程序在完成dex的解密後,都需要通過
ClassLoader來加載( APP本身的Dex文件 ),一般是通過DexClassLoader來加載,安卓8.0以後引入了InMemoryDexClassLoader從內存加載Dex文件
- 最終還需要修復ClassLoader和Application( 具體原因在下方給出 ),才能順利運行原APP的Activity。
修復ClassLoader
為什麼需要修復ClassLoader?
Android的系統組件如Activity、Service等都是由系統組件加載器( PathClassLoader )加載的,以這方式加載的系統組件會有一個所謂的組件生命周期,而我們直接用DexClassLoader加載的類是沒有組件生命周期的,即使能通過對APK的動態加載完成對組件類的加載,但當系統啟動該組件時( 如startActivity )時,依然會出現找不到該類的情況。
以如下代碼為例,通過loadClass加載test.dex裡的MainActivity,能順利被加載並打印,但在startActivity時會報錯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public void startActivity(){ ClassLoader pathClassLoader = getClassLoader();
ClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex", null, null, pathClassLoader);
try { Class DemoTwoMainActivityClass = dexClassLoader.loadClass("com.demo.demo2.MainActivity"); Log.d(TAG, "DemoTwoMainActivityClass: " + DemoTwoMainActivityClass); startActivity(new Intent(this, DemoTwoMainActivityClass));
} catch (ClassNotFoundException e) { throw new RuntimeException(e); }
}
|
報錯如下圖所示:Didn't find class "com.demo.demo2.MainActivity" on path

修復方式一:替換系統組件類加載器
方案概述:替換系統組件類加載器為我們的DexClassLoader,同時將我們DexClassLoader的parent設置為為系統組件加載器( PathClassLoader )
獲取鏈:
- 從
ActivityThread類裡獲取mPackages字段,ActivityThread實例可從currentActivityThread方法返回。
- 從
mPackages字段獲取LoadedApk實例。
- 最後
mClassLoader就是我們的目標,它在LoadedApk類中,通過上面獲取的LoadedApk實例就可以修改它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| public void replaceClassLoader(ClassLoader myClassLoader){ ClassLoader pathClassLoader = getClassLoader(); try { Class ActivityThreadClass = pathClassLoader.loadClass("android.app.ActivityThread"); Method currentActivityThreadMethod = ActivityThreadClass.getDeclaredMethod("currentActivityThread"); Object currentActivity = currentActivityThreadMethod.invoke(null);
Field mPackagesField = ActivityThreadClass.getDeclaredField("mPackages"); mPackagesField.setAccessible(true); ArrayMap mPackages = (ArrayMap) mPackagesField.get(currentActivity); String packageName = getApplicationContext().getPackageName(); Log.d(TAG, "packageName: " + packageName); WeakReference loadedApkWrf = (WeakReference) mPackages.get(packageName); Object loadedApkObj = loadedApkWrf.get();
Class LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk"); Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader"); mClassLoaderField.setAccessible(true); mClassLoaderField.set(loadedApkObj, myClassLoader);
Log.d(TAG, "mPackages: " + mPackages);
} catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } }
public void startActivityWithFirstMethod(){ ClassLoader pathClassLoader = getClassLoader();
ClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex", null, null, pathClassLoader); replaceClassLoader(dexClassLoader); try { Class DemoTwoMainActivityClass = dexClassLoader.loadClass("com.demo.demo2.MainActivity"); Log.d(TAG, "DemoTwoMainActivityClass: " + DemoTwoMainActivityClass); startActivity(new Intent(this, DemoTwoMainActivityClass));
} catch (ClassNotFoundException e) { throw new RuntimeException(e); }
}
|
修復方式二:插入我們的DexClassLoader
方案概述:打破原有的雙親關系,在系統組件類加載器和BootClassLoader中間插入我們自己的DexClassLoader。即讓我們DexClassLoader的parent指向BootClassLoader、系統組件類加載器指的parent向我們我們的DexClassLoader
注:好像有兼容性問題,我測試的時候不成功( Android8.1 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public void startActivityWithSecondMethod(){ ClassLoader pathClassLoader = getClassLoader(); ClassLoader bootClassLoader = pathClassLoader.getParent();
ClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex", null, null, bootClassLoader); try { Field parentField = ClassLoader.class.getDeclaredField("parent"); parentField.setAccessible(true); parentField.set(pathClassLoader, dexClassLoader); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); }
Log.d(TAG, "pathClassLoader parent: " + pathClassLoader.getParent()); Log.d(TAG, "dexClassLoader parent: " + dexClassLoader.getParent()); try { Class DemoTwoMainActivityClass = dexClassLoader.loadClass("com.demo.demo2.MainActivity"); Log.d(TAG, "DemoTwoMainActivityClass: " + DemoTwoMainActivityClass); startActivity(new Intent(this, DemoTwoMainActivityClass));
} catch (ClassNotFoundException e) { throw new RuntimeException(e); }
}
|
修復方式三:合並Elements
方案概述:對PathClassLoader中的Elements進行合並。
獲取鏈:
- 反射獲取
BaseDexClassLoader類,可從其內獲取pathList實例。
- 反射獲取
DexPathList類,可從其內獲取dexElements實例。
dexElements是一個數組,元素類型是DexPathList類內部的Element類,它有一個getDexPath方法,能獲取dexPath字符串。
DexPathList類有個addDexPath方法,配合上面的getDexPath方法就能簡單地實現合並Elements的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| public void mergeDexElements(ClassLoader myDexClassLoader){ Class BaseDexClassLoaderClass = null; ClassLoader pathClassLoader = getClassLoader(); try { BaseDexClassLoaderClass = pathClassLoader.loadClass("dalvik.system.BaseDexClassLoader"); Field pathListField = BaseDexClassLoaderClass.getDeclaredField("pathList"); pathListField.setAccessible(true); Object myPathList = pathListField.get(myDexClassLoader); Object originPathList = pathListField.get(pathClassLoader);
Class DexPathListClass = pathClassLoader.loadClass("dalvik.system.DexPathList"); Field dexElementsField = DexPathListClass.getDeclaredField("dexElements"); dexElementsField.setAccessible(true); Method addDexPathMethod = DexPathListClass.getDeclaredMethod("addDexPath", String.class, File.class); addDexPathMethod.setAccessible(true); Object[] myDexElements = (Object[]) dexElementsField.get(myPathList);
Class DexElementClass = myDexClassLoader.loadClass("dalvik.system.DexPathList$Element"); @SuppressLint("SoonBlockedPrivateApi") Method getDexPathMethod = DexElementClass.getDeclaredMethod("getDexPath"); getDexPathMethod.setAccessible(true); for(int i = 0; i < myDexElements.length; i++){ String dexPath = (String) getDexPathMethod.invoke(myDexElements[i]); Log.d(TAG, "addDexPathMethod " + i + ": " + dexPath);
addDexPathMethod.invoke(originPathList, dexPath, null); }
Log.d(TAG, "end: " + originPathList);
} catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); }
}
public void startActivityWithThirdMethod(){ ClassLoader pathClassLoader = getClassLoader(); ClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex", null, null, pathClassLoader);
mergeDexElements(dexClassLoader);
try { Class DemoTwoMainActivityClass = pathClassLoader.loadClass("com.demo.demo2.MainActivity"); Log.d(TAG, "DemoTwoMainActivityClass: " + DemoTwoMainActivityClass); startActivity(new Intent(this, DemoTwoMainActivityClass));
} catch (ClassNotFoundException e) { throw new RuntimeException(e); }
}
|
Application修正
修正方案參考項目:https://github.com/ngiokweng/dpt-shell ( replaceApplication函數 )
前置:自定義APP的Application
- 自定義Application類,必須繼承自
Application

- 在
AndroidManifest.xml設置<application>裡的android:name為我們自定義的Application類名

需要修正的原因 & 方案概述
若原APP裡有自己實現Application,並且在其attachBaseContext或onCreate裡實現了某些邏輯時,這時很顯然就需要修正Application。
Application修正簡單來說就是將殼的Application替換成原本的Application,主要步驟為:
- 對於
LoadedApk.java裡的mApplication( 可以通過類自帶的getApplication函數獲取該實例 ),首先要將它置為null,然後將mApplicationInfo( getApplicationInfo函數獲取該實例 )裡的className修改成原APP的Application的類名,最後調用makeApplication函數就能成功替換mApplication。
- 對於
ActivityThread.java裡的mInitialApplication( 可以通過getApplication函數獲取該實例 ),只需將上述mApplication後的mApplication賦給mInitialApplication即可。
通過反射獲取mApplication實例前,需要先獲取LoadedApk的實例,然後才能調用getApplication函數返回mApplication實例,反射過程如下:
1 2 3 4 5
| ActivityThread.java: getObjMethod: currentActivityThread() -> 返回ActivityThread實例 prop: mBoundApplication [class: ActivityThread$AppBindData] prop: info [class: LoadedApk] prop: mApplication [target_A]
|
獲取mInitialApplication實例的流程則簡單很多,直接調用ActivityThread的getApplication函數即可。
完整代碼
主要修正邏輯在replaceApplication裡,成功替換上述兩處的Application後,可能還需要手動調用該Application的onCreate函數( attachBaseContext在makeApplication裡已經被調用了 )。
代碼裡在替換Application前會先將其置null,否則makeApplication會將原application直接返回。
注:該代碼還未在真正的加固場景中實踐,可能存在一些問題,待我有能力自己寫一套完整的加殼脫殼程序時再回來修正問題,具體問題還需具體分析…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
| public class DemoOneApplication extends Application { private static final String TAG = "nglog";
public void loadMyDexAndFixClassLoader(){ ClassLoader pathClassLoader = getClassLoader();
ClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex", null, null, pathClassLoader);
mergeDexElements(dexClassLoader);
} public void mergeDexElements(ClassLoader myDexClassLoader){ Class BaseDexClassLoaderClass = null; ClassLoader pathClassLoader = getClassLoader(); try { BaseDexClassLoaderClass = pathClassLoader.loadClass("dalvik.system.BaseDexClassLoader"); Field pathListField = BaseDexClassLoaderClass.getDeclaredField("pathList"); pathListField.setAccessible(true); Object myPathList = pathListField.get(myDexClassLoader); Object originPathList = pathListField.get(pathClassLoader);
Class DexPathListClass = pathClassLoader.loadClass("dalvik.system.DexPathList"); Field dexElementsField = DexPathListClass.getDeclaredField("dexElements"); dexElementsField.setAccessible(true); Method addDexPathMethod = DexPathListClass.getDeclaredMethod("addDexPath", String.class, File.class); addDexPathMethod.setAccessible(true); Object[] myDexElements = (Object[]) dexElementsField.get(myPathList);
Class DexElementClass = myDexClassLoader.loadClass("dalvik.system.DexPathList$Element"); @SuppressLint("SoonBlockedPrivateApi") Method getDexPathMethod = DexElementClass.getDeclaredMethod("getDexPath"); getDexPathMethod.setAccessible(true); for(int i = 0; i < myDexElements.length; i++){ String dexPath = (String) getDexPathMethod.invoke(myDexElements[i]); Log.d(TAG, "addDexPathMethod " + i + ": " + dexPath);
addDexPathMethod.invoke(originPathList, dexPath, null); }
Log.d(TAG, "end: " + originPathList);
} catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); }
}
public static void replaceApplication(ClassLoader classLoader, String realApplicationClassName){ Class ActivityThreadClass = null; try { ActivityThreadClass = classLoader.loadClass("android.app.ActivityThread"); Method currentActivityThreadMethod = ActivityThreadClass.getDeclaredMethod("currentActivityThread"); Object currentActivityThread = currentActivityThreadMethod.invoke(null); Field mBoundApplicationField = ActivityThreadClass.getDeclaredField("mBoundApplication"); mBoundApplicationField.setAccessible(true); Object mBoundApplication = mBoundApplicationField.get(currentActivityThread);
Class LoadedApkClass = classLoader.loadClass("android.app.LoadedApk"); Class AppBindDataClass = classLoader.loadClass("android.app.ActivityThread$AppBindData"); Field infoField = AppBindDataClass.getDeclaredField("info"); infoField.setAccessible(true); Object loadedApkObj = infoField.get(mBoundApplication);
@SuppressLint("SoonBlockedPrivateApi") Method getApplicationMethod = LoadedApkClass.getDeclaredMethod("getApplication"); getApplicationMethod.setAccessible(true); Field mApplicationField = LoadedApkClass.getDeclaredField("mApplication"); mApplicationField.setAccessible(true); Object mApplicationObj = getApplicationMethod.invoke(loadedApkObj); Log.d(TAG, "mApplicationObj: " + mApplicationObj); if(mApplicationObj != null){ mApplicationField.set(mApplicationObj, null); }
Field mAllApplicationsField = ActivityThreadClass.getDeclaredField("mAllApplications"); mAllApplicationsField.setAccessible(true); ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(currentActivityThread); if(mAllApplications.size() != 0){ Application removed = mAllApplications.remove(0); if(removed != null){ Log.d(TAG, "replaceApplicationOnLoadedApk proxy application removed"); } }
Method getApplicationInfoMethod = LoadedApkClass.getDeclaredMethod("getApplicationInfo"); getApplicationInfoMethod.setAccessible(true); Object applicationInfoObj = getApplicationInfoMethod.invoke(loadedApkObj);
Field appInfoField = AppBindDataClass.getDeclaredField("appInfo"); appInfoField.setAccessible(true); Object appInfoObj = appInfoField.get(mBoundApplication);
Class ApplicationInfoClass = classLoader.loadClass("android.content.pm.ApplicationInfo"); Field appInfoClassNameField = ApplicationInfoClass.getDeclaredField("className"); appInfoClassNameField.setAccessible(true);
appInfoClassNameField.set(applicationInfoObj, realApplicationClassName); appInfoClassNameField.set(appInfoObj, realApplicationClassName);
Log.e(TAG, "執行makeApplicationMethod前殼的Application: " + getApplicationMethod.invoke(loadedApkObj));
Method makeApplicationMethod = LoadedApkClass.getDeclaredMethod("makeApplication", boolean.class, Instrumentation.class); makeApplicationMethod.setAccessible(true); makeApplicationMethod.invoke(loadedApkObj, false, null);
Log.e(TAG, "執行makeApplicationMethod後殼的Application: " + getApplicationMethod.invoke(loadedApkObj));
Method atGetApplicationMethod = ActivityThreadClass.getDeclaredMethod("getApplication"); atGetApplicationMethod.setAccessible(true); Field mInitialApplicationField = ActivityThreadClass.getDeclaredField("mInitialApplication"); mInitialApplicationField.setAccessible(true); mInitialApplicationField.set(currentActivityThread, getApplicationMethod.invoke(loadedApkObj));
Log.e(TAG, "mInitialApplication賦值後: " + atGetApplicationMethod.invoke(currentActivityThread));
Class realApplicationClass = classLoader.loadClass(realApplicationClassName); Method onCreateMethod = realApplicationClass.getDeclaredMethod("onCreate"); onCreateMethod.setAccessible(true); onCreateMethod.invoke(atGetApplicationMethod.invoke(currentActivityThread));
} catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); }
}
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); loadMyDexAndFixClassLoader(); replaceApplication(getClassLoader(), "com.demo.demo2.DemoTwoApplication");
}
@Override public void onCreate() { super.onCreate(); } }
|
相關日志輸出:

參考