殼實現原理 & 方案概述
- 殼本身也是一個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(); } }
|
相關日志輸出:
參考