type
status
date
slug
summary
tags
category
icon
password
0x00 参考0x01 引言0x02 问题说明1. 概述2. Target Running State Detect3. Background Activity Launch (BAL)4. Background Persistent Processaaass
0x03 Bypass BAL1. Analyse BAL Restriction2. PIP: Picture In Picture画中画3. Attack - SourceRectHint To EoP(CVE-2023-40116)4. Attack - ActivityOptions To EoP(CVE-2023-21269)5. 未完结的故事-画中画 CVE-2024-347370x04 Side-Channel For Running-State1. Abuse startService2. Abuse ApplicationInfo0x05 Breaking LMKD & BEL1. Privilege Process & High Priority2. Abuse AccountManager
0x00 参考
0x01 引言
之前对Android 悬浮窗覆盖攻击(也属于
Activity Hijack Attack(AHA)
的一种)有了一个简单的了解,但是其利用较为困难,通常需要申请悬浮窗权限,并且并不是一个完整的攻击链条。看到BlackHat2024的议题
SystemUI As EvilPiP: The Hijacking Attacks on Modern Mobile Devices
后决定沿着这条路继续学习,因此之后的内容大部分内容引用与改议题和参考1 中奇安信的报告。除此之外,在Bypass BAL一节中引入对新爆出画中画漏洞CVE-2024-34737 的说明。再次声明:本文仅作学习记录,大量内容引用自https://research.qianxin.com/archives/2014,详细内容请阅读原文,再次感谢议题作者们的杰出工作和无私奉献。
0x02 问题说明
1. 概述
如图为传统劫持软件的大致攻击链条。 首先链条将启动
Service
组件以便进程长时间驻留在后台, 接着组件内的代码将不间断获取目标的运行状态, 以此来判断其是否来到前台。 一旦目标到达前台, 也就意味着用户目前正在浏览目标应用, 当时机合适时, 程序会通过一个带有NEW_TASK
标记的Intent
对象从后台启动Activity以覆盖用户正在浏览的页面(这一步骤正是AHA), 最终达到UI劫持的目的。例如之前文章中提到的Android 悬浮窗覆盖攻击的时机,需要知道是么时候目标App的目标界面在最顶层,然后才实施攻击,其实就是再说这个流程。在一般情况下劫持攻击有两种方案,一种是启动恶意App,由恶意App启动被劫持的App,然后覆盖内容;另一种是恶意App在后台持续监测,等待预期Activity出现后创建一个活动覆盖内容。第一种方案简单,但是大多数情况下无法达到预期的效果,因为有的活动是非导出的,无法直接启动,并且如何诱导用户点击恶意App启动也是一个问题。所以一般采用第二种方案。但是也因此会遇到三个问题,并且这三个问题受到Google安全策略的限制:
- 如何保持恶意软件长期在后台运行
- 如何获取当前设备顶层活动的信息
- 如何在后台静默启动恶意劫持活动
2. Target Running State Detect
在
API22
之前, 攻击者可以通过滥用ActivityManager
下的接口来泄露第三方应用的运行状态。,其中getRunningTasks
接口甚至能够获取到目标任务栈顶的Activity信息, 通过获取这些信息能够准确的在特定时刻实施攻击。但是在后续Google对这些接口进行了限制,使其只能获取当前App的信息。文章提到在
API26
之前, 攻击者仍可以通过procfs
以侧信道方式泄露敏感信息。即以App的身份也能枚举/proc
下其他进程信息,并且通过访问oom_score_adj
获取优先级来判断是否处于前台。但在2017年, Google更新了
SELinux
策略, 彻底禁止了任意应用通过procfs
访问第三方应用数据(类似hidepid=2保护)。 自此之后劫持软件不得不通过PACKAGE_USAGE_STATS
权限与UsageStatsManager
来实现精确劫持, 但这些权限的开启需要复杂的用户交互,从而杜绝了无感的前台活动获取问题。3. Background Activity Launch (BAL)
在API29之前,
Activity Hijack Attack
仍可以被利用。 攻击者会通过调用startActivity启动一个指向Activity且携带NEW_TASK
标记的Intent对象以实现AHA攻击,当新的Activity
进入栈后,旧的活动就会被盖到下面,即这个Activity将立即出现在用户视野内, 覆盖屏幕上原本的内容。在
Android API 29
及以上版本中,没有特权的应用程序无法从后台启动 Activity。这意味着应用程序在没有用户交互的情况下,不能在后台启动新的界面。为了让App合理的进行后台启动,Android提供了一些豁免条件(如下两点),但是由于需要用户的交互和授权,无法在攻击场景下实施。- AccessibilityService/SystemServices/SAW permission:这些是系统级权限,分别用于辅助功能服务、系统服务和屏幕覆盖(System Alert Window)权限。这些权限需要复杂的用户交互和危险的运行时权限。用户必须明确授予这些权限,因为它们可能影响用户的隐私和安全。
- Satisfy BAL Restriction Exemptions in document:
- Requires System System Bind…
- Requires Visible App Bind…
- Requires Holds System Privilege…
- Almost impossible…
4. Background Persistent Processaaass
在 Android 系统中,
BEL(Background Execution Limits)
和 LMKD(Low Memory Killer Daemon)
是两个重要的机制,用于管理后台进程和内存使用。从 Android 8.0(API 级别 26)开始,Android 引入了后台执行限制,以提高电池寿命和系统性能。应用程序在后台运行时受到限制,无法无限制地执行任务。这包括限制后台服务、广播接收器和后台位置更新等。LMKD 是 Android 系统中的一个守护进程,用于在系统内存不足时终止低优先级的进程,以释放内存。LMKD 通过监控系统内存使用情况,决定何时终止哪些进程。它使用一组预定义的阈值和优先级来确定哪些进程应该被终止。这两个机制共同作用,确保 Android 系统在资源有限的情况下仍能高效运行,同时提高了设备的电池寿命和用户体验。
在 Android API 26 及以上版本中,后台服务(Background Service)会被分配较高的
OOM_ADJ(Out of Memory Adjustment)
值和较低的优先级。这意味着后台服务在系统内存不足时更容易被终止。在 API 24 及以上版本中,系统广播技巧被BAN。以前,应用程序可以通过注册系统广播接收器来保持后台运行。oom_score_adj
和 oom_adj
是 Linux 内核中用于内存管理的两个参数,它们决定了在系统内存不足时,哪些进程应该被优先终止。oom_adj
的值范围是 -17 到 15。-17
表示进程永远不会被 OOM 杀手终止,15
表示进程最有可能被终止。oom_score_adj
的值范围是 -1000 到 1000。-1000
表示进程永远不会被 OOM 杀手终止,1000
表示进程最有可能被终止。编写App在运行时启动一个Service,将其安装在API19与API33的设备上, 启动App后回到主页面, 然后查询其进程优先级。可以发现,在高版本中后台进程极难存活。并且第三方ROM也可能存在一些额外的限制,导致进程被杀死,要想保活是极其困难的,即使用户赋予了很多权限,进程也可能会被杀死。
前台服务用于执行用户可察觉的操作。前台服务显示状态栏 通知,让用户知道您的 应用正在前台执行任务并消耗系统资源。虽然前台服务是一个保活的好办法,但是基本不具备隐蔽性,因此基本不考虑使用这种方法。
0x03 Bypass BAL
后台活动启动限制(BAL限制)是Google限制AHA的核心手段, 这种防御手段成功消灭了API29+设备上的所有劫持软件。因此绕过BAL限制就成为了问题的关键。接下来从分析BAL限制开始,逐步引出完整的攻击链。
1. Analyse BAL Restriction
在最新的Android代码中
checkBackgroundActivityStartAllowedByCaller
,如果应用允许被切换,或者应用在前台,且调用者的 UID 有一个可见窗口或有一个非应用可见窗口(例如实时壁纸和悬浮窗,toast
除外),则允许启动 Activity(返回的BalVerdict
中mCode
不为 BAL_BLOCK
= 0
)。由此可见,有可见的窗口是第一豁免条件,对于第二个非应用可见窗口需要应用持有SYSTEM_ALERT_WINDOW(SAW)
特权,此权限需要用户手动交互赋予。AHA
技术本身就假设应用已经存在于后台, 处于后台的同时存在于前台任务栈似乎无法实现, 其次系统会判断应用是否拥有系统级窗体, 非特权应用除了诱导用户申请高危权限以外别无选择。 这一项条件的判断流程严防死守, 几乎无法被绕过。而其它豁免条件则更是苛刻, 这些条件几乎把启动Activity的时机锁死在应用处于前台的时段, 这也解释了AHA技术彻底消失的核心原因。2. PIP: Picture In Picture画中画
Google于
API26
引入的一项系统特性为绕过带来了可能借助 Android 手持设备的画中画 (PIP) 功能,用户可以将当前正在运行 activity 的应用缩到小窗口中显示。画中画对视频应用来说尤其有用,因为用户可以随时执行其他操作,不必打断内容播放。用户可以通过 SystemUI 操控该窗口的位置,并通过应用提供的操作(最多三项)与当前处于画中画模式的应用流畅地互动。画中画功能需要在支持它的应用中明确选择启用,并按 activity 运作(一个应用可以有多个 activity,但其中只有一个可处于画中画模式)。
Activity 通过调用
enterPictureInPictureMode()
请求以进入画中画模式,并以 onPictureInPictureModeChanged()
的形式接收 Activity 回调setPictureInPictureParams()
方法可让 activity 控制其在画中画模式下的宽高比和自定义操作,这样一来,用户无需展开 activity 便可以与之互动。在画中画模式下,activity 处于暂停但继续呈现的状态,并且不直接接收触摸输入或窗口焦点。在同一时间点,只能有一项任务处于画中画模式。在MainActivity进入画中画模式后返回主界面,可以通过
dumpsys activity activities
查看Task栈,其中com.attack.evilpip
处于pinned
模式,且visible
属性为true
,说明处于可见状态。并且在
Android-11
使用该APP过程中并没有主动申请任何权限,只需要在Manifest
中声明指定活动支持PIP模式,PIP
权限是自动赋予的。在议题中提到,2021年, 由Dimitrios Valsamaras发现的漏洞才被Google公开: CVE-2021-0485。 Google对该问题评级为High, 漏洞允许Activity以畸形的尺寸启动PiP模式, 最终PiP窗口将以一个像素点的大小显示在屏幕顶层, 由于用户几乎不可见且无法触摸的尺寸, 应用可以在无用户感知的情况下在“后台”持续占有前台特权。 该问题产生的原因是负责计算PiP窗口边界的组件PipBoundsAlgorithm没有严格校验输入数据。
但是该漏洞早已被修复,仅在未更新补丁的特定版本中才能成功。修复后任何小于最小边界的畸形尺寸都将被修正为48dp。
3. Attack - SourceRectHint To EoP(CVE-2023-40116)
该部分详细内容查看原文,原文给出了即为详细的分析,此处仅介绍利用方式
setSourceRectHint接口是本部分的重点,根据官方文档给出的描述,UI侧可以根据从接口传入的Rect实例指定的矩形区域自动缩放并裁剪当前Activity的内容,接着将其渲染到PiP窗口中。
在使用该方法构造画中画时,能够很清楚的发现一个动画的触发,即先出现一个极小的点(存在半秒钟),然后被放大到一定大小的画中画。
如果有任何Trick或代码逻辑上的问题可以延长1dp窗口的显示时间,甚至让1dp窗口持续驻留在屏幕顶层,那么就可以实现EoP。
原文分析的结论如下:当应用进入PiP模式时,会同时触发过渡动画链与Activity重绘链,当过渡动画结束时,链条会触发回调,并最终进入
setMWSCT
函数。 此时重绘链条触发setSurfaceBoundariesLocked
函数获取SCT对象,并最终执行merge
函数。 除去PiP动画链条,Activity重绘链条为用户高度可控,所以攻击对象是显而易见的。编写代码,输入长宽为5的Rect作为
SourceRectHint
,并在进入PiP模式后阻塞UI线程以阻止Activity重绘,运行PoC,可见由于重绘链条被阻塞,PiP窗口呈现出畸形尺寸并持续保持用户不可见状态。此时Dump任务栈,可见PoC所在任务栈被系统认为处于前台,且visible属性为true。这意味着应用已经可以在无用户感知的情况下持续获得限制豁免。
但该POC的适用于2023年十月安全补丁之前的系统。
4. Attack - ActivityOptions To EoP(CVE-2023-21269)
该部分详细内容查看原文,原文给出了即为详细的分析,此处仅介绍利用方式
前面的研究旨在利用
API32
及以下版本的SCT渲染细节与活动重绘时的视图渲染流程, 构造一个可以持续显示的畸形PiP窗体, 并通过该方法成功突破了BAL限制。 然而, 由于Google调整了API33及以上版本的SCT渲染细节, PoC并不适用于API33+的设备。ActivityOptions#makeLaunchIntoPip
函数是Google于API33新增的开发接口, 根据描述可知, 该函数将实例化一个特殊的ActivityOptions
对象, 开发者可以利用该对象使启动的目标Activity直接进入到PiP模式。ActivityOptions
事实上就提供了toBundle
方法将对象参数封装为Bundle实例, 换句话来说, 开发者可以通过调整ActivityOptions的参数来间接控制活动的启动细节(比如为启动的活动增添过渡动画)
将
ActivityOptions
封装为Bundle
对象, 并通过startActivity
接口将Bundle转交给框架与系统进行处理, 上图为Bundle及其参数通过传播到达的一些重要函数, 很显然其传播路径与Activity启动链高度重叠。 在传播链中需要关注的是封装在Bundle中的mLaunchIntoPipParams
参数, 其与欲利用的PiP特性高度关联, 而链条中的startActivityInner
函数首次将该参数取出并做了处理。在
startActivityInner
函数的结尾部分, 代码调用了ActivityOptions#isLaunchIntoPip
函数判断传入的Bundle是否封装了mLaunchIntoPipParams
参数, 显然结果将满足判断条件并使得执行进入分支内部, 而值得注意的是分支内代码调用了moveActivityToPinnedRootTask
函数,该函数用于处理活动所在Task的各类属性, 换句话说, makeLaunchIntoPip
函数确实影响到了活动的启动细节, 它可以直接将活动启动链转入PiP启动链的起始部分, 使得PiP启动链直接接管活动。通读整个
startActivityInner
函数, restrictedBgActivity
成员的赋值或整个应用任务栈的状态丝毫没有影响到最终moveActivityToPinnedRootTask
函数的调用, 且后续PiP启动链的运行也与这些影响因子没有丝毫关系, 似乎PiP特性并不在BAL限制的管控范围之内? 显然这将导致API33+
设备的EoP漏洞。编写如上所示代码, 编译并运行于API33和API34(目前的最新版本)的AVD上, 最终, 在这两个版本下, PoC可以直接无视BAL限制从后台以PiP模式启动Activity。 这代表着我们目前已经可以在所有Android版本上实现BAL。
但该POC仅适用于2023年8月安全补丁之前的系统
5. 未完结的故事-画中画 CVE-2024-34737
画中画作为一个较新的功能,其本身可能存在一些其他问题。例如 CVE-2024-34737,根据官方的说明,该问题源于
setPictureInPictureParams
接口的滥用。用户可以调用setPictureInPictureParams
在画中画启动后修改参数以调节画中画窗口,但是如果随意不限频率的调用接口,将会导致窗口被锁定在前台,无法点击和退出,这违背了画中画功能的安全准则。使用如下的POC即可使打开的PIP窗口冻结,此时该窗口一直驻留前台无法被退出,但是缺点是该窗口可见,无法实现隐蔽攻击。
修复方法则是限制 app 一分钟只能修改 60 次
PiP aspect ratio
。0x04 Side-Channel For Running-State
该部分详细内容查看原文,原文给出了即为详细的分析,此处仅介绍利用方式
1. Abuse startService
在程序未被许可启动后台服务的前提下系统会限制后台服务的启动, 从而抛出异常。 系统首先通过目标服务句柄的
startRequested
属性判断该服务是否为初次启动, 若为初次启动, 则调用getAppStartModeLOSP
函数判断目标服务的所在进程是否存在于后台, 当目标进程满足分支的全部判断条件时, 代码将构造一个包名"?"
的畸形ComponentName
实例, 并将其返回到ContextImpl
组件内。 在处理畸形ComponentName
时将抛出异常提示开发者startService
请求不被允许。如图所示,在
startService
请求失败的异常并不在SystemServer
内部进行处理, 而是在应用层抛出, 任何应用都可以捕获这个异常, 那么这就导致了一个很明显的信息泄露问题: 恶意软件可以通过startService
接口诱导系统调用特权函数getAppStartModeLOSP
以探测目标进程的前后台状态。那么对于任意一个具有导出服务的应用程序来说, 仅需要简单修改PoC代码, 就足以利用之以泄露目标的运行状态。测试某target启动时,反馈状态发生改变,通过追踪这种变化即可获知应用何时启动。
2. Abuse ApplicationInfo
系统允许开发者通过PackageManager获取第三方应用的
ApplicationInfo
对象, 通过此对象, 开发者得以访问目标应用的部分数据, 这些数据与应用清单文件内的<application>
标签高度相关, 这意味着其提供的大部分数据是无法反映运行时状态的静态数据。 尽管如此, 仍有少部分数据是动态变化的。 以flags成员为例, 该成员为整数类型, 在内存空间中占4个字节, 即32个比特位, 每个比特位都反映了应用的某种状态, 虽然大部分状态由<application>标签控制(静态状态), 但其中一个状态却可以在某种程度上反映应用的运行情况。 该状态由flags值的第22个比特位控制, 在框架代码中, 该控制位以
FLAG_STOPPED
常量体现。 那么此时的问题是, FLAG_STOPPED
是如何反映运行情况的? 根据文档描述, 该常量用以标记应用是否已经停止。应用程序在安装后但在用户首次启动或直接与其交互之前处于“停止”状态。应用程序也可以通过“强制停止”返回到“停止”状态。系统尽量不启动处于“停止”状态的应用程序,除非由用户交互触发。
应用的停止状态是个重要因素, 它将直接影响PoC在观测目标运行情况时的适用范围。 在包管理组件中,
setPackageStoppedState
接口用以标记目标应用是否停止, 而在原生AOSP框架下, 该接口仅在AMS#forceStopPackage函数中被系统调用且传入True值, 这意味着只有通过设置面板来强行停止目标应用, 才能手动将其标记为停止状态。 然而, 大部分用户在关闭应用程序时, 不会执行如此繁琐的操作, 相反, 用户通常会在“最近任务”视图中删除应用任务栈以“关闭”目标, 该操作会触发系统调用AMS#killProcessesForRemovedTask
函数, 尽管此操作可以使系统终止应用的大部分进程组, 但由于该函数的整个调用过程没有使用到setPackageStoppedState
接口, 即便目标进程全部死亡, 也不代表目标会被标记为“停止”。
所以, 在载有原生AOSP系统的移动设备上(例如Pixel手机), 滥用ApplicationInfo
的侧信道技术几乎无法提升劫持攻击的精度, 由于无法捕获到常规意义上应用被“关闭”的时刻, PoC在大部分时间都无法确定劫持目标的时机。 但是在客制化Android系统上, 停止状态的适用范围似乎发生了变化。 经过测试发现,包括Huawei厂商,Oppo, Vivo, Realme等第三方客制化系统均会因为删除任务栈而强行停止应用,也即在正常使用情况下关闭任务会导致应用被停止,而无需手动进入设置选择强行停止。这或许说明滥用ApplicationInfo
的侧信道技术可以在大部分严格管控后台进程的客制化系统上使用。0x05 Breaking LMKD & BEL
所有Android恶意软件的终极梦想是, 一旦感染目标设备, 就能够永久地在后台稳定运行, 保持进程高优先级并突破系统上所有的内存优化手段, 即使进程被用户强制停止, 也能如细菌一样在短时间内fork出大量子进程以持续运行在Android设备上。
1. Privilege Process & High Priority
前面提到,有较高的
oom_adj
值的进程将会优先被杀死,要保持进程的优先级则不得不采用前台服务的手段,应用必须使用startForeground方法创建一个Notification实例以告知用户自身进程正在运行, 而用户也可以通过该Notification实例进入应用的设置页面以强行停止应用。 但是着显然不具有隐蔽性,并且可以由用户随时停止。
如果有方法使优先级足够高的进程甚至是系统持久性进程绑定到自身, 在最好的情况下, 应用将可以获得与可见进程同等级别的
oom_adj_score
。框架层中, 有这样一类特别的组件:
Manager
。 该组件是多个组件的统称, 例如ActivityManager
, WindowManager
等等都属于Manager
组件, 而应用进行的大部分操作(启动Activity,发送广播…)最终都需要由Manager
进行处理。 但值得注意的是, 框架层并非直接与Manager
进行交互, 其首先通过组件对应的IBinder
对象与内核态binder
进行交互, 最终由特权进程system_server
接管Manager以处理相关操作。也就是说, 框架层中的Manager
最终将以特权进程system_server
的身份运行,而Android平台上的很多基础组件(例如Activity, Service等等)都要求App通过框架间接与Manager进行交互。以
AccessibilityService
组件为例, 此组件是AOSP框架向应用层暴露的服务接口, 应用可通过接口与系统内的特权服务组件AccessibilityManagerService
进行一定程度上的交互。 根据AndroidDeveloper
文档所述, 若应用想要向系统申请无障碍服务, 就必须在AndroidManifest
中申明一个特殊的Service组件, 该组件必须可以处理指定的系统Intent。 其Intent对象的Action值为AccessibilityService
组件中SERVICE_INTERFACE
常量的具体值。
接下来, Manager中负责无障碍服务部分的函数AccessibilityManagerService# updateServicesLocked将会实例化一个AccessibilityServiceConnection
对象,接着代码将调用该对象下的bindLocked
函数以绑定目标应用中特定的Service组件。由于Manager
是以system_server
的身份运行的,那么在绑定目标Service时,就相当于特权进程system_server
对目标进程进行了绑定。简单编写一个无障碍服务应用, 并在设置面板中允许该应用的无障碍权限, 可以发现应用已经被
system_server
绑定了。 由于该进程为系统持久性进程, 故其oom_adj_score
为-900, 即系统级优先级, 那么根据OomAdjuster#compute-OomAdjLSP
函数的运行逻辑,被绑定进程将获得可见进程级别的优先级,即score=100, 由于该值属于高优先级, 故该进程被LMKD优化的可能性极小。但是无障碍服务需要复杂的授权步骤,也无法满足隐蔽性需求。
2. Abuse AccountManager
AccountManager
为框架向应用层暴露的一个特殊的Manager
组件。 根据官方文档相关条目的描述, 为了方便集中管理用户在设备内存储的在线账户与凭据信息, Google于API5(2009年)提供了AccountManager
接口以供应用使用, 接口可以通过简单的交互为用户提供存储的关键数据。 而系统内与此接口进行对接的特权服务AccountManagerService
, 在下文称之为账户服务。为了方便应用与系统进行对接与交互, 框架额外为开发者提供了抽象组件AbstractAccountAuthenticator。 组件文档提到, 当应用需要与账户服务进行交互时, 需要在重写的
onBind
方法内返回由该组件封装的IBinder
对象, 接着, 应用就可以通过组件内置的数个接口与系统进行数据交换。 有意思的是, 文档同样要求应用为导出的Service
组件配置意图过滤器, 这与无障碍服务的前置配置需求是一致的,但是却不需要像无障碍服务一样复杂的授权操作。如下图所示,在自己实现的
addAccount
方法中返回一个Bundle,构造{"retry":true}
键值对,从而使得system_server
一直尝试调用addAcount
以实现一个绑定,从而提升进程优先级。经过测试,即使返回了桌面,
addAcount
方法还是一直在被调用,查看进程oom_adj
为0,和前台应用具有一样的优先级,即说明该进程不会被杀死。- 作者:LLeaves
- 链接:https://lleavesg.top//article/EvilPiP
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章