Android AccessibilityService模拟点击监测、上报、防御

版权声明: https://blog.csdn.net/u010577768/article/details/86301199

模拟点击原理

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

猜你喜欢

转载自blog.csdn.net/u010577768/article/details/86301199