您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

Android悬浮窗的坑

时间:02-04来源:作者:点击数:

前言

重构了公司的一个项目,有一处用到了悬浮窗,就是程序后台运行后,悬浮窗依然可以显示到界面上。重构完准备上线,后来发现在6.0手机上有问题,无法显示,百般对比重构之前的代码,总感觉没什么不一样了呀,为什么别人代码写乱七八糟的都可以,我的写么好竟然不行。花了好多时间,百度什么的也找不到原因,真的是花了好多时间才找到了原因,这里记录一下解决问题的思路。

悬浮窗权限

使用悬浮窗需要申请权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
class MainActivity : AppCompatActivity() {
    
    val requestFloatWindowPermissionCode = 100

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !Settings.canDrawOverlays(this)) {
            val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
            startActivityForResult(intent, requestFloatWindowPermissionCode)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == requestFloatWindowPermissionCode 
            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
            if (Settings.canDrawOverlays(this)) {
                Log.i("ABC", "用户给予悬浮窗权限了")
            } else {
                Log.i("ABC", "用户没给悬浮窗权限")
            }
        }
    }
}

Android6.0的坑

我有个解决问题的技巧就是写个小Demo,这样就没有其他不相关的代码,Demo里只写关于悬浮窗的代码,新建一个项目,语言选Kotlin,最小SDK为15,其它都默认,我用的是最新版本的AndroidStudio,什么Gradle什么鬼的都是更新到了最新的,项目创建好之后打开MainActivity,写代码,如下:

class MainActivity : AppCompatActivity() {

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            val button = Button(this).apply { text = "你好"; setOnClickListener {
                Toast.makeText(context, "我不好", Toast.LENGTH_SHORT).show()
            } }

            val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
            windowManager.addView(button, WindowManager.LayoutParams().apply {
                type = WindowManager.LayoutParams.TYPE_TOAST
                flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                width = WindowManager.LayoutParams.WRAP_CONTENT
                height = WindowManager.LayoutParams.WRAP_CONTENT
                gravity = Gravity.START or Gravity.BOTTOM
                format = PixelFormat.TRANSLUCENT
            })
        }
        return super.onTouchEvent(event)
    }

}

对,你没看错,就是onCreate方法都不要,奇怪吧,我就是要代码最小化,不相关的代码都不要,Activity是可以不要onCreate方法的,长知识了吧。这里只关注Android6.0版本,所以版本权限啊什么的不用管,上面代码中把一个Button设置为悬浮窗的界面内容,Buttion的点击事件是弹出一个Toast,悬浮窗的位置是在左下角,代码特别整洁特别少吧!运行后是一个空白的界面,如下:

在这里插入图片描述

点击界面的任意位置,此时在左下角会弹出悬浮窗,如下:

在这里插入图片描述

此时点击“你好”是可以弹出Toast的。

按下返回键,我们发现悬浮窗也不见了。重新运行并点击屏幕,再次出现悬浮窗,此时按Home键,这样创建悬浮窗的Activity就没有销毁,此时发现悬浮窗还在,如下:

在这里插入图片描述

此时点击“你好”,发现不起作用,但是这个代码运行到Android7.0是完全没问题的,这就是神奇之处,在这里花了我好多时间,百度了好多文章也找不到答案,最后实在没办法又去对比了比了n多次重构前的代码,最后发现有一个地方不同,就是获取WindowManager的地方,如下:

val windowManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager

这里是使用application来获取的系统服务,谁能想到啊!!万万也想不到啊,按我之前学习的理解,系统服务一般是单例的,在哪里获取都一样,没想到activity.getSystemService与application.getSystemService竟然是有区别的,一万匹吃草的马跑过,浪费我一天的时间啊,这Android真的是,无处不在的坑!!

使用了application来获取WindowManager之后,问题就解决了,就算结束activity,只要不结束app,悬浮窗都可以显示,而且应用后台运行后,悬浮窗的点击事件也是正常的。

最后试验了下各种上下文获取的WindowManager对象,如下:

println("activity: ${getSystemService(Context.WINDOW_SERVICE)}")
println("activity: ${getSystemService(Context.WINDOW_SERVICE)}")
println("baseContext: ${baseContext.getSystemService(Context.WINDOW_SERVICE)}")
println("application: ${application.getSystemService(Context.WINDOW_SERVICE)}")
println("applicationContext: ${applicationContext.getSystemService(Context.WINDOW_SERVICE)}")

