其他测试框架 用黑客思维做测试——神器 Xposed 框架介绍

虚冰丶夜 · 2015年12月20日 · 最后由 佳堃 回复于 2018年07月21日 · 7179 次阅读
本帖已被设为精华帖!

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;
}

其输入参数即是用户授权后产生的访问令牌,因此我们可以通过以下方法来截取该令牌

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
        }
});

运行后查看日志如下:

12-19 06:19:54.458: I/Xposed(11327): Enter->beforeHookedMethod
12-19 06:19:54.458: I/Xposed(11327): user token: 0a84d0c29a4b576634baacd5097c39b4e36264f440be5b3affba6b1b5b14603e

获取到令牌后就可以根据交互协议进一步获取用户相关的信息了。

当然,攻击者也可以直接修改该令牌值。

param.args[0] = "b6a8d0b02a651a7759051a5c8b1afa02db35636dd4c20c15dcbf050038d7ae2e";

这样用户登录后使用的都是非法的令牌值,也就无法获取合法的资源了。

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());
        }
}); 

日志打印:

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 类来获取系统的时间:

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 方法,就能构造出自己想要的数据:

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 信息检查。

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);
        }
    }
}
}

获取到的 Toast 信息:

Line 5251: I/System.out(  815): XposedHookToast:登录失败,可能原因是用户名或密码错误、密码过期或者帐号锁定  
Line 5959: I/System.out(  815): XposedHookToast:连接服务器失败  

通过这种方式,可以处理自动化脚本运行过程中出现的一些非正常事件,如意外弹窗或者消息栏通知等;也可以用于屏蔽 monkey 运行时可能点击退出或者注销按钮的情况。只要事先设置好目标事件和处理方式,它就能起到很好的监控作用。

4、动态埋点

如果监控的目的不是环境处理,而是信息获取,那么就演化为了埋点。既然通过 Xposed 能直接控制一个方法的调用前后阶段,那埋点对于它来说更像是一个天赋技能,根本不用多做修改和适配,就能直接在不动被测 APP 代码分毫的情况下实现易管理、有策略并且可实时变更得动态埋点。

以 TesterHome 客户端 MainActivity 中 onCreate 方法执行前后的系统剩余内存为例:

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");
    }
});

获取系统剩余内存的方法:

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 可以看到:

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 方法,这个方法会最终执行点击相关的操作和事件通知。

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,果然有直接获取这个实例对象的方法。代码如下:

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 后点击一下获取时间的按钮,可以看到如下日志:

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,与应用界面配置中设定的一致

<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="获取时间" />

甚至通过的 view 的 getLocationOnScreen 方法,我们还可以得到按钮中心点的坐标:

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 自动形成脚本是不是也就不难了。

后话

以上就是我这两天思考和实验的结果,当然都还不完善,只是一些初步的想法,后面有时间,我会针对其中部分功能继续做更为深入的研究,希望能摸索出一些可行的方案,到时候再分享给大家。大家也可以放开脑洞,这么强大的工具能做的事情肯定也不止这些!

以上。

TesterHome 首发,转载请声明

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 45 条回复 时间 点赞

这些文章写的很详细, 很赞. xposed 是测试界的一个神器. 希望大家多用用.
基于 xposed 还可以构造异常场景, 获取覆盖率,以及对 app 建模,

我最早接触他是从看雪论坛上发现的.

干货 今晚就啃这个了

是不是需要 root 权限?

#3 楼 @actionwind 这位同学,我已经看到你在 N 个帖子下面问过这个问题了。能不能自己先看下文档,了解之后再来问呢。。。

#4 楼 @monkey 我看这个文档中并没有说明这种测试方法是否需要 root 权限,所以才问的。需不需要 root 权限是我决定要不要学他的前提之一,因为学这些东西是非常花时间和精力的,所以才先问清楚。为什么你会反对这个呢?

#5 楼 @actionwind 文档不止一处:http://repo.xposed.info/module/de.robv.android.xposed.installer。多 google 一下,你的问题就解决了。

#6 楼 @chenhengjie123 果然是需要 root。

腻害,如此神器应该学习起来

#7 楼 @actionwind 安装需要 Root,运行不需要

—— 来自 TesterHome 官方 安卓客户端

好东西,赞一个。

#5 楼 @actionwind 太过功利了. xposed 是一道高手必经的门槛. 就好像编程不会数据结构和算法也不会影响饭碗. 但是没有它会丢失很大一部分的视野. 推荐你多学点东西. 趁年轻好好提升下. 将来带给你的红利会很大.

#11 楼 @seveniruby 呵呵,谢谢你的建议

说实话这东西用在自动化上就好比高射炮打蚊子,声响大作用小。门槛太高了

#13 楼 @dongdong 东哥好比喻,这个就看怎么结合测试场景了

好东西,多谢分享,怒赞……_^

16楼 已删除

学习了 赞

这个框架有什么劣势呢?

#13 楼 @dongdong 确实,实际上我提的主要也不是自动化方面的,比较相关的也就环境监控和脚本录制。不过门槛还是比较高,感觉可以通过一些上层框架模式化部分通用操作,降低上手门槛。

