本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

0x1、引言

Hi,我是杰哥,在上一节《AccessibilityService实战-微信僵尸老友检测》中带咱们利用所学的AccessibilityService基础知识,借鉴实在老友假转账的原理,实现了自己的专属微信僵尸老友检测东西。相信仔细学完的读者对于自定义无障碍服务的开发流程都了然于胸,今后随手写个自动化小东西估摸着也是手到擒来了~

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御

本节主要是拾遗,补充两点锦上添花的小细节:AccessibilityService实战的保活与防护。不哔哔,直接开始~


0x2、无障碍服务保活

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御

运用保活,陈词滥调的话题了,最早能够追溯到7年前的一个库 MarsDaemon,双进程看护,简略装备几行代码,即可实现进程常驻。

不过好景不长,Android 8.0后这个库就废掉了,后面陆续出现了很多保活的 骚操作,如 1个像素的Activity播映无声音频 等。

// 例:1像素的Activity
class OnePxActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        window.attributes = window.apply { setGravity(Gravity.START or Gravity.TOP) }
            .attributes.apply {
                width = 1
                height = 1
                x = 1
                y = 1
            }
    }
}

当然这些骚操作,并不太通用靠谱,究竟哪个厂商的底层不魔改一下,每家都有自己的一套办理体系。说句大真话:终极保活的技巧便是钞才能——花钱进厂商白名单

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御

没有钞才能也不要紧,有一些通用可行的小技巧,能够提高你的APP的优先级,降低进程被杀的概率~


① 前台服务

把本来处于后台运转AccessibilityService设置为前台服务,需求在AndroidManifest.xml清单文件中声明下述权限:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

不然会报反常:

java.lang.SecurityException: Permission Denial:
 startForeground from pid=2345, uid=10395 requires android.permission.FOREGROUND_SERVICE.

接着在 onCreate() 办法中创立Notification途径,并敞开前台服务,在 onDestory() 办法中中止前台服务,直接给出东西代码,读者按需修正即可:

class ClearCorpseAccessibilityService : AccessibilityService() {
        ...
        override fun onCreate() {
        super.onCreate()
        // 创立Notification途径,并敞开前台服务
        createForegroundNotification()?.let { startForeground(1, it) }
    }
    override fun onDestroy() {
        // 中止前台服务
        stopForeground(true)
        super.onDestroy()
    }
    private fun createForegroundNotification(): Notification? {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
            // 创立告诉途径,一定要写在创立显示告诉之前,创立告诉途径的代码只要在第一次履行才会创立
            // 今后每次履行创立代码检测到该途径已存在,因而不会重复创立
            val channelId = "前台告诉id名,任意"
            notificationManager?.createNotificationChannel(
                NotificationChannel(
                    channelId,
                    "前台告诉称号,任意",
                    NotificationManager.IMPORTANCE_HIGH // 发送告诉的等级,此处为高
                ).apply {
                    // 下述都是非必要的,看自己需求装备
                    enableLights(true)  // 假如设备有指示灯,敞开指示灯
                    lightColor = Color.GREEN    // 设置指示灯色彩
                    enableVibration(true)   // 敞开轰动
                    vibrationPattern = longArrayOf(100, 200, 300, 400)  // 设置轰动频率
                    setShowBadge(true)  // 是否显示角标
                    setBypassDnd(true)  // 是否绕过免打扰形式
                    lockscreenVisibility = Notification.VISIBILITY_PRIVATE  // 是否在锁屏屏幕上显示此频道的告诉
                }
            )
            return NotificationCompat.Builder(this, channelId)
                // 设置点击notification跳转,比方跳转到设置页
                .setContentIntent(
                    PendingIntent.getActivity(
                        this,
                        0,
                        Intent(this, SettingActivity::class.java),
                        FLAG_IMMUTABLE
                    )
                )
                .setSmallIcon(R.drawable.ic_service_enable) // 设置小图标
                .setContentTitle("告诉标题")
                .setContentText("告诉内容")
                .setTicker("告诉提示语")
                .build()
        }
        return null
    }
    ...
}

