基于Android平台的抢单辅助(外挂)之攻与防

郭鹏

背景

1.第三方辅助软件,通过自动化操作代替用户手动操作的辅助软件,通过该软件可以实现自动刷新,自动抢单等操作,市面上很多app都有其对应的辅助软件,例如美团众包、蜂鸟,滴滴打车等包括抢单业务的软件尤其泛滥。

2.第三方辅助软件可以设置刷新频率,抢单距离,抢单类型,抢单内容等可筛选参数,已经做到非常的“人性化”,而且可以更加快速,更加便捷地自动化操作,所以有很多用户愿意用它提高抢单的成功率。

3.这种操作破坏了抢单的公平性,使得正常抢单的用户产生抱怨等负面情绪,从而影响平台整体的信誉度,可能造成潜在的用户流失。

辅助实现方式

1.AccessbilityService辅助

2.Xposed,hook相关接口

3.Adb input tab x y 物理位置坐标点击

4.模拟接口,发送请求

5.人工辅助

由于后三种成本太大且效果并不理想,例如xposed方式需要root权限等,会增加用户使用的难度,从而流失用户,所以市面上大多数辅助软件都是基于AccessbilityService辅助方式进行开发,本文主要从此方式出发,进行相关分析。

辅助四个阶段

1.onAccessibilityEvent

2.getRootInActiveWindow

3.findAccessibilityNodeInfosBy(ViewId,Text,Focus)

4.performAction(AccessibilityNodeInfo.ACTION_CLICK)

基于上面四个接口,正好是AccessibilityService辅助方法的四个阶段。

1.其中onAccessibilityEvent是该服务的核心方法,其中参数event封装来自界面相关事件的信息,比如我们可以获得该事件的事件类型,进而根据此类型选择不同的处理方式,当用户界面发生变化时,系统会发送一系列的AccessibilityEvent事件,从而会触发此接口。

2.getRootInActiveWindow接口会获取当前界面焦点的窗口。

3.findAccessibilityNodeInfosBy(ViewId,Text,Focus)接口会在当前窗口中通过匹配viewid,text以及focus获得符合条件的view集合,获得view后可以获得view的相关属性,例如view的内容等。

4.performAction(AccessibilityNodeInfo.ACTION_CLICK)接口可以根据参数执行点击,长按等触发操作,起到模拟点击的作用。

代码示例如下:

    /**
     * 监听系统发送的AccessibilityEvent事件
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        String pkgName = event.getPackageName().toString();
        handlMessage();
        Toast.makeText(this, "外挂来了" + pkgName + eventType, Toast.LENGTH_LONG).show();
    }

    /**
     * 通过获取当前界面的view后,对指定view进行模拟点击操作
     */
    @SuppressLint("NewApi")
    private void handlMessage() {

        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        if (nodeInfo != null) {
          List<AccessibilityNodeInfo> listtext = nodeInfo
                    .findAccessibilityNodeInfosByText("刷新");
            for (AccessibilityNodeInfo n : listtext) {
                click(n);
                Toast.makeText(this, "模拟点击成功" , Toast.LENGTH_LONG).show();
            }
            List<AccessibilityNodeInfo> listviewid = nodeInfo.findAccessibilityNodeInfosByViewId("test.untiaddon:id/viewid");      
        }
    }

    /**
     * 模拟点击操作,点击操作可传递到父view
     */
    public void click(AccessibilityNodeInfo info){
        info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
        AccessibilityNodeInfo parent = info.getParent();
        while(parent != null){
            if(parent.isClickable()){
                parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                break;
            }
            parent = parent.getParent();
        }
    }

反辅助手段

反辅助手段主要是从上面所讲AccessibilityService辅助方法的四个阶段着手进行相关操作,目的是阻止其各自阶段的顺利进行,具体说明如下:

1.onAccessibilityEvent(事件分发接收阶段)
此阶段辅助主要通过过滤包名,来接收指定包名的应用在其用户界面发生变化时,系统发送的一系列AccessibilityEvent事件。反辅助在此阶段无法做出有效的阻止措施,毕竟动态的改变app的包名也不现实。

2.getRootInActiveWindow(获取当前界面焦点窗口阶段)
反辅助在此阶段可以通过增加悬浮窗等方式组织外挂获取相关页面焦点,同时通过设置相关属性使得事件可以传递下去到相关页面。此种方法在物理返回键和home键方面不好控制。经测试,在华为手机上可以home键可以正常返回,返回键无法响应。在vivo和三星手机上home键也无法返回,返回键也无法监听,且通过广播等方式也无法监听到home键操作。

