概述
模拟点击原理
1、系统启动时,会启动大量系统服务,其中就包括 AccessibilityManagerService
AccessibilityManagerService(这里简称AMS)在创建时,会注册一些系统广播,包括应用状态变化广播 PackageMonitor。
2、PackageMonitor在有应用安装、卸载、更新时都会收到广播,在收到广播后,AMS会获取对应应用中注册的AccessibilityService,并保存该服务的信息,然后如果设置中开启了该服务,AMS中就会bindService方式启动该服务,并返回该服务的代理AccessibilityService.IAccessibilityServiceClientWrapper。通过该代理AMS可以与监听服务所在进程通信。
3、APP进程,UI变化/获取焦点/点击按钮…许多事件都会通过AccessibilityManager发送给AMS。
AccessibilityManager是AMS的代理,系统启动AMS时创建并缓存在ServiceManager.sCache中。
查看源码可知ViewRootImpl中有调用 AccessibilityManager,把UI信息发送给 AMS进程, AMS 进程 拿到事件后,通过 IAccessibilityServiceClientWrapper(模拟点击服务在AMS中的代理).onAccessibilityEvent()把UI信息发送到AccessibilityService模拟点击服务。
4、AccessibilityService 拿到UI信息后,根据信息判断与处理,这个判断和处理是 AccessibilityService模拟点击服务的开发者实现的。AccessibilityService中把对UI的处理封装之后,回调给AMS,AMS再回调给APP进程,在APP进程中根据处理信息对UI做响应操作(点击)。
AMS获取系统所有应用的AccessibilityService的过程
APP进程——AMS——AccessibilityService通信
模拟点击监测、上报、防御
1、获取模拟点击服务信息
这个比较简单,从上面原理可知, AccessibilityManagerService 中保存有所有模拟点击服务的信息,且提供了获取这些服务信息的API,在APP端可以拿到AMS的代理 AccessibilityManager,通过它就能拿到所有监听服务的信息。
AccessibilityManager manager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
List<AccessibilityServiceInfo> accessServiceList = manager.getInstalledAccessibilityServiceList();
2、上报模拟点击事件
第三方模拟点击服务的上报
进入监控页面时,通过 AccessibilityManager,拿到所有模拟点击服务信息,包括服务包名,服务是否开启,并上报到后台。
模拟点击的上报
android自定义控件里,所有View都实现了AccessibilityEventSource接口,在很多状态下,包括页面获取焦点、点击、…等事件中会调用 sendAccessibilityEvent()发送事件,而sendAccessibilityEvent()最终调用的是AccessibilityManager.sendAccessibilityEvent(AccessibilityEvent event)。
也就是通过 AccessibilityManagerService 的代理把事件发送给 AMS服务,再发送给有监听本应用的第三方模拟点击服务。
所以,只要重写需要上报的控件的 sendAccessibilityEvent(),在该方法中执行上报逻辑,就可以实现模拟点击的上报了。
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
public void sendAccessibilityEvent(int eventType) {
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
} else {
sendAccessibilityEventInternal(eventType);
}
}
}
3、防御模拟点击实现
1) 屏蔽 AccessibilityService 文案检查
a: AccessibilityService根据文案查找控件,调用的是event.getSource().findAccessibilityNodeInfosByText(“按钮文案”)
该方法最终调用的是 findViewsWithText(),而这个方法在我们自己应用开发中几乎不会用到(用这种方式获取view,在应用开发中也是不靠谱的)
所以只要把需要监听的控件的 findViewsWithText()重写,把本控件重查找结果中移除。
@Override
public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
outViews.remove(this);
}
ii: 第三方有可能通过一些工具,或者破解,获取到关键按钮的id,通过id查找控件 event.getSource().findAccessibilityNodeInfosById(按钮id)
针对这种,可以对按钮的点击事件做修改, 用onTouch代替onClick,这样模拟点击也就失效了。
4、关于大量已有控件的替换
借鉴Android v7支持库的思路,即通过AppCompatDelegate代理自动替换UI控件
在 BaseActivity 中重写 getDelegate 方法,将方法的返回值替换为修改过的AppCompatDelegate,实现自动替换UI控件
public class BaseActivity extends Activity {
@Override
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = DefenseDelegate.create(this, this);
}
return mDelegate;
}
}
public class DefenseDelegateV14 extends AppCompatDelegateImplV14 {
@Override
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
switch (name) {
case "TextView":
return new DefenseTextView(context, attrs);
}
return super.callActivityOnCreateView(parent, name, context, attrs);
}
}
最后,附上笔记、和部分关键源码,便于查看
一、 AccessibilityManager 是 AccessibilityManagerService 系统服务在客户端APP进程(被监听/模拟点击的应用所在进程)的代理
通过该API,可以通过 ServiceManager 去获取 AccessibilityManagerService 的代理,通过该代理 APP进程 可以与 AMS通信
AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
二、 AccessibilityService 第三方的监听/模拟点击服务,位于第三方进程
该服务会在 AccessibilityManagerService 中启动,并与AMS交互。
AccessibilityService 作为服务端, AMS为客户端,AMS在bindService()启动 AccessibilityService 服务后,持有AccessibilityService的代理,通过该代理与 AccessibilityService 通信
public class TestAccessibilityService extends AccessibilityService {
private List<String> tags = new ArrayList<>();//保存要模拟点击的控件的文字内容
private List<Integer> ids = new ArrayList<>();//保存要模拟点击的控件的id
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getSource() != null) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
String text = "";
if (event.getSource().getText() != null) {
text = event.getSource().getText().toString();
}
for (String tag : tags) {
//找出所有匹配的事件节点
List<AccessibilityNodeInfo> install_nodes = event.getSource().findAccessibilityNodeInfosByText(tag);
boolean byText = false;
if (install_nodes != null && !install_nodes.isEmpty()) {
AccessibilityNodeInfo node;
for (int i = 0; i < install_nodes.size(); i++) {
node = install_nodes.get(i);
if (node.getClassName().equals(BUTTON_CLASSNAME) && node.isEnabled()) {
//执行点击
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
....
}
for (Integer id : ids) {
if (id == null) continue;
//找出所有匹配的事件节点
List<AccessibilityNodeInfo> install_nodes2 = event.getSource().findAccessibilityNodeInfosByViewId(id);
findAccessibilityNodeInfosByViewId
boolean byText = false;
if (install_nodes2 != null && !install_nodes2.isEmpty()) {
AccessibilityNodeInfo node;
for (int i = 0; i < install_nodes2.size(); i++) {
node = install_nodes2.get(i);
if (node.getClassName().equals(BUTTON_CLASSNAME) && node.isEnabled()) {
//执行点击
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
....
}
}
}
}
三、 AccessibilityManagerService 是一个系统服务,位于系统进程,在系统启动时就会启动该服务,并把该服务的代理缓存在 ServiceManager 的Map缓存中
在该服务启动时,会监听一系列系统广播,获取并保存系统安装的所有应用的【模拟点击服务的包名、类名信息】
1、AMS拿到所有应用的模拟点击服务,并启动
public class AccessibilityManagerService extends IAccessibilityManager.Stub {
public AccessibilityManagerService(Context context) {
...
registerBroadcastReceivers();//该系统服务创建时,注册广播,监听
new AccessibilityContentObserver(mMainHandler).register(
context.getContentResolver());
}
private UserState getUserStateLocked(int userId) {
UserState state = mUserStates.get(userId);
if (state == null) {
state = new UserState(userId);
mUserStates.put(userId, state);
}
return state;
}
private void registerBroadcastReceivers() {
/* PackageMonitor是一个系统广播,当应用的状态变化,包括安装、卸载、更新,是会触发该广播
* 在AMS系统服务中注册该广播,在系统中有应用状态发生变化时,收到广播,获取
*/
PackageMonitor monitor = new PackageMonitor() {
@Override
public void onSomePackagesChanged() {
synchronized (mLock) {
if (getChangingUserId() != mCurrentUserId) {
return;
}
UserState userState = getCurrentUserStateLocked();
userState.mInstalledServices.clear();
if (!userState.isUiAutomationSuppressingOtherServices()) {
//readConfigurationForUserStateLocked()中会获取模拟点击服务信息,并保存到AMS中
if (readConfigurationForUserStateLocked(userState)) {
//onUserStateChangedLocked()中会启动获取到的所有模拟点击服务
onUserStateChangedLocked(userState);
}
}
}
}
}
...
monitor.register(mContext, null, UserHandle.ALL, true);
....
}
/* readInstalledAccessibilityServiceLocked() 会去读取状态变化的应用中,的 AccessibilityService 信息
* 也就是这里可以拿到所有应用注册的 AccessibilityService 服务,包括第三方恶意软件的监听/模拟点击服务
*/
private boolean readConfigurationForUserStateLocked(UserState userState) {
boolean somethingChanged = readInstalledAccessibilityServiceLocked(userState);
...
somethingChanged |= readAccessibilityShortcutSettingLocked(userState);
somethingChanged |= readAccessibilityButtonSettingsLocked(userState);
return somethingChanged;
}
private void onUserStateChangedLocked(UserState userState) {
...
updateServicesLocked(userState);
updateAccessibilityShortcutLocked(userState);
...
updateAccessibilityButtonTargetsLocked(userState);
}
private void updateServicesLocked(UserState userState) {
Map<ComponentName, Service> componentNameToServiceMap = userState.mComponentNameToServiceMap;
boolean isUnlockingOrUnlocked = LocalServices.getService(UserManagerInternal.class)
.isUserUnlockingOrUnlocked(userState.mUserId);
for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) {
AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i);
ComponentName componentName = ComponentName.unflattenFromString(
installedService.getId());
//先从缓存里拿
Service service = componentNameToServiceMap.get(componentName);
....
if (userState.mEnabledServices.contains(componentName)) {
if (service == null) {//拿不到就创建新的服务
service = new Service(userState.mUserId, componentName, installedService);
}
...
//启动所有模拟监听服务
service.bindLocked();
}
...
}
...
}
}
关于WebView加载的网页控件能否接受模拟点击
据传谷歌自身的一个开源项目talkback中,可以通过AccessibilityService模拟点击网页控件。具体实现需要研究该开源项目,并自测验证后才能确定。
参考资料:
模拟点击原理与防御
模拟点击View中发出AccessibilityEvent
最后
以上就是畅快衬衫为你收集整理的Android AccessibilityService模拟点击监测、上报、防御的全部内容,希望文章能够帮你解决Android AccessibilityService模拟点击监测、上报、防御所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复