简介
相信做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中也用到了反射,关于反射,可以参考我的另一篇文章:
关于动态代理,可以参考我的另一篇文章:
这篇文章是2017年的最后一篇文章了,最后感谢大家阅读,祝大家2018新年快乐,事业有成!