安卓注入框架Xposed用法详解

    之前安卓注入框架Xposed分析与简单应用只是简单的了解了一下xposed框架,知道如何hook函数,并没有深入去使用,也不知道这个框架能用到哪种程度,最近详细的总结了下,简单来说就是,没有hook不到的地方,只有你想不到的地方。

接管系统所有广播包

    在Android四大组件中,Broadcast是一种广泛运用的在应用程序之间传输信息的机制。而BroadcastReceiver 是对发送出来的Broadcast进行过滤接受并响应的一类组件。应用通过BroadcastReceiver对一个外部的事件做出响应,这是非常有意思的,当开机,锁屏等外部事件到来的时候,可以利用BroadcastReceiver进行相应的处理。如果你能接管所有的广播包,基本上就接管了整个系统的信息传输过程。广播包也是频繁唤醒手机的一个重要原因,通过管理这些广播包,可以达到省电效果。

通过阅读源代码中
IntentFirewall这个类,126行代码开始有一些以check开头的方法,说明如下:

This is called from ActivityManager to check if a start activity intent should be allowed. It is assumed the caller is already holding the global ActivityManagerService lock.

说明很清晰的告诉我们,所有activity启动时,会到这里做相应的检测是否被允许,因此我们hook掉checkBroadcast方法,就可以控制所有的广播包走向。checkBroadcast代码如下:

1
2
3
4
5

public boolean checkBroadcast(Intent intent, int callerUid, int callerPid,
String resolvedType, int receivingUid)
{

return checkIntent(mBroadcastResolver, intent.getComponent(), TYPE_BROADCAST, intent, callerUid, callerPid, resolvedType, receivingUid);
}

这里,我通过hook该方法并获取到第一个参数Intent intent,然后就可以获取到广播类型,同时也可以获取到该广播的发送者(callerUid)和接受者(receivingUid),hook代码如下:

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
hook_method("com.android.server.firewall.IntentFirewall",
lpparam.classLoader,
"checkBroadcast",
Intent.class, // intent
int.class, // callerUid
int.class, // callerPid
String.class, // resolvedType
int.class, // receivingUid
new XC_MethodHook() {

@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
int callerUid = (int) param.args[1];
int receivingUid = (int) param.args[4];
XposedBridge.log("hook IntentFirewall.checkBroadcast : " + "broadcast from " + callerUid + " to " + receivingUid);

Intent intent = (Intent) param.args[0];
String action = intent.getAction();

if (action == null)
return;
if (action.equals("android.intent.action.SCREEN_OFF"))
XposedBridge.log("hook IntentFirewall.checkBroadcast : " + "screen off");
if (action.equals("android.intent.action.SCREEN_ON"))
XposedBridge.log("hook IntentFirewall.checkBroadcast : " + "screen on");
}
});

打印锁屏和亮屏的广播事件,结果如下所示:
过滤日志adb logcat | grep checkBroadcast

checkBroadcast

嵌套hook监控前台应用

    在编写代码时候,要实现实时监控前台应用是相当棘手的一件事情,并且在5.11以后的版本获取所有运行app都已经受到限制,stackoverflow中给出了一种方法,Get Running Apps on M with no permissions & get the foreground app on Android 5.1.1+,测试了获取前台应用的代码相当耗性能。这里基于xposed框架给出另外一种方法。

同样通过阅读源代码中的NetworkPolicyManagerService这个类,找到回调函数onForegroundActivitiesChanged

1
2
3
4
5
6
7
8
9
10
11
12
13
14

private IProcessObserver mProcessObserver = new IProcessObserver.Stub() {

@Override
public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
}

@Override
public void onProcessStateChanged(int pid, int uid, int procState) {
synchronized (mRulesLock) {

}
}
}

ActivityManager服务中IProcessObserver有个回调函数onForegroundActivitiesChanged。而动态设置网络连接规则的时候,NetworkPolicyManagerService服务通过检测系统发出的一些相关事件(在NetworkPolicyManagerService的启动systemReady函数中注册),其中会调用ActivityManager服务中IProcessObserver的onForegroundActivitiesChanged及onProcessDied回调事件,因此,我们通过hook服务类NetworkPolicyManagerService中的onForegroundActivitiesChanged回调函数来监控前台应用,但是这个过程必须保证在systemReady函数已启动注册了该服务,因此需要嵌套hook。具体过程如下:

  1. hook systemReady函数
  2. 通过param.thisObject获取hook方法所在类的实例,
    NetworkPolicyManagerService.class
  3. 通过getObjectField获取类中的对象mProcessObserver
  4. hook对象mProcessObserver中的onForegroundActivitiesChanged方法

完整代码如下所示:

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
hook_method("com.android.server.net.NetworkPolicyManagerService",
lpparam.classLoader,
"systemReady",
new XC_MethodHook() {

@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("hook NetworkPolicyManagerService.systemReady");
XposedBridge.log("hook NetworkPolicyManagerService.systemReady : " + param.thisObject.getClass());

Object mProcessObserverClass = XposedHelpers.getObjectField(param.thisObject, "mProcessObserver");
XposedBridge.log("hook NetworkPolicyManagerService.systemReady : " + mProcessObserverClass.getClass());

hook_method(mProcessObserverClass.getClass(),
"onForegroundActivitiesChanged",
int.class, // pid
int.class, // uid
boolean.class, // foregroundActivities
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
if ((boolean) param.args[2])
XposedBridge.log("hook NetworkPolicyManagerService.onForegroundActivitiesChanged : foreground uid = " + param.args[1]);
}
});
}
});

打印前台应用,结果如下所示:
过滤日志adb logcat | grep onForegroundActivitiesChanged

onForegroundActivitiesChanged

xposed进程读取文件

    有时候需要xposed进程根据我们自身进程的设置来执行不同的逻辑功能,因此需要xposed进程读取我们进程的配置文件。xposed框架中提供了读取data/data/package name/shared_prefs目录下的xml配置文件功能类XSharedPreferences,该类继承了系统类SharedPreferences并提供类额外的reload方法,当我们配置文件更新后,可以调用该方法重新加载。

    需要注意的是,该类只支持读操作,如果你尝试去执行写操作会抛异常。至于为什么作者没有添加写权限,作者也给出了详细的解释,详见:Cannot Write into a Shared Preferences File

    我在使用的过程中,需要读取files目录下的json文件内容,使用时发现该类只能读取xml文件,作为jar导入包又不适合修改,因此仿照XSharedPreferences写了一个可以读取任何文件内容的XFile类,其实可以逐渐完善读更多文件类型的内容,包括数据库等。

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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210

package com.example.idhyt.xposedExtend;

import android.os.Environment;
import android.util.Log;


import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import de.robv.android.xposed.SELinuxHelper;
import de.robv.android.xposed.services.FileResult;


/**
*
* Created by idhyt on 2015/12/10.
*
* This class is used to read file from data/data/xxx/files directory,
* same as XSharedPreferences, read-only and without listeners support.
*/



public class XFiles {
private static final String TAG = "XFiles";
private final File mFile;
private final String mFilename;
private ByteArrayOutputStream mFileOutputStream;
private boolean mLoaded = false;
private long mLastModified;
private long mFileSize;

/**
* Read settings from the specified file.
* @param file The file to read.
*/

public XFiles(File file) {
mFile = file;
mFilename = mFile.getAbsolutePath();
startLoadFromDisk();
}

/**
*
* @param packageName The package name.
*/

public XFiles(String packageName) {
this(packageName, packageName + "_files");
}

/**
*
* @param packageName The package name.
* @param fileName The file name with suffix (.txt, .json, etc..)
*/

public XFiles(String packageName, String fileName) {
mFile = new File(Environment.getDataDirectory(), "data/" + packageName + "/files/" + fileName);
mFilename = mFile.getAbsolutePath();
startLoadFromDisk();
}

/**
* Tries to make the files file world-readable.
*
* This will only work if executed as root (e.g. {@code initZygote()}) and only if SELinux is disabled.
*
* @return {@code true} in case the file could be made world-readable.
*/

public boolean makeWorldReadable() {
if (!SELinuxHelper.getAppDataFileService().hasDirectFileAccess())
return false; // It doesn't make much sense to make the file readable if we wouldn't be able to access it anyway.

if (!mFile.exists()) // Just in case - the file should never be created if it doesn't exist.
return false;

return mFile.setReadable(true, false);
}

/**
* Returns the file that is backing these preferences.
*
* <p><strong>Warning:</strong> The file might not be accessible directly.
*/

public File getFile() {
return mFile;
}

private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("XFiles-load") {
@Override
public void run() {
synchronized (XFiles.this) {
loadFromDiskLocked();
}
}
}.start();
}

