作者:evilpan
原文鏈接:https://mp.weixin.qq.com/s/shbmbR0AizKHJSAJsLSbfA
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
前言
隨著社會越來越重視安全性,各種防御性編程或者漏洞緩解措施逐漸被加到了操作系統中,比如代碼簽名、指針簽名、地址隨機化、隔離堆等等,許多常見的內存破壞漏洞在這些緩解措施之下往往很難進行穩定的利用。因此,攻擊者們的目光也逐漸更多地投入到邏輯漏洞上。邏輯漏洞通常具有很好的穩定性,不用受到風水的影響;但同時也隱藏得較深、混跡在大量業務代碼中難以發現。而且由于形式各異,不太具有通用性,從投入產出比的角度來看可能不是一個高優先級的研究方向。但無論如何,這都始終是一個值得關注的攻擊面。因此,本文就以 Android 平臺為目標介紹一些常見的邏輯漏洞。
四大組件
接觸過 Android 的人應該都聽說過 “四大組件”,開發應用首先需要學習的就是各個組件的生命周期。所謂四大組件,分別是指 Activity、Service、Broadcast Receiver 和 Content Provider,關于這些組件的實現細節可以參考官方的文檔: Application Fundamentals。
在安全研究中,四大組件值得我們特別關注,因為這是應用與外界溝通的重要橋梁,甚至在應用內部也是通過這些組件構建起了相互間松耦合的聯系。比如應用本身可以不申請相機權限,但可以通過組件間的相互通信讓(系統)相機應用打開攝像頭并取得拍到的照片,仿佛是自身進行拍照的一樣。
而在組件交互的過程中,最為核心的數據結構就是 Intent,這是大部分組件之間進行通信的載體。
Intent 101
根據官方的說法,Intent 是 “對某種要執行的操作的抽象描述”,直譯過來也可以叫做 “意圖”,比如說想要打開攝像機拍照、想要打開瀏覽器訪問網址,想要打開設置界面,……都可以用 Intent 來描述。
Intent 的主要形式有兩種,分別是顯式 Intent 和隱式 Intent;二者的差別主要在于前者顯式指定了 Component,后者沒有指定 Component,但是會通過足夠的信息去幫助系統去理解意圖,比如 ACTION、CATAGORY 等。
Intent 的最主要功能是用來啟動 Activity,因此我們以這個場景為例,從源碼中分析一下 Intent 的具體實現。啟動 Activity 的常規代碼片段如下:
Intent intent = new Intent(context, SomeActivity.class);
startActivity(intent);
這里用的是顯式 Intent,但不是重點。一般在某個 Activity 中調用,因此調用的是 Activity.startActivity,代碼在 frameworks/base/core/java/android/app/Activity.java 中,這里不復制粘貼了,總而言之調用鏈路如下:
- Activity.startActivity()
- Activity.startActivityForResult()
- Instrumentation.execStartActivity()
- ActivityTaskManager.getService().startActivity()
- IActivityTaskManager.startActivity()
最后一條調用是個接口,這是個很常見的 pattern 了,下一步應該去找其實現,不出意外的話這個實現應該在另一個進程中。事實上也正是在 system_server 中:
- ActivityTaskManagerService.startActivity()
- ActivityTaskManagerService.startActivityAsUser()
- ActivityStarter.execute()
最后一個方法通過前面傳入的信息去準備啟動 Activity,包括 caller、userId、flags,callingPackage 以及最重要的 intent 信息,如下:
private int startActivityAsUser(...) {
// ...
return getActivityStartController()
.obtainStarter(
intent, "startActivityAsUser")
.setCaller(caller)
.setCallingPackage(callingPackage)
.setCallingFeatureId(callingFeatureId)
.setResolvedType(resolvedType)
.setResultTo(resultTo)
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setStartFlags(startFlags)
.setProfilerInfo(profilerInfo)
.setActivityOptions(bOptions)
.setUserId(userId)
.execute();
}
ActivityStarter.execute() 主要的邏輯如下:
int execute() {
// ...
if (mRequest.activityInfo == null) {
mRequest.resolveActivity(mSupervisor);
}
res = resolveToHeavyWeightSwitcherIfNeeded();
res = executeRequest(mRequest);
}
其中,resolveActivity 用于獲取要啟動的 Activity 信息,例如在隱式啟動的情況下,可能有多個符合要求的目標,也會彈出菜單詢問用戶選用哪個應用打開。executeRequest 中則主要進行相關權限檢查,在所有權限滿足條件后再調用 startActivityUnchecked 去執行真正的調用。
其中大部分流程我在 Android12 應用啟動流程分析 中已經介紹過了,這里更多是關注 Intent 本身的作用。從上面的分析中發現,可以將其看作是多進程通信中的消息載體,而其源碼定義也能看出 Intent 本身是可以可以序列化并在進程間傳遞的結構。
public class Intent implements Parcelable, Cloneable { ... }
Intent 本身有很多方法和屬性,這里暫時先不展開,后面介紹具體漏洞的時候再進行針對性的分析。后文主要以四大組件為著手點,分別介紹一些常見的漏洞模式和設計陷阱。
Activity
Activity 也稱為活動窗口,是與用戶直接交互的圖形界面。APP 主要開發工作之一就是設計各個 activity,并規劃他們之間的跳轉和連結。通常一個 activity 表示一個全屏的活動窗口,但也可以有其他的存在形式,比如浮動窗口、多窗口等。作為 UI 窗口,一般使用 XML 文件進行布局,并繼承 Activity 類實現其生命周期函數 onCreate 和 onPause 等生命周期方法。
如果開發者定義的 Activity 想通過 Context.startActivity 啟動的話,就必須將其聲明到 APP 的 manifest 文件中,即 AndroidManifest.xml。應用被安裝時,PackageManager 會解析其 manifest 文件中的相關信息并將其注冊到系統中,以便在 resolve 時進行搜索。
在 adb shell 中可以通過 am start-activity 去打開指定的 Activity,通過指定 Intent 去進行啟動:
am start-activity [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]
[--sampling INTERVAL] [--streaming] [-R COUNT] [-S]
[--track-allocation] [--user <USER_ID> | current] <INTENT>
作為用戶界面的載體,Activity 承載了許多用戶輸入/處理、以及外部數據接收/展示等工作,因此是應用對外的一個主要攻擊面。下面就介紹幾種較為常見的攻擊場景。
生命周期
Activity 經典的生命周期圖示如下:

通常開發者只需要實現 onCreate 方法,但是對于一些復雜的業務場景,正確理解其生命周期也是很必要的。以筆者在內測中遇到的某應用為例,其中某個 Activity 中執行了一些敏感的操作,比如開啟攝像頭推流,或者開啟了錄音,但只在 onDestroy 中進行了推流/錄音的關閉。這樣會導致在 APP 進入后臺時候,這些操作依然在后臺運行,攻擊者可以構造任務棧使得受害者在面對惡意應用的釣魚界面時候仍然執行目標應用的后臺功能,從而形成特殊的釣魚場景。正確的做法應該是在 onPaused 回調中對敏感操作進行關閉。
攻擊者實際可以通過連續發送不同的 Intent 去精確控制目標 Activity 生命周期回調函數的觸發時機,如果開發時沒有注意也會造成應用功能的狀態機異常甚至是安全問題。
Implicit Exported
前面說過,開發者定義的 Activity 要想使用 startActivity 去啟動,就必須在 AndroidManifest.xml 中使用 <activity> 進行聲明,一個聲明的示例如下:
<activity xmlns:android="http://schemas.android.com/apk/res/android" android:theme="@android:01030055" android:name="com.evilpan.RouterActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="demo" android:host="router"/>
</intent-filter>
</activity>
activity 中支持許多屬性。其中一個重要的屬性就是 android:exported,表示當前 Activity 是否可以被其他應用的組件啟動。該屬性有幾個特點:
- 屬性可以缺省,缺省值默認為
false; - 如果 Activity 沒有顯式設置該屬性,且該 Activity 中定義了
<intent-filter>,那么缺省值就默認為true;
也就是說,開發者可能沒有顯式指定 Activity 導出,但由于指定了 intent-filter,因此實際上也是導出的,即可以被其他應用喚起對應的 Activity。這種情況在早期很常見,比如 APP 設計了一組更換密碼的界面,需要先輸入舊密碼然后再跳轉到輸入新密碼的界面,如果后者是導出的,攻擊者就可以直接喚起輸入新密碼的界面,從而繞過了舊密碼的校驗邏輯。
Google 已經深刻意識到了這個問題,因此規定在 Android 12 之后,如果應用的 Activity 中包含 intent-filter,就必須要顯式指定 android:exported 為 true 或者 false,不允許缺省。在 Android 12 中未顯式指定 exported 屬性且帶有 intent-filter 的 Activity 的應用在安裝時候會直接被 PackageManager 拒絕。
Fragment Injection
Activity 作為 UI 核心組件,同時也支持模塊化的開發,比如在同一個界面中展示若干個可復用的子界面。隨著這種設計思路誕生的就是 Fragments 組件,即 “片段”。使用 FragmentActivity 可以在一個 Activity 中組合一個或者多個片段,方便進行代碼復用,片段的生命周期受到宿主 Activity 的影響。
Fragment Injection 漏洞最早在 2013 年爆出,這里只介紹其原理,本節末尾附有原始的文章以及論文。漏洞的核心是系統提供的 PreferenceActivity 類,開發者可以對其進行繼承實現方便的設置功能,該類的 onCreate 函數有下面的功能:
protected void onCreate() {
// ...
String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
// ...
if (initialFragment != null) {
switchToHeader(initialFragment, initialArguments);
}
}
private void switchToHeaderInner(String fragmentName, Bundle args) {
getFragmentManager().popBackStack(BACK_STACK_PREFS,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
if (!isValidFragment(fragmentName)) {
throw new IllegalArgumentException("Invalid fragment for this activity: "
+ fragmentName);
}
Fragment f = Fragment.instantiate(this, fragmentName, args);
}
可以看到從 Intent 中獲取了一個字符串和一個 Bundle 參數,并最終傳入 switchToHeaderInner 中,用于實例化具體的 Fragment。實例化的過程如下:
public static Fragment instantiate(Context context, String fname, Bundle args) {
// ...
Class clazz = sClassMap.get(fname);
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment)clazz.newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.mArguments = args;
}
return f;
}
經典的反射調用,將傳入的字符串實例化為 Java 類,并設置其參數。這是什么,這就是反序列化啊!而實際的漏洞也正是出自這里,由于傳入的參數攻擊者可控,那么攻擊者可以將其設置為某個內部類,從而觸及開發者預期之外的功能。在原始的報告中,作者使用了 Settings 應用中的某個設置 PIN 密碼的 Fragment 作為目標傳入,這是個私有片段,從而導致了越權修改 PIN 碼的功能。在當時的其他用戶應用中,還有許多也使用了 PreferenceActivity,因此漏洞影響廣泛,而且造成的利用根據應用本身的功能而異(也就是看有沒有好用的 Gadget)。
注意上面的代碼摘自最新的 Android 13,其中 switchToHeaderInner 方法加入了 isValidFragment 的判斷,這正是 Android 當初的修復方案之一,即強制要求 PreferenceActivity 的子類實現該方法,不然就在運行時拋出異常。不過即便如此,還是有很多開發者為了圖方便直接繼承然后返回 true 的。
Fragment Injection 看似是 PreferenceActivity 的問題,但其核心還是對于不可信輸入的校驗不完善,在后文的例子中我們會多次看到類似的漏洞模式。
參考文章:
- A New Vulnerability in the Android Framework: Fragment Injection
- ANDROID COLLAPSES INTO FRAGMENTS.pdf (wp)
- Understanding fragment injection
- How to fix Fragment Injection vulnerability
點擊劫持
Activity 既然作為 UI 的主要載體,那么與用戶的交互也是其中關鍵的一項功能。在傳統 Web 安全中就已經有過點擊劫持的方法,即將目標網站想要讓受害者點擊的案件放在指定位置(如iframe),并在宿主中使用相關組件對目標進行覆蓋和引導,令受害者在不知不覺中執行了敏感操作,比如點贊投幣收藏一鍵離職等。
Android 中也出現過類似的攻擊手段,比如在系統的敏感彈窗前面覆蓋攻擊者自定義的 TextView,引導受害者確認某些有害操作。當然這需要攻擊者的應用擁有浮窗權限(SYSTEM_ALERT_WINDOW),在較新的 Android 系統中,該權限的申請需要用戶多次的確認。
近兩年中在 AOSP 中也出現過一些點擊劫持漏洞,包括但不限于:
- CVE-2020-0306:藍牙發現請求確認框覆蓋
- CVE-2020-0394:藍牙配對對話框覆蓋
- CVE-2020-0015:證書安裝對話框覆蓋
- CVE-2021-0314:卸載確認對話框覆蓋
- CVE-2021-0487:日歷調試對話框覆蓋
- ...
對于系統應用而言,防御點擊劫持的方法一般是通過使用 android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS 權限并在布局參數中指定 SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS 來防止 UI 被覆蓋。
而對于普通應用,沒法申請 HIDE_NON_SYSTEM_OVERLAY_WINDOWS 權限,防御措施一般有兩種,一是通過將布局的 filterTouchesWhenObscured 設置為 true 來禁止窗體被覆蓋后的輸入事件;二是重載 View.onFilterTouchEventForSecurity 方法,并在其中檢測其他應用的覆蓋情況。在 Android 12 中系統已經默認開啟了 filterTouchesWhenObscured 屬性,這也是 security by default 的一種經典實現。
關于點擊劫持的操作細節和緩解方案,可以參考 OPPO 安全實驗室的這篇文章: 《不可忽視的威脅:Android中的點擊劫持攻擊》
另外一個與點擊劫持類似的漏洞稱為 StrandHogg,細節可以參考下述的原始文章。其關鍵點是使用了 Activity 的 allowTaskReparenting 和 taskAffinity 屬性,將其任務棧偽裝成目標應用,這樣在打開目標應用時由于 TaskStack 后進先出的特性會導致用戶看到的是攻擊者的應用,從而造成應用的釣魚場景。
后來還是同一個安全團隊有提出了 StrandHogg 2.0 版本,主要利用了 ActivityStarter 中的 AUTOMERGE 特性。假設有 A、B 兩個應用,在 A1 中調用 startActivites(B1, A2, B2) 之后,任務棧會從 (A1, B1) 以及 (A2, B2) 合并為 (A1, B1, A2, B2),也就是在同一個任務棧中覆蓋了其他應用的 Activity,從而導致釣魚場景。不過這個漏洞比較特化,因此谷歌很早就已經修復了,詳情可以閱讀下面的參考文章:
- The StrandHogg vulnerability
- StrandHogg 2.0 – New serious Android vulnerability
- StrandHogg 2.0 (CVE-2020-0096) 修復方案
Intent Redirection
Intent Redirection,顧名思義就是將用戶傳入的不可信輸入進行了轉發,類似于服務端的 SSRF 漏洞。一個典型漏洞例子如下:
protected void onCreate (Bundle savedInstanceState) {
Intent target = (Intent) getIntent().getParcelableExtra("target");
startActivity(target);
}
將用戶傳入的 target Parcelable 直接轉換成了 Intent 對象,并將這個對象作為 startActivity 的參數進行調用。就這個例子而言,可能造成的危害就是攻擊者可以用任意構造的 Intent 數據去啟動目標 APP 中的任意應用,哪怕是未導出的私有應用。而目標未導出的應用中可能進一步解析了攻擊者提供的 Intent 中的參數,去造成進一步的危害,比如在內置 Webview 中執行任意 Javascript 代碼,或者下載保存文件等。
實際上 Intent Redirection 除了可能用來啟動私有 Activity 組件,還可以用于其他的的接口,包括:
注:每種方法可能還有若干衍生方法,比如 startActivityForResult
前面三個可能比較好理解,分別是啟動界面、啟動服務和發送廣播。最后一個 setResult 可能會在排查的時候忽略,這主要用來給當前 Activity 的調用者返回額外數據,主要用于 startActivityForResult 的場景,這同樣也可能將用戶的不可信數據污染到調用者處。
從防御的角度上來說,建議不要直接把外部傳入的 Intent 作為參數發送到上述四個接口中,如果一定要這么做的話,需要事先進行充分的過濾和安全校驗,比如:
- 將組件本身的
android:exported設置為false,但這只是防止了用戶主動發送的數據,無法攔截通過setResult返回的數據; - 確保獲取到的
Intent來自于可信的應用,比如在組件上下文中調用getCallingActivity().getPackageName().equals("trust.app"),但注意惡意的應用可以通過構造數據令getCallingActivity返回null; - 確保待轉發的
Intent沒有有害行為,比如 component 不指向自身的非導出組件,不帶有FLAG_GRANT_READ_URI_PERMISSION等(詳見后文 ContentProvider 漏洞); - ...
但事實證明,即便是 Google 自己,也未必能夠確保完善的校驗。無恒實驗室近期提交的高危漏洞 CVE-2022-20223 就是個很典型的例子:
private void assertSafeToStartCustomActivity(Intent intent) {
// Activity can be started if it belongs to the same app
if (intent.getPackage() != null && intent.getPackage().equals(packageName)) {
return;
}
// Activity can be started if intent resolves to multiple activities
List<ResolveInfo> resolveInfos = AppRestrictionsFragment.this.mPackageManager
.queryIntentActivities(intent, 0 /* no flags */);
if (resolveInfos.size() != 1) {
return;
}
// Prevent potential privilege escalation
ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
if (!packageName.equals(activityInfo.packageName)) {
throw new SecurityException("Application " + packageName
+ " is not allowed to start activity " + intent);
}
}
其中使用了 ActivityInfo.packageName 來判斷啟動目標的包名是否與當前 caller 的包名一致,可事實上顯式 Intent 是通過 componentName 去指定啟動目標,優先級高于 Intent.packageName 且后者可以被偽造,這就造成了檢查的繞過。上述短短幾行代碼中其實還有另外一個漏洞,感興趣的可以參考下面的參考鏈接。
因此,遇到潛在的 Intent 重定向問題時,可以多花點時間仔細審查,說不定就能夠找到一個可利用的場景。
- Remediation for Intent Redirection Vulnerability
- AOSP Bug Hunting with appshark (1): Intent Redirection
Service
Service 的主要功能有兩個,一是給 APP 提供一個后臺的長時間運行環境,二是對外提供自身的服務。與 Activity 的定義類似,Service 必須要在 manifest 中進行聲明才能使用。注意 Service 中的代碼也是和 Activity 一樣運行在主線程的,并且默認和應用處于進程。
根據 Service 的兩大主要功能區分,啟動 Service 也有對應的兩種形式:
Context.startService():啟動后臺服務并讓系統進行調度;Context.bindService():讓(外部)應用綁定服務,并使用其提供的接口,可以理解為 RPC 的服務端;
兩種方式啟動服務的生命周期圖示如下:

