Xposed 框架
Xposed 框架是一款可以在不修改APK的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。
基本原理
Zygote 进程是 Android 的核心,所有的应用程序进程以及系统服务进程都是由Zygote进程 fork 出来的。Xposed Framework 深入到了 Android 核心机制中,通过改造 Zygote 来实现一些很牛逼的功能。Zygote 的启动配置在/init.rc 脚本中,由系统启动的时候开启此进程,对应的执行文件是/system/bin/app_process,这个文件完成类库加载及一些初始化函数调用的工作。
当系统中安装了 Xposed Framework 之后,会拿自己实现的 app_process 覆盖掉 Android 原生提供的文件,使得app_process在启动过程中会加载XposedBridge.jar这个jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持。
更详细的框架介绍和插件开发过程可直接参看官方教程或者已有的一些中文教程
本文的主要来由是思寒在我另外一篇文章中的一句留言:
把xposed单独再发文章吧. 这是个杀手级别的框架. 是测试利器. 只是很多人并不懂其中的奥妙
既然是杀手级的东西,那肯定有不少独到的招式和技能。所以,趁着周末我也思考和整理了一下,基于该框架,测试人员都能做些什么,目前想到的主要有以下几点:
§ 渗透测试
§ 测试数据构造
§ 环境监控
§ 动态埋点
§ 热补丁
§ 自动化录制
下面就针对以上几点,结合例子作些简单的分享(部分原理和过程可能不会做太细致的解释,看不懂的可以留言)。
1、渗透测试
以Testerhome的android客户端认证授权模块为例,这里使用了OAuth 2.0的授权协议,其中有个比较重要的访问令牌access_token。通过看源码我们可以发现,在TesterUser类中有个setAccess_token方法
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
其输入参数即是用户授权后产生的访问令牌,因此我们可以通过以下方法来截取该令牌
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.testerhome.nativeandroid")) return; XposedBridge.log("Loaded app: " + lpparam.packageName); findAndHookMethod("com.testerhome.nativeandroid.models.TesterUser", lpparam.classLoader,"setAccess_token", String.class,new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // this will be called before the clock was updated by the original method XposedBridge.log("Enter->beforeHookedMethod"); XposedBridge.log("original token: " + (String)param.args[0]); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // this will be called after the clock was updated by the original method } }); |
运行后查看日志如下:
[代码]java代码:
1 2 | 12-19 06:19:54.458: I/Xposed(11327): Enter->beforeHookedMethod 12-19 06:19:54.458: I/Xposed(11327): user token: 0a84d0c29a4b576634baacd5097c39b4e36264f440be5b3affba6b1b5b14603e |
获取到令牌后就可以根据交互协议进一步获取用户相关的信息了。
当然,攻击者也可以直接修改该令牌值。
[代码]java代码:
1 | param.args[0] = "b6a8d0b02a651a7759051a5c8b1afa02db35636dd4c20c15dcbf050038d7ae2e"; |
这样用户登录后使用的都是非法的令牌值,也就无法获取合法的资源了。
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 | findAndHookMethod("com.testerhome.nativeandroid.models.TesterUser", lpparam.classLoader,"getAccess_token",new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // this will be called before the clock was updated by the original method XposedBridge.log("Enter->beforeHookedMethod:getAccess_token"); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // this will be called after the clock was updated by the original method XposedBridge.log("Enter->afterHookedMethod:getAccess_token"); XposedBridge.log("hooked token: " + (String)param.getResult()); } }); |
日志打印:
[代码]java代码:
1 2 3 4 5 | 12-19 10:37:43.590: I/Xposed(15821): Enter->beforeHookedMethod:setAccess_token 12-19 10:37:43.590: I/Xposed(15821): original token: 9b7c274a07e7dbcdb99840b0aa3dfb3d9c200972c4c5706750c2922650af36a6 12-19 10:37:43.662: I/Xposed(15821): Enter->beforeHookedMethod:getAccess_token 12-19 10:37:43.662: I/Xposed(15821): Enter->afterHookedMethod:getAccess_token 12-19 10:37:43.662: I/Xposed(15821): hooked token: b6a8d0b02a651a7759051a5c8b1afa02db35636dd4c20c15dcbf050038d7ae2e |
类似的场景还有很多,主要就是通过阅读代码(有源码或者反编译的情况下),找到关键函数以及编码上的一些漏洞,获取关键信息或者篡改方法的出入参,达到攻击和渗透测试的目的。
2、测试数据构造
有时在客户端应用测试的过程中需要构造一些特殊的数据,如位置、网络制式、系统版本、屏幕长宽比、电量等等。其中,有些数据可手动构造,但有部分就完全不行了。此时,Xposed框架也能帮你搞定。
以系统时间为例,我们编写一个Demo应用,通过Calendar类来获取系统的时间:
[代码]java代码:
1 2 3 4 5 6 7 8 | Calendar c = Calendar.getInstance(); int year = c.get(Calendar.YEAR); int month = c.get(Calendar.MONTH); int day = c.get(Calendar.DAY_OF_MONTH); int hour = c.get(Calendar.HOUR_OF_DAY); int minute = c.get(Calendar.MINUTE); String time = ""+year+"-"+month+"-"+day+" "+hour+":"+minute; timeTV.setText(time); |
然后,我们只需要Hook系统Calendar类的get方法,就能构造出自己想要的数据:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 | findAndHookMethod("java.util.Calendar", lpparam.classLoader,"get",int.class,new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // this will be called before the clock was updated by the original method XposedBridge.log("Enter->beforeHookedMethod:Calendar.get"); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // this will be called after the clock was updated by the original method XposedBridge.log("Enter->afterHookedMethod:Calendar.get"); param.setResult((int)11); } }); |
同理,其它类似的数据都能通过Hook系统的方法来构造,甚至连已Root的手机都能伪装成未Root的(我们公司有个手机打卡软件,就可以用Root欺骗和位置伪造的方式在家里打卡,当然我没这么干过,表查我)。
3、环境监控
由于Xposed框架在系统启动的时候就加载完成了,所以其监控能力比我们自己写的后台Service应用要强很多。至于监控的对象,可以是系统的通知、弹窗、Toast信息、用户点击、电量、信号变化这类显式可感知的事件,也可以是内存、CPU、IO此类内部数据,甚至到统一的异常处理方法(如java.lang.Thread.UncaughtExceptionHandler)、底层socket接口、页面渲染方法等等,主要看你需要什么,而非它能做什么。
例子直接使用之前一篇文章通过辅助工具进行安卓 Toast 文本检查的方法中介绍过的Toast信息检查。
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public class XposedHook implements IXposedHookZygoteInit {
@Override public void initZygote(StartupParam startupParam) throws Throwable { //设定hook目标类和方法 XposedHelpers.findAndHookMethod(Toast.class, "show", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { //获取Toast对象 Toast t = (Toast) param.thisObject; try { //获取唯一的TextView,即Toast文本 View view = t.getView(); List<textview> list = new ArrayList<textview>(); if (view instanceof TextView) { list.add((TextView) view); } else if (view instanceof ViewGroup) { finaAllTextView(list, (ViewGroup) view); } if (list.size() != 1) { throw new RuntimeException("number of TextViews in toast is not 1"); } TextView text = list.get(0); //获取文本内容 CharSequence toastMsg = text.getText(); System.out.println("XposedHookToast:"+toastMsg);
} catch (RuntimeException e) { XposedBridge.log(e); } } }); } //获取对象中的所有TextView private void finaAllTextView(List<textview> addTo, ViewGroup view) { int count = view.getChildCount(); for (int i = 0; i < count; ++i) { View child = view.getChildAt(i); if (child instanceof TextView) { addTo.add((TextView) child); } else if (child instanceof ViewGroup) { finaAllTextView(addTo, view); } } } }</textview></textview></textview> |
获取到的Toast信息:
[代码]java代码:
1 2 | Line 5251: I/System.out( 815): XposedHookToast:登录失败,可能原因是用户名或密码错误、密码过期或者帐号锁定 Line 5959: I/System.out( 815): XposedHookToast:连接服务器失败 |
通过这种方式,可以处理自动化脚本运行过程中出现的一些非正常事件,如意外弹窗或者消息栏通知等;也可以用于屏蔽monkey运行时可能点击退出或者注销按钮的情况。只要事先设置好目标事件和处理方式,它就能起到很好的监控作用。
4、动态埋点
如果监控的目的不是环境处理,而是信息获取,那么就演化为了埋点。既然通过Xposed能直接控制一个方法的调用前后阶段,那埋点对于它来说更像是一个天赋技能,根本不用多做修改和适配,就能直接在不动被测APP代码分毫的情况下实现易管理、有策略并且可实时变更得动态埋点。
以TesterHome客户端MainActivity中onCreate方法执行前后的系统剩余内存为例:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | findAndHookMethod("com.testerhome.nativeandroid.views.MainActivity", lpparam.classLoader,"onCreate",Bundle.class,new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // this will be called before the clock was updated by the original method XposedBridge.log("Enter->beforeHookedMethod:onCreate"); Activity app = (Activity) param.thisObject; long availMem =getAvailMemory(app); XposedBridge.log("availMem before onCreate:"+availMem+"KB");
} @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // this will be called after the clock was updated by the original method XposedBridge.log("Enter->afterHookedMethod:onCreate"); Activity app = (Activity) param.thisObject; long availMem =getAvailMemory(app); XposedBridge.log("availMem after onCreate:"+availMem+"KB"); } }); |
获取系统剩余内存的方法:
[代码]java代码:
1 2 3 4 5 6 | public long getAvailMemory(Activity app) { ActivityManager am = (ActivityManager)app.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); am.getMemoryInfo(mi); return mi.availMem >> 10; } |
安装模块重启后运行TesterHome可以看到:
[代码]java代码:
1 2 3 4 | 12-19 23:00:48.442: I/Xposed(4476): Enter->beforeHookedMethod:onCreate 12-19 23:00:48.442: I/Xposed(4476): availMem before onCreate:1901876KB 12-19 23:00:48.478: I/Xposed(4476): Enter->afterHookedMethod:onCreate 12-19 23:00:48.478: I/Xposed(4476): availMem after onCreate:1900760KB |
具体所需的埋点数据和过程可以参考恒温的论客户端埋点
用这种方式埋点有以下几个好处:
1. 无需动APP源码,适配成本低;
2. 方式灵活,有能力介入任何过程,可收集的信息和数据完全;
3. 易于管理,可随时添加启用或删除弃用埋点;
4. 无需开发参与,测试可根据场景自己实现埋点方案;
当然,也有个很大的坑点:
1. 只适合内部测试使用,无法发布给真实用户用于线上监控。
5、热补丁
与动态埋点原理类似,既然我们可以通过添加前后过程来测试一个方法,那么当发现这个方法出现问题时,自然也可以通过动态的添加前后过程来修复该方法,也即热补丁。
目前国内安卓上比较成熟的热补丁方案主要有Dexposed 、 AndFix 、 ClassLoader 三种,前两个都是阿里的,第三个是腾讯的。其中Dexposed方案正是基于Xposed框架,但由于它只对应用自身进程的方法进行Hook,所以不需要root权限。
关于这个,更具体的信息直接看这篇文章好了Alibaba-Dexposed框架在线热补丁修复的使用
6、自动化脚本录制
这个实际上是环境监控的细分能力,既然能监控设备的所有事件,那么如果我们有针对性的对系统交互类接口和事件进行监听,记录用户和设备之间的交互流程和信息,是不是有可能直接在用户操作一遍后把对应的自动化脚本就生成出来呢?
让我们继续看个小例子:
被测应用仍为上文获取时间的Demo,界面上就一个TextView和Button,要做的事就是捕获按钮的点击事件,并解析得到该Button的信息。为保证通用性和一致性,这里要Hook的方法肯定得尽量偏底层,通过看源代码和事件点击分发的相关机制,最终定位到android.view.View类中的performClick方法,这个方法会最终执行点击相关的操作和事件通知。
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 | public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); return true; }
return false; } |
但是它的参数和返回值中都不包含view相关的信息,那么捕获到这个点击事件后进一步要怎么获取和它关联的控件信息呢?起初我也陷入了这样的迷圈中,不断去找有view相关参数或者返回值的方法。但后来转念一想,这个方法本来就在view这个类对象实例中,看了下xposed的api,果然有直接获取这个实例对象的方法。代码如下:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | findAndHookMethod("android.view.View", lpparam.classLoader,"performClick",new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // this will be called before the clock was updated by the original method XposedBridge.log("Enter->beforeHookedMethod:performClick"); } @SuppressLint("NewApi") @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // this will be called after the clock was updated by the original method XposedBridge.log("Enter->afterHookedMethod:performClick"); View node = (View)param.thisObject; XposedBridge.log("NodeInfo:"+node.toString());
} }); |
非常的简洁和顺理成章,然后执行的结果会是怎样呢?安装模块、重启设备、启动Demo后点击一下获取时间的按钮,可以看到如下日志:
[代码]java代码:
1 2 3 | 12-20 02:30:49.958: I/Xposed(7346): Enter->beforeHookedMethod:performClick 12-20 02:30:49.958: I/Xposed(7346): Enter->afterHookedMethod:performClick 12-20 02:30:49.958: I/Xposed(7346): NodeInfo:android.widget.Button{52910148 VFED..C. ...PH... 24,76-168,148 #7f080001 app:id/button1} |
非常强大,我们看到了按钮的id:button1,与应用界面配置中设定的一致
[代码]xml代码:
1 | <button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignleft="@+id/textView1" android:layout_below="@+id/textView1" android:layout_margintop="15dp" android:text="获取时间"></button> |
甚至通过的view的getLocationOnScreen方法,我们还可以得到按钮中心点的坐标:
[代码]java代码:
1 2 | 12-20 02:57:53.672: I/Xposed(8340): NodeInfo X:24 12-20 02:57:53.672: I/Xposed(8340): NodeInfo Y:186 |
类似的其它事件也可以这样捕获并提取关联控件的信息,有了这些数据后我们再按照Appium或者其它框架的API自动形成脚本是不是也就不难了。