Xposed 入门与模块示例 - 电量伪装

Xposed - 一个 Hook 框架,相比经常在看雪里见到的 libinject 这样的动态注入方式,我们可以叫他为静态注入框架,也可以理解成劫持框架(因为它替换了 app_process)。本文将 Xposed 作为基础功能,讲解基础的使用,并拿一个很有意思的示例作为演示(官网的那个太low了)。

1. Xposed 基本原理

如果用"不要脸"来形容动态注入的话,Xposed 可以称为"显摆着不要脸"。我们知道,Zygote 是所有 Android 应用程序的始祖,所有应用程序的进程都是通过请求 Zygote fork 出来的。Zygote 的启动是由 init 进程通过 init.rc 脚本启动的(可以参考《浅谈 SystemServer》),即通过执行 /system/bin/app_process 启动。

但 Xposed 在安装后会把这个可执行文件替换掉,原本作为 Zygote 进程,要启动的入口是 com.android.internal.os.ZygoteInit.main(),替换后,如果 Xposed 可用则被换为了 de.robv.android.xposed.XposedBridge.main(),在这里完成各个模块的初始化。

加载各个模块时,其实就是对 Hook 点的初始化,这里所谓的 Hook 点就是要 Hook 的函数,也是 Xposed 的价值体现。Xposed 会将 Hook 点由 java 函数变为 native 函数,并将其注册到它自己的 C++ 实现函数上,这样每次在执行 Hook 点时,都会执行到它的 C++ 函数中,然后再调回 java 层的真实实现,这个过程看起来跟动态代理模式一样一样的。

原理点到为止,下面进入正题。

2. Xposed 的基础使用

2.1 使用 IXposedHookLoadPackage

当然首先,你需要在已经 Root 了的 Android 系统版本小于 5.0 的手机上装一个 Xposed Installer 客户端,附个官网下载网站连接,然后打开安装框架,如果能成功说明你的手机支持 ~ ~ 不然请换手机或点右上角。

在这里使用 Eclipse 作为开发工具,新建工程(包名为 com.zero.xposed)后,修改 AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zero.xposed"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="15" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="Easy example which makes the status bar clock red and adds a smiley" />
        <meta-data
            android:name="xposedminversion"
            android:value="30" />
    </application>

</manifest>

这是个最简单的没有界面的 Xposed 模块(作为入门够用),xposedmodule 指示模块可用,xposeddescription 表示在客户端的模块列表里显示的描述,xposedminversion 代表最低支持的 Xposed 版本。

那么,现在要写 Hook 点了,总得有 SDK 吧,也就是 XposedBridgeApi-<version>.jar,其中 <version> 表示版本号,再提供下下载网址连接
引入工程后(顺道一提,不要放在 libs 下,在 build-path 中直接 addJars,不然在模块加载时会报 IllegalAccessError),我们在 com.zero.xposed 包下新建一个类 HelloXposedWorld.java:

package com.zero.xposed;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class HelloXposedWorld implements IXposedHookLoadPackage {
    @Override
    public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;
        XposedBridge.log("Hello SystemUI, Loaded app: " + lpparam.packageName);        
    }
}

该类我们实现 IXposedHookLoadPackage 接口,目的是为了在每次一个新的应用程序启动时都会被执行到一次且只一次我们的代码。在这里,我们过滤了一下包名,如果当前是在 systemui,我们就简单的打印了一条日志。

现在还差一步,就是还没有指定 HelloXposedWorld 这个类可以用,我们在 assets 目录下创建一个文件,名为 xposed_init,里面只有一行:


com.zero.xposed.HelloXposedWorld

指定我们的类名,大功告成,调试安装后,到客户端模块列表中可以看到我们自己的模块,启用后重启手机,查看log:

飞信截图20150411012618.png

看到这行日志那么说明成功了。

2.2 使用 findAndHookMethod

下面我们要指定 Hook 点了,即要用到 XposedHelpers.findAndHookMethod 函数:

public static XC_MethodHook.Unhook findAndHookMethod(
        String className, // 函数所在类名
        ClassLoader classLoader, // 指定 Classloader
        String  methodName,  // 方法名
        Object... parameterTypesAndCallback) { // 参数类型列表和Hook回调
    return findAndHookMethod(findClass(className, classLoader), methodName, parameterTypesAndCallback);
}

参数的意思已经标注到定义中,参数类型列表是不定参,按顺序传入参数的类型,最后一个参数一定是回调函数,用于在Hook点被执行时的回调:

new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param)
                        throws Throwable {
        //TODO Hook点执行前的工作
    }

    @Override
    protected void afterHookedMethod(MethodHookParam param)
                        throws Throwable {
        //TODO Hook点执行后的工作
    }
}

相信非常好懂,比如官网的例子,


findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", 
    new XC_MethodHook() {
        @Override
        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
            TextView tv = (TextView) param.thisObject;
            String text = tv.getText().toString();
            tv.setText(text + " :)");
            tv.setTextColor(Color.RED);
        }
});

这样就 Hook 了 com.android.systemui.statusbar.policy.Clock 类的 updateClock 方法,在实际的 updateClock 执行之后会执行指定的 afterHookedMethod 函数体,当然这个 Hook 点只有在 systemui 中有效。

3. 电量伪装

3.1 基本需求

其实写本文初衷既不是为了分析原理(明显没说透),也不是为了演示使用说明(明显没说全),其实就是为了演示电量伪装这个例子(当然也为了让大家快速入门),它的基本使用场景和使用方法就是:

场景重现:几个小伙伴出去玩耍,要一起去高档餐厅吃饭或者去看电影(最近速度与激情7要上映,大家一定要去看,嗯嗯),那么到了团购的时间了,团购这种事向来都是一个人一次性团了,其他人自觉不自觉(还钱)那就另说了,反正我是没脸去要钱的~~ 在外团购自然是用手机(总不能去网吧 - -),这时候手机没电的童鞋们大可以说:“我手机没电了,你们团一下吧,回头我把钱再给你。”

使用方法,出去玩之前请装上 Xposed 和 电量伪装模块,到了团购时,拿出手机,拿右上角带着叹号的空空的电池标识晃瞎他们的眼,咳咳~~

3.2 Hook 点

有关显示电量信息的,我们一下就能想到几个地方了:

  1. 设置 - 电池
  2. status bar 右上角
  3. 第三方电池信息读取软件(如360省电王)

如果我们只在 systemui 中 Hook statusbar 更新内容的函数,然后修改其中的内容,那样覆盖面就太窄了,如果能一劳永逸,一个模块功能应用于所有应用程序,那就基本全部覆盖了。

我们知道,当我们在做类似省电王的功能时,读取电池的变化靠的是监听 Intent.ACTION_BATTERY_CHANGED 广播,如下面这样的 BroadcastReceiver:

class BatteryBroadcastReceiver extends BroadcastReceiver{
 
    @Override
    public void onReceive(Context context, Intent intent) {
     
        if(intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)){
            int level = intent.getIntExtra("level", 0); // 电量级别
            switch (intent.getIntExtra("status", BatteryManager.BATTERY_STATUS_UNKNOWN)) {
            case BatteryManager.BATTERY_STATUS_CHARGING:
                // 充电状态
                break;
            case BatteryManager.BATTERY_STATUS_DISCHARGING:
                // 放电状态
                break;
            case BatteryManager.BATTERY_STATUS_NOT_CHARGING:
                // 未充电
                break;
            case BatteryManager.BATTERY_STATUS_FULL:
                // 充满电
                break;
            case BatteryManager.BATTERY_STATUS_UNKNOWN:
                // 未知道状态
                break;
            }
        }
    }
}

第三方应用,包括 systemui 也都是这么做的。
是不是懂了该怎么做了,Hook 点就是 Intent 类的这个函数:

public int getIntExtra(String name, int defaultValue)

3.3 代码实现

开始着手,我们继续在第二节的工程上修改:

package com.zero.xposed;

import android.content.Intent;
import android.os.BatteryManager;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class HelloXposedWorld implements IXposedHookLoadPackage {

    @Override
    public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
        XposedHelpers.findAndHookMethod(
                "android.content.Intent", 
                lpparam.classLoader,
                "getIntExtra", 
                String.class, 
                int.class, 
                new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param)
                            throws Throwable {
                        Intent intent = (Intent) param.thisObject;
                        final String action = intent.getAction();
                        if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
                            if (BatteryManager.EXTRA_LEVEL.equals(param.args[0] + "")) {
                                param.setResult(1);
                            } else if ("status".equals(param.args[0] + "")) {
                                XposedBridge.log("status");
                                param.setResult(BatteryManager.BATTERY_STATUS_NOT_CHARGING);
                            }
                        }
                    }

                    @Override
                    protected void afterHookedMethod(MethodHookParam param)
                        throws Throwable {
                    }
                }
         );
    }
}

简单来讲就是当进程有执行 Intent 的 getIntExtra 方法时,判断该 Intent 的 ACTION 是否是 Intent.ACTION_BATTERY_CHANGED,如果是,再判断如果第一个参数是BatteryManager.EXTRA_LEVEL,则说明是在取电量级别,强制把返回结果置为1,第一个参数如果是"status",则说明是在取电池状态,强制返回未充电,且该 Hook 点不限进程。

3.3 效果

那么编译运行,重启手机后看看效果吧~~~

3.3.1 充电级别显示

statusbar 显示如预期:
7e9f61c43251c0a7576421f251585e14.jpg

通知栏中金山电池医生和 360 省电王显示如预期:
飞信截图20150411021536.png

包括连接PC时桌面悬浮窗显示(豌豆荚):
飞信截图20150411021632.png

3.3.2 充电状态显示

通知栏显示如 3.3.1 节所示,360省电王显示如预期,金山电池医生失败。
360省电王界面,如预期:
飞信截图20150411023304.png

360 超级Root,如预期:
飞信截图20150411023431.png

PC 桌面悬浮窗(豌豆荚),见3.3.1节的图,如预期。

金山电池医生,失败:
飞信截图20150411023622.png

着实没想到,金山判断电池状态的方法居然不是靠 Intent.ACTION_BATTERY_CHANGED!?大概有些别的判断条件在里面了,在这里就不再深究了。

4. 总结

本文对 Xposed 原理做了基本的陈述,对基本的入门使用做了介绍,最后引入一个很有意思的需求的设计实现 - 电量伪装,满意了咩 ~ @辰哥。

标签: none

已有 4 条评论

  1. MissR MissR

    原来只知道用,不知道真么写,楼主这篇算是入门了,很清晰的。以后就关注了~~另外,这签名我第一眼看上去凭借着蹩脚的外语想到的是:逗比,逗比就是屌丝,很有意思哈哈哈

  2. liang liang

    楼主我想知道如何hook手机通讯录呢,然后返回一个空的列表,希望可以给与帮助。。

  3. djgzhiyong djgzhiyong

    鄙视你,哈哈o(^▽^)o

  4. vh vh

    希望可以出个伪装手机正在充电的模块

添加新评论