Xposed了解以及在Android中的应用

6,483 阅读11分钟

简介

相信做Android开发的人都多多少少听过Xposed这个神器吧,即使没有用过,也多少听过,号称Android上最强大的神器,是由Android大神rovo89开发出来的,下面我简单分析下Xposed以及在Android中的应用,在分析的时候会有相关涉及到的简单原理描述。

首先,xposed是一个框架,上面有很多模块,这些模块都依赖于xposed这个框架,之所以称xposed是第一神器,就是因为这些模块可以完成许多匪夷所思的功能,例如:修改微信的界面,自动抢红包模块,自定义程序的文本,修改地理位置等等其他模块,这些模块通常可以完全在正常情况下难以实现的,比如抢红包,大家都知道,自动抢红包就是监听了微信的收到红包的相关接口,从而修改其中逻辑,达到自动化的效果,可以说效果杠杠的。

Xposed原理

xposed 原理就是修改系统的关键文件,然后当APP调用系统API时,首先经过xposed,而这些基于xposed的模块就可以选择性的在App调用这些api的时候做一些自己想要的事情,或者修改返回的结果,这样app在运行的时候效果就会改变,但app本身并没有被破坏,只是调用系统api的时候,Android系统的表现发生了变化,举个例子,"奥迪"进去,"奥拓"出来,哈哈,这就是Hook,所以,说白了,Xposed就是个强大的钩子框架,实际上Xposed作为Java层的重要Hook手段,在一些模拟的场合发挥了重要的作用,比如需要测试模拟的地理位置,就可以Hook掉相关的接口,返回我们需要的值即可,Xposed的底层原理是通过替换/system/bin/app_precesss 程序控制zygote进程,使得它在系统启动的过程中会加载Xposed framework的一个jar文件即XposedBridge.jar,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持,并且能够允许开发者独立的替代任何class,例如framework本身,系统UI又或者随意的一个app。

我们都知道,zygote进程是Android中所有进程的父进程,是init进程之后的第一个进程,所有的进程都是由zygote进程孵化出来,zygote进程会完成虚拟机的初始化,库的加载,预置类库的加载和初始化等等操作,而以后如果需要开启新的应用程序,zygote进程便会Fork一份出来,从而使应用程序共享了已经加载的资源等等,加快了启动速度,也提高了性能。而Xposed由于劫持了zygote进程,就相当于劫持了所有的应用程序,从而可以实现Hook掉目标程序达到自己想要的目的,听起来是不是很牛逼,实际上确实很牛逼,那就看看实际的效果吧。

安装

首先,使用Xposed需要设备是root的,因为安装Xposed框架需要root权限,使用过程不需要root,当然网上可以找到"免root使用Xposed之类的文章",有兴趣的同学可以去研究,这里需要注意的是:在安装Xposed框架的时候,要选择跟自己的手机版本相符合的,不然很容易失败,甚至设备变砖,特别是第三方深度定制的系统,在应用层开发都一堆兼容性问题,底层的问题可能会更多,因此要注意选择好对应的版本就好了,安装Xposed的过程就不讲了,自己参考,一般能找到对应版本的话,应该问题不难,在这里以Google Nexus6,Android 6.0.1版本为说明,如果你安装之后看到类似下面的,说明已经激活成功了,

那就开始下面的使用吧

使用以及需要注意的问题

● 首先是新建Android工程,然后在libs目录添加jar文件,如图:

然后添加到gradle编译,这里要注意的是:不能是compile files,而必须是provider files() 代码是:

 provided files('libs/XposedBridgeApi-82.jar')

这是第一个需要注意的地方,必须是provided方式导入,如果compile files方式导入的话,会报找不到文件的错误,这里注意就好。

● 编写Hook的类,需要实现IXposedHookLoadPackage,方法为:

public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam)

其中XC_LoadPackage.LoadPackageParam的定义如下

public static final class LoadPackageParam extends Param {
        public ApplicationInfo appInfo;//应用程序的信息
        public ClassLoader classLoader;//类加载器,用来加载需要Hook的类
        public boolean isFirstApplication;
        public String packageName;//包名
        public String processName;//进程名

        LoadPackageParam() {
            throw new RuntimeException("Stub!");
        }
    }

一般情况下会先判断包名,然后再进行下一步的Hook行为,比如:

if (loadPackageParam.packageName.equals("com.example.xposeproject")) {
            Class<?> clazz = loadPackageParam.classLoader.loadClass("com.example.xposeproject.ScrollingActivity");
            XposedHelpers.findAndHookMethod(clazz, "getTest", new Object[]{new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    //方法执行前调用
                    super.beforeHookedMethod(param);
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    //方法执行后调用,这里可以修改为自己需要的结果
                    super.afterHookedMethod(param);
                    Log.d("[app]", "you are hook!");
                    param.setResult("you are Hook");
                }
            }});
        }