运转后,在顶部告诉栏能够看到前台服务的Notification:

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御


② 撤销电池优化约束

Android 6.0后为了省电,增加了休眠形式,体系待机一段时刻后会杀死后台正常运转的进程,但体系会有一个 后台运转白名单

前期的原生体系中,顺次点击:设置 → 电池 → 电池优化 → 未优化运用,能够看到这个白名单。

而在后续的体系中(如我的Android 10),得去 运用和告诉 找:找到自己的运用点击电池后台约束

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御

接着是:判别APP是否受电池优化约束请求撤销电池优化约束 的东西代码:

// 需在AndroidManifest.xml中增加下述权限
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
// 判别APP是否被约束
@RequiresApi(api = Build.VERSION_CODES.M)
private fun isIgnoringBatteryOptimizations() =
    (getSystemService(Context.POWER_SERVICE) as PowerManager)
        .isIgnoringBatteryOptimizations(packageName)
// 请求撤销约束
@RequiresApi(api = Build.VERSION_CODES.M)
fun requestIgnoreBatteryOptimizations() {
    try {
        startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
            data = Uri.parse("package:$packageName")
        })
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

请求时会有这样的弹窗 (不同手机体系各有差异):

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御


③ 引导用户敞开自发动

先是判别运用 是否敞开自发动权限,很惋惜,笔者并没有找到 通用且公开的API,只找到有人经过 反射办法取得的,测验了一下,并不靠谱。

所以一个折中的计划:存一个自发动引导页是否打开的符号,弹之前判别下弹过没,没弹过就弹,弹过就不弹,假如弹了就修正下符号。

接着是跳转到设置页,由于厂商对体系的不同定制,导致 敞开自发动 (有些都不叫这个姓名) 的设置进口就五花八门,所以需求开发者依据不同品牌机型自行适配。

先判别是哪家的手机,然后跳转对应的设置页,笔者依据网上的几篇文章,简略地整理了一下 (因笔者测验机有限,未能全部测验覆盖,不对的欢迎谈论区提出):