@SuppressWarnings({ "rawtypes", "unchecked" })
private void loadFromDiskLocked() {
if (mLoaded) {
return;
}

ByteArrayOutputStream fileOutputStream = null;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
FileResult fileResult = null;

try {
fileResult = SELinuxHelper.getAppDataFileService().getFileInputStream(mFilename, mFileSize, mLastModified);
InputStream inputStream = fileResult.stream;
if (inputStream != null) {
int length = inputStream.available();
byte [] buffer = new byte[length];

int readLength;
while ((readLength = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, readLength);
}
fileOutputStream = byteArrayOutputStream;
}

} catch (FileNotFoundException ignored) {
// SharedPreferencesImpl has a canRead() check, so it doesn't log anything in case the file doesn't exist
} catch (IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
if (fileResult != null && fileResult.stream != null) {
try {
fileResult.stream.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}

mLoaded = true;
if (fileOutputStream != null) {
mFileOutputStream = fileOutputStream;
mLastModified = fileResult.mtime;
mFileSize = fileResult.size;
} else {
mFileOutputStream = new ByteArrayOutputStream();
}
notifyAll();
}

/**
* Reload the settings from file if they have changed.
*
* <p><strong>Warning:</strong> With enforcing SELinux, this call might be quite expensive.
*
* @return true if execute reload;
*/

public synchronized boolean reload() {
if (hasFileChanged()) {
startLoadFromDisk();
return true;
}
return false;
}

/**
* Check whether the file has changed since the last time it has been loaded.
*
* <p><strong>Warning:</strong> With enforcing SELinux, this call might be quite expensive.
*/

public synchronized boolean hasFileChanged() {
try {
FileResult result = SELinuxHelper.getAppDataFileService().statFile(mFilename);
return mLastModified != result.mtime || mFileSize != result.size;
} catch (FileNotFoundException ignored) {
// SharedPreferencesImpl doesn't log anything in case the file doesn't exist
return true;
} catch (IOException e) {
Log.w(TAG, "hasFileChanged", e);
return true;
}
}

private void awaitLoadedLocked() {
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}

public String getFileContent() {
synchronized (this) {
awaitLoadedLocked();
return mFileOutputStream.toString();
}
}

public JSONObject getJsonFileContent() throws JSONException {
return new JSONObject(getFileContent());
}
}

判断xposed框架是否生效

    正如上边所说,xposed框架中对文件只有读权限,因此获取信息就变成了单项了,xposed进程只能根据我们的配置文件进行相应的逻辑操作。由于应用进程和xposed进程是不同的进程,如果应用进程要判断xposed框架是否启用,就需要进程间数据通信,如果仅仅只是判断xposed是否启动而去启动一个服务处理数据,感觉有点小题大做了。这里给出一种简单有效的方法。

1.首先我们生成一个配置文件setting.xml
2.定义一个函数

1
2
3
4

public void setXposedStatus(boolean bStatus) {
this.getSharedPreferences("setting", Context.MODE_WORLD_READABLE).edit().putBoolean("xposed_enabled", bStatus).apply();
}

3.应用每次启动时候调用函数setXposedStatus
4.在xposed中hook函数setXposedStatus调用前,并将参数改为true

这样我们要想知道xposed框架是否生效可用,读取setting.xmlxposed_enabled字段值即可。

总结

    xposed框架不仅能够hook任意你想hook的函数,并且自身也有一个很大的特点,就是一旦用户安装并启用,你所有的操作都不需要任何权限就可实现。基于xposed框架的优秀应用也特别多,比如著名的省电应用绿色守护。再如有一些app对敏感数据进行加密,加密算法又很复杂,那么通过hook解密函数就可以轻松的拿到明文。xposed框架的奇妙之处远远不仅于此,国外也有人对其进行二次封装作出了更神奇的用法,例如XClasses,如果你有什么奇淫技巧,我只想对你说四个字,请带上我!