殼實現原理 & 方案概述

  • 殼本身也是一個APP程序,只不過它的作用只是用來解密原APP程序,然後加載。
  • 殼程序在完成dex的解密後,都需要通過ClassLoader來加載( APP本身的Dex文件 ),一般是通過DexClassLoader來加載,安卓8.0以後引入了InMemoryDexClassLoader從內存加載Dex文件
  • 最終還需要修復ClassLoader和Application( 具體原因在下方給出 ),才能順利運行原APP的Activity。

修復ClassLoader

為什麼需要修復ClassLoader?

Android的系統組件如ActivityService等都是由系統組件加載器( 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(){
// 這裡取的是MainActivity的ClassLoader, 即pathClassLoader
ClassLoader pathClassLoader = getClassLoader();

//public DexClassLoader(String dexPath, String optimizedDirectory,
// String librarySearchPath, ClassLoader parent) {
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

Untitled

修復方式一:替換系統組件類加載器

方案概述:替換系統組件類加載器為我們的DexClassLoader,同時將我們DexClassLoaderparent設置為為系統組件加載器( PathClassLoader )

獲取鏈:

  1. ActivityThread類裡獲取mPackages字段,ActivityThread實例可從currentActivityThread方法返回。
  2. mPackages字段獲取LoadedApk實例。
  3. 最後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");
// ActivityThreadClass裡的static方法, 該方法會返回單例模式下的ActivityThread對象
Method currentActivityThreadMethod = ActivityThreadClass.getDeclaredMethod("currentActivityThread");
// 1. 獲取ActivityThread實例對象
Object currentActivity = currentActivityThreadMethod.invoke(null);

Field mPackagesField = ActivityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
ArrayMap mPackages = (ArrayMap) mPackagesField.get(currentActivity);
// mPackages的鍵是應用的包名
String packageName = getApplicationContext().getPackageName();
Log.d(TAG, "packageName: " + packageName);
WeakReference loadedApkWrf = (WeakReference) mPackages.get(packageName);
// 2. 獲取LoadedApk實例對象
Object loadedApkObj = loadedApkWrf.get();

Class LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk");
Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
// 3. 設置LoadedApk類下的mClassLoader
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(){
// 這裡取的是MainActivity的ClassLoader, 即pathClassLoader
ClassLoader pathClassLoader = getClassLoader();

//public DexClassLoader(String dexPath, String optimizedDirectory,
// String librarySearchPath, ClassLoader parent) {
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(){
// 這裡取的是MainActivity的ClassLoader, 即pathClassLoader
ClassLoader pathClassLoader = getClassLoader();
ClassLoader bootClassLoader = pathClassLoader.getParent();

// 1. 將我們DexClassLoader的parent指向bootClassLoader
ClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex", null, null, bootClassLoader);
try {
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
// 2. 將pathClassLoader的parent拍向我們的DexClassLoader
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進行合並。

獲取鏈:

  1. 反射獲取BaseDexClassLoader類,可從其內獲取pathList實例。
  2. 反射獲取DexPathList類,可從其內獲取dexElements實例。
  3. dexElements是一個數組,元素類型是DexPathList類內部的Element類,它有一個getDexPath方法,能獲取dexPath字符串。
  4. 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 {
// 1. 獲取pathList實例, 它在BaseDexClassLoaderClass裡
BaseDexClassLoaderClass = pathClassLoader.loadClass("dalvik.system.BaseDexClassLoader");
Field pathListField = BaseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object myPathList = pathListField.get(myDexClassLoader);
Object originPathList = pathListField.get(pathClassLoader);

// 2. 獲取我的DexClassLoader的dexElements, 後續通過反射取其中的dexPath然後調用addDexPath添加到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++){
// 3. myDexElements每個元素的dexPath
String dexPath = (String) getDexPathMethod.invoke(myDexElements[i]);
Log.d(TAG, "addDexPathMethod " + i + ": " + dexPath);

// 4. 添加到originPathList, 即pathClassLoader的pathList
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(){
// 這裡取的是MainActivity的ClassLoader, 即pathClassLoader
ClassLoader pathClassLoader = getClassLoader();

ClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex", null, null, pathClassLoader);

// DO: 將我們的dexClassLoader合並到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

  1. 自定義Application類,必須繼承自Application

Untitled

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

Untitled

需要修正的原因 & 方案概述

若原APP裡有自己實現Application,並且在其attachBaseContextonCreate裡實現了某些邏輯時,這時很顯然就需要修正Application

Application修正簡單來說就是將殼的Application替換成原本的Application,主要步驟為:

  1. 對於LoadedApk.java裡的mApplication( 可以通過類自帶的getApplication函數獲取該實例 ),首先要將它置為null,然後將mApplicationInfo( getApplicationInfo函數獲取該實例 )裡的className修改成原APP的Application的類名,最後調用makeApplication函數就能成功替換mApplication
  2. 對於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實例的流程則簡單很多,直接調用ActivityThreadgetApplication函數即可。

完整代碼

主要修正邏輯在replaceApplication裡,成功替換上述兩處的Application後,可能還需要手動調用該ApplicationonCreate函數( attachBaseContextmakeApplication裡已經被調用了 )。

代碼裡在替換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);
// try {
// Class DemoTwoApplicationClass = pathClassLoader.loadClass("com.demo.demo2.DemoTwoApplication");
// Log.e(TAG, "DemoTwoApplicationClass: " + DemoTwoApplicationClass);
//
//
// } catch (ClassNotFoundException e) {
// throw new RuntimeException(e);
// }

}
public void mergeDexElements(ClassLoader myDexClassLoader){
Class BaseDexClassLoaderClass = null;
ClassLoader pathClassLoader = getClassLoader();
try {
// 1. 獲取pathList實例, 它在BaseDexClassLoaderClass裡
BaseDexClassLoaderClass = pathClassLoader.loadClass("dalvik.system.BaseDexClassLoader");
Field pathListField = BaseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object myPathList = pathListField.get(myDexClassLoader);
Object originPathList = pathListField.get(pathClassLoader);

// 2. 獲取我的DexClassLoader的dexElements, 後續通過反射取其中的dexPath然後調用addDexPath添加到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++){
// 3. myDexElements每個元素的dexPath
String dexPath = (String) getDexPathMethod.invoke(myDexElements[i]);
Log.d(TAG, "addDexPathMethod " + i + ": " + dexPath);

// 4. 添加到originPathList, 即pathClassLoader的pathList
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");
// 1. 先獲取ActivityThread實例, 然後再取mBoundApplication實例
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
Field mBoundApplicationField = ActivityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
Object mBoundApplication = mBoundApplicationField.get(currentActivityThread);

// 2. 通過mBoundApplication(AppBindData類)獲取LoadedApk實例
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);

// 3. 獲取殼的Application(在LoadedApk裡), 若它不為null則置為null
@SuppressLint("SoonBlockedPrivateApi") Method getApplicationMethod = LoadedApkClass.getDeclaredMethod("getApplication");
getApplicationMethod.setAccessible(true);
Field mApplicationField = LoadedApkClass.getDeclaredField("mApplication");
mApplicationField.setAccessible(true);
Object mApplicationObj = getApplicationMethod.invoke(loadedApkObj); // 這是殼的Application
Log.d(TAG, "mApplicationObj: " + mApplicationObj);
if(mApplicationObj != null){
mApplicationField.set(mApplicationObj, null);
}

// 4. 獲取ActivityThread裡的mAllApplications, 若mAllApplications.size()不為0, 則利用remove來移除其中的元素
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");
}
}

// 5. 獲取LoadedApk類的ApplicationInfo
Method getApplicationInfoMethod = LoadedApkClass.getDeclaredMethod("getApplicationInfo");
getApplicationInfoMethod.setAccessible(true);
Object applicationInfoObj = getApplicationInfoMethod.invoke(loadedApkObj);

// 6. 獲取ActivityThread$AppBindData裡的ApplicationInfo
Field appInfoField = AppBindDataClass.getDeclaredField("appInfo");
appInfoField.setAccessible(true);
Object appInfoObj = appInfoField.get(mBoundApplication);

// 7. 將上面2個applicationInfo的className設為原APP Application的className
Class ApplicationInfoClass = classLoader.loadClass("android.content.pm.ApplicationInfo");
Field appInfoClassNameField = ApplicationInfoClass.getDeclaredField("className");
appInfoClassNameField.setAccessible(true);

// 8. 替換殼application的類名
// 疑問: applicationInfoObj 應該等於 appInfoObj? 還是場景所致? 不管了, 先按dpt-shell裡實現
appInfoClassNameField.set(applicationInfoObj, realApplicationClassName);
appInfoClassNameField.set(appInfoObj, realApplicationClassName);

Log.e(TAG, "執行makeApplicationMethod前殼的Application: " + getApplicationMethod.invoke(loadedApkObj));

// 9. 調用LoadedApk類的makeApplication, 這步應該是用來生成原APP的application, 前提是要加載了原APP的dex
// makeApplication調用後, LoadedApk裡的Application就有值, 但ActivityThread裡的mInitialApplication還未有值, 需要手動賦給它
Method makeApplicationMethod = LoadedApkClass.getDeclaredMethod("makeApplication", boolean.class, Instrumentation.class);
makeApplicationMethod.setAccessible(true);
makeApplicationMethod.invoke(loadedApkObj, false, null); // 裡面會調用attachBaseContext

Log.e(TAG, "執行makeApplicationMethod後殼的Application: " + getApplicationMethod.invoke(loadedApkObj));

/** 10. 將makeApplication生成的那個新application賦給mInitialApplication
* ActivityThread.java:
* private void handleBindApplication(AppBindData data) {
* app = data.info.makeApplication(data.restrictedBackupMode, null);
* mInitialApplication = app; // 由此可知mInitialApplication是在makeApplication後賦值, 我們修正application時理應做同樣的操作
* }
*
*/
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));

// 11. 手動調用realApplication的onCreate
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);

// 1. 先加載原APP的dex和修正classLoader
loadMyDexAndFixClassLoader();

// 2. 修正Application
replaceApplication(getClassLoader(), "com.demo.demo2.DemoTwoApplication");

}

@Override
public void onCreate() {
super.onCreate();
}
}

相關日志輸出:

Untitled

參考