object RomUtil {
    // 体系名
    const val ROM_MIUI = "MIUI" // 小米
    const val ROM_EMUI = "EMUI" // 华为
    const val ROM_OPPO = "OPPO" // OPPO
    const val ROM_VIVO = "VIVO" // VIVO
    const val ROM_SMARTISAN = "SMARTISAN"   // 锤子
    const val ROM_FLYME = "FLYME"   // 魅族
    const val ROM_QIKU = "QIKU" // 360
    // 对应体系有的特点
    private const val KEY_VERSION_MIUI = "ro.miui.ui.version.name"
    private const val KEY_VERSION_EMUI = "ro.build.version.emui"
    private const val KEY_VERSION_OPPO = "ro.build.version.opporom"
    private const val KEY_VERSION_SMARTISAN = "ro.smartisan.version"
    private const val KEY_VERSION_VIVO = "ro.vivo.os.version"
    // getprop指令去体系build.prop查找是否有对应特点来判别
    private fun getProp(name: String): String? {
        val line: String?
        var input: BufferedReader? = null
        try {
            val process = Runtime.getRuntime().exec("getprop $name")
            input = BufferedReader(InputStreamReader(process.inputStream), 1024)
            line = input.readLine()
            input.close()
        } catch (ex: IOException) {
            Log.e(TAG, "Unable to read prop $name", ex)
            return null
        } finally {
            if (input != null) {
                try {
                    input.close()
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
        }
        return line
    }
    // 判别体系的办法
    private fun check(rom: String): Boolean {
        val tempRom: String?
        if (!getProp(KEY_VERSION_MIUI).isNullOrBlank()) {
            tempRom = ROM_MIUI
        } else if (!getProp(KEY_VERSION_EMUI).isNullOrBlank()) {
            tempRom = ROM_EMUI
        } else if (!getProp(KEY_VERSION_OPPO).isNullOrBlank()) {
            tempRom = ROM_OPPO
        } else if (!getProp(KEY_VERSION_VIVO).isNullOrBlank()) {
            tempRom = ROM_VIVO
        } else if (!getProp(KEY_VERSION_SMARTISAN).isNullOrBlank()) {
            tempRom = ROM_SMARTISAN
        } else {
            val version = Build.DISPLAY
            tempRom = if (version.uppercase().contains(ROM_FLYME)) {
                ROM_FLYME
            } else {
                Build.MANUFACTURER.uppercase()
            }
        }
        return rom == tempRom
    }
    fun isXiaomi() = check(ROM_MIUI)
    fun isHuawei() = check(ROM_EMUI)
    fun isVivo() = check(ROM_VIVO)
    fun isOppo() = check(ROM_OPPO)
    fun isFlyme() = check(ROM_FLYME)
    fun is360() = check(ROM_QIKU) || check("360")
    fun isSmartisan() = check(ROM_SMARTISAN)
    // 打开自发动设置页
    fun openStart(context: Context) {
        if (Build.VERSION.SDK_INT < 23) return
        var intent = Intent()
        var componentName: ComponentName? = null
        when {
            isXiaomi() -> {
                componentName = ComponentName(
                    "com.miui.securitycenter",
                    "com.miui.permcenter.autostart.AutoStartManagementActivity"
                )
            }
            isHuawei() -> {
                componentName = ComponentName(
                    "com.huawei.systemmanager",
                    "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"
                )
            }
            isOppo() -> {
                componentName = if (Build.VERSION.SDK_INT >= 26) {
                    ComponentName(
                        "com.coloros.safecenter",
                        "com.coloros.safecenter.startupapp.StartupAppListActivity"
                    )
                } else {
                    ComponentName(
                        "com.color.safecenter",
                        "com.color.safecenter.permission.startup.StartupAppListActivity"
                    )
                }
            }
            isVivo() -> {
                componentName = if (Build.VERSION.SDK_INT >= 26) {
                    ComponentName(
                        "com.vivo.permissionmanager",
                        "com.vivo.permissionmanager.activity.PurviewTabActivity"
                    )
                } else {
                    ComponentName(
                        "com.iqoo.secure",
                        "com.iqoo.secure.ui.phoneoptimize.SoftwareManagerActivity"
                    )
                }
            }
            isFlyme() -> {
                componentName = ComponentName.unflattenFromString(
                    "com.meizu.safe/.permission.PermissionMainActivity"
                )
            }
            else -> {
                if (Build.VERSION.SDK_INT >= 9) {
                    intent.action = "android.settings.APPLICATION_DETAILS_SETTINGS";
                    intent.data = Uri.fromParts("package", context.packageName, null);
                } else if (Build.VERSION.SDK_INT <= 8) {
                    intent.action = Intent.ACTION_VIEW
                    intent.setClassName(
                        "com.android.settings",
                        "com.android.settings.InstalledAppDetails"
                    );
                    intent.putExtra(
                        "com.android.settings.ApplicationPkgName",
                        context.packageName
                    )
                }
                intent = Intent(Settings.ACTION_SETTINGS)
            }
        }
        componentName?.let { intent.setComponent(it) }
        try {
            context.startActivity(intent)
        } catch (e: Exception) {
            // 抛出反常的话直接打开设置页
            context.startActivity(Intent(Settings.ACTION_SETTINGS))
        }
    }
}

④ 引导用户在多任务列表窗口加锁

如题,引导用户对 多任务列表的APP窗口加锁,这样点击将清理加速时不会导致运用被杀,如:

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御

别的,还有一个骚操作:在多任务列表把App窗口给隐藏了,避免用户手多划掉,东西代码如下:

fun Context.hideAppWindow(isHide: Boolean) {
    try {
        (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)
            .appTasks[0].setExcludeFromRecents(isHide)
    } catch (e: Exception) {
        //...
    }
}

⑤ 引导用户打开APP后台高耗电开关

部分厂商的手机有这个(如Vivo),设置办法:设置 → 电池 → 后台高耗电 → 找到自己的APP敞开


0x3、无障碍服务防护

在一开始学习AccessibilityService的时候就说到过,这个服务设计的初衷是:为了帮助残障人士能够更好的运用App

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御

而在国内一些开发者利用它 能监控与操作其它APP的特性 + 体系远超人类的反应速度,在某些竞赛类场景开发出了 做弊外挂,如抢单、秒杀等,对本来公正的竞赛环境产生不公。

作为一名一般的Android开发者,仍是要居安思危,指不定哪天自己开发的APP也会惨招毒手,提早了解一些AccessibilityService的防护办法,不至于真产生时手足无措~


① 检测用户是否装置外挂软件

树立外挂软件黑名单PackageManager遍历手机已装置的APP,判别是有有黑名单里的包名和运用名,有给个提示,然后退出APP。

但,这需求权限,而且触及到了隐私,所以,能够尝试换个思路 → 检测监控包名的AccessibilityService

能够经过 AccessibilityManagerService 获取所有已装置及已发动的AccessibilityService运用,而它是com.android.server.accessibility包下的类,无法直接运用。但能够经过 AccessibilityManager 来直接操作(Binder)。提供了两个获取 List<AccessibilityServiceInfo> 的办法:

  • getInstalledAccessibilityServiceList() → 取得所有已装置的AccessibilityService;
  • getEnabledAccessibilityServiceList() → 取得所有已发动的AccessibilityService;

以第一个获取办法为例,写出遍历的东西代码:

// 取得正在监控方针包名的AccessibilityService
fun getInstalledAccessibilityServiceList(targetPackage: String): List<AccessibilityServiceInfo> {
    val serviceList = arrayListOf<AccessibilityServiceInfo>()
    val manager =
        applicationContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
            ?: return serviceList
    val infoList = manager.installedAccessibilityServiceList
    if (infoList.isNullOrEmpty()) return serviceList
    infoList.forEach {
        if (it.packageNames == null) serviceList.add(it) else {
            it.packageNames.forEach { pkgName ->
                if (targetPackage == pkgName) serviceList.add(
                    it
                )
            }
        }
    }
    return serviceList
}

简略调用下 (检测监听微信的无障碍服务有哪些):

getInstalledAccessibilityServiceList("com.tencent.mm").forEach { info ->
    logD(
        " \n【监听的包名 (null代表所有)】${info.packageNames?.toList()}\n【监听的服务】${info.id}\n【设置页】${info.settingsActivityName}\n【服务描述信息】${
            info.description.replace("\n", "").replace(" ", "")
        }"
    )
}

运转后控制台输出信息如下 (注:info.packageNames为null表示监控所有包名):

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御

能够看到手机装置的所有监听微信的无障碍服务App信息都被打印出来了,接着便是查看这儿面有没有外挂黑名单里的包名了。

至于检测机遇,能够守时或许在特守时刻节点进行,尽量别只在App发动时,究竟用户能够先发动App然后再打开外挂。别的,检测时也能够顺带把觉得能够的App信息也上签到后台,用于完善黑名单。

当然,这种 检测到就不给用的策略 有些过于粗犷,有时还可能造成误伤,究竟运用包名只要不上架商场,随意起啊,你封一次我改一次,所有还得从App自身触发去防护~

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御


② 重写TextView的findViewsWithText()屏蔽案牍查看

咱们知道AccessibilityServices中定位节点的两种惯例办法,一个是id,一个是依据text文本,后者 findAccessibilityNodeInfosByText() 最终调用的实践是View的 findViewsWithText()。只需对这个办法进行重写即可屏蔽案牍查看,代码示例如下:

class DefensiveTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
    AppCompatTextView(context, attrs) {
    override fun findViewsWithText(
        outViews: ArrayList<View>?,
        searched: CharSequence?,
        flags: Int
    ) {
        outViews?.remove(this)
    }
}

③ 屏蔽点击事件

上面是屏蔽找的,接着是屏蔽点击时刻的,由于AccessibilityServices履行点击最终会调用View的OnClickListener回调onClick()。所以,一种最直接的办法便是自定义View,然后 用onTouch()替换onClick()

除此之外还有别的一种办法 → 重写performAccessibilityAction()返回true,以此疏忽掉AccessibilityService传递过来的事件。实现办法的话,除了自定义View重写外,还能够调用 setAccessibilityDelegate() 对控件进行设置,直接给出设置的扩展代码,用时直接调就好:

// 控件是否屏蔽无障碍相关
fun View.disableAccessibility(disable: Boolean = true) {
    if (!disable) {
        this.accessibilityDelegate = null
    } else {
        this.accessibilityDelegate = object : View.AccessibilityDelegate() {
            override fun performAccessibilityAction(
                host: View?,
                action: Int,
                args: Bundle?
            ): Boolean {
                // performAction办法触发的行为,拦截View响应无障碍服务模仿事件的API
                return true
            }
            override fun sendAccessibilityEvent(host: View?, eventType: Int) {
                // 篡改或屏蔽View发送的无障碍事件
            }
            override fun onInitializeAccessibilityEvent(
                host: View?,
                event: AccessibilityEvent?
            ) {
                // 阻止View生成AccessibilityNodeInfo, 然后防止无障碍抓取到内容
            }
            override fun onInitializeAccessibilityNodeInfo(
                host: View?,
                info: AccessibilityNodeInfo?
            ) {
                // 阻止View发送出去的AccessibilityEvent
            }
            override fun dispatchPopulateAccessibilityEvent(
                host: View?,
                event: AccessibilityEvent?
            ): Boolean {
                // 阻止 AccessibilityEvent 向子 View 传递
                return false
            }
            override fun onRequestSendAccessibilityEvent(
                host: ViewGroup?,
                child: View?,
                event: AccessibilityEvent?
            ): Boolean {
                // 阻止子View请求发送无障碍事件音讯
                return false
            }
        }
    }
}
// 调用处:
button.disableAccessibility()

主要是重写setAccessibilityDelegate(),其它计划可按需增删~

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御

对了,泼个冷水哈,上述两种屏蔽办法,都能够经过上一节教的 手势模仿点击 来破解~


④ 自动发送Event搅扰

咱们都知道AccessibilityServices的玩法其实便是:监听方针APP发出的AccessibilityEvent来履行相应操作。

而在APP里,其实能够调用View的 sendAccessibilityEvent() 来自动发送Event,所以一种防护的思路便是闲来无事发几个Event,尝试搅扰外挂程序的正常逻辑。代码示例如:

textview.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
button.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOWS_CHANGED)

不过,这个操作其实有些鸡肋,究竟收到Event后都是检测页面是否有特定因素,然后再履行下一步的。

道高一尺,魔高一丈,上面说到的防护技巧都是有办法绕过的,比方你屏蔽了案牍查看,那我就OCR文字辨认,乃至依据图片匹配。个人感觉还得是 风控,收集用户操作记录,检测到反人类的反常行为时告警,如每次点击都是点一个坐标,如页面操作时刻超短等等。


0x4、小结

本节对 AccessibilityService保活和防护 相关进行了学习,相信咱们学完也会有所裨益。关于AccessibilityService的知识点,就差一篇 源码解读 了,但不影响咱们学习开发自动化脚本,所以将会在本专栏末尾进行讲解。而下节会讲一下运用 AccessibilityService 的最佳拍档 —— Android悬浮框,它两的关系可谓是:吃面不吃蒜,香味少一半。敬请期待~

【杰哥带你玩转Android自动化】AccessibilityService拾遗-保活与防御


参考文献

  • AutoStartUtil【打开自发动设置界面】

  • AccessibilityService剖析与防护

  • 专栏:Android 无障碍相关