前言
Xposed这位老兄大家可能不认识,微信自动抢红包大家听过吧、微信记录器作弊大家听过吧、地理位置模拟大家听过吧,我很负责任的告诉大家,这些都是Xposed干的,对的,就是它,相信大家充着“谁抢我红包”的愤怒,也想结识下这个牛逼的人物吧,草民在此(淫荡一笑),我们开始吧。
Xposed 介绍
大名鼎鼎得Xposed,是Android平台上最负盛名的一个框架,百度百科介绍是:“Xposed框架是一款可以在不修改APK的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作”,在这个框架下,我们可以加载很多插件App,这些插件App可以直接或间接劫持、篡改、伪造一些信息。有了Xposed后,理论上我们的插件APP可以hook到系统任意一个Java进程zygote、systemserver、systemui。
Xposed 原理
Zygote 进程简析及与Xposed关系
在Android系统中,应用程序进程以及系统服务进程SystemServer都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的,Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使得每一个应用程序进程都有一个独立的Dalvik虚拟机实例,这也是Xposed选择替换app_process的原因。
Zygote进程在启动的过程中,除了会创建一个Dalvik虚拟机实例之外,还会注册一些Android核心类的JNI方法到Dalvik虚拟机实例中去,以及将Java运行时库加载到进程中来。而一个应用程序进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库,这也就是可以将XposedBridge这个jar包加载到每一个Android应用程序中的原因,想更多了解Zygote 进程可以去看下老罗的文章Android系统进程Zygote启动过程的源代码分析。
Hook/Replace 简析
Xposed 框架中真正起作用的是对方法的hook。在Repackage技术中,如果要对APK做修改,则需要修改Smali代码中的指令,而另一种动态修改指令的技术需要在程序运行时基于匹配搜索来替换smali代码,但因为方法声明的多样性与复杂性,这种方法也比较复杂。
在Android系统启动的时候,zygote进程加载XposedBridge将所有需要替换的Method通过JNI方法hookMethodNative指向Native方法
xposedCallHandler,xposedCallHandler在转入handleHookedMethod这个Java方法执行用户规定的Hook Func。
Xposed框架的原理是通过替换/system/bin/app_process程序控制zygote进程,使得app_process在启动过程中会加载XposedBridge.jar这个jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持。
与采取传统的Inhook方式详见Dynamic Dalvik Instrumentation这篇文章 相比,Xposed在开机的时候完成对所有的Hook Function的劫持,在原Function执行的前后加上自定义代码,由于是通过安装基于Xposed框架的App来修改系统,所以风险会比直接修改系统文件来得少,有一定风险,如变砖、无限重启等,需谨慎!
工程组成
XposedInstaller
这是Xposed的插件管理和功能控制APP,也就是说Xposed整体管控功能就是由这个APP来完成的,它包括启用Xposed插件功能,下载和启用指定插件APP,还可以禁用Xposed插件功能等。注意,这个app要正常无误得运行必须能拿到root权限。
Xposed
这个项目属于Xposed框架,其实它就是单独搞了一套xposed版的zygote。这个zygote会替换系统原生的zygote。所以,它需要由XposedInstaller在root之后放到/system/bin下。
XposedBridge
这个项目也是Xposed框架,它属于Xposed框架的Java部分,编译出来是一个XposedBridge.jar包。
XposedTools
Xposed和XposedBridge编译依赖于Android源码,而且还有一些定制化的东西。所以XposedTools就是用来帮助我们编译Xposed和XposedBridge的。
使用示例
环境搭建
一、下载、安装XposedInstaller
安装后如图所示:
XposedInstaller.apk XposedInstaller.apk...
二、gradle 配置
dependencies {
// ** 省略部分代码
provided 'de.robv.android.xposed:api:82'
//如果需要引入文档,方便查看的话
provided 'de.robv.android.xposed:api:82:sources'
}
三、AndroidManifest 配置
在application标签下添加配置:
<?xml version="1.0" encoding="utf-8"?>
<manifest xxx
<application xxx>
<!-- 1、标识自己是否为一个Xposed模块 -->
<meta-data
android:name="xposedmodule"
android:value="true"/>
<!-- 2、Xposed模块的描述信息 -->
<meta-data
android:name="xposeddescription"
android:value="a sample for xposed"/>
<!-- 3、支持Xposed框架的最低版本 -->
<meta-data
android:name="xposedminversion"
android:value="53"/>
</application>
</manifest>
至此,准备工作都已完毕,下面可以扩展xposed模块了~
模块扩展
这里我简单模拟一下获取imei号的劫持篡改。
一、activity 代码示例:
TextView tvImei = (TextView) findViewById(R.id.tv_imei);
TelephonyManager telephonyManager = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);
String imei = telephonyManager.getDeviceId();
tvImei.setText("imei:" + imei);
二、TelephonyHooks
通过源代码跟踪,我们发现telephonyManager.getDeviceId()
在android.telephony.TelephonyManager包下,所以我们需要劫持此方法。
public class TelephonyHooks implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
// 可以排除非当前包名
if (!lpparam.packageName.equals("com.walid.xposedlocation")) {
return;
}
XC_MethodHook hook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("beforeHookedMethod");
}
@Override
protected void afterHookedMethod(MethodHookParam methodHookParam) throws Throwable {
methodHookParam.setResult("walid");
XposedBridge.log("Hook device id is successful!!! ");
}
};
findAndHookMethod("android.telephony.TelephonyManager", lpparam.classLoader, "getDeviceId", hook);
}
}
三、在assets目录下创建xposed_init文件并添加TelephonyHooks文件所在完整目录
例如我的是:
com.walid.xposedlocation.TelephonyHooks
四、运行项目
运行结果如下:
劫持篡改前运行结果此刻有的同学要吐槽了:
问:说您老忙活半天,这不还是没篡改成功吗,结果还是真正的imei号呀!是在逗我呢吗?
答: 由于xposed框架从根上Hook了Android Java虚拟机,所以它需要root,且每次为它启用新插件APP都需要重新启动才能生效,这点草民也有些想吐槽,不过这个权衡之后草民还是接受了,毕竟人无完人,何况程序呢。
五、开启xposed模块并软重启
1、选中模块
模块列表2、选择重启
重新启动3、运行结果
运行结果长叹一口气,我们终于看到劫持篡改后的结果了~
结语
这篇简短的教程,草民并没有像其他人一样给予大家demo地址之类的,因为草民并不想让大家直接下载demo,run一下就可以了,还是希望大家多动手,同时如果有不清楚的同学,随时联系草民,同时这样的hook技术可以应用到任何场合,包括反编译、分析竞品、植入广告、抢红包等等,还是希望将技术应用到该用的地方,而不是应用在非法或者违法常规的事情上面,忘谨记!!!
网友评论
public class MyHooo implements IXposedHookLoadPackage {
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
final String packageName = lpparam.packageName;
if (!packageName.equals("com.hao.hhoapp")) {
return;
}
XC_MethodHook hook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("beforeHookedMethod");
}
@Override
protected void afterHookedMethod(MethodHookParam methodHookParam) throws Throwable {
methodHookParam.setResult(true);
XposedBridge.log("Hook device id is successful!!! ");
}
};
findAndHookMethod("android.webkit.WebView", lpparam.classLoader, "setWebContentsDebuggingEnabled", hook);
}
}
但是还是不能用Chrome调试webview
if (!lpparam.packageName.equals("com.walid.xposedlocation")) {
return;
}
请问增加了包名过滤之后, 其他app调用getDeviceId(), 是返回系统原值吗?
public class AndroidIdHook implements IXposedHookLoadPackage {
//带参数的方法拦截
private void hookMethods(String className, String methodName, XC_MethodHook xmh) {
try {
Class<?> clazz = Class.forName(className);
for (Method method : clazz.getDeclaredMethods())
if (method.getName().equals(methodName) && !Modifier.isAbstract(method.getModifiers()) && Modifier.isPublic(method.getModifiers())) {
XposedBridge.hookMethod(method, xmh);
}
} catch (Exception e) {
XposedBridge.log(e);
}
}
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
/**
* android.provider.Settings.Secure类的getString方法
* 其参数有2个:ContentResolver resolver, String name
*/
hookMethods("android.provider.Settings.Secure", "getString", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
if (param.args.length == 2 && (param.args[0] instanceof ContentResolver) && (param.args[1] instanceof String)) {
param.setResult("androidId");
}
}
});
}
}