经过经验发现,只要是不同的对象,获取的WindowManager就不是同一个对象,如果是同一个对象,则获取的就是同一个,比如在一个Activity里面多次调用getSystemService来获取WindowManager对象将是同一个,在不同的Activity里面获取的将是不同的对象。而application和applicationContext获取的是相同的,并且它们是全局的,所以不论在哪个Activity中调用application去获取都将得到同一个WindownManager对象。

Android7.0的坑

因为我们公司只需要适配Android6.0和Android7.1.1,所以这里把Android7.1.1的悬浮窗实现也记录一下。

上面的代码跑到Android7.1.1中会报如下错误:

WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

这个问题比较好解决,百度也会有相应的答案,从Android7.1.1(api25)开始,不能使用TOAST类型了,因为此类型是不需要申请权限的,太流氓了,未经用户同意就可以显示悬浮窗有点流氓,所以要改成使用PHONE类型,此类型需要悬浮窗权限的,官方说明在此,我们在代码中加入动态权限申请:

// 如果是7.1.1以上的系统(包含7.1.1),则需要判断是否有悬浮窗权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !Settings.canDrawOverlays(this)) {
    val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
    startActivity(intent)
}

这段代码会判断是否是7.0以上的系统,如果是就判断是否有悬浮窗权限,如果没有就打开系统设置中权限申请界面,如下图:

在这里插入图片描述

如上图,需要指定我们的应用为“可出现在其他应用上的应用”,也就是添加悬浮窗权限,但是这里并没有显示出我们的应用,这是因为我们的应用并没有在清单文件中声明悬浮窗权限,如下:

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

添加这个权限之后,系统的申请悬浮窗界面就会列出我们的应用了,如下:

在这里插入图片描述

如上图,可以看到我们的应用的悬浮窗权限是“不允许”,我们设置为“允许”,再次运行,发现还是报这个错误:

WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

问题的原因是Android7.1.1及以上的系统不能使用TYPE_TOAST这个类型了,需要使用TYPE_PHONE,代码如下:

// 如果是7.1.1以上的系统(包含7.1.1),则需要使用TYPE_PHONE类型
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) WindowManager.LayoutParams.TYPE_PHONE
       else WindowManager.LayoutParams.TYPE_TOAST

最后有个小提示,我发现我们公司的那个app运行到android7.1.1手机上并不会跳出申请悬浮窗权限的界面,这是因为我们的app设置的targetSdkVersion为21(Android5.0),而动态权限申请是从Android6.0开始的,所以为了兼容旧版本,只要targetSdkVersion小于6.0就不需要动态申请权限,只需要在清单文件中声明即可。不过有些手机可能定制过的,即使targetSdkVersion小于6.0也会弹出隐私权限的动态申请对话框。

Android8.0的坑

type要使用WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,否则会报如下异常:

permission denied for window type 2002

type的赋值逻辑如下:

type = when {
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 -> WindowManager.LayoutParams.TYPE_PHONE
    else -> WindowManager.LayoutParams.TYPE_TOAST
}

Toast攻击

转载于:https://unit42.paloaltonetworks.com/unit42-android-toast-overlay-attack-cloak-and-dagger-with-no-permissions/https://www.trendmicro.com/en_us/research/17/k/toast-overlay-weaponized-install-android-malware-single-attack-chain.html

Palo Alto Networks 42部门的研究人员发现了Android覆盖系统中的一个严重漏洞,该漏洞可以通过使用“ Toast类型”覆盖来进行新的Android覆盖攻击。所有操作系统版本低于8.0的Android设备均受此漏洞影响,并且补丁程序作为2017年9月Android安全公告的一部分提供。Android 8.0刚刚发布,不受此漏洞的影响。因为Android 8.0是最新的,所以此漏洞会影响当前市场上几乎所有的Android设备(请参阅表1),并且用户应尽快应用更新。

