:從組件暴露到權(quán)限繞過的完整挖掘鏈路)
1. 從一個(gè)容易被忽視的組件說起移動(dòng)安全學(xué)習(xí)這條路上很多人一上來就扎進(jìn)逆向、脫殼、Hook 框架這些硬核方向結(jié)果學(xué)了大半年連一個(gè) App 的四大組件都沒摸清楚。我剛開始也是這樣覺得 BroadcastReceiver 這種發(fā)廣播、收廣播的東西太簡單了能有什么安全問題直到有一次在分析一個(gè)本地應(yīng)用時(shí)隨手用adb發(fā)了一條廣播直接把一個(gè)本該隱藏的頁面拉了起來才意識(shí)到這個(gè)組件的水比想象中深得多。BroadcastReceiver中文叫廣播接收器是 Android 四大組件之一。它的核心職責(zé)就一件事接收并處理廣播消息。系統(tǒng)開機(jī)、網(wǎng)絡(luò)變化、電量低、屏幕點(diǎn)亮這些系統(tǒng)級事件都會(huì)以廣播的形式發(fā)出來應(yīng)用之間也可以自定義廣播來傳遞消息。聽起來很樸素但問題恰恰出在誰都能發(fā)、誰都能收這個(gè)特性上。如果開發(fā)者沒有做好權(quán)限控制和數(shù)據(jù)校驗(yàn)攻擊者就能通過構(gòu)造惡意廣播觸發(fā)應(yīng)用內(nèi)部的敏感邏輯甚至實(shí)現(xiàn)越權(quán)操作、信息泄露、拒絕服務(wù)。這篇筆記面向的是正在學(xué)習(xí)移動(dòng)安全、想系統(tǒng)掌握組件漏洞挖掘方法的人。不管你是剛?cè)腴T的新手還是已經(jīng)會(huì)寫簡單 PoC 但不知道如何系統(tǒng)排查的老手我都會(huì)從原理講到實(shí)操把 BroadcastReceiver 的漏洞挖掘鏈路完整拆一遍。核心關(guān)鍵詞就幾個(gè)廣播接收器、組件暴露、權(quán)限繞過、Intent 注入、動(dòng)態(tài)注冊。搞懂這幾個(gè)詞背后的機(jī)制你就能獨(dú)立完成一次完整的廣播組件安全審計(jì)。需要提前說明的是下面所有案例和代碼都是基于常見開發(fā)實(shí)踐構(gòu)造的模擬場景不針對任何真實(shí)應(yīng)用。工具使用、命令操作都經(jīng)過脫敏處理你可以直接在自己的測試環(huán)境里復(fù)現(xiàn)。2. BroadcastReceiver 的兩種注冊方式與暴露面差異2.1 靜態(tài)注冊Manifest 里的常駐哨兵靜態(tài)注冊是在AndroidManifest.xml里通過receiver標(biāo)簽聲明的系統(tǒng)在應(yīng)用安裝時(shí)就會(huì)解析并注冊這個(gè)接收器。它的特點(diǎn)是生命周期跟隨應(yīng)用進(jìn)程即使應(yīng)用沒啟動(dòng)只要匹配到廣播系統(tǒng)也能把進(jìn)程拉起來處理。receiver android:name.MyReceiver android:exportedtrue intent-filter action android:namecom.example.MY_ACTION / /intent-filter /receiver這里最關(guān)鍵的一個(gè)屬性是android:exported。它決定了這個(gè)接收器是否允許其他應(yīng)用發(fā)送廣播過來。在 Android 12API 31之前如果聲明了intent-filterexported默認(rèn)就是true從 Android 12 開始必須顯式聲明這個(gè)屬性否則安裝時(shí)直接報(bào)錯(cuò)。很多老應(yīng)用在升級 targetSdk 時(shí)忘了改這個(gè)結(jié)果要么編譯不過要么被迫加上exportedtrue反而把攻擊面暴露得更徹底。我實(shí)際審計(jì)時(shí)第一步永遠(yuǎn)是全局搜索exportedtrue的 receiver。這一步能快速篩出所有對外暴露的入口效率比逐個(gè)文件看高得多。2.2 動(dòng)態(tài)注冊代碼里的臨時(shí)接線動(dòng)態(tài)注冊是在代碼中調(diào)用registerReceiver()完成的通常在 Activity 或 Service 的onCreate/onResume里注冊在onDestroy/onPause里注銷。IntentFilter filter new IntentFilter(com.example.DYNAMIC_ACTION); registerReceiver(myReceiver, filter);動(dòng)態(tài)注冊的暴露面取決于注冊時(shí)是否指定了權(quán)限以及IntentFilter的匹配規(guī)則。如果沒加權(quán)限限制任何應(yīng)用都能發(fā)送匹配的廣播。更隱蔽的是動(dòng)態(tài)注冊的接收器只在組件存活期間有效靜態(tài)掃描 Manifest 根本發(fā)現(xiàn)不了必須結(jié)合代碼審計(jì)或運(yùn)行時(shí) Hook 才能定位。提示Android 14API 34對動(dòng)態(tài)注冊的 receiver 增加了強(qiáng)制要求——如果應(yīng)用 targetSdk 升級到 34注冊時(shí)必須顯式指定RECEIVER_EXPORTED或RECEIVER_NOT_EXPORTED否則會(huì)拋異常。這個(gè)變化讓很多裸奔的動(dòng)態(tài)注冊被迫顯式聲明反而方便了審計(jì)人員快速定位。兩種注冊方式的暴露面差異可以用一張表來對比維度靜態(tài)注冊動(dòng)態(tài)注冊聲明位置AndroidManifest.xml代碼中 registerReceiver生命周期隨應(yīng)用進(jìn)程隨注冊組件靜態(tài)掃描可見性高低默認(rèn) exported舊版本有 filter 則為 true取決于權(quán)限參數(shù)常見漏洞越權(quán)調(diào)用、Intent 注入權(quán)限缺失、生命周期泄漏理解這張表你就知道為什么審計(jì)時(shí)要靜態(tài)掃 Manifest 動(dòng)態(tài)看代碼兩條腿走路。3. 廣播漏洞的四種典型形態(tài)與成因3.1 未授權(quán)訪問誰都能敲的門這是最常見也最容易被低估的問題。一個(gè) receiver 聲明了exportedtrue卻沒有在onReceive里做任何調(diào)用方校驗(yàn)?zāi)敲慈魏螒?yīng)用都能發(fā)送對應(yīng) action 的廣播觸發(fā)里面的邏輯。假設(shè)某個(gè)應(yīng)用有個(gè) receiver 收到com.example.RESET_PASSWORD廣播后就把密碼重置為默認(rèn)值且不校驗(yàn)發(fā)送方。攻擊者只需要幾行代碼Intent intent new Intent(com.example.RESET_PASSWORD); intent.putExtra(user, admin); sendBroadcast(intent);密碼就被重置了。這類漏洞的本質(zhì)是把敏感操作暴露給了不可信輸入源。開發(fā)者往往覺得這個(gè) action 名字只有我知道但 action 字符串在反編譯后一覽無余根本算不上秘密。3.2 權(quán)限校驗(yàn)形同虛設(shè)有些開發(fā)者意識(shí)到了風(fēng)險(xiǎn)于是加了權(quán)限檢查但檢查方式有問題。常見錯(cuò)誤有三種第一種是只在onReceive里檢查getCallingUid()但廣播的發(fā)送方 UID 在接收時(shí)并不總是可靠可獲取的尤其是通過sendBroadcast發(fā)送時(shí)接收方拿到的 calling Uid 可能是系統(tǒng)或 null。第二種是自定義權(quán)限的 protectionLevel 設(shè)置不當(dāng)。比如聲明了一個(gè)normal級別的權(quán)限任何應(yīng)用只要在 Manifest 里申請就能獲得等于沒設(shè)防。正確的做法是用signature級別只有相同簽名的應(yīng)用才能持有。permission android:namecom.example.PRIVATE_PERM android:protectionLevelsignature / receiver android:name.SecureReceiver android:permissioncom.example.PRIVATE_PERM android:exportedtrue ... /receiver第三種是權(quán)限聲明了但發(fā)送方?jīng)]帶接收方也不強(qiáng)制校驗(yàn)導(dǎo)致權(quán)限成了擺設(shè)。3.3 Intent 注入與數(shù)據(jù)污染廣播攜帶的 Intent 里可以放各種 extra 數(shù)據(jù)。如果接收方直接把這些數(shù)據(jù)拼接到 SQL 語句、文件路徑、URL 或者反射調(diào)用的類名里就可能引發(fā)注入類漏洞。舉個(gè)模擬場景某 receiver 收到廣播后取出extra里的filename然后openFileOutput(filename)。攻擊者傳入../../databases/secret.db就可能讀寫到非預(yù)期目錄。再比如把 extra 里的類名通過反射實(shí)例化攻擊者就能加載任意類后果更嚴(yán)重。這類漏洞的挖掘要點(diǎn)是追蹤 onReceive 里所有來自 Intent 的數(shù)據(jù)流看它們最終流向了哪里。流向文件、數(shù)據(jù)庫、網(wǎng)絡(luò)、反射、命令執(zhí)行的地方都是高危匯聚點(diǎn)。3.4 拒絕服務(wù)與邏輯轟炸即使沒有敏感操作一個(gè) exported 的 receiver 也可能被濫用。攻擊者可以高頻發(fā)送廣播觸發(fā)接收方做耗時(shí)操作比如大量 IO、網(wǎng)絡(luò)請求導(dǎo)致應(yīng)用卡頓甚至 ANR。如果onReceive里還有空指針解引用、數(shù)組越界等隱患一條精心構(gòu)造的廣播就能讓應(yīng)用崩潰。這類問題單獨(dú)看危害不大但在某些場景下可以作為組合攻擊的一環(huán)比如配合其他漏洞制造競態(tài)條件。4. 手工挖掘的完整操作鏈路4.1 第一步靜態(tài)定位所有 receiver拿到一個(gè) APK先反編譯。我習(xí)慣用apktool解出 Manifest 和 smali再用jadx看 Java 偽代碼兩者互補(bǔ)。apktool d target.apk -o target_dir打開target_dir/AndroidManifest.xml搜索receiver。重點(diǎn)記錄三個(gè)信息android:name、android:exported、android:permission以及intent-filter里的 action。如果 exported 是 true 且沒有 permission直接標(biāo)記為高優(yōu)先級目標(biāo)。如果 exported 沒寫根據(jù) targetSdk 判斷默認(rèn)值小于 31 且有 filter 的默認(rèn) true大于等于 31 的必須顯式聲明。4.2 第二步定位 onReceive 實(shí)現(xiàn)并做數(shù)據(jù)流分析用 jadx 打開 APK根據(jù) receiver 的類名找到對應(yīng)的 Java 文件看onReceive方法體。這一步的核心是回答三個(gè)問題這個(gè)方法里做了什么敏感操作寫文件、發(fā)網(wǎng)絡(luò)、改配置、啟動(dòng)組件這些操作的輸入?yún)?shù)來自哪里是否直接取自 Intent有沒有對調(diào)用方做校驗(yàn)校驗(yàn)邏輯是否可繞過我一般會(huì)在 jadx 里用CtrlF搜索getStringExtra、getParcelableExtra、getIntExtra這些關(guān)鍵詞快速定位數(shù)據(jù)入口然后順著變量往下追。4.3 第三步構(gòu)造 PoC 驗(yàn)證定位到可疑點(diǎn)后寫一個(gè)簡單的測試應(yīng)用或者用adb直接發(fā)廣播驗(yàn)證。用 adb 發(fā)送帶 extra 的廣播adb shell am broadcast -a com.example.MY_ACTION \ --es filename ../../databases/secret.db \ --ei user_id 1如果要指定發(fā)送方包名模擬特定應(yīng)用發(fā)送可以加-p參數(shù)。如果要發(fā)送有序廣播用am broadcast默認(rèn)就是有序的接收方可以通過setResult回傳數(shù)據(jù)。對于需要?jiǎng)討B(tài)注冊才能觸發(fā)的 receiveradb 直接發(fā)可能無效因?yàn)榻邮掌鬟€沒注冊。這時(shí)候需要先啟動(dòng)對應(yīng)組件或者用 Frida 在運(yùn)行時(shí) HookregisterReceiver來觀察注冊的 filter 和權(quán)限。4.4 第四步驗(yàn)證權(quán)限繞過路徑如果 receiver 聲明了權(quán)限不要急著放棄。先確認(rèn)權(quán)限的 protectionLevel。用aapt或apkanalyzer查看權(quán)限定義aapt dump permissions target.apk如果看到protectionLevelnormal或者dangerous說明任何應(yīng)用都能申請到這個(gè)權(quán)限防護(hù)基本無效。只有signature級別才真正限制了調(diào)用方必須是同簽名應(yīng)用。還有一種情況是權(quán)限聲明在 receiver 上但發(fā)送方用的是sendBroadcast(intent, receiverPermission)這種帶權(quán)限參數(shù)的重載。如果接收方?jīng)]在 Manifest 里聲明對應(yīng)權(quán)限或者聲明了但沒生效也可能繞過。5. 動(dòng)態(tài)注冊接收器的運(yùn)行時(shí)排查技巧靜態(tài)掃描搞不定的動(dòng)態(tài)注冊必須上運(yùn)行時(shí)手段。我常用的組合是 Frida adb logcat。5.1 用 Frida Hook registerReceiver寫一個(gè)簡單的 Frida 腳本Hookandroid.content.ContextWrapper.registerReceiver打印出每次注冊的 filter、action 和權(quán)限參數(shù)Java.perform(function () { var ContextWrapper Java.use(android.content.ContextWrapper); ContextWrapper.registerReceiver.overload( android.content.BroadcastReceiver, android.content.IntentFilter ).implementation function (receiver, filter) { console.log([registerReceiver] filter actions:); var actions filter.actionsIterator(); while (actions.hasNext()) { console.log( action: actions.next()); } return this.registerReceiver(receiver, filter); }; });跑起來之后操作應(yīng)用觸發(fā)注冊邏輯logcat 或 Frida 控制臺(tái)就會(huì)打印出所有動(dòng)態(tài)注冊的 action。拿到 action 后再用 adb 發(fā)廣播驗(yàn)證。5.2 觀察廣播的實(shí)際處理路徑有時(shí)候一個(gè) action 被多個(gè) receiver 接收或者被系統(tǒng)攔截??梢杂胐umpsys activity broadcasts查看當(dāng)前系統(tǒng)里的廣播狀態(tài)雖然信息有限但能確認(rèn)廣播是否被發(fā)出。更直接的辦法是在onReceive上下斷點(diǎn)。如果應(yīng)用可調(diào)試用 jdb 或 Android Studio 附加調(diào)試如果不可調(diào)試用 Frida Hook 目標(biāo)類的onReceive方法打印調(diào)用棧和 Intent 內(nèi)容var TargetReceiver Java.use(com.example.MyReceiver); TargetReceiver.onReceive.implementation function (context, intent) { console.log([onReceive] action: intent.getAction()); console.log([onReceive] extras: intent.getExtras()); console.log(Java.use(android.util.Log).getStackTraceString( Java.use(java.lang.Exception).$new())); return this.onReceive(context, intent); };調(diào)用棧能告訴你這個(gè)廣播是從哪里發(fā)出來的是系統(tǒng)發(fā)的還是某個(gè)應(yīng)用發(fā)的對判斷攻擊面很有幫助。5.3 動(dòng)態(tài)注冊的生命周期陷阱動(dòng)態(tài)注冊的 receiver 如果忘記在onDestroy里unregisterReceiver會(huì)導(dǎo)致內(nèi)存泄漏同時(shí)接收器在組件銷毀后仍然存活擴(kuò)大了攻擊窗口。審計(jì)時(shí)可以重點(diǎn)看注冊和注銷是否成對出現(xiàn)尤其是那些在onCreate注冊但只在異常分支注銷的代碼。6. 那些文檔里不會(huì)寫的踩坑經(jīng)驗(yàn)6.1 Android 版本差異能把人坑哭同一個(gè) APK在 Android 10 上能觸發(fā)的廣播漏洞在 Android 13 上可能完全沒反應(yīng)。原因通常是高版本對隱式廣播做了限制。Android 8.0API 26之后大部分隱式廣播不能再靜態(tài)注冊接收必須動(dòng)態(tài)注冊。這意味著你按 Manifest 里的 action 發(fā)廣播系統(tǒng)可能直接丟棄。解決辦法是確認(rèn)目標(biāo) action 是否在隱式廣播限制名單里。如果在就得想辦法觸發(fā)動(dòng)態(tài)注冊路徑或者用-p指定包名發(fā)送顯式廣播如果 receiver 支持。6.2 exported 的默認(rèn)值隨 targetSdk 變化我見過太多人因?yàn)闆]注意 targetSdk 而誤判。一個(gè) receiver 沒寫 exportedtargetSdk 是 30那有 filter 就是 truetargetSdk 是 31那必須顯式寫沒寫的話應(yīng)用根本裝不上。審計(jì)前先看AndroidManifest.xml里的targetSdkVersion這個(gè)數(shù)字決定了默認(rèn)行為。6.3 權(quán)限校驗(yàn)的調(diào)用方 UID 不可靠前面提過廣播接收時(shí)getCallingUid()經(jīng)常拿不到發(fā)送方。真正可靠的做法是發(fā)送方在 Intent 里帶上自己的標(biāo)識(shí)接收方通過簽名校驗(yàn)或 token 驗(yàn)證。如果只依賴 UID很容易被偽造或拿到 null 后邏輯走偏。6.4 有序廣播的 result 回傳可能泄露數(shù)據(jù)有序廣播允許接收方通過setResult把數(shù)據(jù)回傳給發(fā)送方。如果接收方把敏感信息塞進(jìn) result而發(fā)送方是攻擊者數(shù)據(jù)就泄露了。審計(jì)時(shí)看到setResult、setResultData、setResultExtras要格外留意。6.5 別忽略系統(tǒng)廣播的濫用有些應(yīng)用監(jiān)聽BOOT_COMPLETED、CONNECTIVITY_CHANGE這類系統(tǒng)廣播來做初始化。如果這些 receiver 是 exported 的攻擊者可以手動(dòng)發(fā)送同名廣播提前觸發(fā)初始化邏輯可能繞過某些狀態(tài)檢查。雖然高版本對系統(tǒng)廣播的偽造有限制但在低版本或特定條件下仍然可行。7. 從挖掘到修復(fù)的閉環(huán)思路挖到漏洞只是第一步能給出可落地的修復(fù)方案才算完整。針對 BroadcastReceiver 的安全問題修復(fù)思路可以歸納為幾條最小暴露原則。不需要對外接收廣播的 receiver一律exportedfalse。動(dòng)態(tài)注冊時(shí)Android 14 以上顯式用RECEIVER_NOT_EXPORTED。權(quán)限收緊。確實(shí)需要跨應(yīng)用通信的用signature級別的自定義權(quán)限并在 receiver 上聲明android:permission。發(fā)送方也要用帶權(quán)限參數(shù)的sendBroadcast重載雙向校驗(yàn)。輸入校驗(yàn)。onReceive里所有來自 Intent 的數(shù)據(jù)在使用前都要做白名單校驗(yàn)。文件路徑要規(guī)范化后檢查是否在允許目錄內(nèi)類名要限制在預(yù)期集合里SQL 參數(shù)要參數(shù)化。敏感操作二次確認(rèn)。重置密碼、修改配置這類操作不能只靠一條廣播觸發(fā)應(yīng)該加上 token 或用戶交互確認(rèn)。生命周期管理。動(dòng)態(tài)注冊的 receiver 確保在對應(yīng)生命周期回調(diào)里注銷避免泄漏和窗口擴(kuò)大。我在實(shí)際審計(jì)中通常會(huì)把發(fā)現(xiàn)的問題按可利用性和影響面兩個(gè)維度排優(yōu)先級。一個(gè) exported 且無權(quán)限、onReceive里直接寫文件的 receiver優(yōu)先級最高一個(gè) exported 但只打日志的優(yōu)先級最低。這樣輸出的報(bào)告才有重點(diǎn)修復(fù)建議也能按輕重緩急排。最后分享一個(gè)我自己的習(xí)慣每審計(jì)完一個(gè)應(yīng)用都會(huì)把遇到的 receiver 模式整理成一個(gè)小清單比如哪些 action 對應(yīng)哪些敏感操作哪些權(quán)限配置是無效的。積累多了之后再看到新的 APK掃一眼 Manifest 就能大致判斷出哪些地方值得深挖。這種模式識(shí)別能力比記住某個(gè)具體漏洞更有價(jià)值。