—— 来自 TesterHome 官方 安卓客户端

#18 楼 @doctorq 最直观的就是不太好装。而且目前对 art 支持不是很成熟。

—— 来自 TesterHome 官方 安卓客户端

#20 楼 @xubin98246 @doctorq github 上有个项目叫做 EagleEye. 在这个基础上做了 anndroid ndk 和 sdk 的 hook 封装, 也是巨复杂. 类似的其他框架还有 frida, 可以通杀所有平台. 不过缺点更多. BAT 在 xposed 基础上做封装的比较多. 对 ART 的支持 xposed 官方还在改造中

恩,用这个的确是新的测试视野 赞 1 个,Hook 神器,需 root 的。我目前研究不深,等空闲点在看。

#13 楼 @dongdong 嗯,学一些主流的测试工具性价比更高一些。当然有时间和精力多研究些深点的也挺好

#20 楼 @xubin98246 楼主你好,按照你的方法大概已实现了弹出按钮的监控,但如何让他不去点确定按钮呢? 可以结合 uiautomator 来做吗?

#18 楼 @doctorq @seveniruby 今天在和自动化结合着用的时候,又发现个蛮大的坑,它没法和 instrumentation 共存。看了下源码,如果 hook 对象进程是 instrumentation 启动的,它就直接退了:

if (instrumentationName != null) {
    XposedBridge.log("Instrumentation detected, disabling framework for " + reportedPackageName);
    disableHooks = true;
    return;
}

#24 楼 @xiaoluosun 不是太清楚你的具体需求,监控弹出的按钮,又不去点击?

dexposed 不需要 root,之前想做一个 apk 来 hook 正在测试的应用有没有 crash,但是总报一些异常,就没有深入研究下去了

#26 楼 @xubin98246 就是你在文中提到的,monkey 会不小心点到退出按钮。这时可以监控到按钮弹出,但怎么才能不让它去点确定键呢?

#28 楼 @xiaoluosun 你可以直接禁用弹出这个对话框的方法,或者禁用确定按钮的监听事件 ,在 beforeHookedMethod 中直接调用 param.setResult(null);

#29 楼 @xubin98246 还可以这样,,6 啊。多谢,多谢。

您好,想跟您请教个问题,安装 framework 时遇到的问题:手机 (huawei P6 android4.2.2) 已经 root 了,但是在挂载/system 为可写入时,报错 mount:Operation not permitted 无法将/system 可写入挂载。然后我手动在命令行执行 mount -o remount,rw /system,都没有任何提示信息的。

#31 楼 @echo77 由于华为手机的主题和 Xposed 框架有冲突,所以 EMUI 的 Xposed 框架安装包都是独立定制的,你可以试试这个

#32 楼 @xubin98246 还有个问题想请教一下,XposedInstaller 安装 xposed 框架的时候,都是通过 su 命令来获取 root 权限,但是我的华为手机里面并没有 su 命令啊,难道是我 root 的方式不对?

#34 楼 @echo77 root 不完全,用 superuser 更新下二进制文件

#35 楼 @xubin98246 不仅是 root 不完全,由于华为手机的一些特殊设置,还需要安装 busybox,同时 rm -f /system/set_immutable.list 删除这个文件,之后才能成功安装 Xposed 框架。多谢指点啦

Xposed,现在都转移动端测试了?传统 WEB 好少,,,

很强大,用不用主要看测试场景。

好强大,感觉测试可以做的越来越多了~~

你好,我是测试媛的山地(测试媛群号:418093807),我们最近想做一个线下的交流活动,不知道 i 感兴趣不?我 qq:461785243,感兴趣的话可以交流下测试技术

很好,这才是 xposed 的正道。

为什么我安装 xposed 后,手机就一直处于重启状态,开都开不了?

大神你好,我这边在执行 hook 的方法的时候添加了一个通过包名过滤,但是没有写死,通过解析 sd 卡的一个配置文件来确定我想过滤的包名。然后通过变更这个配置文件来更换我想过滤的包名。然后在解析这个文件的时候,大多数是好的,但是有一些就提示 fileNotFound permission denied。但是我能确定的是我想 hook 的这个 apk 他是有动态权限申请的,并且也经过授权了。不知道是什么问题,代码是一样的,但是结果不一样,用的是华为的机器,有没有可能是 xposed 兼容性的问题,还是其他什么地方?跪求!

@xubin98246 求大神指点。

万一 回复

已经很久没有使用这个框架了,所以你的问题我没法直接回答。不过,有几个问题定位思路你可以参考下:1、换个模拟器试试,相对而言模拟器的权限开放更加彻底;2、单独将 hook 失败的 apk 拿出来验证下,不用配置文件的方式;3、确认下 hook 失败的 apk 权限设置和其它 apk 有什么区别。

@ywhyw 应该是系统级应用没有权限访问 sd 卡下的配置文件,你可以试试把配置文件放在/system 分区下

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册