这篇文章写了有段时间了,但出于一些原因一直没有发。最近一段时间出现了很多利用自动化工具刷资讯类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时的设置项是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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命令可能拿不到其他进程,所以这不是一个好办法。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 同一时间只允许启动一个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获取到。类似下面的代码。

1
2
3
4
5
6
7
8
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。