覆盖攻击使攻击者可以利用受影响的设备上运行的其他窗口和应用程序。要发起此类攻击,恶意软件通常需要请求“在顶部绘制”权限。但是,此新发现的覆盖攻击不需要任何特定权限或条件即可生效。发起此攻击的恶意软件不需要拥有覆盖权限,也不需要从Google Play安装。借助这种新的覆盖攻击,恶意软件可以诱使用户启用Android Accessibility Service并授予设备管理员特权或执行其他危险操作。如果授予了这些特权,则可以在设备上发起许多强大的攻击,包括窃取凭据,以静默方式安装应用程序以及为赎金而锁定设备。

这项研究的灵感来自加州大学圣塔芭芭拉分校的Yanick Fratantonio和佐治亚理工学院的Chenxiong Qian,Simon P. Chung,Wenke Lee(PDF链接)发表的论文“ 斗篷和匕首:从两种权限到对UI反馈循环的完全控制 ” 。本文最近在2017年5月的IEEE安全与隐私2017大会上发表。本文提出了几种创新的可访问性攻击,但假设恶意应用程序必须明确请求两个权限并从Google Play安装。我们的新研究表明,即使该应用不是来自Google Play,并且仅声明一个权限“ BIND_ACCESSIBILITY_SERVICE”,本文中提出的可访问性攻击也可以成功启动。

与Cloak和Dagger一样,此覆盖攻击通过修改屏幕区域以更改用户看到的内容,诱使他们启用其他权限或识别其输入来起作用。有关此攻击进行中的视频可在此处获得:

此漏洞已分配给CVE-2017-0752,并且已在9月的Android安全公告中披露。

零条件下的叠加攻击

使用Toast的新叠加攻击

“ Toas t”窗口(TYPE_TOAST)是Android上受支持的覆盖类型之一。Toast叠加层通常用于在所有其他应用程序上显示快速消息。例如,一条消息,指示用户在不发送电子邮件的情况下离开时将电子邮件另存为草稿。它自然会继承所有其他Windows类型的配置选项。但是,我们的研究发现,使用Toast窗口作为覆盖窗口可以使一个应用程序编写另一个应用程序的界面,而无需请求通常需要的SYSTEM_ALERT_WINDOW特权。

通过此发现,已安装的应用程序可以使用Toast窗口在屏幕上制作覆盖图。这样,应用程序可以在没有任何特殊权限的情况下发起覆盖攻击。精巧的叠加层包括两种视图(图1),这两种视图都嵌入在Toast窗口中。在下面的示例中,view1覆盖了底部的GUI并监视用户单击以推断攻击进度,而view2是可单击的视图,攻击者试图诱使受害者单击。


图1:使用Toast窗口制作叠加层

Android OS版本<= 7.0上的漏洞

此漏洞是由于缺少权限检查引起的。Android AOSP(版本<= 7.0)中的相关代码段如图2所示。通常,将窗口覆盖在其他应用程序的顶部需要权限检查和操作检查。但是,对于TYPE_TOAST,这些检查都没有。该请求将被自动批准。根据图2中的内联注释,该应用将被授予对TYPE_TOAST窗口的完全控制权。


图2:没有对TYPE_TOAST的权限检查

Android OS 7.1版上的漏洞

Android 7.1引入了两层缓解措施:超时和一次每个UID的单个Toast窗口(请参阅表1)。第一个缓解措施为每个Toast窗口强行指定最大超时(3.5s)(图3)。超时后,Toast窗口将消失以模仿Android上的正常Toast行为。毫不奇怪,这可以通过故意对齐的重复弹出Toast窗口来克服。对于第二个缓解措施,Android 7.1允许每个应用一次只显示一个Toast窗口(图4)。这两种防御机制对使用Toast窗口覆盖物欺骗受害者构成了挑战。但是,它不能解决根本原因:一个应用程序不需要任何许可就可以在其他任何应用程序顶部显示Toast窗口。


图3:Android 7.1中Toast窗口的超时(缓解1)

图4:在Android 7.1中,每个UID仅允许一个Toast窗口(缓解2)

对于Android 7.1,要实现相同的覆盖攻击,恶意应用程序可以使用LooperThread连续显示Toast窗口(图5)。由于只能同时使用一个叠加层,因此恶意应用无法监视用户是否单击了叠加层中的预期区域。另一种方法是显示一个覆盖图,以吸引用户单击它,休眠几秒钟,然后切换到另一个覆盖图以供将来使用。显然,此缓解措施已降低了覆盖攻击的成功率。这种替代方法也适用于2.3.7至4.3的Android版本。因为,标志“ FLAG_WATCH_OUTSIDE_TOUCH”已在Toast窗口中被强制删除(图6)。