代码示例如下:

    /**
     * 显示辅助(外挂)屏蔽悬浮窗
     *
     * @param context
     */
    public static void showPopupWindow(final Context context) {
        try {
            if (isShown) {
                return;
            }
            isShown = true;
            // 获取WindowManager
            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            mView = setUpView(context);
            final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
            // 类型
            params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
            // WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
            // 设置flag
            int flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
            // 如果设置了WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,弹出的View收不到Back键的事件
            params.flags = flags;
            // 不设置这个弹出框的透明遮罩显示为黑色
            params.format = PixelFormat.TRANSPARENT;
            // FLAG_NOT_TOUCH_MODAL不阻塞事件传递到后面的窗口
            // 设置 FLAG_NOT_FOCUSABLE 悬浮窗口较小时,后面的应用图标由不可长按变为可长按
            // 不设置这个flag的话,home页的划屏会有问题
            params.width = 0;
            params.height = 0;
            params.gravity = Gravity.CENTER;
            mWindowManager.addView(mView, params);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static View setUpView(final Context context) {
        View view = LayoutInflater.from(context).inflate(R.layout.activity_float_window, null);
        // 点击back键可消除
        view.setOnKeyListener(new View.OnKeyListener() {

            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                switch (keyCode) {
                    case KeyEvent.KEYCODE_BACK:

                        Activity currentActivity = (Activity) context;
                        if (currentActivity != null) {
                            currentActivity.finish();
                        }
                        return true;
                    default:
                        return false;
                }
            }
        });
        return view;
    }

    /**
     * 移除辅助(外挂)屏蔽悬浮窗
     *
     */
    public static void removePopwindow() {
        try {
            if (isShown && null != mView) {
             mWindowManager.removeView(mView);
                isShown = false;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3.findAccessibilityNodeInfosByX(获取具体view节点阶段)
反辅助在此阶段能做的事情比较多,具体分为下面两个方面:

3.1.定义组件方式方面,动态生成view组件,不采用xml方式生成view组件;xml中的view节点,通过图片方式进行展示,不通过名称text来展示,不设置view的id,通过getChildView中的index获取;可以考虑使用h5来实现抢单功能,即把相关页面放入webview中进行操作,使得辅助即使获得webview节点,也很难获得其中h5中的相关节点进行操作。
设置组件属性方面,通过对view节点的父view设置setImportantForAccessibility(VIEW.IMPORTANTFORACCESSIBILITYNOHIDE_DESCENDANTS)属性,使得该父view及其所有子view无法通过辅助获取view接单,从而无法对view节点进行模拟点击等自动化操作。

通过以上方式可以阻止辅助通过viewid或者text来获取view节点。

3.2.通过对抢单流程设计,增加抢单确认提示框,加大外挂操作成本,降低外挂操作速度。

4.performAction(模拟点击阶段)
反辅助在此阶段通过重写view的performAccessibilityAction方法或者直接为View设置setAccessibilityDelegate重写其中View.AccessibilityDelegate的performAccessibilityAction方法来阻止模拟点击操作,使得模拟点击操作无法触发onclick等回调。

服务端解决手段: 可根据对获取抢单列表接口以及抢单接口的监控,对固定时间间隔频繁访问的请求做出相应处理,具体确实是否属于辅助触发的抢单需要制定相应的判断策略。

由于无论辅助如何更新变化,最终一定要调用抢单接口才能进行抢单,所以服务端方面可以在本质上解决辅助问题,单本文作者是客户端同学,在此服务端方面不做过多介绍。

检测方式

1.通过应用包名检测指应用是否安装
通过检测网络数据包过滤数据包内容判断指定应用是否安装

2.通过检测辅助软件是否安装,可以得到安装辅助软件的用户,进而进一步处理,起到一定的警示作用。

但是辅助软件通过诱导用户刷机,更改了系统sdk,这样会使得检测安装包是否安装的接口失效,从而达到屏蔽检测的目的。 反辅助在此阶段可以通过自定义接口代替系统接口来完成检测工作,但是自定义接口由于权限等问题无法完全代替系统接口,所以此方案无法保证解决刷机后屏蔽检测问题。

总结

通过以上反辅助方式,已经在实战中取得了效果,但是辅助手段多样化,更新快的事实警示我们,本质上解决此问题,除了客户端做出相应策略阻止辅助生效外,服务端的策略可以从辅助软件无法避开的接口访问根源进行彻底封杀。有兴趣的同学,欢迎发表自己的见解与意见。愿我们一起在反辅助(外挂)的道路上砥砺前行。