Android 組件邏輯漏洞漫談
前言
隨著社會越來越重視安全性,各種防御性編程或者漏洞緩解措施逐漸被加到了操作系統(tǒng)中,比如代碼簽名、指針簽名、地址隨機化、隔離堆等等,許多常見的內(nèi)存破壞漏洞在這些緩解措施之下往往很難進行穩(wěn)定的利用。因此,攻擊者們的目光也逐漸更多地投入到邏輯漏洞上。邏輯漏洞通常具有很好的穩(wěn)定性,不用受到風(fēng)水的影響;但同時也隱藏得較深、混跡在大量業(yè)務(wù)代碼中難以發(fā)現(xiàn)。而且由于形式各異,不太具有通用性,從投入產(chǎn)出比的角度來看可能不是一個高優(yōu)先級的研究方向。但無論如何,這都始終是一個值得關(guān)注的攻擊面。因此,本文就以 Android 平臺為目標(biāo)介紹一些常見的邏輯漏洞。
四大組件
接觸過 Android 的人應(yīng)該都聽說過 “四大組件”,開發(fā)應(yīng)用首先需要學(xué)習(xí)的就是各個組件的生命周期。所謂四大組件,分別是指 Activity、Service、Broadcast Receiver 和 Content Provider,關(guān)于這些組件的實現(xiàn)細(xì)節(jié)可以參考官方的文檔: Application Fundamentals[1]。
在安全研究中,四大組件值得我們特別關(guān)注,因為這是應(yīng)用與外界溝通的重要橋梁,甚至在應(yīng)用內(nèi)部也是通過這些組件構(gòu)建起了相互間松耦合的聯(lián)系。比如應(yīng)用本身可以不申請相機權(quán)限,但可以通過組件間的相互通信讓(系統(tǒng))相機應(yīng)用打開攝像頭并取得拍到的照片,仿佛是自身進行拍照的一樣。
而在組件交互的過程中,最為核心的數(shù)據(jù)結(jié)構(gòu)就是 Intent[2],這是大部分組件之間進行通信的載體。
Intent 101
根據(jù)官方的說法,Intent 是 “對某種要執(zhí)行的操作的抽象描述”,直譯過來也可以叫做 “意圖”,比如說想要打開攝像機拍照、想要打開瀏覽器訪問網(wǎng)址,想要打開設(shè)置界面,……都可以用 Intent 來描述。
Intent 的主要形式有兩種,分別是顯式 Intent 和隱式 Intent;二者的差別主要在于前者顯式指定了 Component,后者沒有指定 Component,但是會通過足夠的信息去幫助系統(tǒng)去理解意圖,比如 ACTION、CATAGORY 等。
Intent 的最主要功能是用來啟動 Activity,因此我們以這個場景為例,從源碼中分析一下 Intent 的具體實現(xiàn)。啟動 Activity 的常規(guī)代碼片段如下:
Intent intent = new Intent(context, SomeActivity.class);
startActivity(intent);這里用的是顯式 Intent,但不是重點。一般在某個 Activity 中調(diào)用,因此調(diào)用的是 Activity.startActivity,代碼在 frameworks/base/core/java/android/app/Activity.java 中,這里不復(fù)制粘貼了,總而言之調(diào)用鏈路如下:
? Activity.startActivity()
? Activity.startActivityForResult()
? Instrumentation.execStartActivity()
? ActivityTaskManager.getService().startActivity()
? IActivityTaskManager.startActivity()
最后一條調(diào)用是個接口,這是個很常見的 pattern 了,下一步應(yīng)該去找其實現(xiàn),不出意外的話這個實現(xiàn)應(yīng)該在另一個進程中。事實上也正是在 system_server 中:
? ActivityTaskManagerService.startActivity()
? ActivityTaskManagerService.startActivityAsUser()
? ActivityStarter.execute()
最后一個方法通過前面?zhèn)魅氲男畔⑷?zhǔn)備啟動 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 信息,例如在隱式啟動的情況下,可能有多個符合要求的目標(biāo),也會彈出菜單詢問用戶選用哪個應(yīng)用打開。executeRequest 中則主要進行相關(guān)權(quán)限檢查,在所有權(quán)限滿足條件后再調(diào)用 startActivityUnchecked 去執(zhí)行真正的調(diào)用。
其中大部分流程我在 Android12 應(yīng)用啟動流程分析[3] 中已經(jīng)介紹過了,這里更多是關(guān)注 Intent 本身的作用。從上面的分析中發(fā)現(xiàn),可以將其看作是多進程通信中的消息載體,而其源碼定義也能看出 Intent 本身是可以可以序列化并在進程間傳遞的結(jié)構(gòu)。
public class Intent implements Parcelable, Cloneable { ... }Intent 本身有很多方法和屬性,這里暫時先不展開,后面介紹具體漏洞的時候再進行針對性的分析。后文主要以四大組件為著手點,分別介紹一些常見的漏洞模式和設(shè)計陷阱。
Activity
Activity[4] 也稱為活動窗口,是與用戶直接交互的圖形界面。APP 主要開發(fā)工作之一就是設(shè)計各個 activity,并規(guī)劃他們之間的跳轉(zhuǎn)和連結(jié)。通常一個 activity 表示一個全屏的活動窗口,但也可以有其他的存在形式,比如浮動窗口、多窗口等。作為 UI 窗口,一般使用 XML 文件進行布局,并繼承 Activity 類實現(xiàn)其生命周期函數(shù) onCreate 和 onPause 等生命周期方法。
如果開發(fā)者定義的 Activity 想通過 Context.startActivity 啟動的話,就必須將其聲明到 APP 的 manifest 文件中,即 AndroidManifest.xml[5]。應(yīng)用被安裝時,PackageManager 會解析其 manifest 文件中的相關(guān)信息并將其注冊到系統(tǒng)中,以便在 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 承載了許多用戶輸入/處理、以及外部數(shù)據(jù)接收/展示等工作,因此是應(yīng)用對外的一個主要攻擊面。下面就介紹幾種較為常見的攻擊場景。
生命周期
Activity 經(jīng)典的生命周期圖示如下:

通常開發(fā)者只需要實現(xiàn) onCreate 方法,但是對于一些復(fù)雜的業(yè)務(wù)場景,正確理解其生命周期也是很必要的。以筆者在內(nèi)測中遇到的某應(yīng)用為例,其中某個 Activity 中執(zhí)行了一些敏感的操作,比如開啟攝像頭推流,或者開啟了錄音,但只在 onDestroy 中進行了推流/錄音的關(guān)閉。這樣會導(dǎo)致在 APP 進入后臺時候,這些操作依然在后臺運行,攻擊者可以構(gòu)造任務(wù)棧使得受害者在面對惡意應(yīng)用的釣魚界面時候仍然執(zhí)行目標(biāo)應(yīng)用的后臺功能,從而形成特殊的釣魚場景。正確的做法應(yīng)該是在 onPaused 回調(diào)中對敏感操作進行關(guān)閉。
攻擊者實際可以通過連續(xù)發(fā)送不同的 Intent 去精確控制目標(biāo) Activity 生命周期回調(diào)函數(shù)的觸發(fā)時機,如果開發(fā)時沒有注意也會造成應(yīng)用功能的狀態(tài)機異常甚至是安全問題。
Implicit Exported
前面說過,開發(fā)者定義的 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[6] 中支持許多屬性。其中一個重要的屬性就是 android:exported,表示當(dāng)前 Activity 是否可以被其他應(yīng)用的組件啟動。該屬性有幾個特點:
1. 屬性可以缺省,缺省值默認(rèn)為
false;2. 如果 Activity 沒有顯式設(shè)置該屬性,且該 Activity 中定義了
<intent-filter>,那么缺省值就默認(rèn)為true;
也就是說,開發(fā)者可能沒有顯式指定 Activity 導(dǎo)出,但由于指定了 intent-filter,因此實際上也是導(dǎo)出的,即可以被其他應(yīng)用喚起對應(yīng)的 Activity。這種情況在早期很常見,比如 APP 設(shè)計了一組更換密碼的界面,需要先輸入舊密碼然后再跳轉(zhuǎn)到輸入新密碼的界面,如果后者是導(dǎo)出的,攻擊者就可以直接喚起輸入新密碼的界面,從而繞過了舊密碼的校驗邏輯。
Google 已經(jīng)深刻意識到了這個問題,因此規(guī)定在 Android 12 之后,如果應(yīng)用的 Activity 中包含 intent-filter,就必須要顯式指定 android:exported 為 true 或者 false,不允許缺省。在 Android 12 中未顯式指定 exported 屬性且?guī)в?intent-filter 的 Activity 的應(yīng)用在安裝時候會直接被 PackageManager 拒絕。
Fragment Injection
Activity 作為 UI 核心組件,同時也支持模塊化的開發(fā),比如在同一個界面中展示若干個可復(fù)用的子界面。隨著這種設(shè)計思路誕生的就是 Fragments[7] 組件,即 “片段”。使用 FragmentActivity 可以在一個 Activity 中組合一個或者多個片段,方便進行代碼復(fù)用,片段的生命周期受到宿主 Activity 的影響。
Fragment Injection 漏洞最早在 2013 年爆出,這里只介紹其原理,本節(jié)末尾附有原始的文章以及論文。漏洞的核心是系統(tǒng)提供的 PreferenceActivity 類,開發(fā)者可以對其進行繼承實現(xiàn)方便的設(shè)置功能,該類的 onCreate 函數(shù)有下面的功能:
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 參數(shù),并最終傳入 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;
}經(jīng)典的反射調(diào)用,將傳入的字符串實例化為 Java 類,并設(shè)置其參數(shù)。這是什么,這就是反序列化啊!而實際的漏洞也正是出自這里,由于傳入的參數(shù)攻擊者可控,那么攻擊者可以將其設(shè)置為某個內(nèi)部類,從而觸及開發(fā)者預(yù)期之外的功能。在原始的報告中,作者使用了 Settings 應(yīng)用中的某個設(shè)置 PIN 密碼的 Fragment 作為目標(biāo)傳入,這是個私有片段,從而導(dǎo)致了越權(quán)修改 PIN 碼的功能。在當(dāng)時的其他用戶應(yīng)用中,還有許多也使用了 PreferenceActivity,因此漏洞影響廣泛,而且造成的利用根據(jù)應(yīng)用本身的功能而異(也就是看有沒有好用的 Gadget)。
注意上面的代碼摘自最新的 Android 13,其中 switchToHeaderInner 方法加入了 isValidFragment 的判斷,這正是 Android 當(dāng)初的修復(fù)方案之一,即強制要求 PreferenceActivity 的子類實現(xiàn)該方法,不然就在運行時拋出異常。不過即便如此,還是有很多開發(fā)者為了圖方便直接繼承然后返回 true 的。
Fragment Injection 看似是 PreferenceActivity 的問題,但其核心還是對于不可信輸入的校驗不完善,在后文的例子中我們會多次看到類似的漏洞模式。
參考文章:
? A New Vulnerability in the Android Framework: Fragment Injection[8]
? ANDROID COLLAPSES INTO FRAGMENTS.pdf (wp)[9]
? Understanding fragment injection[10]
? How to fix Fragment Injection vulnerability[11]
點擊劫持
Activity 既然作為 UI 的主要載體,那么與用戶的交互也是其中關(guān)鍵的一項功能。在傳統(tǒng) Web 安全中就已經(jīng)有過點擊劫持的方法,即將目標(biāo)網(wǎng)站想要讓受害者點擊的案件放在指定位置(如iframe),并在宿主中使用相關(guān)組件對目標(biāo)進行覆蓋和引導(dǎo),令受害者在不知不覺中執(zhí)行了敏感操作,比如點贊投幣收藏一鍵離職等。
Android 中也出現(xiàn)過類似的攻擊手段,比如在系統(tǒng)的敏感彈窗前面覆蓋攻擊者自定義的 TextView,引導(dǎo)受害者確認(rèn)某些有害操作。當(dāng)然這需要攻擊者的應(yīng)用擁有浮窗權(quán)限(SYSTEM_ALERT_WINDOW),在較新的 Android 系統(tǒng)中,該權(quán)限的申請需要用戶多次的確認(rèn)。
近兩年中在 AOSP 中也出現(xiàn)過一些點擊劫持漏洞,包括但不限于:
? CVE-2020-0306:藍(lán)牙發(fā)現(xiàn)請求確認(rèn)框覆蓋
? CVE-2020-0394:藍(lán)牙配對對話框覆蓋
? CVE-2020-0015:證書安裝對話框覆蓋
? CVE-2021-0314:卸載確認(rèn)對話框覆蓋
? CVE-2021-0487:日歷調(diào)試對話框覆蓋
? ...
對于系統(tǒng)應(yīng)用而言,防御點擊劫持的方法一般是通過使用 android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS 權(quán)限并在布局參數(shù)中指定 SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS 來防止 UI 被覆蓋。
而對于普通應(yīng)用,沒法申請 HIDE_NON_SYSTEM_OVERLAY_WINDOWS 權(quán)限,防御措施一般有兩種,一是通過將布局的 filterTouchesWhenObscured 設(shè)置為 true 來禁止窗體被覆蓋后的輸入事件;二是重載 View.onFilterTouchEventForSecurity 方法,并在其中檢測其他應(yīng)用的覆蓋情況。在 Android 12 中系統(tǒng)已經(jīng)默認(rèn)開啟了 filterTouchesWhenObscured 屬性,這也是 security by default 的一種經(jīng)典實現(xiàn)。
關(guān)于點擊劫持的操作細(xì)節(jié)和緩解方案,可以參考 OPPO 安全實驗室的這篇文章: 《不可忽視的威脅:Android中的點擊劫持攻擊》
另外一個與點擊劫持類似的漏洞稱為 StrandHogg,細(xì)節(jié)可以參考下述的原始文章。其關(guān)鍵點是使用了 Activity 的 allowTaskReparenting 和 taskAffinity 屬性,將其任務(wù)棧偽裝成目標(biāo)應(yīng)用,這樣在打開目標(biāo)應(yīng)用時由于 TaskStack 后進先出的特性會導(dǎo)致用戶看到的是攻擊者的應(yīng)用,從而造成應(yīng)用的釣魚場景。
后來還是同一個安全團隊有提出了 StrandHogg 2.0 版本,主要利用了 ActivityStarter 中的 AUTOMERGE 特性。假設(shè)有 A、B 兩個應(yīng)用,在 A1 中調(diào)用 startActivites(B1, A2, B2) 之后,任務(wù)棧會從 (A1, B1) 以及 (A2, B2) 合并為 (A1, B1, A2, B2),也就是在同一個任務(wù)棧中覆蓋了其他應(yīng)用的 Activity,從而導(dǎo)致釣魚場景。不過這個漏洞比較特化,因此谷歌很早就已經(jīng)修復(fù)了,詳情可以閱讀下面的參考文章:
? The StrandHogg vulnerability[12]
? StrandHogg 2.0 – New serious Android vulnerability[13]
? StrandHogg 2.0 (CVE-2020-0096) 修復(fù)方案[14]
Intent Redirection
Intent Redirection,顧名思義就是將用戶傳入的不可信輸入進行了轉(zhuǎn)發(fā),類似于服務(wù)端的 SSRF 漏洞。一個典型漏洞例子如下:
protected void onCreate (Bundle savedInstanceState) {
Intent target = (Intent) getIntent().getParcelableExtra("target");
startActivity(target);
}將用戶傳入的 target Parcelable 直接轉(zhuǎn)換成了 Intent 對象,并將這個對象作為 startActivity 的參數(shù)進行調(diào)用。就這個例子而言,可能造成的危害就是攻擊者可以用任意構(gòu)造的 Intent 數(shù)據(jù)去啟動目標(biāo) APP 中的任意應(yīng)用,哪怕是未導(dǎo)出的私有應(yīng)用。而目標(biāo)未導(dǎo)出的應(yīng)用中可能進一步解析了攻擊者提供的 Intent 中的參數(shù),去造成進一步的危害,比如在內(nèi)置 Webview 中執(zhí)行任意 Javascript 代碼,或者下載保存文件等。
實際上 Intent Redirection 除了可能用來啟動私有 Activity 組件,還可以用于其他的的接口,包括:
? startActivity[15]
? startService[16]
? sendBroadcast[17]
? setResult[18]
注:每種方法可能還有若干衍生方法,比如 startActivityForResult
前面三個可能比較好理解,分別是啟動界面、啟動服務(wù)和發(fā)送廣播。最后一個 setResult 可能會在排查的時候忽略,這主要用來給當(dāng)前 Activity 的調(diào)用者返回額外數(shù)據(jù),主要用于 startActivityForResult 的場景,這同樣也可能將用戶的不可信數(shù)據(jù)污染到調(diào)用者處。
從防御的角度上來說,建議不要直接把外部傳入的 Intent 作為參數(shù)發(fā)送到上述四個接口中,如果一定要這么做的話,需要事先進行充分的過濾和安全校驗,比如:
1. 將組件本身的
android:exported設(shè)置為false,但這只是防止了用戶主動發(fā)送的數(shù)據(jù),無法攔截通過setResult返回的數(shù)據(jù);2. 確保獲取到的
Intent來自于可信的應(yīng)用,比如在組件上下文中調(diào)用getCallingActivity().getPackageName().equals("trust.app"),但注意惡意的應(yīng)用可以通過構(gòu)造數(shù)據(jù)令getCallingActivity返回null;3. 確保待轉(zhuǎn)發(fā)的
Intent沒有有害行為,比如 component 不指向自身的非導(dǎo)出組件,不帶有FLAG_GRANT_READ_URI_PERMISSION等(詳見后文 ContentProvider 漏洞);4. ...
但事實證明,即便是 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 來判斷啟動目標(biāo)的包名是否與當(dāng)前 caller 的包名一致,可事實上顯式 Intent 是通過 componentName 去指定啟動目標(biāo),優(yōu)先級高于 Intent.packageName 且后者可以被偽造,這就造成了檢查的繞過。上述短短幾行代碼中其實還有另外一個漏洞,感興趣的可以參考下面的參考鏈接。
因此,遇到潛在的 Intent 重定向問題時,可以多花點時間仔細(xì)審查,說不定就能夠找到一個可利用的場景。
? Remediation for Intent Redirection Vulnerability[19]
Service
Service[20] 的主要功能有兩個,一是給 APP 提供一個后臺的長時間運行環(huán)境,二是對外提供自身的服務(wù)。與 Activity 的定義類似,Service 必須要在 manifest 中進行聲明才能使用。注意 Service 中的代碼也是和 Activity 一樣運行在主線程的,并且默認(rèn)和應(yīng)用處于進程。
根據(jù) Service 的兩大主要功能區(qū)分,啟動 Service 也有對應(yīng)的兩種形式:
1.
Context.startService():啟動后臺服務(wù)并讓系統(tǒng)進行調(diào)度;2.
Context.bindService():讓(外部)應(yīng)用綁定服務(wù),并使用其提供的接口,可以理解為 RPC 的服務(wù)端;
兩種方式啟動服務(wù)的生命周期圖示如下:

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

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