幸运的是,我们已经确认,在Android O(8.0)Beta中已添加了适当的权限检查。


图5:通过循环绕过超时限制

图6:在版本2.3.7 – 4.3中删除了FLAG_WATCH_OUTSIDE_TOUCH标志
版本 Distribution Without permission Monitor outside touch 超时 Multiple overlays for same UID Verified in real devices
2.3.3
2.3.7
0.7% Yes Yes(No in 2.3.7) No Yes No
4.0.3
4.0.4
0.7% Yes No No Yes Yes
4.1.x 2.7% Yes No No Yes No
4.2.x 3.8% Yes No No Yes No
4.3 1.1% Yes No No Yes No
4.4 16.0% Yes Yes No Yes Yes
5.0 7.4% Yes Yes No Yes Yes
5.1 21.8% Yes Yes No Yes Yes
6.0 32.3% Yes Yes No Yes Yes
7.0 12.3% Yes Yes No Yes Yes
7.1 1.2% Yes Yes Yes (3.5 s) No Yes
8.0 0.0% No _ _ _ Yes

表1:Android版本中的缓解重叠攻击

覆盖攻击的可能后续措施

利用上述漏洞,可以成功执行“披风和匕首”文件中演示的所有可访问性攻击。此外,在这里我们展示了使用TYPE_TOAST浮动窗口的其他几种实用攻击。

通过设备管理员的攻击

通过覆盖攻击,已安装的恶意应用程序可以诱骗用户为应用程序提供设备管理员权限。这样,它将具有发起破坏性攻击的能力,包括:

  • 锁定设备屏幕
  • 重置设备PIN
  • 清除设备数据
  • 防止用户卸载应用程序

诱使用户授予设备管理员权限的一种方法是发起Clickjacking覆盖攻击。野生的恶意软件已经发起了这种类型的攻击。如图7所示,恶意软件显示带有“继续”按钮的“安装完成”对话框。但是,此对话框实际上是TYPE_SYTEM_OVERLAY窗口,其下方是设备管理员激活对话框。从Android API文档中,TYPE_SYSTEM_OVERLAY是“ 系统覆盖窗口,需要将其显示在其他所有内容的顶部 ”和“ 这些窗口不得获取输入焦点 ”。因此,一旦用户单击“继续”按钮,单击事件实际上就会发送到真实设备管理员激活窗口上的“激活”按钮。

使用TYPE_TOAST窗口进行的攻击也可以实现这一点,将查看标志设置为FLAG_NOT_FOCUSABLE和FLAG_NOT_TOUCHABLE,我们可以在没有任何特殊权限的情况下发起类似的攻击。


图7 Android恶意软件使用Clickjacking覆盖进行活动设备管理

储物柜和勒索软件攻击

Android Locker和Ransomware已经流行了很多年。大多数Android Ransomware通过以下方法实现屏幕锁定:

SYSTEM_ALERT_WINDOW:具有此权限的Android应用程序可以在其他任何应用程序顶部显示一个浮动窗口。通过设置适当的窗口类型和视图标志(例如TYPE_SYSTEM_ERROR,TYPE_SYSTEM_OVERLAY和FLAG_FULLSCREEN),这种浮动窗口将无法被用户移动。此技术可防止用户访问其设备。

设备管理员:具有此特权的Android应用程序可以重置屏幕密码,然后锁定设备屏幕。如果屏幕被锁定并且PIN重置,则受感染的设备会成为受害者的砖头。

使用TYPE_TOAST类型的窗口和默认的视图标志,我们不需要任何额外的权限即可通过显示全屏浮动窗口来实现屏幕锁定的效果,并且用户无法删除这种窗口。尽管在Android 7.1上显示TYPE_TOAST窗口是有时间限制的,但是我们可以如前所述连续不断地循环弹出Toast窗口。因此,我们可以绕过Android 7.1上的限制。

整治

我们的团队于2017 年5月30 日向Google报告了此漏洞,Google迅速验证了该漏洞的存在。Google于2017 年9月5 日修补并披露了此漏洞。

有关补丁的更多信息,请参见Android安全公告。

我们建议所有用户为其Android设备部署补丁,以保护自己免受可能利用此漏洞的攻击者的攻击。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门