2015-11-28

安卓注入框架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代码如下:


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

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。具体过程如下:

  1. hook systemReady函数
  2. 通过param.thisObject获取hook方法所在类的实例, 即NetworkPolicyManagerService.class
  3. 通过getObjectField获取类中的对象mProcessObserver
  4. 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

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.xmlxposed_enabled字段值即可。

总结

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

没有评论:

发表评论

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...