藍色部分都是在客戶端去進行調用,系統收到請求后會啟動對應的服務,如果對應的進程沒有啟動也會通知 zygote 去啟動。不管是哪種方法創建服務,系統都會為其調用 onCreate 和 onDestroy 方法。整體流程和 Activity 的啟動流程類似,這里不再贅述。
shell 中同樣提供了 start-activity 命令來方便啟動服務:
am start-service [--user <USER_ID> | current] <INTENT>
下面來介紹一些 Service 組件相關的漏洞。
生命周期
前面介紹了 Service 啟動的生命周期,總體和 Activity 流程差不多,但需要注意有幾點不同:
- 與 Activity 生命周期回調方法不同,不需要調用 Serivce 回調方法的超類實現,比如 onCreate、onDestory 等;
Service類的直接子類運行在主線程中,同時處理多個阻塞的請求時候一般需要在新建線程中執行;IntentService是 Service 的子類,被設計用于運行在 Worker 線程中,可以串行處理多個阻塞的 Intent 請求;API-30 以后被標記為廢棄接口,建議使用 WorkManager 或者 JobIntentService 去實現;- 客戶端通過
stopSelf或者stopService來停止綁定服務,但服務端并沒有對應的onStop回調,只有在銷毀前收到onDestory; - 前臺服務必須為狀態欄提供通知,讓用于意識到服務正在運行;
對于綁定服務而言,Android 系統會根據綁定的客戶端引用計數來自動銷毀服務,但如果服務實現了 onStartCommand() 回調,就必須顯式地停止服務,因為系統會將其視為已啟動的狀態。此外,如果服務允許客戶端再次綁定,就需要實現 onUnbind 方法并返回 true,這樣客戶端在下次綁定時候會接收到同樣的 IBinder,示例圖如下所示:

