深入 Android 应用安全区域与防截录全链路

在做金融类 App 的截屏防护时,最常见的做法是给 Window 加上 FLAG_SECURE。但有一次发现加了标志后,录屏工具依然能抓到部分画面——排查之后,根因在 SurfaceFlinger 的合成策略上。FLAG_SECURE 只是第一道防线,真正可靠的内容保护要从 View 层一路跟到硬件合成器。

FLAG_SECURE 的生效范围

FLAG_SECURE 标记的是 Window 对应的 Surface 为安全图层:

window.setFlags(
    WindowManager.LayoutParams.FLAG_SECURE,
    WindowManager.LayoutParams.FLAG_SECURE
)

系统截屏服务和 MediaProjection 在捕获屏幕时会跳过安全图层,显示为纯黑。最近任务缩略图同理。

它有两个覆盖盲区:一是 Dialog 和 PopupWindow 需要单独设置,不会从宿主 Activity 继承;二是部分厂商 ROM 在硬件合成回退时可能绕过这个标记。

val dialog = Dialog(context)
dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)

SurfaceFlinger 里的安全图层合成

SurfaceFlinger 每次 VSYNC 时执行合成:收集可见 Layer → Z-order 排序 → 调用 HWC 或 GPU 合成 → 输出到 Display。

安全图层的处理发生在第三步。HWC 检测到安全图层后,创建受保护的硬件叠加层(Protected Overlay)——它的数据路径与普通图层物理隔离,GPU 无法读取其像素数据。

风险出在混合合成场景。当安全图层与非安全图层叠加时,如果 HWC 的 Overlay Plane 不够用,系统会回退到 GLES(GPU)合成路径。GPU 合成路径下,安全标记的隔离效果取决于厂商实现,部分 ROM 确实存在绕过的情况。

验证当前合成路径:

adb shell dumpsys SurfaceFlinger | grep -A5 "Secure"
# 检查安全图层是否走了 DEVICE (HWC) 而非 CLIENT (GLES)

如果安全图层落在 CLIENT 合成上,这台设备就存在风险。

DRM 安全视频管线的三层保护

涉及付费视频内容时,仅靠 FLAG_SECURE 不够。Android 的 Widevine DRM 分为三级:

等级解密位置数据暴露风险
L1TEE 可信执行环境
L2硬件安全模块
L3软件层

完整的安全视频链路需要三层协同:Widevine L1 在 TEE 中解密,MediaCodec secure mode 做安全解码,FLAG_SECURE 配合 Protected Overlay 完成安全渲染。

val mediaDrm = MediaDrm(UUID.fromString(WIDEVINE_UUID))
mediaDrm.setPropertyString("securityLevel", "L1")

val format = MediaFormat.createVideoFormat(MIME_TYPE_AVC, w, h)
format.setInteger(MediaFormat.KEY_PROTECTED, 1) // 启用安全解码

val codec = MediaCodec.createByCodecName(selectSecureCodec())
codec.configure(format, secureSurface, mediaDrm.mediaCrypto, 0)

安全解码器的输出 Surface 绑定到 DRM Session,解码后的帧直接送入 SurfaceFlinger 的安全 Overlay——全程不经过应用进程的用户态内存。

验证安全管线是否生效:

adb shell dumpsys media.drm
# securityLevel 应为 L1,mCurrentState 含 SECURE_DECODER

Compose 中的适配陷阱

陷阱一:Compose Dialog 不继承 Activity 的 FLAG_SECURE

Compose 渲染到 AndroidComposeView 上,Activity 的 FLAG_SECURE 只作用于 Window 级。通过 Compose 弹出的 Dialog 如果需要单独保护,必须在原生层设置:

@Composable
fun SecureDialog(content: @Composable () -> Unit) {
    Dialog(onDismissRequest = {}) {
        SideEffect {
            (LocalContext.current as? Activity)?.window
                ?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        }
        content()
    }
}

陷阱二:截图检测需在 Activity 层注册

Android 14 的 ScreenCaptureCallback 可以感知截屏行为,但在 Compose 中必须在 Activity 侧注册:

if (Build.VERSION.SDK_INT >= 34) {
    registerScreenCaptureCallback(mainExecutor) {
        logSecurityEvent("screenshot_detected")
    }
}

陷阱三:局部遮盖不等于系统级保护

部分场景只需要隐藏特定区域(如银行卡号),而非整个页面。Compose 可通过 drawWithContent 做局部遮盖——但它只是渲染层效果,不能替代系统级的 FLAG_SECURE

纵深防御的工程实践

端侧内容安全覆盖三个层次:View 层(FLAG_SECURE)、合成层(硬件 Overlay 验证)、解码层(DRM 安全管线)。实践中我倾向于用一个 SecurityManager 集中管理:

object SecurityManager {
    fun apply(window: Window, level: SecurityLevel) {
        when (level) {
            SecurityLevel.BASIC ->
                window.addFlags(FLAG_SECURE)
            SecurityLevel.DRM ->
                window.addFlags(FLAG_SECURE).also { verifyPipeline() }
            SecurityLevel.MAX ->
                window.addFlags(FLAG_SECURE).also {
                    verifyPipeline(); enableCaptureDetection(window)
                }
        }
    }
}

每个页面在 onCreate 时声明自身安全等级,统一入口避免遗漏——我两年前的一次线上事故就是新页面忘了加 FLAG_SECURE

没有哪一层是绝对可靠的,每一层都可能被特定手段绕过。但三层叠加之后,攻击成本会大幅上升——在实际项目中,这比死磕某个「完美方案」实用得多。