我这里以HookScrollingActivity的一个getTest方法为例子,ScrollingActivity的代码如下,会省略掉一些非关键信息:

public class ScrollingActivity extends AppCompatActivity {
    private Handler mHandler=new Handler(Looper.getMainLooper());
    private TextView text;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      //省略一些代码
        text=findViewById(R.id.text);
        text.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                text.setText(text.getText().toString()+":"+getTest());
            }
        });
    }

    private String getTest() {
        Log.d("[app]","没有被劫持");
        return "没有被劫持";
    }
}

text点击的时候改变自身文本,然后我们在afterHookedMethod接口方法里面修改了返回值

param.setResult("you are Hook")

这句代码就是修改返回值,就是修改getTest的返回值为"you are hook"的意思,Hook方法一般用XposedHelpers.findAndHookMethod方法,参数一般有需要Hook的类,方法名,以及参数和回调,

findAndHookMethod(Class<?> clazz, String methodName, 
Object... parameterTypesAndCallback) 

从方法的原型便可以看得出来,clazz是由loadPackageParam的classLoader加载的,而方法名是需要Hook的方法,回调函数有2个,分别是:

beforeHookedMethod(MethodHookParam param)
afterHookedMethod(MethodHookParam param)

分别是方法调用前和方法调用后,MethodHookParam定义如下

public static final class MethodHookParam extends Param {
        public Object[] args = null;//参数
        public Member method;//方法名
        public Object thisObject;//被Hook类的实例,如果是Hook实例方法则不为空
        //如果Hook的是静态方法,根据JVM调用方法的原理,
        //这个参数为null,下面例子会说明

        MethodHookParam() {
            throw new RuntimeException("Stub!");
        }

        public Object getResult() {
            throw new RuntimeException("Stub!");
        }

        public void setResult(Object result) {
            throw new RuntimeException("Stub!");
        }
        ...
    }

我们需要修改的是改变文本,很显然在方法的执行后返回为我们需要的,好了,Hook方法就暂时这样

● 进行Hook文件的配置,我们需要在assets目录下面新建一个xposed_init文件,写上我们Hook文件的路径,如图:

xposed_init写的是文件的路径,这里的路径包括了包名+类名的

● AndroidManifest.xml文件配置Xposed信息,在节点之内配置下面的信息:

        <!-- 说明是xpose模块 -->
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <!-- 模块描述 -->
        <meta-data
            android:name="xposeddescription"
            android:value="Xpose模块描述" />
        <!-- XposedBridgeApi的最低版本号 -->
        <meta-data
            android:name="xposedminversion"
            android:value="54" />

我们先来看看没有Hook之前的界面:点击textview就可

一般情况下Xposed框架会自动检测是否有新的模块,我们已经编写好了模块了,然后在Xposed框架里面勾上我们选择的模块,然后重启即可,重启之后再次点击看看效果吧:

12-31 04:14:42.376 24325-24325/com.example.xposeproject D/[app]:
you are hook!

通过界面和日志,我们可以看到,Hook getText方法成功了,返回了我们需要的值,实际上我们同样可以Hook其他的系统api接口来返回我们需要的值,更多的Hook,大家可以去多实践,下面讲讲Hook第三方的应用

Xposed Hook第三方的应用

通过上面的例子,我们可以看到Xposed不仅可以Hook自身的应用,其实也可以Hook第三方的应用,像那些什么红包插件,自动打卡插件等等都是Hook了第三方的应用的,下面来Hook 另一个应用,首先新建另一个Android工程,并且加入一个自定义的application,代码如下:

public class UserApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("[app]","自定义的Application");
    }
}

其主页面代码如下:

public class MainActivity extends AppCompatActivity {
    private static final int abc = 100;
    private static final int abcd = 200;
    private static final int abcde = 200;
    private static final int abcdef = 200;
    private TextView textView;
    private Handler mHandler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.text);
        textView.setText("这是测试Hook第三方应用的");
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Intent intent=new Intent("com.example.hook");
                sendBroadcast(intent);
            }
        }, 2000);
    }
}

要求是在application内注册一个广播接收器用来接收action为"com.example.hook"的广播,并且通过Xpose修改abc 的值,下面我们来分析一下,我们前面说了XC_LoadPackage.LoadPackageParam里面有个packageName包名参数,我们可以通过这个过滤掉自己想要Hook的应用,在这里的测试包名为:"com.example.xposehooktest",因此,我们也是可以这样做的,由于要求在application注入一个广播接收器,因此我们可以在Hook application的onCreate方法加入一个广播接收器,如下代码:

private UserBrodCast mBroadCast;
 if (loadPackageParam.packageName.equals("com.example.xposehooktest")) {
            //找到application
            Class<?> clazz = loadPackageParam.classLoader.loadClass("com.example.xposehooktest.UserApplication");
            XposedHelpers.findAndHookMethod(clazz, "onCreate", new Object[]{new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                    /**
                     * 这里需要注意的是param.thisObject表示的是获取该类的实例,
                     * 如果Hook的是实例方法,那么param.thisObject表示该类的实例,
                     * 但如果Hook的是静态方法的话,那么param.thisObject是为null的
                     * 因为根据虚拟机执行方法的原理,调用静态方法不需要类的实例
                     */
                    Object object = param.thisObject;
                    Context context = null;
                    if (object instanceof Context) {
                        context = (Context) object;
                    }
                    //在这里可以做一些特定的事情,这里以注册一个广播接收器为例子
                    IntentFilter intentFilter = new IntentFilter("com.example.hook");
                    context.registerReceiver(mBroadCast=new UserBrodCast(), intentFilter);
                }
            }});
}

可以看到我们在方法之后执行了注册广播接收器,接收器代码如下:

private class UserBrodCast extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d("[app]", "这是Hook的动态注册的广播");
        }
    }

当然这里只是打印而已,实际上可以做更多的事情,下面我们来Hook 主页的onCreate方法并且跟反射结合起来修改变量的值,如下代码:

 //这里再以textview改变文本的例子,这里Hook 了Activity的onCreate方法
            final Class<?> clazz2 = loadPackageParam.classLoader.loadClass("com.example.xposehooktest.MainActivity");
            XposedHelpers.findAndHookMethod(clazz2, "onCreate", new Object[]{Bundle.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                            super.beforeHookedMethod(param);
                        }

                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            super.afterHookedMethod(param);
                            Field[] fields = clazz2.getDeclaredFields();
                            for (int i = 0; i < fields.length; i++) {
                                Log.d("[app]", "名称为:" + fields[i].getName());
                            }

                            //尝试修改final static修饰的
                            Field abcField=clazz2.getDeclaredField("abc");
                            abcField.setAccessible(true);
                            XposedHelpers.setStaticIntField(clazz2,"abc",1001);
                            Log.d("[app]","Xpose修改的值为:"+abcField.get(param.thisObject));
                            //获取textview
                            Field field = clazz2.getDeclaredField("textView");
                            field.setAccessible(true);
                            final TextView textView = (TextView) field.get(param.thisObject);
                            Handler mHandler=new Handler(Looper.getMainLooper());
                            mHandler.postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    Log.d("[app]","动态改变文本成功");
                                    textView.setText(textView.getText().toString()+"you are Hook,This is third application");
                                }
                            },2000);
                        }
                    }});

可以看到这里获取了所有的字段,并且反射获取了TextView来动态修改文本,这里面用到了setStaticIntField方法,实际上XposedHelpers里面还有很多类似这样的方法,我这里只是为了举例子而已,实际上原理都是一样的,有同学说了,那动态注册的广播如何销毁呢,我们可以在onTrimMemory方法里面再次Hook,然后销毁啊,代码如下:

XposedHelpers.findAndHookMethod(clazz, "onTrimMemory", new Object[]{int.class,new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                    Object object = param.thisObject;
                    Context context = null;
                    if (object instanceof Context) {
                        context = (Context) object;
                    }
                    context.unregisterReceiver(mBroadCast);
                }
            }});

代码上就这样了,然后重启看看效果吧, 如图textview已经成功更新文本了

打印如下:

可以看到跟预期的相符合,当然了,如果你有兴趣,可以Hook更多的方法,做更多的事情,这里限于篇幅,就介绍到此了。

总结

我们前面主要介绍了Xposed模块开发的基本步骤(5个主要步骤):

● 将XposedBridgeApi.jar导入到目录下,同时Add As Library…加入编译

● 修改build.gradle,将compile改为provided(这个非常重要)

● 在AndroidManifest.xml中的application节点之内添加meta-data元素

● 新建主类,实现IXposedHookLoadPackage接口,重写handleLoadPackage方法 在main目录下新建assets文件夹,然后实现自己的目的,然后在assets中新建xposed_init文件,写入packagename+主类,最后重启即可。

实际上,Xposed作为Java层Hook的重要工具,往往在一些特殊的情况下用到,除了Xposed,Java的反射和动态代理也是实现Hook的手段,反射负责找到对象,而动态代理负责替换对象,在例子中,我们在Xposed中也用到了反射,关于反射,可以参考我的另一篇文章:

Java反射以及在Android中的特殊应用

关于动态代理,可以参考我的另一篇文章:

Java代理以及在Android中的一些简单应用

这篇文章是2017年的最后一篇文章了,最后感谢大家阅读,祝大家2018新年快乐,事业有成!