提起AccessibilityService,你最容易联想到的确定是微信抢红包插件!但这个服务的设计初衷,是为了帮助残障人士能够更好的使用App。java
一些“调皮”的开发者利用AccessibilityService能够监控与操做其余App的特性加上系统远超人类的反应速度,在某些竞争类场景开发出了做弊外挂,最多见的就是你所嫉愤的微信抢红包插件了。node
微信抢红包插件对本来平等的竞争环境产生了不公,不过这是微信团队要操心解决的事。可万万没想到,有一天,我正在写的App也遭此毒手!!!这都欺负到头上了能忍吗?不能啊!android
OK,因此咱们今天先来分析一下AccessibilityService运行原理,而后分享一些我在应对此类竞争场景下基于AccessibilityService等自动化做弊工具的防护措施。bash
先说下背景:微信
场景是和抢红包相似的另外一种:抢单。用户下单后订单会通过系统,在配送端App发布,配送人员在配送端App经过距离、价钱、时间等维度进行筛选并抢单而后配送。显而易见,价高距离短的订单很是抢手,这样就造成一种竞争环境,因而,自动抢单外挂也就有了存在的理由。app
而后咱们来看下外挂进化史:async
第一代外挂ide
第一代外挂还比较粗糙,须要依赖按键精灵来实现,且须要Root权限。工具
【防护】简单反编译拆包了解后,考虑暂时没有更好的办法禁止按键精灵对App的模拟点击,直接封禁Root可能会有大量误杀,第一代防护仅简单的检查是否安装了按键精灵,而后限制用户抢单。oop
第二代外挂
可能由于第一代的防护过于粗糙,第二代外挂很快有了新的改进,再也不须要单独安装按键精灵这个App,他们把按键精灵集成到了本身的app里……
【防护】此时团队内部简单商量后决定,快刀斩乱麻,直接封禁Root权限,检测到Root后将限制抢单。
第三代外挂
禁止Root后终于消停了一段时间,但显然人民群众的智慧是无限的,很快新的免Root外挂出世了……通过反编译外挂后,第三代外挂采用了AccessibilityService来实现。
【防护】此时已知的外挂并很少,因此除了继续封禁Root之外,还创建了可远程配置的外挂package name黑名单列表,若检测到已安装app列表里存在特定外挂包名后,将会进行抢单限制。 package name须要先获取到安装包来查看包名,随着外挂数量逐步上涨,外挂安装包获取难度大的缺点开始暴露了。
【第三代防护】
此时针对上一个版本的防护措施作了一次优化: 1.优先检查外挂package name 2.次级检测外挂app name,加package name白名单防误判。这样就不须要再获取app的安装包了 3.增长骑手举报反馈入口 4.收集了已启动的辅助模式列表备用(本想再快到斩乱麻的禁止辅助模式的开启,但这个误杀范围实在是太大了,最终仍是停留在了想想的阶段)
第四代外挂
在经过app name封禁后,外挂们挣扎了几回都被即时遏制了。但很快,咱们收到了最新的外挂信息:新出来的外挂没有图标,看不到名字…… (大家厉害大家厉害!!!)
哎呀~真是活久见,两波历来没见过的人在互相进步啊这是!!!禁止外挂安装这种简单的防护措施已经挡不住这帮疯狂的人类了,我只能一头扎进了AccessibilityService的源码中,看这究竟是个啥东西,而后去思考相应的防护方案。
这不是一篇AccessibilityService教程文章,没有AccessibilityService完整的使用示例代码和源码,但为了上下文不至于断档太大,咱们这里仍是会简单贴一些小段代码。同时须要说明的是,严谨的来讲AccessibilityService只是一个Service,文本查找点击事件等操做对于一个Service来讲是彻底无法作到的。但为了行文方便,因此后面某些AccessibilityService代指辅助模式服务。
public class MyAccessibilityService extends AccessibilityService {
...
@Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
//获取eventType
int eventType = accessibilityEvent.getEventType();
if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
//查询文案为BUTTON3的View
List<AccessibilityNodeInfo> button3 = nodeInfo.findAccessibilityNodeInfosByText("BUTTON3");
nodeInfo.recycle();
for (AccessibilityNodeInfo item : button3) {
//对这个View执行点击操做
item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
}
...
}
复制代码
AccessibilityService真的很简单,只要写一个Service继承AccessibilityService,而后还有其余一些配置,以后每当你监控的应用界面有变更时就会回调到这个onAccessibilityEvent这个方法,你能够在里面取得此时变更的event类型是什么,还能拿到当前这个应用可视化的View树,而后取得其中的某个View来执行某些操做。
那至于其原理,用屁股想一想也知道是确定是被监控的App发生界面改变时通知了系统,而后系统又通知给了咱们注册的Service。嗯……屁股想的没错……那App怎么通知系统的?系统又怎么通知咱们的呢?
哎呀,屁股想不出来了,不要紧,屁股决定脑壳,脑壳知道怎么办。这个时候咱们就该钻到源码里来一探究竟了。Emmm~就先从咱们继承的这个AccessibilityService为入口进行研究吧!
哎呀~RTFSC,这乱糟糟的一片源代码催眠的一把好手,咱们仍是不看了,我给你画个图吧……
我理出一份AccessibilityService类图:
乍一看好像乱糟糟的,没事,我慢慢给你絮叨,确定比直接看源码来的直观有意思。
1.AccessibilityService有两个抽象方法,onAccessibilityEvent()
和onInterrupt()
,就是咱们要本身实现的那两个,重点记onAccessibilityEvent()
,它会出现不少次,咱们姑且先命名它为AS-onAccessibilityEvent()
.onAccessibilityEvent()
的参数类型是AccessibilityEvent
,这个类简而意之就是当系统中发生某些事件时,会发送这个类的对象来告知监控方,经过这个对象能够知道是什么类型的事件、什么控件发出来等等。
2.另外AccessibilityService
继承了Service
,但它仅复写了onBind
方法。在onBind
方法中return了一个IAccessibilityServiceClientWrapper
对象。
@Override
public final IBinder onBind(Intent intent) {
return new IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() {
...
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
AccessibilityService.this.onAccessibilityEvent(event);
}
...
}
复制代码
3.IAccessibilityServiceClientWrapper
继承了IAccessibilityServiceClient.Stub
,嗯~看到这你应该就明白一大块了,AccessibilityService
是一个跨进程通讯Service。IAccessibilityServiceClientWrapper
是这个类的重点关注对象了,那他做为一个AIDL的一个server端,他有哪些对外提供的方法呢?
interface IAccessibilityServiceClient {
void init(in IAccessibilityServiceConnection connection, int connectionId, IBinder windowToken);
void onAccessibilityEvent(in AccessibilityEvent event);
void onInterrupt();
void onGesture(int gesture);
void clearAccessibilityCache();
void onKeyEvent(in KeyEvent event, int sequence);
void onMagnificationChanged(in Region region, float scale, float centerX, float centerY);
void onSoftKeyboardShowModeChanged(int showMode);
void onPerformGestureResult(int sequence, boolean completedSuccessfully);
}
复制代码
这里你又看到了onAccessibilityEvent()
,咱们姑且叫他IASC-onAccessibilityEvent()
.
4.而后咱们在回头看看IAccessibilityServiceClientWrapper
的构造方法中的三个参数,Context、 Looper 、Callbacks
。Context
不说了,Looper
是一个MainLooper
, 他们两个的做用是建立一个HandlerCaller对象,HandlerCaller你能够很粗狂的就把它当作Handler,想了解细节能够本身看一下源码:
public IAccessibilityServiceClientWrapper(Context context, Looper looper,Callbacks callback) {
mCallback = callback;
mCaller = new HandlerCaller(context, looper, this, true /*asyncHandler*/);
}
复制代码
5.而后咱们来看看Callbacks
是个啥:
public interface Callbacks {
void onAccessibilityEvent(AccessibilityEvent event);
void onInterrupt();
void onServiceConnected();
void init(int connectionId, IBinder windowToken);
boolean onGesture(int gestureId);
boolean onKeyEvent(KeyEvent event);
void onMagnificationChanged(@NonNull Region region,
float scale, float centerX, float centerY);
void onSoftKeyboardShowModeChanged(int showMode);
void onPerformGestureResult(int sequence, boolean completedSuccessfully);
void onFingerprintCapturingGesturesChanged(boolean active);
void onFingerprintGesture(int gesture);
void onAccessibilityButtonClicked();
void onAccessibilityButtonAvailabilityChanged(boolean available);
}
复制代码
这和刚才那个IAccessibilityServiceClient
不是同样嘛?没错,是这样的,并且这里面也有一个onAccessibilityEvent
,咱们叫它Callbacks-onAccessibilityEvent
。
上面你应该看到Callbacks
是一个匿名内部类,他实现的Callbacks-onAccessibilityEvent
方法的就是一句:AccessibilityService.this.onAccessibilityEvent(event);
直接调用了AS-onAccessibilityEvent()
,先记下来哈。
6.哦对,IAccessibilityServiceClientWrapper
还实现了一个HandlerCaller.Callback
接口:
public interface Callback {
public void executeMessage(Message msg);
}
复制代码
7.最后咱们看一下IAccessibilityServiceClientWrapper
对两个接口IAccessibilityServiceClient
和HandlerCaller.Callback
的实现:
...
public void onAccessibilityEvent(AccessibilityEvent event, boolean serviceWantsEvent) {
Message message = mCaller.obtainMessageBO(
DO_ON_ACCESSIBILITY_EVENT, serviceWantsEvent, event);
mCaller.sendMessage(message);
}
...
@Override
public void executeMessage(Message message) {
switch (message.what) {
case DO_ON_ACCESSIBILITY_EVENT: {
...
mCallback.onAccessibilityEvent(event);
...
}
} return;
...
}
}
...
复制代码
我只保留了最关键的代码,咱们以onAccessibilityEvent
为线索方法捋一遍哈。当AIDL的Client端调用了IASC-onAccessibilityEvent
时,会经过Handler发送一个message给本身,接收到之后会调用Callbacks-onAccessibilityEvent
,Callbacks-onAccessibilityEvent
咱们刚才看过啦,会调用AS-onAccessibilityEvent()
,这是个抽象方法,也就是咱们本身实现的MyAccessibilityService中的自定义代码。
有点懵??不明白到底在干啥?不要紧,我还画了个搓搓的流程图:
当View发生改变时,会发出一个AccessibilityEvent出来,这个Event会经过Binder驱动发送给IAccessibilityServiceClientWrapper,调用他的onAccessibilityEvent(AccessibilityEvent)方法,这个方法经过Handler发送了一个Message给本身,目的是为了从Binder线程转回主线程。而后调用了mCallback.onAccessibilityEvent(event)
,间接的调用了AccessibilityService.this.onAccessibilityEvent(event);
,也就是咱们本身实现的。
这么顺下来,AccessibilityService的内部逻辑是不就感受很简单了?
咱们梳理了一遍AccessibilityService的内部执行逻辑后,就会触发不少新的问题,好比onBind是谁来调用的啊?为何中间还要用Hander给本身发送一遍消息呢?当咱们本身实现onAccessibilityEvent方法时会作一些点击一类的操做,这个是怎么作到的啊?
哎呀,问题好多,这个源码梳理下来确定要睡第二觉了,咱们不看了不看了,直接上图吧:
1.一个可爱的用户在设置页面启动了某个辅助模式服务 2.系统发送了一条广播到AccessibilityManagerService,收到广播后,AccessibilityManagerService绑定了咱们写的AccessibilityService,就这样调用了onBind方法。AIDL的Server端准备好了~ AccessibilityManagerService是一个系统服务,由SystemService启动。
3.受到监控的App某个View发生了改变,其内部都会调用AccessibilityManager来发送event,其具体发送的对象是ViewRootImpl类来作的。 4.发出event后会经过Binder驱动调用到AccessibilityService,最终调用了咱们复写的onAccessibilityEvent方法。 5.每个View在AccessibilityService中都会被映射为一个AccessibilityNodeInfo对象,咱们经过这个对象去查找具体View、触发事件,其本质是调用了AccessibilityInteractionClient类的对应方法。 6.AccessibilityInteractionClient咱们在Uiautomator也常常看到。后面咱们会继续单独分析,先大概说一下是个什么东西,官方注释是这样的: This class is a singleton that performs accessibility interaction which is it queries remote view hierarchies about snapshots of their views as well requests from these hierarchies to perform certain actions on their views. 这个类是一个能够执行可访问性交互的单例对象,它查询远程视图层次结构,查看视图的快照,以及来自这些层次结构的请求,以便在视图上执行某些操做。 7.若是利用AccessibilityInteractionClient操做正在被监控的App,好比点击按钮,那么View发生变化,又发送出一个Event,这样便造成一个循环。
在咱们了解了AccessibilitySevice从View产生event事件发出到被辅助服务接收再操做View的一个流程以后,咱们仅仅知道了事件是如何通知到AccessibilityService的,而具体是如何经过文本查找View,点击View则是AccessibilityInteractionClient来作的,那么下面咱们就经过AccessibilityInteractionClient 的源码探究一下里面的秘密。
咱们主要以findAccessibilityNodeInfosByText和performAccessibilityAction(ACTION_CLICK)两个方法往下追。
总体代码较为简单,基本是一条线往下调用的逻辑,因此我又画了一张图:
1.AccessibilityInteractionClient没作什么操做,直接经过Binder调用了AccessibilityManagerService对应的方法。
2.AccessibilityManagerService最终仍是经过Binder调用了ViewRootImpl对应的方法。
3.ViewRootImpl仅做为Binder中的服务端接收调用,真正的操做交给AccessibilityInteractionController来作。
4.AccessibilityInteractionController对应的方法被调用以后,并无直接进行操做,而是经过Handler作了一次转发,以便从Binder线程转到UI线程。
5.以performAccessibilityAction(ACTION_CLICK)点击事件为例,最终调用的实际是View的mOnClickListener。
6.以findAccessibilityNodeInfosByText为例,最终调用的实际是View的findViewsWithText方法,其方法内部实际对比的值是mContentDescription。须要特别说明的是TextView重写了该方法,其内部实际对比的值是mText。
咱们既然已经了解了AccessibilityService的运行原理,其内部就是一个跨进程通讯,没什么神秘的。最终操做View的是AccessibilityInteractionClient,AccessibilityInteractionClient是怎么操做的经过源码很容易的追到了View层具体的实现,那么作防护的话简直是手到擒来!
以前在外挂防护上,一直困扰个人一个问题是:AccessibilityService相似一个解耦很开的观察者模式,做为被观察者没法察觉到观察者究竟有哪些,这致使咱们很是的被动。
不过研究过AccessibilityService源码以后,咱们知道,每一个AccessibilityService在都是由AccessibilityManagerService注册的,那岂不是说咱们能够经过AccessibilityManagerService取得全部以安装或以启动的辅助模式应用?那么AccessibilityManagerService有提供相关方法吗? 有的:
AccessibilityManagerService.java
public class AccessibilityManagerService extends IAccessibilityManager.Stub {
...
@Override
public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(int userId) {...}
}
复制代码
值得注意的是,这个方法帮咱们筛去了UiAutomationService。返回值AccessibilityServiceInfo是AccessibilityService的一些配置信息,其中包含咱们最关心的packageNames(AccessibilityService 监控哪些package发出的Event)
这里有一个小问题,AccessibilityManagerService是com.android.server.accessibility包下的类,咱们没有办法直接使用。不过不要紧,你能够经过AccessibilityManager来间接的操做AccessibilityManagerService,其内部利用Binder间接的调用了AccessibilityManagerService,获得List以后,你能够经过遍历了解到本身的应用正在被那些辅助模式监控或“辅助”。
具体方法以下:
/**
* 取得正在监控目标包名的AccessibilityService
*/
private List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(String targetPackage) {
List<AccessibilityServiceInfo> result = new ArrayList<>();
AccessibilityManager accessibilityManager = (AccessibilityManager) getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (accessibilityManager == null) {
return result;
}
List<AccessibilityServiceInfo> infoList = accessibilityManager.getInstalledAccessibilityServiceList();
if (infoList == null || infoList.size() == 0) {
return result;
}
for (AccessibilityServiceInfo info : infoList) {
if (info.packageNames == null) {
result.add(info);
} else {
for (String packageName : info.packageNames) {
if (targetPackage.equals(packageName)) {
result.add(info);
}
}
}
}
return result;
}
复制代码
咱们一直知道AccessibilityServices在监控目标app发出的AccessibilityEvent,从而对应的做出某些操做。
例如某些微信红包插件会监控Notification的弹出,那么咱们是否能够随意发送这样的Event出来,从而混干扰外挂插件的运行逻辑?
没错,能够这样作的,具体方式以下:
textView.sendAccessibilityEvent(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
复制代码
但这个方案的缺陷是,大部分的外挂插件对特定类型的事件并非特别感兴趣,他们仅在收到Event后检查页面上是否有某些特定的元素,从而决定是否进行下一步操做。
大部分状况下是一个比较鸡肋的措施,但也许会在某些场景起到意想不到的做用!
在没有探究AccessibilityServices源码以前,不了解AccessibilityServices检索文本信息原理的咱们可能惟一能想到的应对措施就是将关键问题替换为图片。
这能够解决问题,可是问题替换为图片不但会有性能上的损耗,并且会丢失大部分本来TextView的兼容特性。
不过在了解AccessibilityServices源码以后,咱们知道其内部核心原理就是调用TextView的findViewsWithText方法,再也不须要费劲心思将文本转为图片,你须要作的仅仅是复写这个方法就够了:
public class DefensiveTextView extends android.support.v7.widget.AppCompatTextView {
...
@Override
public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
outViews.remove(this);
}
}
复制代码
这样AccessibilityServices文案检查将会在这个View上失效。
像上面同样,经过源码了解原理以后,咱们知道AccessibilityServices执行点击事件最终在调用View的mOnClickListener。
那咱们只须要在这上面作文章就行了,最快捷的办法是利用onTouch代替onClick。
上述方式不管是检测已安装的AccessibilityServices列表仍是屏蔽AccessibilityServices的文本检查和点击事件,针对的都是AccessibilityServices自己。当你出台这样的方式后,确实后会让现有的外挂消停一段时间,但能够预见,很快会有基于其余自动化措施的外挂面世,好比相似按键精力同样的模拟Touch事件,图像识别等,在出现应对这些手段以前,你仍是须要一些笨笨的老办法,收集已知外挂,禁止其安装。
检查方法很是简单,一句带过:设立黑名单,遍历系统内部全部已安装的app,鉴别package name 和app name。
在拨开了AccessibilityServices源码的外衣以后,咱们会发现其实它的原理真的很简单,惟一的核心是在Client - System - Server三者之间利用Binder作跨进程通讯,几乎没有太多的逻辑操做,一直在互相调用。
因此看着神奇且神秘的AccessibilityServices其实并无什么了不得。
另外要说的是,在没了解AccessibilityServices源码以前,咱们能想到的防护措施可能很是少且低效,好比本来只用复写一个方法,你却须要动态生成图片。了解源码以后,你即可以单刀直入,直切重点用最有效最简单的方式实现你想要的东西,因此阅读源码真的很重要!
最后先总结一下防护措施吧。