前言
依据 Android 官方文档,考虑到一些用户不能很好地使用 Android 设备,比如由于视力、身体、年龄方面的限制,造成阅读内容、触控操作、声音信息等方面的获取困难,因此 Android 提供了 Accessibility 特性和服务帮助用户更好地使用 Android 设备。正由于这个介绍,在国内更普遍地被称为无障碍或残疾人模式。
提供无障碍服务的应用具有很多特殊的能力,比如获取当前用户正在运行的 app 信息、自动点击界面按钮、读取通知栏信息等……Google 提供无障碍服务特性的本意是为了给身体存在缺陷的用户提供操作便利,但是却被黑灰产用于监视用户手机、免 root 抢红包、自动化灌装 app 等。为了了解无障碍服务本身具备的能力和存在的风险点,本文将学习、介绍下无障碍服务的基本用法和安全现状。
无障碍服务
在分析无障碍服务当前存在的风险之前,先来看看无障碍服务的开发步骤及实例代码,搞清楚它是个什么东西……
以 Huawei 手机为例,进入 “设置-辅助功能-无障碍”,可看到手机上已安装的提供无障碍服务的 APP 列表,是否启用其无障碍服务需要用户手动授权确认:
下面来看下如何创建自己的无障碍服务 Demo 程序。完整创建过程可参见 Android 官方教程文档。
1.1 配置服务类
Android Studio 新建项目,创建 Service 类,AndroidManifest.xml 配置如下:
<service
android:name=".service.AccessibilityService"
android:label="@string/accessibility_service_label"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
</service>
可以看到,除了像其他普通 Service 服务一样,需要在 AndroidManifest.xml中声明该服务之外,该服务还必须配置以下两项:
- android:permission:需要指定 BIND_ACCESSIBILITY_SERVICE 权限,这是 Android 4.0 以上的系统要求的;
- intent-filter:“android.accessibilityservice.AccessibilityService” ,这个 name 是固定不变的。
其中 accessibility_service_config.xml 配置文件如下:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_service_description"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackGeneric"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true" />
以上配置项的含义:
配置项 | 作用 |
---|---|
description | 对该无障碍功能的描述,将在“系统-辅助功能-无障碍-已安装的服务”处展示 |
accessibilityEventTypes | 表示该服务对界面中的哪些变化感兴趣,即哪些事件通知,比如窗口打开、滑动、焦点变化、长按等,具体的值可以在 AccessibilityEvent 类中查到,如 typeAllMask 表示接受所有的事件通知 |
accessibilityFeedbackType | 表示反馈方式,比如是语音播放,还是震动 |
notificationTimeout | 接受事件的时间间隔,通常将其设置为100即可 |
canRetrieveWindowContent | 表示该服务能否访问活动窗口中的内容,也就是如果你希望在服务中获取窗体内容,则需要设置其值为 true |
canPerformGestures | 表示是否允许进行手势分发 |
packageNames | 非必需项,表示对该服务是用来监听哪个包的产生的事件 |
补充下上面两处配置涉及的 values/strings.xml:
<resources>
<string name="app_name">hacker</string>
<!--无障碍服务-->
<string name="accessibility_service_label">Tr0e</string>
<string name="accessibility_service_description">Accessibility Service demo create by Tr0e</string>
<string name="power">模拟长按电源</string>
<string name="volume">调高手机音量</string>
<string name="swipe">模拟右滑手势</string>
<string name="simulateTouch">模拟点击事件</string>
<string name="scroll">模拟下拉手势</string>
</resources>
还有该 demo 程序的一个悬浮按钮 UI 布局文件 layout/floating_bar.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/power"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power" />
<Button
android:id="@+id/volume_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/volume" />
<Button
android:id="@+id/swipe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/swipe" />
<Button
android:id="@+id/simulateTouch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/simulateTouch" />
<Button
android:id="@+id/scroll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/scroll" />
</LinearLayout>
1.2 服务类实现
接下来来看重点,如何实现无障碍服务类的具体逻辑,以下 Demo 程序主要实现几个功能:
- 监视用户当前的点击事件所属的 package 名称;
- 模拟实现应用的自动化安装(灰产恶意灌装 app 常用手段);
- 监视用户通知栏通知事件的信息(如微信聊天、手机短信验证码……);
- 悬浮按钮:模拟长按电源、调高手机音量、模拟右滑手势、模拟点击事件、模拟下拉手势。
直接上代码,该解释的已添加了注释:
package com.Tr0e.hacker.service;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.GestureDescription;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.media.AudioManager;
import android.os.Build;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Button;
import android.widget.FrameLayout;
import androidx.annotation.RequiresApi;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import com.Tr0e.hacker.R;
public class AccessibilityService extends android.accessibilityservice.AccessibilityService {
private static final String TAG = "AccessibilityService";
FrameLayout mLayout;
/**
* 监视点击事件并作出响应
* @param event
*/
@SuppressLint("LongLogTag")
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_VIEW_CLICKED:
//监视点击事件,打印当前点击事件来源于哪个应用
String packageName = event.getPackageName().toString();
PackageManager packageManager = this.getPackageManager();
try {
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0);
CharSequence applicationLabel = packageManager.getApplicationLabel(applicationInfo);
Log.e(TAG, "[点击事件] " + "App name is: " + event.getPackageName() + "(" + applicationLabel + ")");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
//监视通知事件,打印通知内容,可用于监视聊天通知、短信等
List<CharSequence> texts = event.getText();
String content = "";
if (!texts.isEmpty()) {
for (CharSequence text : texts) {
content = content + text.toString();
}
}
Log.e(TAG, "[通知事件] " + content);
break;
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
break;
}
//模拟自动安装apk
AccessibilityNodeInfo source = event.getSource();
if (source != null) {
boolean installPage = event.getPackageName().equals("com.android.packageinstaller");
if (installPage) {
installAPK(event);
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void installAPK(AccessibilityEvent event) {
AccessibilityNodeInfo source = getRootInActiveWindow();
List<AccessibilityNodeInfo> installInfos = source.findAccessibilityNodeInfosByText("继续安装");
nextClick(installInfos);
List<AccessibilityNodeInfo> openInfos = source.findAccessibilityNodeInfosByText("打开");
nextClick(openInfos);
runInBack(event);
}
private void runInBack(AccessibilityEvent event) {
event.getSource().performAction(AccessibilityService.GLOBAL_ACTION_BACK);
}
private void nextClick(List<AccessibilityNodeInfo> infos) {
if (infos != null){
for (AccessibilityNodeInfo info : infos) {
if (info.isEnabled() && info.isClickable())
info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
@SuppressLint("LongLogTag")
@Override
public void onInterrupt() {
Log.e(TAG, "Something went wrong");
}
/**
* 用户开启无故障服务时自动触发,此处用于初始化悬浮UI界面
*/
@Override
protected void onServiceConnected() {
super.onServiceConnected();
//无障碍服务的配置代码,已通过accessibility_service_config.xml实现
// AccessibilityServiceInfo info = new AccessibilityServiceInfo();
// info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED | AccessibilityEvent.TYPE_VIEW_FOCUSED;
// info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
// info.notificationTimeout = 100;
// this.setServiceInfo(info);
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
mLayout = new FrameLayout(this);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
layoutParams.format = PixelFormat.TRANSLUCENT;
layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.gravity = Gravity.LEFT;
LayoutInflater inflater = LayoutInflater.from(this);
inflater.inflate(R.layout.floating_bar, mLayout);
windowManager.addView(mLayout, layoutParams);
configurePowerButton();
configureSimulateTouch();
configureVolumeButton();
configureScrollButton();
configureSwipeButton();
}
/**
* 开启电源管理界面(关机或重启)
*/
private void configurePowerButton(){
Button power = (Button) mLayout.findViewById(R.id.power);
power.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
}
});
}
/**
* 调高音量
*/
private void configureVolumeButton() {
Button volumeUpButton = (Button) mLayout.findViewById(R.id.volume_up);
volumeUpButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI);
}
});
}
/**
* 模拟向右滑动的手势
*/
private void configureSwipeButton() {
Button swipeButton = (Button) mLayout.findViewById(R.id.swipe);
swipeButton.setOnClickListener(new View.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onClick(View view) {
Path swipePath = new Path();
swipePath.moveTo(1000, 1000);
swipePath.lineTo(100, 1000);
GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 500));
dispatchGesture(gestureBuilder.build(), null, null);
}
});
}
/**
* 模拟点击事件
*/
private void configureSimulateTouch(){
Button btnSimulateTouch = (Button) mLayout.findViewById(R.id.simulateTouch);
btnSimulateTouch.setOnClickListener(new View.OnClickListener() {
@SuppressLint("LongLogTag")
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onClick(View v) {
Log.e(TAG, "onClick: Simulate Touch");
Path tap = new Path();
tap.moveTo(200,200);
GestureDescription.Builder tapBuilder = new GestureDescription.Builder();
tapBuilder.addStroke(new GestureDescription.StrokeDescription(tap, 0, 500));
dispatchGesture(tapBuilder.build(), null, null);
}
});
}
/**
* 模拟下拉刷新的手势
*/
private void configureScrollButton() {
Button scrollButton = (Button) mLayout.findViewById(R.id.scroll);
scrollButton.setOnClickListener(new View.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onClick(View view) {
AccessibilityNodeInfo scrollable = findScrollableNode(getRootInActiveWindow());
if (scrollable != null) {
scrollable.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId());
}
}
});
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private AccessibilityNodeInfo findScrollableNode(AccessibilityNodeInfo root) {
Deque<AccessibilityNodeInfo> deque = new ArrayDeque<>();
deque.add(root);
while (!deque.isEmpty()) {
AccessibilityNodeInfo node = deque.removeFirst();
if (node.getActionList().contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD)) {
return node;
}
for (int i = 0; i < node.getChildCount(); i++) {
deque.addLast(node.getChild(i));
}
}
return null;
}
}
可以看到,编写自己的无障碍服务类,需要继承 AccessibilityService 类,同时实现以下方法:
- onAccessibilityEvent(AccessibilityEvent event) 方法:当我们监听的目标应用界面或者界面等信息,会通过 onAccessibilityEvent 回调我们的事件,接着进行事件的处理;
- onServiceConnected() 方法:系统成功绑定该服务时被触发,也就是当你在设置中开启相应的服务,系统成功的绑定了该服务时会触发,通常我们可以在这里做一些初始化操作;
- onInterrupt() 方法:服务进程异常时执行的代码。
关于事件类型 getEventType() 返回值:
- TYPE_VIEW_LONG_CLICKED 长按事件;
- TYPE_VIEW_CONTEXT_CLICKED 点击事件;
- TYPE_WINDOW_STATE_CHANGED 表示用户界面被更改;
- TYPE_NOTIFICATION_STATE_CHANGED:通知栏的改变;
- TYPE_WINDOWS_CHANGED:表示系统窗口的时间变更;
关于无障碍服务的其他开发细节,本文不再展开介绍,更多信息可参考:
1) Android之用AccessibilityService实现红包插件;
2) Android之辅助服务上篇:AccessibilityService使用;
3) Android官方开发指导文档:创建自己的无障碍服务。
1.3 无障碍演示
在 MainActivity 中通过如下代码来跳转到系统设置,引导用户打开当前 app 所提供的无障碍服务:
/**
* 开启无障碍服务
*/
Button button= (Button) findViewById(R.id.Button7);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//检测当前无障碍服务已开启的应用列表信息
AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
List<AccessibilityServiceInfo> accessibilityServiceList = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
for (AccessibilityServiceInfo info : accessibilityServiceList) {
Log.e(TAG, "当前已开启的无障碍服务的信息: " + info.getResolveInfo().toString());
}
//跳转到系统设置页面,由用户手动点击确认是否开启对应的无障碍服务
Intent intent = new Intent();
intent.setAction(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
});
触发以上 Button 事件,将跳转到系统“设置-辅助功能-无障碍”处,点击“已安装的服务”并开启服务:
开启完无障碍服务以后,来看下整个实例程序的运行效果:
1)开启 Tr0e app 的无障碍服务按钮后,显示悬浮按钮界面如下:
其中点击事件的模拟按钮,模拟的是指定屏幕位置的长按操作,比如在上图的手机桌面点击“模拟点击事件”按钮,将是文件夹位置处的点击操作:
2)对于当前点击事件的监视效果(打印所属的包名信息)和通知事件的监视(微信聊天、操作通知等):
3)对于 apk 包的自动安装的效果,请自行执行 adb push poc.apk /sdcard
后在文件管理器点击 apk 文件进行安装体验……总结来说就是该程序可以让你减少手动点击“继续安装”、“完成”、“打开”等安装过程中的确认按钮。
上面成功实现自动安装的截图实际上非华为手机,而是某国产手机(此处不点名)。华为已不用 “com.android.packageinstaller” 作为默认安装器,具体原因下文对于无障碍服务的安全性分析部分会提到。
以上就是实例程序的完整功能演示,相信读者可以感受到无障碍服务的“恐怖”权限与能力了……
1.4 框架层原理
推荐两篇文章:AccessibilityService分析与防御、基于Accessibility Service的Android外挂插件实现原理及防御措施,这两篇文章很好地解释了无障碍服务的框架层服务的原理和防御手段。本文对此不做过多展开,只简单介绍大致框架,以下摘自第二篇文章。
1、Accessibility Service内部工作机制
从 Google 的官方文档和 Android 相关源代码分析,得到 Accessibility Service 类和相关类及其关系如图 1 所示。
- AccessibilityService 是 Service 的子类,并重写了 onBind 方法,在 onBind 方法中创建并返回了一个 IAccessibilityServiceClientWrapper 实例对象;
- 另外在 AccessibilityService 中还预留了 onAccessibilityEvent 接口,提供给程序员在自己的 AccessibilityService 子类中实现针对 AccessibilityEvent 事件的逻辑处理。
- IAccessibilityServiceClientWrapper 继承自 IAccessibilityServiceClient 类,该类是一个 AIDL 接口,同时 IAccessibilityServiceClientWrapper 还继承了 IAccessibilityServiceClient.Stub 类,并且实现了 HandlerCaller.Callback 接口;
由此可以确定 AccessibilityService 是一个系统服务,使用 Android 的跨进程通信技术。
在 IAccessibilityServiceClientWrapper 的构造方法中有两个较重要的参数:
- 一个是 looper,在 Accessibility Service 的实现中就是主线程,可使应用程序从 binder 线程回到主线程;
- 另一个是 Callbacks 类型的回调对象 callback,callback 参数初始化一个 HandlerCaller 对象。
HandlerCaller 的主要作用是通过持有 Context 所在线程的 Handler 实例对象,不断往 Context 所在线程的消息队列发送消息,然后在 Handler 实例对象的回调方法 handleMessage 中调用回调对象的 executeMessage 方法,而回调对象正是 IAccessibilityServiceClientWrapper,所以最终调用的是 IAccessibilityServiceClientWrapper 的 executeMessage 方法,具体流程如图 2 所示。
2、绑定和监控其它APP原理
Android 使用 Accessibility Service 监控其它应用程序并获得 UI 更改方法如下:
从 Google 的官方文档和 Android 相关源代码分析得到相关类及其关系如图3所示。
- 当用户在设置页面开启某个 Accessibility Service 的无障碍应用程序后,Android 系统会发送一条广播到 AccessibilityManager Service 系统服务;
- 收到该广播后,AccessibilityManager Service 会绑定开启的 Accessibility Service,即调用上文提到的继承自 Service 的 AccessibilityService 子类的onBind方法;
- 根据上文分析流程,在 AccessibilityService 内部,onBind 方法会创建并返回一个 IAccessibilityServiceClientWrapper 对象,开启 AIDL 跨进程通信。
- 当某个受到监控的应用程序 UI 发生改变时,Android 会通过 View 类中的 ViewRootImpl 对象调用其中的 AccessibilityManager 对象发送 AccessibilityEvent 事件;
- 发出的 AccessibilityEvent 事件会通过 Binder 类调用 AccessibilityService 的 onAccessibilityEvent 接口,并将 AccessibilityEvent 事件作为参数传递给 AccessibilityService 的具体实现类,即最终调用重写的 onAccessibilityEvent方法。
在 Android 的 Accessibility Service 实现框架中还有很重要的一点:UI 中的每一个 View 对象都会存储为一个 AccessibilityNodeInfo 对象。通过这个对象在 UI 节点树中可以查找到对应的 View 对象,并像操作原 View 对象一样对 AccessibilityNodeInfo 对象进行操作,例如执行点击操作,其本质是对应用中的元数据使用反序列技术,通过 AccessibilityInteractionClient 类实现 ,流程如图 4 所示。
安全性浅析
介绍完无障碍服务的实例程序,下面开始来看看无障碍服务的安全性问题。先引用下 Android Accessibility安全性研究报告(360烽火实验室) 的统计数据。
由于 Accessibility 的设计初衷只是面向于少数群体,长时间里属于一个较冷门的功能,但是近两年免 ROOT 自动安装和自动抢红包的出现,使得 Accessibility 进入了更多开发者的视野,不再被人们忽略,Accessibility 样本也从最开始的合理利用发展到用于提升用户体验,再到踩入了自动抢红包这种灰色地带,下图是带有 Accessibility 功能的恶意样本数量统计:
不难发现,随着 Accessibility 使用的普及,Accessibility 恶意样本的数量也在急速增加,注意上图中最后一列仅仅是 2016 年上半年的数量,换个维度,其实这个数量是 2015 年上半年的 245.3%,接近于 2015 年同期的 2.5 倍!而今是 2022 年 8 月,这个数据估计得大到惊人……
2.1 诱导用户授权
对于提供无障碍服务的应用,想要正常提供服务,必不可少的前提就是让用户手动点击确认、开启无障碍服务。但是通过前面的演示代码可以看到,“设置-辅助功能-无障碍” 处对于提高无障碍服务的应用的描述,是开发者可以自定义的,这导致了黑灰产可诱导用户进行授权。
比如 滥用Accessibility service自动安装应用 一文就介绍了一个相关案例。一款名为 “WiFi密码查看器(增强版)” 的应用滥用 Accessibility Service。应用启动后诱导用户开启 “WIFI信号增强服”,其实就是开启恶意应用自身的 Accessibility Service,为实现应用自动安装做铺垫。
2.2 批量灌装应用
在 2015 年里,各大应用市场均提供了 “免ROOT自动安装” 的功能选项,同时由于这个功能的推出,使得越来越多的开发者去探究了解 Accessibility 这项技术。免 ROOT 自动安装,又有“智能安装”的说法。应用市场在没 ROOT 权限的条件下,安装或更新软件时会弹出应用安装界面,而用户想要安装或更新多个应用时,需要用户多次主动去点击安装按钮,造成用户使用上的不便,免 ROOT 自动安装正为了解决用户希望免去反复的点击操作这个需求而产生。虽然此功能没有面向特殊人群而是面向了普遍用户,但是免去了用户更新软件时反复操作,提升了用户体验。
以 360 手机助手作为一个范例,用户手机即使没有 ROOT,开启了 360 手机助手的辅助功能以后,也可以方便地进行应用的批量安装、更新或卸载,不再需要用户繁琐地点击安装或卸载按钮。前面的实例程序也简单演示了如何通过无障碍服务实现自动安装一个 APK 的方法。免 ROOT 自动安装逻辑流程图如下:
需要注意的是,免 ROOT 自动安装应用虽然给用户提供了一个安装、更新应用的便捷体验,但也成为黑灰产进行批量恶意灌装应用的技术手段……灌装应用可以给部分 APP 提高日活量、抢占市场等,必然成为黑灰产关注的一个盈利点。
华为手机实际上对此做了防灌装的防护,默认不再采用 “com.android.packageinstaller” 作为安装器,而是使用 “com.huawei.appmarket” 替代,对于非来自华为应用市场的应用,会做安全提醒:
即使借助无障碍服务的模拟点击来触发“继续安装”流程,将提示用户输入锁屏密码进行确认(恶意程序无法借助无障碍服务来实现这点):
而如果当前手机未开启锁屏密码的情况下,则要求用户输入华为账户的密码才能继续安装。如果没登录华为账户,则需要进行图形验证码验证后才能安装……
华为这套对抗手段,可以说是不给黑灰产的灌装方法留一丝活路了。
2.3 自动化外挂类
借助无障碍服务对手机窗口组件的自动化点击的能力,除了实现上面提到的免 ROOT 安装外,更多地还被用于给类“外挂”,如微信自动抢红包。
回到 Accessibility 本身,将 Accessibility 服务用于自动抢红包,既没有面向特殊人群,也没有提升用户体验,已经背离了安卓官方的设计意义,而且自动抢红包软件具有外挂属性,会造成一定程度上的不公平现象,正如外挂软件一样难以判断其好坏性质一样,用于自动抢红包功能的实现代表着 Accessibility 的使用已经进入灰色地带。
Android之用AccessibilityService实现红包插件 一文介绍了微信自动抢红包功能的相关代码的实现,但是经本人测试发现并无法生效,微信红包的“开”按钮并无法被自动点击……我觉得这个完全在情理之中,毕竟微信团队不可能放纵这种“外挂”行为逍遥法外。
但是无障碍服务的自动化点击的能力的灰色利用远不仅仅微信抢红包这么简单……比如钉钉自动打卡签到、论坛自动点赞、支付宝能量自动收集……相关示例程序可以参见Github项目:AccessibilityServiceMonitor。
我不信所有 APP 目前都跟微信一样做了对抗……安全客上一篇文章:Android Accessibility点击劫持攻防 还提到了一个很有意思的案例:
补刀小视频和快手互为竞品应用,目标群体类似。而快手用户量级明显多于补刀,快手很多用户有互粉需求,于是补刀小视频开发了快手互粉助手来吸引快手用户安装,互粉助手这个功能主要是利用 Accessibility。
2.4 监视用户手机
前面的实例演示可以看到,无障碍服务可以随意监视用户手机当前正在运行的 APP(点击事件监听),从而统计用户的行为习惯,进一步可根据用户特征定向发送广告等。
另外更为致命的是,无障碍服务的应用可以监听到系统通知栏信息,那么用户的微信聊天信息,以及其他信息……此处不宜过多展开。
总结
了解了无障碍服务的使用方法和安全威胁以后,最后补充下几个防御手段:
1、检测 or 禁止相关外挂的辅助模式开启
以下代码可以查看目标应用正在被那些辅助模式监控或“辅助”:
/**
* 取得正在监控目标包名的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;
}
2、使用安全键盘防止监听与自动化操作
参照 2.2 章节华为对无障碍服务自动化安装 APP 的防范手段,可以采用安全键盘防止点击事件监听,同时可通过用户交互(输入锁屏密码、账户密码、图形验证码等)来防止自动化点击操作。
3、屏蔽UI组件的文本搜索、点击事件
上文对于自动化安装的实例代码的核心是借助 findAccessibilityNodeInfosByText
函数搜索到 UI 界面中“继续安装”的组件:
List<AccessibilityNodeInfo> installInfos = source.findAccessibilityNodeInfosByText("继续安装");
在没有探究 AccessibilityServices 源码之前,不了解 AccessibilityServices 检索文本信息原理的我们可能唯一能想到的应对措施就是将关键问题替换为图片。这可以解决问题,但是问题替换为图片不但会有性能上的损耗,而且会丢失大部分原本 TextView 的兼容特性。
在了解 AccessibilityServices 源码之后,我们知道 findAccessibilityNodeInfosByText
函数内部核心原理就是最终调用 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 上失效。
同理,上面实例程序对于自动点击的核心函数为 performAction
:
info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
像上面一样,通过源码了解原理之后,我们知道 AccessibilityServices 通过 performAction 函数执行点击事件最终在调用 View 的 mOnClickListener 。那我们只需要在这上面做文章就好了,最快捷的办法是利用 onTouch 代替 onClick。我怀疑微信当前对于抢红包外挂的防御方案就是这么做的……
本文参考文章: