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 首发,转载请声明


↙↙↙阅读原文可查看相关链接,并与作者交流