漏洞細(xì)節(jié)可參考:Oversecured detects dangerous vulnerabilities in the TikTok Android app[33]
ContentProvider
Content Provider[34],即內(nèi)容提供程序,簡稱為 Provider。Android 應(yīng)用通常實現(xiàn)為 MVC 結(jié)構(gòu)(Model-View-Controller),Model 部分即為數(shù)據(jù)來源,供自身的 View 即圖形界面進行展示。但有時候應(yīng)用會想要將自身的數(shù)據(jù)提供給其他數(shù)據(jù)使用,或者從其他應(yīng)用中獲取數(shù)據(jù)。
定義一個 ContentProvider 的方式,只需要繼承自 ContentProvider[35] 類并實現(xiàn)六個方法: query,insert, update, delete, getType 以及 onCreate。其中除了 onCreate 是系統(tǒng)在主線程調(diào)用的,其他方法都由客戶端程序進行主動調(diào)用。自定義的 provider 必須在程序清單中進行聲明,后文會詳細(xì)介紹。
可以看到 Provider 主要實現(xiàn)了類似數(shù)據(jù)庫的增刪改查接口,從客戶端來看,查詢過程也和查詢傳統(tǒng)數(shù)據(jù)庫類似,例如,下面是查詢系統(tǒng)短信的代碼片段:
Cursor cursor = getContentResolver().query(
Telephony.Sms.Inbox.CONTENT_URI, // 指定要查詢的表名
new String[] { Telephony.Sms.Inbox.BODY }, // projection 指定索要查詢的列名
selectionClause, // 查詢的過濾條件
selectionArgs, // 查詢過濾的參數(shù)
Telephony.Sms.Inbox.DEFAULT_SORT_ORDER); // 返回結(jié)果的排序
while (cursor.moveToNext()) {
Log.i(TAG, "msg: " + cursor.getString(0));
}其中 ContentResolver 是 ContentInterface 子類,后者是 ContentProvider 的客戶端遠(yuǎn)程接口,可以實現(xiàn)其透明的遠(yuǎn)程代理調(diào)用。 content_uri 可以看作是查詢的表名,projection 可以看作是列名,返回的 cursor 是查詢結(jié)果行的迭代器。
與前面三個組件不同,在 shell 中訪問 provider 組件的工具是 content。
下面來介紹 Provider 中常見的問題。
Permissions
鑒于 provider 作為數(shù)據(jù)載體,那么安全訪問與權(quán)限控制自然是重中之重。例如上面代碼示例中訪問短信的接口,如果所有人都能隨意訪問,那就明顯會帶來信息泄露問題。前面簡單提到過,應(yīng)用中定義的 Provider 必須要在其程序清單文件中進行聲明,使用的是 provider[36] 標(biāo)簽。其中有我們常見的 exported 屬性,表示是否可被外部訪問,permission 屬性則表示訪問所需的權(quán)限,當(dāng)然也可以分別對讀寫使用不同的權(quán)限,比如 readPermission/writePermission 屬性。
比如,前文提到的短信數(shù)據(jù)庫聲明如下:
<provider android:name="SmsProvider"
android:authorities="sms"
android:multiprocess="false"
android:exported="true"
android:singleUser="true"
android:readPermission="android.permission.READ_SMS" />其他應(yīng)用若想訪問,則需在清單文件中聲明請求對應(yīng)權(quán)限。
<uses-permission android:name="android.permission.READ_SMS" />這都很好理解,其他組件也有類似的特性。除此之外,Provider 本身還提供了更為細(xì)粒度的權(quán)限控制,即 grantUriPermissions[37]。這是一個布爾值,表示是否允許臨時為客戶端授予該 provider 的訪問權(quán)限。臨時授予權(quán)限的運行流程一般如下:
1. 客戶端給 Provider 所在應(yīng)用發(fā)送一個 Intent,指定想要訪問的 Content URI,比如使用
startActivityForResult發(fā)送;2. 應(yīng)用收到 Intent 后,判斷是否授權(quán),如果確認(rèn)則準(zhǔn)備一個 Intent,并設(shè)置好 flags 標(biāo)志位
FLAG_GRANT_[READ|WRITE]_URL_PERMISSION,表示允許讀/寫對應(yīng)的 Content URI(可以不和請求的 URI 一致),最后使用setResult(code, intent)返回給客戶端;3. 客戶端的 onActivityResult 收到返回的 Intent,使用其中的 URI 來臨時對目標(biāo) Provider 進行訪問;
以讀為例,Intent.flags 中如果包含 FLAG_GRANT_READ_URI_PERMISSION[38],那么該 Intent 的接收方(即客戶端)會被授予 Intent.data 部分 URI 的臨時讀取權(quán)限,直至接收方的生命周期結(jié)束。另外,Provider 應(yīng)用也可以主動調(diào)用 Context.grantUriPermission 方法來授予目標(biāo)應(yīng)用對應(yīng)權(quán)限:
public abstract void grantUriPermission (String toPackage,
Uri uri,
int modeFlags)
public abstract void revokeUriPermission (String toPackage,
Uri uri,
int modeFlags)grantUriPermissions 屬性可以在 URI 粒度對權(quán)限進行讀寫控制,但有一個需要注意的點:通過 grantUriPermissions 臨時授予的權(quán)限,會無視 readPermission、writePermission、permission 和 exported 屬性施加的限制。也就是說,即便 exported=false,客戶端也沒有申請對應(yīng)的 uses-permission,可一旦被授予權(quán)限,依然可以訪問對應(yīng)的 Content Provider!
另外,<provider> 還有一個子標(biāo)簽 grant-uri-permission[39],即便 grantUriPermissions 被設(shè)置為 false,通過臨時獲取權(quán)限依然可以訪問該標(biāo)簽下定義的 URI 子集,該子集可以用前綴或者通配符去指定 URI 的可授權(quán)路徑范圍。
Provider 權(quán)限設(shè)置不當(dāng)可能會導(dǎo)致應(yīng)用數(shù)據(jù)被預(yù)期之外的惡意程序訪問,輕則導(dǎo)致信息泄露,重則會使得自身沙盒數(shù)據(jù)被覆蓋而導(dǎo)致 RCE,后文會看到多個這樣的案例。
FileProvider
前面說過自定義 Provider 需要實現(xiàn)六個方法,但 Android 中已經(jīng)針對某些常用場景的 Provider 編寫好了對應(yīng)的子類,用戶可根據(jù)需要繼承這些子類并實現(xiàn)少部分子類方法即可。其中一個常用場景就是用 ContentProvider 分享應(yīng)用的文件,系統(tǒng)提供了 FileProvider 來方便應(yīng)用自定義文件分享和訪問,但是使用不當(dāng)?shù)脑捄芸赡軙霈F(xiàn)任意文件讀寫的問題。
FileProvider[40] 提供了使用 XML 去指定文件訪問控制的功能,一般 Provider 應(yīng)用只需繼承 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。該文件中定義了可供訪問的文件路徑,F(xiàn)ileProvider 只會對提前指定的文件生成 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 標(biāo)簽支持多種類型的子標(biāo)簽,分別對應(yīng)不同目錄的子路徑:
?
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,表示系統(tǒng)的根目錄 /。FileProvider 生成的 URI 格式一般是 content://authority/{name}/{path},比如對于上述 Provider,可用 content://com.evilpan.fileprovider/root/proc/self/maps 來訪問 /proc/self/maps 文件。
由此可見,F(xiàn)ileProvider 指定 root-path 是一個危險的標(biāo)志,一旦攻擊者獲得了臨時權(quán)限,就可以讀取所有應(yīng)用的私有數(shù)據(jù)。
比如,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 文件內(nèi)容如下:
<?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>獲取臨時權(quán)限之后就可以實現(xiàn)應(yīng)用的任意文件讀寫。
The Hidden ...
在 ContentProvider 類中,除了前面說過的 6 個必須實現(xiàn)的方法,還有一些其他隱藏的方法,一般使用默認(rèn)實現(xiàn),也可以被子類覆蓋實現(xiàn),比如
? openFile
? openFileHelper
? call
? ...
這些隱藏的方法可能在不經(jīng)意間造成安全問題,本節(jié)會通過一些案例去分析其中的原因。
openFile
如果 ContentProvider 想要實現(xiàn)共享文件讀寫的功能,還可以通過覆蓋 openFile 方法去實現(xiàn),該方法的默認(rèn)實現(xiàn)會拋出 FileNotFoundException 異常。
雖然開發(fā)者實現(xiàn)上不太會直接就返回打開的本地文件,而是有選擇地返回某些子目錄文件。但是如果代碼寫得不嚴(yán)謹(jǐn),就可能會出現(xiàn)路徑穿越等問題,一個經(jīng)典的漏洞實現(xiàn)如下:
@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,其默認(rèn)實現(xiàn)是調(diào)用 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;
}有時候開發(fā)者雖然知道要要防御路徑穿越,但防御的姿勢不對,也存在被繞過的可能,比如:
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);
}詳見:Path Traversal Vulnerability[41]
openFileHelper
ContentProvider 中還有一個鮮為人知的 openFileHelper 方法,其默認(rèn)實現(xiàn)是使用當(dāng)前 Provider 中的 _data 列數(shù)據(jù)去打開文件,源碼如下:
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);
}這個方法的主要作用是方便子類用于快速實現(xiàn) openFile 方法,通常不會直接在子類去覆蓋。不過由于其中基于 _data 列去打開文件的特性可能會攻擊者插入惡意數(shù)據(jù)后間接地實現(xiàn)任意文件讀寫。
一個經(jīng)典案例就是三星手機的 SemClipboardProvider,在插入時未校驗用戶數(shù)據(jù):
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 進程中,擁有極高的運行權(quán)限,攻擊者通過利用這個漏洞去就能實現(xiàn)系統(tǒng)層面的任意文件讀寫,其 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[42]
call
ContentProvider 中提供了 call 方法,用于實現(xiàn)調(diào)用服務(wù)端定義方法,其函數(shù)簽名如下:
public Bundle call (String authority,
String method,
String arg,
Bundle extras)
public Bundle call (String method,
String arg,
Bundle extras)默認(rèn)的實現(xiàn)是個空函數(shù)直接返回 null,開發(fā)者可以通過覆蓋該函數(shù)去實現(xiàn)一些動態(tài)方法,返回值也會傳回到調(diào)用者中。
看起來和常規(guī)的 RPC 調(diào)用類似,但這里有個小陷阱,開發(fā)者文檔中也特別標(biāo)注了:Android 系統(tǒng)并沒有對 call 函數(shù)進行權(quán)限檢查,因為系統(tǒng)不知道在 call 之中對數(shù)據(jù)進行了讀還是寫,因此也就無法根據(jù) Manifest 中定義的權(quán)限約束進行判斷。因此要求開發(fā)者自己對 call 中的邏輯進行權(quán)限校驗。
如果開發(fā)者實現(xiàn)了該方法,但是又未進行校驗或者校驗不充分,就可能出現(xiàn)越權(quán)調(diào)用的情況。一個案例是 CVE-2021-23243, OPPO 某系統(tǒng)應(yīng)用中 HostContentProviderBase 的 call 方法實現(xiàn)中,直接用 DexClassLoader 去加載了傳入 dex 文件,直接導(dǎo)致攻擊者的代碼在特權(quán)進程中運行,所有繼承該基類的 Provider 都會受到影響 ()。
另外在某些系統(tǒng) Provider 中,可以通過 call 方法去獲取某些遠(yuǎn)程對象實例,例如在文章 Android 中的特殊攻擊面(三)—— 隱蔽的 call 函數(shù)[43] 中,作者就通過 SliceProvider 與 KeyguardSliceProvider 獲取到了系統(tǒng)應(yīng)用內(nèi)部的 PendingIntent 對象,進一步利用實現(xiàn)了偽造任意廣播的功能。
其他
除了上述和四大組件直接相關(guān)的漏洞,Android 系統(tǒng)中還有許多不太好分類的漏洞,本節(jié)主要挑選其中幾個最為常見的漏洞進行簡單介紹。
PendingIntent
PendingIntent[44] 是對 Intent 的表示,本身并不是 Intent 對象,但是是一個 Parcelable 對象。將該對象傳遞給其他應(yīng)用后,其他應(yīng)用就可以以發(fā)送方的身份去執(zhí)行所指向的 Intent 指定的操作。 PendingIntent 使用下述靜態(tài)方法之一進行創(chuàng)建:
? getActivity(Context, int, Intent, int);
? getActivities(Context, int, Intent[], int);
? getBroadcast(Context, int, Intent, int);
? getService(Context, int, Intent, int);
PendingIntent 本身只是系統(tǒng)對原始數(shù)據(jù)描述符的一個引用,可以大致將其理解為 Intent 的指針。也因為如此,即便創(chuàng)建 PendingIntent 的應(yīng)用關(guān)閉后,其他應(yīng)用仍然可以使用該數(shù)據(jù)。如果原始應(yīng)用后來進行了重啟并以同樣的參數(shù)創(chuàng)建了一個 PendingIntent,那么實際上返回 PendingIntent 與之前創(chuàng)建的會指向同樣的 token。注意判斷 Intent 是否相同是使用 filterEquals[45] 方法,其中會判斷 action,data, type,identity,class,categories 是否相同,注意 extra 并不在此列,因此僅有 extra 不同的 Intent 也會被認(rèn)為是相等的。
由于 PendingIntent 可代表其他應(yīng)用的特性,在某些場景下可能被用于濫用。例如,如果開發(fā)者創(chuàng)建了這樣一個默認(rèn)的 PendingIntent 并傳遞給其他應(yīng)用:
pi = PendingIntent.getActivity(this, 0, new Intent(), 0);
bundle.putParcelable("pi", pi)
// send bundle惡意的應(yīng)用在收到此 PendingIntent 后,可以獲取到原始的 intent,并使用 Intent.fillin 去填充空字段,如果原始 Intent 是上述空 Intent,那么攻擊者就可以將其修改為特定的 Intent,從而以目標(biāo)的身份去啟動應(yīng)用,包括未導(dǎo)出的私有應(yīng)用。一個經(jīng)典的案例就是早期的 broadAnywhere[46] 漏洞,Android Settings 應(yīng)用中的 addAccount 方法內(nèi)創(chuàng)建了一個 PendingIntent 廣播,但 intent 內(nèi)容為空,這導(dǎo)致收到 intent 的的惡意應(yīng)用可以 fillin 填充廣播的 action,從而實現(xiàn)越權(quán)發(fā)送系統(tǒng)廣播,實現(xiàn)偽造短信、回復(fù)出廠設(shè)置等功能。
為了緩解這類問題,Andorid 中對 Intent.fillin 的改寫做了諸多限制,比如已有的字段不能修改,component 和 selector 字段不能修改(除非額外設(shè)置 FILL_IN_COMPONENT/SELECTOR),隱式 Intent 的 action 不能修改等。
不過有研究者提出了針對隱式 Intent 的利用方法,即通過修改 flag 添加 FLAG_GRANT_WRITE_URI_PERMISSION,并修改 data 的 URI 指向受害者私有的 Provider,將 package 改為攻擊者;同時攻擊者在自身的 Activity 中聲明相同的 intent filter,這樣在轉(zhuǎn)發(fā) intent 時會啟動攻擊者應(yīng)用,同時也獲取了目標(biāo)私有 Provider 的訪問權(quán)限,從而實現(xiàn)私有文件竊取或者覆蓋。關(guān)于該攻擊思路詳情可以閱讀下面的參考文章。
? broadAnywhere:Broadcast組件權(quán)限繞過漏洞(Bug: 17356824)[47]
? PendingIntent重定向:一種針對安卓系統(tǒng)和流行App的通用提權(quán)方法——BlackHat EU 2021議題詳解(上)[48]
? PendingIntent重定向:一種針對安卓系統(tǒng)和流行App的通用提權(quán)方法——BlackHat EU 2021議題詳解(下)[49]
在 Android 12+ 之后,PendingIntent 在創(chuàng)建時候要求顯式指定
FLAG_MUTABLE或者FLAG_IMMUTABLE,表示是否可以修改。
DeepLink
在大部分操作系統(tǒng)中都有 deeplink 的概念,即通過自定義 schema 打開特定的應(yīng)用。比如通過點擊 https://evilpan.com/ 可以喚起默認(rèn)瀏覽器打開目標(biāo)網(wǎng)頁,點擊 tel://10086 會喚起撥號界面,點擊 weixin://qr/xxx 會喚起微信,等等。其他系統(tǒng)暫且不論,在 Android 中這主要是通過隱式 Intent 去實現(xiàn)的。
應(yīng)用要想注冊類似的自定義協(xié)議,需要在應(yīng)用清單文件中進行聲明:
<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 可以直接通過點擊鏈接去觸發(fā),因此更受攻擊者喜愛。如果處理對應(yīng) Intent 的組件沒有過濾好用戶傳入的內(nèi)容,很可能會造成 1-click 的漏洞。相關(guān)案例可以參考文章:Android 中的特殊攻擊面(二)——危險的deeplink
Webview
在 Andorid 系統(tǒng)中,Webview[50] 主要用于應(yīng)用在自身的 Activty 中展示網(wǎng)頁內(nèi)容,并提供了一些額外的接口來給開發(fā)者實現(xiàn)自定義的控制。更高的拓展性也就意味著更多出錯的可能,尤其是如今 Android 客戶端開發(fā)式微,Java 開發(fā)也朝著 “大前端” 的方向發(fā)展。原本許多使用原生應(yīng)用實現(xiàn)的邏輯逐漸轉(zhuǎn)移到了 web 頁面中,比如 h5、小程序等,這樣一來,webview 的攻擊面也就擴寬了不少。
常規(guī)的 Webview 安全問題主要是在與一些配置的不安全,比如覆蓋 onReceivedSslError 忽略 SSL 錯誤導(dǎo)致中間人攻擊,setAllowFileAccessFromFileURLs 導(dǎo)致本地私有文件泄露等。但現(xiàn)在的漏洞更多出在 JSBridge 上,這是 Java 代碼與網(wǎng)頁中的 JavaScript 代碼溝通的橋梁。
由于 Webview 或者說 JS 引擎的沙箱特性,網(wǎng)頁中的 Javascript 代碼本身無法執(zhí)行許多原生應(yīng)用才能執(zhí)行的操作,比如無法從 Javascript 中發(fā)送廣播,無法訪問應(yīng)用文件等。而由于業(yè)務(wù)的復(fù)雜性,很多邏輯又必須在 Java 層甚至是 Native 層才能實現(xiàn),因此這就需要用到 JSBridage。傳統(tǒng)的 JSBridge 通過 Webview.addJavascriptInterface 實現(xiàn),一個簡單示例如下:
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 層返回數(shù)據(jù)給 Javascript 一般是通過直接使用 loadUrl 執(zhí)行 JS 代碼實現(xiàn)。當(dāng)然除了這種方式注冊 Bridge,還有很多應(yīng)用特異的實現(xiàn),比如使用 console.log 傳輸數(shù)據(jù)并在 Java 層使用 onConsoleMessage 回調(diào)去接收。但無論如何,這都導(dǎo)致攻擊面的增加,大型應(yīng)用甚至注冊了上百個 jsapi 來供網(wǎng)頁調(diào)用。
從歷史漏洞來看,Webview 漏洞的成因主要是 jsapi 域名校驗問題和 Bridge 代碼本身的漏洞,由于篇幅原因就不展開了。
后記
本文中主要通過 Android 中的四大組件介紹了一系列相關(guān)的邏輯問題,盡可能地囊括了筆者所了解的歷史漏洞。由于個人認(rèn)知水平有限,總是難免掛一漏萬,但即便如此,文章的篇幅還是比預(yù)想中的超出了億點點。從溫故知新的角度看,挖掘這類邏輯漏洞最好的策略還是使用靜態(tài)分析工具,搜集更多 Sink 模式并編寫有效的規(guī)則去進行掃描,實在沒有條件的話用 (rip)grep 也是可以的。
參考資料
? Galaxy Leapfrogging 蓋樂世蛙跳 Pwning the Galaxy S8[51]
? Chainspotting: Building Exploit Chains with Logic Bugs[52] (如何用11個exp攻破三星S8[53])
? Huawei Mate 9 Pro Mobile Pwn2Own 2017[54]
? Detect dangerous vulnerabilities in the TikTok Android app - Oversecured[55]
? 魔形女漏洞白皮書 - 京東探索研究院信息安全實驗室[56]
? HACKING XIAOMI'S ANDROID APPS - Part1[57]
? Automating Pwn2Own with Jandroid[58]
引用鏈接
[1] Application Fundamentals: https://developer.android.com/guide/components/fundamentals[2] Intent: https://developer.android.com/reference/android/content/Intent[3] Android12 應(yīng)用啟動流程分析: https://evilpan.com/2021/12/05/apk-startup/[4] Activity: https://developer.android.com/reference/android/app/Activity[5] AndroidManifest.xml: https://developer.android.com/guide/topics/manifest/manifest-intro[6] activity: https://developer.android.com/guide/topics/manifest/activity-element[7] Fragments: https://developer.android.com/guide/components/fragments[8] A New Vulnerability in the Android Framework: Fragment Injection: https://securityintelligence.com/new-vulnerability-android-framework-fragment-injection/[9] ANDROID COLLAPSES INTO FRAGMENTS.pdf (wp): https://securityintelligence.com/wp-content/uploads/2013/12/android-collapses-into-fragments.pdf[10] Understanding fragment injection: https://www.synopsys.com/blogs/software-security/fragment-injection/[11] How to fix Fragment Injection vulnerability: https://support.google.com/faqs/answer/7188427?hl=en[12] The StrandHogg vulnerability: https://promon.co/security-news/the-strandhogg-vulnerability/[13] StrandHogg 2.0 – New serious Android vulnerability: https://promon.co/resources/downloads/strandhogg-2-0-new-serious-android-vulnerability/[14] StrandHogg 2.0 (CVE-2020-0096) 修復(fù)方案: https://android.googlesource.com/platform/frameworks/base/+/a952197bd161ac0e03abc6acb5f48e4ec2a56e9d[15] startActivity: https://developer.android.com/reference/android/app/Activity#startActivity(android.content.Intent)[16] startService: https://developer.android.com/reference/android/content/Context#startService(android.content.Intent)[17] sendBroadcast: https://developer.android.com/reference/android/content/Context#sendBroadcast(android.content.Intent)[18] setResult: https://developer.android.com/reference/android/app/Activity#setResult(int,%20android.content.Intent)[19] Remediation for Intent Redirection Vulnerability: https://support.google.com/faqs/answer/9267555?hl=en[20] Service: https://developer.android.com/guide/components/services[21] 綁定服務(wù): https://developer.android.com/guide/components/bound-services[22] service: https://developer.android.com/guide/topics/manifest/service-element[23] vulnerabilities in the TikTok Android app: https://blog.oversecured.com/Oversecured-detects-dangerous-vulnerabilities-in-the-TikTok-Android-app/#vulnerability-via-independentprocessdownloadservice-aidl-interface[24] launchAnyWhere: Activity組件權(quán)限繞過漏洞解析(Google Bug 7699048 ): http://retme.net/index.php/2014/08/20/launchAnyWhere.html[25] Broadcast Receiver: https://developer.android.com/guide/components/broadcasts[26] BroadcastReceiver: https://developer.android.com/reference/android/content/BroadcastReceiver[27] protectionLevel: https://developer.android.com/reference/android/R.attr#protectionLevel[28] PpmtReceiver 漏洞: https://paper.seebug.org/1050/[29] CVE-2018-9581: https://wwws.nightwatchcybersecurity.com/2018/11/11/cve-2018-9581/[30] framework 的 AndroidManifest.xml: https://android.googlesource.com/platform/frameworks/base/+/master/core/res/AndroidManifest.xml[31] Vulnerabilities with Custom Permissions: https://github.com/commonsguy/cwac-security/blob/master/PERMS.md[32] Custom Permission Vulnerability and the 'L' Developer Preview: https://commonsware.com/blog/2014/08/04/custom-permission-vulnerability-l-developer-preview.html[33] Oversecured detects dangerous vulnerabilities in the TikTok Android app: https://blog.oversecured.com/Oversecured-detects-dangerous-vulnerabilities-in-the-TikTok-Android-app/[34] Content Provider: https://developer.android.com/guide/topics/providers/content-providers[35] ContentProvider: https://developer.android.com/reference/android/content/ContentProvider[36] provider: https://developer.android.com/guide/topics/manifest/provider-element[37] grantUriPermissions: https://developer.android.com/guide/topics/manifest/provider-element#gprmsn[38] FLAG_GRANT_READ_URI_PERMISSION: https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_READ_URI_PERMISSION[39] grant-uri-permission: https://developer.android.com/guide/topics/manifest/grant-uri-permission-element[40] FileProvider: https://developer.android.com/reference/androidx/core/content/FileProvider[41] Path Traversal Vulnerability: https://support.google.com/faqs/answer/7496913?hl=en[42] A Very Powerful Clipboard: Analysis of a Samsung in-the-wild exploit chain: https://googleprojectzero.blogspot.com/2022/11/a-very-powerful-clipboard-samsung-in-the-wild-exploit-chain.html[43] Android 中的特殊攻擊面(三)—— 隱蔽的 call 函數(shù): https://paper.seebug.org/1269/[44] PendingIntent: https://developer.android.com/reference/android/app/PendingIntent[45] filterEquals: https://developer.android.com/reference/android/content/Intent#filterEquals(android.content.Intent)[46] broadAnywhere: https://android.googlesource.com/platform/packages/apps/Settings/+/f5d3e74ecc2b973941d8adbe40c6b23094b5abb7%5E%21/#F0[47] broadAnywhere:Broadcast組件權(quán)限繞過漏洞(Bug: 17356824): http://retme.net/index.php/2014/11/14/broadAnywhere-bug-17356824.html[48] PendingIntent重定向:一種針對安卓系統(tǒng)和流行App的通用提權(quán)方法——BlackHat EU 2021議題詳解(上): https://blog.csdn.net/weixin_59152315/article/details/123481053[49] PendingIntent重定向:一種針對安卓系統(tǒng)和流行App的通用提權(quán)方法——BlackHat EU 2021議題詳解(下): https://blog.csdn.net/weixin_59152315/article/details/123503289[50] Webview: https://developer.android.com/reference/android/webkit/WebView[51] Galaxy Leapfrogging 蓋樂世蛙跳 Pwning the Galaxy S8: https://paper.seebug.org/1050/[52] Chainspotting: Building Exploit Chains with Logic Bugs: https://labs.f-secure.com/archive/chainspotting-building-exploit-chains-with-logic-bugs/[53] 如何用11個exp攻破三星S8: https://paper.seebug.org/628/[54] Huawei Mate 9 Pro Mobile Pwn2Own 2017: https://labs.withsecure.com/publications/nhuawew-blog-post[55] Detect dangerous vulnerabilities in the TikTok Android app - Oversecured: https://blog.oversecured.com/Oversecured-detects-dangerous-vulnerabilities-in-the-TikTok-Android-app/[56] 魔形女漏洞白皮書 - 京東探索研究院信息安全實驗室: https://dawnslab.jd.com/mystique-paper/[57] HACKING XIAOMI'S ANDROID APPS - Part1: http://blog.takemyhand.xyz/2021/07/hacking-on-xiaomis-android-apps.html[58] Automating Pwn2Own with Jandroid: https://labs.f-secure.com/blog/automating-pwn2own-with-jandroid/
