之前安卓注入框架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代码如下:
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代码如下:
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
嵌套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
。
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。具体过程如下:
- hook systemReady函数
- 通过
param.thisObject
获取hook方法所在类的实例, 即NetworkPolicyManagerService.class
- 通过
getObjectField
获取类中的对象mProcessObserver
- hook对象
mProcessObserver
中的onForegroundActivitiesChanged
方法
完整代码如下所示:
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
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
类,其实可以逐渐完善读更多文件类型的内容,包括数据库等。
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.定义一个函数
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.xml
中xposed_enabled
字段值即可。
总结
xposed框架不仅能够hook任意你想hook的函数,并且自身也有一个很大的特点,就是一旦用户安装并启用,你所有的操作都不需要任何权限就可实现。基于xposed框架的优秀应用也特别多,比如著名的省电应用绿色守护
。再如有一些app对敏感数据进行加密,加密算法又很复杂,那么通过hook解密函数就可以轻松的拿到明文。xposed框架的奇妙之处远远不仅于此,国外也有人对其进行二次封装作出了更神奇的用法,例如XClasses,如果你有什么奇淫技巧,我只想对你说四个字,请带上我!