聊聊Android自动化操作工具的检测

这篇文章写了有段时间了,但出于一些原因一直没有发。最近一段时间出现了很多利用自动化工具刷资讯类App的现象(主要是现在看新闻/视频给钱的App太多了),作案的工具也从按键精灵类的脚本到了辅助服务App,于是把这篇文章修改了一下发了出来。

写这篇文章的原因是上半年时货XX向我们反馈说,有人在卖一种设备,这种设备可以帮司机抢单,据说一个设备卖680,每个月还要300的月卡,现在市面上最少有2000台这种设备,一个月净挣60W。他们希望我们能帮忙把这种作弊的司机检测出来。

我听说之后,不由感叹灰黑产真的很赚钱啊。

在Android上想要实现自动化操作的话,要么是利用 adb shell input ,要么是利用按键精灵类脚本(通过input或者输入法),要么利用UiAutomator、UiAutomator2、辅助服务。这种抢单的工具,需要监控有可抢的订单的出现,必然是利用的后者。

恰巧,我之前看过辅助服务和UiAutomator的相关源码也发了一篇 AccessibilityEvent相关的文章 ,虽然后面一些部分的文章坑掉了,但对整个流程和实现的原理有一定了解的。借着这次机会,我把之前画的一些图、做的一些工作进行一个总结分享给大家。

场景

在货XX的场景中,当有订单出现时,订单会出现在司机端的App中,正常情况下司机们需要拼网速、拼手速去抢单。使用了外挂设备后,出现订单后会以迅雷不及掩耳之势自动抢单,类似抢红包一样。我们拿到了一个外挂设备,那是一个类似充电宝一样的东西,根据使用教程,我们需要开启手机的调试模式,通过usb插上外挂,等外挂执行后可以断掉外挂并关闭调试模式,很明显,这是利用UiAutomator做的,外挂设备类似一台小PC,接上手机后会推送一个jar包到手机中并启动UiAutomator。

而最近出现的这些刷资讯类App的工具,一类是基于按键精灵的脚本,在此不多说,还有一类是基于辅助服务的。这些工具会帮你轮流启动它支持的各种资讯类App,然后模拟人的操作点击文章,从而获得平台给予的奖励。

原理

这里不对辅助服务和UiAutomator的使用进行介绍了,网上关于使用的文章一大把,这里从原理层进行一下分析。为什么要放在一起说呢?因为辅助服务和UiAutomator看似不一样,实际上有着千丝万缕的联系。

辅助服务

大家对辅助服务的了解应该是来自于微信自动抢红包,第三方应用商店的自动安装也是利用这个做的。

如果之前完全没有接触过辅助服务,建议还是先移步我上文提到的之前写的那篇文章或者其他入门文章,虽然我那篇文章只是一部分,但也能让你有个大概的了解。

简单来说,一旦系统中有一个拥有辅助服务权限的App被开启了,那么在其他App发生特定行为时(如点击、窗口变化、文字变化等),该App会向 AccessibilityServiceManager 发送一个 AccessibilityEventAccessibilityServiceManager 会查询是否有拥有辅助服务权限的App关心这个事件,如果有,那么就把这个事件分发给对应的App。

那么怎么模拟用户进行点击、滑动等操作呢?关于这部分的文章后来被我坑掉了,这里大概说一下。App中的View都对应一个 AccessibilityNodeInfo ,如果需要进行点击等操作,辅助服务App会通过API找到对应的 AccessibilityNodeInfo ,然后执行其 performAction 方法。执行操作的命令并不是直接发送到被控制的App,而是经过 AccessibilityServiceManager 去分发的。

总体来说就是, AccessibilityServiceManager 把辅助服务App和被控制的App联系起来了。很久以前画过关键类的关系图,可以参考一下。其中红色部分代表辅助服务App,橙色部分代表 AccessibilityServiceManager ,绿色部分代表被控制的App。

UiAutomator

UiAutomator本意是用于自动化测试,但现在用它来搞事的也不少。我们直接来看它的一些核心类。

关键的地方在右下角(“AMS”指 AccessibilityServiceManager ,而非 ActivityServiceManager ),UiAutomator会向 AccessibilityServiceManager 注册,可以理解为UiAutomator就是一个辅助服务App,它会接收任何App的任何 AccessibilityEvent 。我们看一下它向 AccessibilityServiceManager 注册的源码,如果你之前有开发过辅助服务的App,你就会发现注册时很多设置项与开发辅助服务App时的设置项是一样的。