服務的聲明周期相比于 Activity 更加復雜,因為涉及到進程間的綁定關系,因此也就更可能在不了解的情況下編寫出不健壯甚至有問題的代碼。
Implicit Export
和 Activity 一樣,Service 也要在 manifest 中使用 service 去聲明,也有 android:exported 屬性。甚至關于該屬性的默認值定義也是一樣的,即默認是 false,但包含 intent-filter 時,默認就是 true。同樣,在 Android 12 及以后也強制性要求必須顯式指定服務的導出屬性。
服務劫持
與 Activity 不同的是,Android 不建議使用隱式 Intent 去啟動服務。因為服務在后臺運行,沒有可見的圖形界面,因此用戶看不到隱式 Intent 啟動了哪個服務,且發送者也不知道 Intent 會被誰接收。
服務劫持是一個典型的漏洞,攻擊者可以為自己的 Service 聲明與目標相同的 intent-filter 并設定更高的優先級,這樣可以截獲到本應發往目標服務的 Intent,如果帶有敏感信息的話還會造成數據泄露。
而在 bindService 中這種情況的危害則更加嚴重,攻擊者可以偽裝成目標 IPC 服務去返回錯誤甚至是有害的數據。因此,在 Android 5.0 (API-21)開始,使用隱式 Intent 去調用 bindService 會直接拋出異常。
如果待審計的目標應用在 Service 中提供了 intent-filter,那么就需要對其進行重點排查。
AIDL
綁定服務可以被用來用作 IPC 服務端,如果服務端綁定的時候返回了 AIDL 接口的實例,那么就意味著客戶端可以調用該接口的任意方法。一個實際案例是 Tiktok 的 IndependentProcessDownloadService,在 DownloadService 的 onBind 中返回了上述 AIDL 接口的實例:
com/ss/android/socialbase/downloader/downloader/DownloadService.java:
if (this.downloadServiceHandler != null) {
return this.downloadServiceHandler.onBind(intent);
}
而其中有個 tryDownload 方法可以指定 url 和文件路徑將文件下載并保存到本地。雖然攻擊者沒有 AIDL 文件,但還是可以通過反射去構造出合法的請求去進行調用,PoC 中關鍵的代碼如下:
private ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName cName, IBinder service) {
processBinder(service);
}
public void onServiceDisconnected(ComponentName cName) { }
};
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent("com.ss.android.socialbase.downloader.remote");
intent.setClassName(
"com.zhiliaoapp.musically",
"com.ss.android.socialbase.downloader.downloader.IndependentProcessDownloadService");
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
}
private void processBinder(IBinder binder) {
ClassLoader cl = getForeignClassLoader(this, "com.zhiliaoapp.musically");
Object handler = cl.loadClass("com.ss.android.socialbase.downloader.downloader.i$a")
.getMethod("asInterface", IBinder.class)
.invoke(null, binder);
Object payload = getBinder(cl);
cl.loadClass("com.ss.android.socialbase.downloader.downloader.i")
.getMethod("tryDownload", cl.loadClass("com.ss.android.socialbase.downloader.model.a"))
.invoke(handler, payload);
}
private Object getBinder(ClassLoader cl) throws Throwable {
Class utilsClass = cl.loadClass("com.ss.android.socialbase.downloader.utils.g");
Class taskClass = cl.loadClass("com.ss.android.socialbase.downloader.model.DownloadTask");
return utilsClass.getDeclaredMethod("convertDownloadTaskToAidl", taskClass)
.invoke(null, getDownloadTask(taskClass, cl));
}
關鍵在于使用 Context.getForeignClassLoader 獲取其他應用的 ClassLoader。
Intent Redirect
這個其實和 Activity 中的對應漏洞類似,客戶端啟動/綁定 Service 的時候也指定了隱式或者顯式的 Intent,其中的不可信數據如果被服務端用來作為啟動其他組件的參數,就有可能造成一樣的 Intent 重定向問題。注意除了 getIntent() 之外還有其他數據來源,比如服務中實現的 onHandleIntent 的參數。
其實最早提出 Intent 重定向危害的 "LaunchAnywhere" 漏洞就是出自系統服務,準確來說是 AccountManagerService 的漏洞。AccountManager 正常的執行流程為:
- 普通應用(記為 A)去請求添加某類賬戶,調用 AccountManager.addAccount;
- AccountManager 會去查找提供賬號的應用(記為 B)的 Authenticator 類;
- AccountManager 調用 B 的 Authenticator.addAccount 方法;
- AccountManager 根據 B 返回的 Intent 去調起 B 的賬戶登錄界面(AccountManagerResponse.getParcelable);
在第 4 步時,系統認為 B 返回的數據是指向 B 的登陸界面的,但實際上 B 可以令其指向其他組件,甚至是系統組件,就造成了一個 Intent 重定向的漏洞。這里 Intent 的來源比較曲折,但本質還是攻擊者可控的。
關于該漏洞的細節和利用過程可參考:launchAnyWhere: Activity組件權限繞過漏洞解析(Google Bug 7699048 )
Receiver
Broadcast Receiver,簡稱 receiver,即廣播接收器。前面介紹的 Activity 和 Service 之間的聯動都是一對一的,而很多情況下我們可能想要一對多或者多對多的通信方案,廣播就承擔了這個功能。比如,Android 系統本身就會在發生各種事件的時候發送廣播通知所有感興趣的應用,比如開啟飛行模式、網絡狀態變化、電量不足等等。這是一種典型的發布/訂閱的設計模式,廣播數據的載體也同樣是 Intent。
與前面 Activity 與 Service 不同的是,Receiver 可以在 manifest 中進行聲明注冊,稱為靜態注冊;也可以在應用運行過程中進行動態注冊。但無論如何,定義的廣播接收器都要繼承自 BroadcastReceiver 并實現其聲明周期方法 onReceive(context, intent)。
注意 BroadcastReceiver 的父類是 Object,不像 Activity 與 Service 是 Context,因此 onReceive 還會額外傳入一個 context 對象。
shell 中發送廣播的命令如下:
am broadcast [--user <USER_ID> | all | current] <INTENT>
下面還是按順序介紹一些常見的問題。
Implicit Export
使用靜態注冊的 receiver 倒沒什么特殊,示例如下:
<receiver android:name=".MyBroadcastReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
</intent-filter>
</receiver>
同樣存在和之前一樣的默認 export 問題,相信大家已經看膩了,就不再啰嗦了。接著看動態注冊的情況,比如:
BroadcastReceiver br = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);
與清單中的定義相比,動態注冊的方式可能更容易忽略導出權限的問題。上述代碼片段動態注冊了一個廣播,但沒有顯式聲明 exported 屬性,因此默認是導出的。事實上使用 registerReceiver 似乎沒有簡單的方法去設置 exported=false,而 Google 官方的建議是對于不需要導出的廣播接收器使用 LocalBroadcastManager.registerReceiver 進行注冊,或者在注冊的時候指定 permission 權限。
對于指定 permission 權限的情況,如果是自定義權限,需要在應用清單中聲明,比如:
<permission android:name="com.evilpan.MY_PERMISSION"
android:protectionLevel="signature"/>
<uses-permission android:name="com.evilpan.MY_PERMISSION" />
signature 表示只有在請求授權的應用使用與聲明權限的應用相同的證書進行簽名時系統才會授予的權限。如果證書匹配,則系統會在不通知用戶或征得用戶明確許可的情況下自動授予權限。詳見 protectionLevel。
最后在動態注冊時指定該權限即可:
this.registerReceiver(br, filter, "com.evilpan.MY_PERMISSION", null);
注冊未帶有權限限制的導出廣播接收器會導致接收到攻擊者偽造的惡意數據,如果在 onReceive 時校驗不當,可能會出現越權或者 Intent 重定向等漏洞,造成進一步的安全危害。
這類安全問題很多,比較典型的就有 Pwn2Own 上用于攻破三星 Galaxy S8 的 PpmtReceiver 漏洞。
信息泄露
上面主要是從限制廣播發送方的角度去設置權限,但其實這個權限也能限制廣播的接收方,只不過發送消息的時候要進行額外的指定,比如要想只讓擁有上述權限的接收方受到廣播,則發送代碼如下:
Intent it = new Intent(this, ...);
it.putExtra("secret", "chicken2beautiful")
sendBroadcast(it, "com.evilpan.MY_PERMISSION");
如果不帶第二個參數的話,默認是所有滿足條件的接受方都能受到廣播信息的。此時若是發送的 Intent 中帶有敏感數據,就可能會造成信息泄露問題。
一個實際案例就是 CVE-2018-9581,系統在廣播 android.net.wifi.RSSI_CHANGED 時攜帶了敏感數據 RSSI,此廣播能被所有應用接收,從而間接導致物理位置信息泄露。(搞笑?)
可見對于 Broadcast Receiver 而言,permission 標簽的作用尤其明顯。對于系統廣播而言,比如 BOOT_COMPLETED,通常只有系統應用才有權限發送。這都是在 framework 的 AndroidManifest.xml 中進行定義的。
而對于應用的自定義廣播,通常是使用上述自定義權限,那么也就自然想到一個問題,如果多個應用定義了同一個權限會怎么樣?其實這是正是一個歷史漏洞,在早期 Android 的策略是優先采用第一個定義的權限,但在 Andorid 5 之后就已經明確定義了兩個應用不同定義相同的權限(除非他們的簽名相同),否則后安裝的應用會出現 INSTALL_FAILED_DUPLICATE_PERMISSION 錯誤警告。感興趣的考古愛好者可以參考下面的相關文章:
- Vulnerabilities with Custom Permissions
- Custom Permission Vulnerability and the 'L' Developer Preview
Intent Redirection
原理不多說了,直接看案例吧。漏洞出在 Tiktok 的 NotificationBroadcastReceiver 中,定義了 intent-filter 導致組件默認被設置為導出,因此可以接收到外部應用的廣播,而且又將廣播中的不可信數據直接拿來啟動 Activity,如下:

漏洞細節可參考:Oversecured detects dangerous vulnerabilities in the TikTok Android app
ContentProvider
Content Provider,即內容提供程序,簡稱為 Provider。Android 應用通常實現為 MVC 結構(Model-View-Controller),Model 部分即為數據來源,供自身的 View 即圖形界面進行展示。但有時候應用會想要將自身的數據提供給其他數據使用,或者從其他應用中獲取數據。
定義一個 ContentProvider 的方式,只需要繼承自 ContentProvider 類并實現六個方法: query,insert, update, delete, getType 以及 onCreate。其中除了 onCreate 是系統在主線程調用的,其他方法都由客戶端程序進行主動調用。自定義的 provider 必須在程序清單中進行聲明,后文會詳細介紹。
可以看到 Provider 主要實現了類似數據庫的增刪改查接口,從客戶端來看,查詢過程也和查詢傳統數據庫類似,例如,下面是查詢系統短信的代碼片段:
Cursor cursor = getContentResolver().query(
Telephony.Sms.Inbox.CONTENT_URI, // 指定要查詢的表名
new String[] { Telephony.Sms.Inbox.BODY }, // projection 指定索要查詢的列名
selectionClause, // 查詢的過濾條件
selectionArgs, // 查詢過濾的參數
Telephony.Sms.Inbox.DEFAULT_SORT_ORDER); // 返回結果的排序
while (cursor.moveToNext()) {
Log.i(TAG, "msg: " + cursor.getString(0));
}
其中 ContentResolver 是 ContentInterface 子類,后者是 ContentProvider 的客戶端遠程接口,可以實現其透明的遠程代理調用。 content_uri 可以看作是查詢的表名,projection 可以看作是列名,返回的 cursor 是查詢結果行的迭代器。
與前面三個組件不同,在 shell 中訪問 provider 組件的工具是 content。
下面來介紹 Provider 中常見的問題。
Permissions
鑒于 provider 作為數據載體,那么安全訪問與權限控制自然是重中之重。例如上面代碼示例中訪問短信的接口,如果所有人都能隨意訪問,那就明顯會帶來信息泄露問題。前面簡單提到過,應用中定義的 Provider 必須要在其程序清單文件中進行聲明,使用的是 provider 標簽。其中有我們常見的 exported 屬性,表示是否可被外部訪問,permission 屬性則表示訪問所需的權限,當然也可以分別對讀寫使用不同的權限,比如 readPermission/writePermission 屬性。
比如,前文提到的短信數據庫聲明如下:
<provider android:name="SmsProvider"
android:authorities="sms"
android:multiprocess="false"
android:exported="true"
android:singleUser="true"
android:readPermission="android.permission.READ_SMS" />
其他應用若想訪問,則需在清單文件中聲明請求對應權限。
<uses-permission android:name="android.permission.READ_SMS" />
這都很好理解,其他組件也有類似的特性。除此之外,Provider 本身還提供了更為細粒度的權限控制,即 grantUriPermissions。這是一個布爾值,表示是否允許臨時為客戶端授予該 provider 的訪問權限。臨時授予權限的運行流程一般如下:
- 客戶端給 Provider 所在應用發送一個 Intent,指定想要訪問的 Content URI,比如使用
startActivityForResult發送; - 應用收到 Intent 后,判斷是否授權,如果確認則準備一個 Intent,并設置好 flags 標志位
FLAG_GRANT_[READ|WRITE]_URL_PERMISSION,表示允許讀/寫對應的 Content URI(可以不和請求的 URI 一致),最后使用setResult(code, intent)返回給客戶端; - 客戶端的 onActivityResult 收到返回的 Intent,使用其中的 URI 來臨時對目標 Provider 進行訪問;
以讀為例,Intent.flags 中如果包含 FLAG_GRANT_READ_URI_PERMISSION,那么該 Intent 的接收方(即客戶端)會被授予 Intent.data 部分 URI 的臨時讀取權限,直至接收方的生命周期結束。另外,Provider 應用也可以主動調用 Context.grantUriPermission 方法來授予目標應用對應權限:
public abstract void grantUriPermission (String toPackage,
Uri uri,
int modeFlags)
public abstract void revokeUriPermission (String toPackage,
Uri uri,
int modeFlags)
grantUriPermissions 屬性可以在 URI 粒度對權限進行讀寫控制,但有一個需要注意的點:通過 grantUriPermissions 臨時授予的權限,會無視 readPermission、writePermission、permission 和 exported 屬性施加的限制。也就是說,即便 exported=false,客戶端也沒有申請對應的 uses-permission,可一旦被授予權限,依然可以訪問對應的 Content Provider!
另外,<provider> 還有一個子標簽 grant-uri-permission,即便 grantUriPermissions 被設置為 false,通過臨時獲取權限依然可以訪問該標簽下定義的 URI 子集,該子集可以用前綴或者通配符去指定 URI 的可授權路徑范圍。
Provider 權限設置不當可能會導致應用數據被預期之外的惡意程序訪問,輕則導致信息泄露,重則會使得自身沙盒數據被覆蓋而導致 RCE,后文會看到多個這樣的案例。
FileProvider
前面說過自定義 Provider 需要實現六個方法,但 Android 中已經針對某些常用場景的 Provider 編寫好了對應的子類,用戶可根據需要繼承這些子類并實現少部分子類方法即可。其中一個常用場景就是用 ContentProvider 分享應用的文件,系統提供了 FileProvider 來方便應用自定義文件分享和訪問,但是使用不當的話很可能會出現任意文件讀寫的問題。
FileProvider 提供了使用 XML 去指定文件訪問控制的功能,一般 Provider 應用只需繼承 FileProvider 類:
public class MyFileProvider extends FileProvider {
public MyFileProvider() {
super(R.xml.file_paths)
}
}
file_paths 是用戶自定義的 XML,也可以在清單文件中使用 meta-data 去指定:
<provider xmlns:android="http://schemas.android.com/apk/res/android" android:name="com.evilpan.MyFileProvider" android:exported="false" android:authorities="com.evilpan.fileprovider" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@7F15000E"/>
</provider>
resource 指向 res/xml/file_paths.xml。該文件中定義了可供訪問的文件路徑,FileProvider 只會對提前指定的文件生成 Content URI。一個文件路徑配置示例如下:
<paths>
<root-path name="root" path=""/>
<files-path name="internal_files" path="."/>
<cache-path name="cache" path=""/>
<external-path name="external_files" path="images"/>
</paths>
paths 標簽支持多種類型的子標簽,分別對應不同目錄的子路徑:
files-path: Context.getFilesDir()cache-path: Context.getCacheDir()external-path: Environment.getExternalStorageDirectory()external-files-path: Context.getExternalFilesDir()external-cache-path: Context.getExternalCacheDir()external-media-path: Context.getExternalMediaDirs()[0]
比較特殊的是 root-path,表示系統的根目錄 /。FileProvider 生成的 URI 格式一般是 content://authority/{name}/{path},比如對于上述 Provider,可用 content://com.evilpan.fileprovider/root/proc/self/maps 來訪問 /proc/self/maps 文件。
由此可見,FileProvider 指定 root-path 是一個危險的標志,一旦攻擊者獲得了臨時權限,就可以讀取所有應用的私有數據。
比如,TikTok 歷史上就有過這么一個真實的漏洞:
<provider android:name="android.support.v4.content.FileProvider" android:exported="false" android:authorities="com.zhiliaoapp.musically.fileprovider" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/k86"/>
</provider>
這里直接使用了 FileProvider,甚至都不需要繼承。xml/k86.xml 文件內容如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:amazon="http://schemas.amazon.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<root-path name="name" path=""/>
<external-path name="share_path0" path="share/"/>
<external-path name="download_path2" path="Download/"/>
<cache-path name="gif" path="gif/"/>
...
</paths>
獲取臨時權限之后就可以實現應用的任意文件讀寫。
The Hidden ...
在 ContentProvider 類中,除了前面說過的 6 個必須實現的方法,還有一些其他隱藏的方法,一般使用默認實現,也可以被子類覆蓋實現,比如
- openFile
- openFileHelper
- call
- ...
這些隱藏的方法可能在不經意間造成安全問題,本節會通過一些案例去分析其中的原因。
openFile
如果 ContentProvider 想要實現共享文件讀寫的功能,還可以通過覆蓋 openFile 方法去實現,該方法的默認實現會拋出 FileNotFoundException 異常。
雖然開發者實現上不太會直接就返回打開的本地文件,而是有選擇地返回某些子目錄文件。但是如果代碼寫得不嚴謹,就可能會出現路徑穿越等問題,一個經典的漏洞實現如下:
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
File file = new File(getContext().getFilesDir(), uri.getPath());
if(file.exists()){
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException(uri.getPath());
}
另外一個同族的類似方法是 openAssetFile,其默認實現是調用 openFile:
public @Nullable AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
ParcelFileDescriptor fd = openFile(uri, mode);
return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
}
有時候開發者雖然知道要要防御路徑穿越,但防御的姿勢不對,也存在被繞過的可能,比如:
public ParcelFileDescriptor openFile(Uri uri, String mode) {
File f = new File(DIR, uri.getLastPathSegment());
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}
這里想用 getLastPathSegment 去只獲取最后一級的文件名,但實際上可以被 URL encode 的路徑繞過,比如 %2F..%2F..path%2Fto%2Fsecret.txt 會返回 /../../path/to/secret.txt。
還有一種錯誤的防御是使用 UriMatcher.match 方法去查找 ../,這也會被 URL 編碼繞過。正確的防御和過濾方式如下:
public ParcelFileDescriptor openFile (Uri uri, String mode) throws FileNotFoundException {
File f = new File(DIR, uri.getLastPathSegment());
if (!f.getCanonicalPath().startsWith(DIR)) {
throw new IllegalArgumentException();
}
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}
openFileHelper
ContentProvider 中還有一個鮮為人知的 openFileHelper 方法,其默認實現是使用當前 Provider 中的 _data 列數據去打開文件,源碼如下:
protected final @NonNull ParcelFileDescriptor openFileHelper(@NonNull Uri uri,
@NonNull String mode) throws FileNotFoundException {
Cursor c = query(uri, new String[]{"_data"}, null, null, null);
int count = (c != null) ? c.getCount() : 0;
if (count != 1) {
// If there is not exactly one result, throw an appropriate
// exception.
if (c != null) {
c.close();
}
if (count == 0) {
throw new FileNotFoundException("No entry for " + uri);
}
throw new FileNotFoundException("Multiple items at " + uri);
}
c.moveToFirst();
int i = c.getColumnIndex("_data");
String path = (i >= 0 ? c.getString(i) : null);
c.close();
if (path == null) {
throw new FileNotFoundException("Column _data not found.");
}
int modeBits = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(new File(path), modeBits);
}
這個方法的主要作用是方便子類用于快速實現 openFile 方法,通常不會直接在子類去覆蓋。不過由于其中基于 _data 列去打開文件的特性可能會攻擊者插入惡意數據后間接地實現任意文件讀寫。
一個經典案例就是三星手機的 SemClipboardProvider,在插入時未校驗用戶數據:
public Uri insert(Uri uri, ContentValues values) {
long row = this.database.insert(TABLE_NAME, "", values);
if (row > 0) {
Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
getContext().getContentResolver().notifyChange(newUri, null);
return newUri;
}
throw new SQLException("Fail to add a new record into " + uri);
}
而該 Provider 又在 system_server 進程中,擁有極高的運行權限,攻擊者通過利用這個漏洞去就能實現系統層面的任意文件讀寫,其 PoC 如下:
ContentValues vals = new ContentValues();
vals.put("_data", "/data/system/users/0/newFile.bin");
URI semclipboard_uri = URI.parse("content://com.sec.android.semclipboardprovider")
ContentResolver resolver = getContentResolver();
URI newFile_uri = resolver.insert(semclipboard_uri, vals);
return resolver.openFileDescriptor(newFile_uri, "w").getFd();
該漏洞與其他漏洞一起曾被用于在野攻擊中,由 Google TAG 團隊捕獲,對這一條 Fullchain 的分析可以參考 Project Zero 近期的文章:A Very Powerful Clipboard: Analysis of a Samsung in-the-wild exploit chain
call
ContentProvider 中提供了 call 方法,用于實現調用服務端定義方法,其函數簽名如下:
public Bundle call (String authority,
String method,
String arg,
Bundle extras)
public Bundle call (String method,
String arg,
Bundle extras)
默認的實現是個空函數直接返回 null,開發者可以通過覆蓋該函數去實現一些動態方法,返回值也會傳回到調用者中。
看起來和常規的 RPC 調用類似,但這里有個小陷阱,開發者文檔中也特別標注了:Android 系統并沒有對 call 函數進行權限檢查,因為系統不知道在 call 之中對數據進行了讀還是寫,因此也就無法根據 Manifest 中定義的權限約束進行判斷。因此要求開發者自己對 call 中的邏輯進行權限校驗。
如果開發者實現了該方法,但是又未進行校驗或者校驗不充分,就可能出現越權調用的情況。一個案例是 CVE-2021-23243, OPPO 某系統應用中 HostContentProviderBase 的 call 方法實現中,直接用 DexClassLoader 去加載了傳入 dex 文件,直接導致攻擊者的代碼在特權進程中運行,所有繼承該基類的 Provider 都會受到影響 ()。
另外在某些系統 Provider 中,可以通過 call 方法去獲取某些遠程對象實例,例如在文章 Android 中的特殊攻擊面(三)—— 隱蔽的 call 函數 中,作者就通過 SliceProvider 與 KeyguardSliceProvider 獲取到了系統應用內部的 PendingIntent 對象,進一步利用實現了偽造任意廣播的功能。
其他
除了上述和四大組件直接相關的漏洞,Android 系統中還有許多不太好分類的漏洞,本節主要挑選其中幾個最為常見的漏洞進行簡單介紹。
PendingIntent
PendingIntent 是對 Intent 的表示,本身并不是 Intent 對象,但是是一個 Parcelable 對象。將該對象傳遞給其他應用后,其他應用就可以以發送方的身份去執行所指向的 Intent 指定的操作。 PendingIntent 使用下述靜態方法之一進行創建:
- getActivity(Context, int, Intent, int);
- getActivities(Context, int, Intent[], int);
- getBroadcast(Context, int, Intent, int);
- getService(Context, int, Intent, int);
PendingIntent 本身只是系統對原始數據描述符的一個引用,可以大致將其理解為 Intent 的指針。也因為如此,即便創建 PendingIntent 的應用關閉后,其他應用仍然可以使用該數據。如果原始應用后來進行了重啟并以同樣的參數創建了一個 PendingIntent,那么實際上返回 PendingIntent 與之前創建的會指向同樣的 token。注意判斷 Intent 是否相同是使用 filterEquals 方法,其中會判斷 action,data, type,identity,class,categories 是否相同,注意 extra 并不在此列,因此僅有 extra 不同的 Intent 也會被認為是相等的。
由于 PendingIntent 可代表其他應用的特性,在某些場景下可能被用于濫用。例如,如果開發者創建了這樣一個默認的 PendingIntent 并傳遞給其他應用:
pi = PendingIntent.getActivity(this, 0, new Intent(), 0);
bundle.putParcelable("pi", pi)
// send bundle
惡意的應用在收到此 PendingIntent 后,可以獲取到原始的 intent,并使用 Intent.fillin 去填充空字段,如果原始 Intent 是上述空 Intent,那么攻擊者就可以將其修改為特定的 Intent,從而以目標的身份去啟動應用,包括未導出的私有應用。一個經典的案例就是早期的 broadAnywhere 漏洞,Android Settings 應用中的 addAccount 方法內創建了一個 PendingIntent 廣播,但 intent 內容為空,這導致收到 intent 的的惡意應用可以 fillin 填充廣播的 action,從而實現越權發送系統廣播,實現偽造短信、回復出廠設置等功能。
為了緩解這類問題,Andorid 中對 Intent.fillin 的改寫做了諸多限制,比如已有的字段不能修改,component 和 selector 字段不能修改(除非額外設置 FILL_IN_COMPONENT/SELECTOR),隱式 Intent 的 action 不能修改等。
不過有研究者提出了針對隱式 Intent 的利用方法,即通過修改 flag 添加 FLAG_GRANT_WRITE_URI_PERMISSION,并修改 data 的 URI 指向受害者私有的 Provider,將 package 改為攻擊者;同時攻擊者在自身的 Activity 中聲明相同的 intent filter,這樣在轉發 intent 時會啟動攻擊者應用,同時也獲取了目標私有 Provider 的訪問權限,從而實現私有文件竊取或者覆蓋。關于該攻擊思路詳情可以閱讀下面的參考文章。
- broadAnywhere:Broadcast組件權限繞過漏洞(Bug: 17356824)
- PendingIntent重定向:一種針對安卓系統和流行App的通用提權方法——BlackHat EU 2021議題詳解(上)
- PendingIntent重定向:一種針對安卓系統和流行App的通用提權方法——BlackHat EU 2021議題詳解(下)
在 Android 12+ 之后,PendingIntent 在創建時候要求顯式指定
FLAG_MUTABLE或者FLAG_IMMUTABLE,表示是否可以修改。
DeepLink
在大部分操作系統中都有 deeplink 的概念,即通過自定義 schema 打開特定的應用。比如通過點擊 https://evilpan.com/ 可以喚起默認瀏覽器打開目標網頁,點擊
應用要想注冊類似的自定義協議,需要在應用清單文件中進行聲明:
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="weixin" android:host="qr"/>
</intent-filter>
由于這類隱式 Intent 可以直接通過點擊鏈接去觸發,因此更受攻擊者喜愛。如果處理對應 Intent 的組件沒有過濾好用戶傳入的內容,很可能會造成 1-click 的漏洞。相關案例可以參考文章:Android 中的特殊攻擊面(二)——危險的deeplink
Webview
在 Andorid 系統中,Webview 主要用于應用在自身的 Activty 中展示網頁內容,并提供了一些額外的接口來給開發者實現自定義的控制。更高的拓展性也就意味著更多出錯的可能,尤其是如今 Android 客戶端開發式微,Java 開發也朝著 “大前端” 的方向發展。原本許多使用原生應用實現的邏輯逐漸轉移到了 web 頁面中,比如 h5、小程序等,這樣一來,webview 的攻擊面也就擴寬了不少。
常規的 Webview 安全問題主要是在與一些配置的不安全,比如覆蓋 onReceivedSslError 忽略 SSL 錯誤導致中間人攻擊,setAllowFileAccessFromFileURLs 導致本地私有文件泄露等。但現在的漏洞更多出在 JSBridge 上,這是 Java 代碼與網頁中的 JavaScript 代碼溝通的橋梁。
由于 Webview 或者說 JS 引擎的沙箱特性,網頁中的 Javascript 代碼本身無法執行許多原生應用才能執行的操作,比如無法從 Javascript 中發送廣播,無法訪問應用文件等。而由于業務的復雜性,很多邏輯又必須在 Java 層甚至是 Native 層才能實現,因此這就需要用到 JSBridage。傳統的 JSBridge 通過 Webview.addJavascriptInterface 實現,一個簡單示例如下:
class JsObject {
@JavascriptInterface
public String toString() { return "injectedObject"; }
}
webview.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");
Java 層返回數據給 Javascript 一般是通過直接使用 loadUrl 執行 JS 代碼實現。當然除了這種方式注冊 Bridge,還有很多應用特異的實現,比如使用 console.log 傳輸數據并在 Java 層使用 onConsoleMessage 回調去接收。但無論如何,這都導致攻擊面的增加,大型應用甚至注冊了上百個 jsapi 來供網頁調用。
從歷史漏洞來看,Webview 漏洞的成因主要是 jsapi 域名校驗問題和 Bridge 代碼本身的漏洞,由于篇幅原因就不展開了。
后記
本文中主要通過 Android 中的四大組件介紹了一系列相關的邏輯問題,盡可能地囊括了筆者所了解的歷史漏洞。由于個人認知水平有限,總是難免掛一漏萬,但即便如此,文章的篇幅還是比預想中的超出了億點點。從溫故知新的角度看,挖掘這類邏輯漏洞最好的策略還是使用靜態分析工具,搜集更多 Sink 模式并編寫有效的規則去進行掃描,實在沒有條件的話用 (rip)grep 也是可以的。
參考資料
- Galaxy Leapfrogging 蓋樂世蛙跳 Pwning the Galaxy S8
- Chainspotting: Building Exploit Chains with Logic Bugs (如何用11個exp攻破三星S8)
- Huawei Mate 9 Pro Mobile Pwn2Own 2017
- Detect dangerous vulnerabilities in the TikTok Android app - Oversecured
- 魔形女漏洞白皮書 - 京東探索研究院信息安全實驗室
- HACKING XIAOMI'S ANDROID APPS - Part1
- Automating Pwn2Own with Jandroid
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2018/
暫無評論