今年度的資安檢測基準新增了「螢幕覆蓋攻擊」的檢測項目。剛好最近送測的專案也因此被點出不符規範,加上後續的專案預期都會受到影響,因此特別整理這篇筆記,讓自己能快速掌握概念,並理解各種防護方式的實作細節。

什麼是螢幕覆蓋攻擊(Screen Overlay Attack)?

Android Doc - Privacy and Security - TapjackingAndroid Doc - Privacy and Security - Tapjacking

圖片來源: Android Doc - Privacy and Security - Tapjacking

根據 行動應用資安聯盟 - 行動應用App基本資安檢測基準V4.0 裡的描述:

攻擊者的應用程式會在行動應用程式上繪製一個視窗,誤導使用者將自己點擊的入侵視窗當作正常視窗。

換句話說,螢幕覆蓋攻擊本質上就是「用一層假的 UI 蓋住真的 UI」。攻擊者可能會模擬系統警示框、模擬 App 畫面,或做成任何能誘導操作的浮動視窗。

在網頁領域,它的行為類似 ClickJacking;而在 Android 官方文件中,這類攻擊則被稱為 Tapjacking

如何避免 App 遭遇螢幕覆蓋攻擊?

理解攻擊手法後,防範策略可以分成這幾個方向:

  1. 避免 App 不被其他 App 遮蔽。
  2. 在 App 畫面被遮蔽時,使觸控事件無效。
  3. 🙃 警告使用者不要亂安裝奇怪的東西跟亂給權限… 🙃

以下整理幾種常見、有效、並且可依系統版本選擇的防護方式。

方法一、WindowManager.LayoutParams.FLAG_SECURE

  • 支援版本:API 1 起即可使用。
  • 效果:阻止 Overlay、阻止截圖、阻止投影。
1
2
3
4
5
// 禁止其他程式於上層繪製 / 禁止截圖 / 禁止投影  
activity.window.setFlags(  
    WindowManager.LayoutParams.FLAG_SECURE,  
    WindowManager.LayoutParams.FLAG_SECURE  
)

Activity.onCreate() 加上上述設定後,不僅可以防止 Overlay,也會一併禁止截圖與投影。這也是許多銀行、醫院等高資安需求 App 的常見做法。

方法二、 Window.setHideOverlayWindows(true)

  • 支援版本:Android 12 (API 31) 以上
  • 效果: 阻止所有非系統 Overlay
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**  
 * [資安檢測項目] TapJacking  
 * 強制移除非系統的 Overlay UI  
 * 避免惡意程式以浮動 UI 混淆使用者操作  
 *  
 * @param activity  
 */  
private fun setHideOverlayWindows(activity: Activity) {  
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {  
        // 啟用此功能,防止非系統浮層繪製在此視窗上  
        activity.window.setHideOverlayWindows(true)  
    }  
}

這個方法的效果與方法一類似,但不會影響截圖與投影功能。如果你的 App 不希望完全禁止截圖,Android 12+ 裝置可以優先採用這個方法。

方法三、android:filterTouchesWhenObscured="true"

  • 支援版本: Android 9 (API 28) 以前建議採用
  • 效果: 在 View 被遮蔽時,忽略觸控事件
1
2
3
<com.google.android.material.button.MaterialButton  
    android:id="@+id/btn_one_click"
    android:filterTouchesWhenObscured="true" />

啟用後,只要該 View 偵測到自己被 Overlay 遮蔽,就會自動忽略觸控事件,避免使用者在不知情的情況下點擊錯誤的 UI。

如果只是要防止 TapJacking 引發預期外的 App 行為,可以直接針對敏感 UI 元件設定這個參數就可以了。

如果覺得一個一個設定很麻煩,可以在 style.xml 中統一設定,再套用到 UI 元件上。

方法四、覆寫 View.onFilterTouchEventForSecurity()

filterTouchesWhenObscured 背後的底層邏輯,就是由這個方法負責。
onFilterTouchEventForSecurity() 回傳 false 時,代表觸控事件發生時,畫面有被遮蔽。

以下是其預設實作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**  
 * Filter the touch event to apply security policies.  
 *  
 * @param event The motion event to be filtered.  
 * @return True if the event should be dispatched, false if the event should be dropped.  
 *  
 * @see #getFilterTouchesWhenObscured  
 */  
public boolean onFilterTouchEventForSecurity(MotionEvent event) {  
    //noinspection RedundantIfStatement  
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0  
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {  
        // Window is obscured, drop this touch.  
        return false;  
    }  
    return true;  
}

透過覆寫這個方法,開發者可以:

  • 在偵測到畫面遭遮蔽時提示使用者
  • 紀錄 Log 以協助除錯
  • 執行專案內必要的安全措施

以下程式碼範例,就是在事件觸發當下,顯示 Toast 提示使用者。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
override fun onFilterTouchEventForSecurity(event: MotionEvent?): Boolean {  
    val noOverlayDetected = super.onFilterTouchEventForSecurity(event)  
    if (!noOverlayDetected) {  
        Toast.makeText(  
            context,  
            R.string.toast_tapjacking_detected,  
            Toast.LENGTH_SHORT).show()  
    }  
    return noOverlayDetected  
}

個人實務經驗分享

此次需要修改的 App 具備以下條件:

  1. 大量客製化 UI
  2. 採用 Single Activity + 多 Fragment 架構
  3. 資安送檢時間緊迫,需要快速交付

最終採用以下方案:

  1. Android 12+:使用方法二 Window.setHideOverlayWindows(true)
  2. Android 12 以下: 使用方法三 + 方法四 的組合

我建立了一個自訂 ViewGroup SecureConstraintLayout,負責統一處理遮蔽與提示邏輯:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import android.content.Context  
import android.util.AttributeSet  
import android.view.MotionEvent  
import android.widget.Toast  
import androidx.constraintlayout.widget.ConstraintLayout  
import com.emct.newagriculturaldisaster.R  
  
/**  
 *  Created by DanielHuang on 2025/11/13
*/ 
class SecureConstraintLayout @JvmOverloads constructor(  
    context: Context, attrs: AttributeSet? = null  
) : ConstraintLayout(context, attrs) {  
  
    init {  
        filterTouchesWhenObscured = true  
    }  
  
    override fun onFilterTouchEventForSecurity(event: MotionEvent?): Boolean {  
        val noOverlayDetected = super.onFilterTouchEventForSecurity(event)  
        if (!noOverlayDetected) {  
            Toast.makeText(  
                context,  
                R.string.toast_tapjacking_detected,  
                Toast.LENGTH_SHORT).show()  
        }  
        return noOverlayDetected  
    }  
}

如此一來,只要使用該自訂 ViewGroup 包住敏感 UI,就能確保在被遮蔽時給予即時提示,提高整體安全性。

總結

以上就是我在 Android 上處理螢幕覆蓋攻擊的整理與實作心得。 如果你最近也在面對相同的資安檢測項目,希望這篇文章能幫你省下一些摸索時間。 如果內容有不足或更好的作法,也非常歡迎留言交流。

感謝你的閱讀,下次見 👋

參考資料