private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client,
           int flags) {
      IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
               ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
       final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
       // 这里设置了接收所有类型的事件
       info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
       info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
       info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
               | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
               | AccessibilityServiceInfo.FLAG_FORCE_DIRECT_BOOT_AWARE;
       info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT
               | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION
               | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY
               | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS);
       try {
           // Calling out with a lock held is fine since if the system
           // process is gone the client calling in will be killed.
           manager.registerUiTestAutomationService(mToken, client, info, flags);
           mClient = client;
       } catch (RemoteException re) {
           throw new IllegalStateException("Error while registering UiTestAutomationService.", re);
       }
   }

所以说,辅助服务与UiAutomator有千丝万缕的联系,它们都利用 AccessibilityServiceManager 来与被控制的App联系,包括接收被控制App发出的 AccessibilityEvent 和查找被控制App的 AccessibilityNodeInfo ,但进行点击等操作时二者是有区别的,UiAutomator直接利用了 InputManager 来进行操作。

检测方案

我们先来看UiAutomator如何检测。有一个很简单的办法是如果启动了UiAutomator,那么执行 ps 命令会有一个叫 uiautomator 的进程出现,但我们知道Android对这块限制越来越严了,在高版本的Android上,通过 ps 命令可能拿不到其他进程,所以这不是一个好办法。

$ ps -A | grep uiautomator
shell  19937 884 4095976  44436 futex_wait_queue_me 7d86d3d3b0 S uiautomator

所以我想了另一个办法,因为之前看过相关源码,所以我知道使用UiAutomator时有一个重要特点:同一时间只能启动一个UiAutomator,且一旦启动了UiAutomator,会把当前已经开启的其他辅助服务App停掉。看一下我截取的源码来证实我的说法。

// 同一时间只允许启动一个UiAutomator
if (userState.mUiAutomationService != null) {
    throw new IllegalStateException("UiAutomationService " + serviceClient
            + "already registered!");
}

…………

// 从UiAutomator注册的源码可以知道它没有设置FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES这个flag
// 也就是说默认是会“SUPPRESS_ACCESSIBILITY_SERVICES”的。
if ((flags & UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES) == 0) {
    // Set the temporary state, and use it instead of settings
    userState.mIsTouchExplorationEnabled = false;
    userState.mIsDisplayMagnificationEnabled = false;
    userState.mIsNavBarMagnificationEnabled = false;
    userState.mIsAutoclickEnabled = false;
    
    // mEnabledServices里存放的是当前已开启的辅助服务App,这里把它清空了。
    userState.mEnabledServices.clear();
}

所以我给出的检测方案是: 当前系统辅助服务的状态为开启状态,但是开启了的Service列表却是空的时,表明此时有UiAutomator进程在运行

辅助服务开关状态及当前开启的Service列表可以通过系统API获取到。类似下面的代码。

AccessibilityManager am = (AccessibilityManager) getSystemService(
        Context.ACCESSIBILITY_SERVICE);

// 这个标志表示当前系统是否有辅助服务App被开启
boolean enabled = am.isEnabled();
// 这个列表即当前开启了的辅助服务App
List<AccessibilityServiceInfo> lists = am.getEnabledAccessibilityServiceList(
    AccessibilityServiceInfo.FEEDBACK_ALL_MASK);

那么用辅助服务来搞事的怎么防呢?上面我们可以拿到已开启的辅助服务App列表,我们可以考虑在这里做文章,把那些应用商店、抢红包等App排除掉,那些没见过的App很可能是来搞事的App。这里点名批评一下kingroot,kingroot会自己偷偷把自己的辅助服务权限开启(因为它有root权限,可以为所欲为),但它为什么要开启自己的这个权限?它又利用这个在后面偷偷干了什么?我就不胡乱猜测了。

当然,用这种方式来检测通过辅助服务搞事的工具工作量很大,可能需要去维护一个疑似搞事App的列表。如果确认自己的App不会用到辅助服务相关的东西,测试时也不利用UiAutomator等工具来做,那其实可以屏蔽掉 AccessibilityEvent 的发送,也可以屏蔽掉执行 AccessibilityServiceManager 发送过来的命令的,但这里我就不详细展开了,留给大家去思考。

总结

这里介绍了通过UiAutomator和辅助服务来进行自动化操作的工具的原理和检测方式。从原理上讲,二者都利用了 AccessibilityServiceManager 来串联工具和被操控的App。检测方案如下:

  • 对于UiAutomator,如果当前系统辅助服务的状态为开启状态,但开启了的Service列表却是空的表明有UiAutomator进程在运行。
  • 通过当前已开启的辅助服务Service可以知道当前开启了哪些应用的辅助服务,通过这个列表来找可疑的异常的App。
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章