有隙可乘 - Android 序列化漏洞分析實戰(zhàn)
一、背景
大家應(yīng)該看到過一篇《2022年的十大安全漏洞與利用》的文章,文章中提到一個漏洞:
利用Android Parcel序列化和反序列不匹配,借助應(yīng)用FileProvider未限制路徑,可以獲取系統(tǒng)級startAnyWhere能力,從而獲取用戶敏感信息,修改系統(tǒng)配置,獲取系統(tǒng)特權(quán)等等。
這里面有三個關(guān)鍵詞:
- Parcel不匹配漏洞
- startAnyWhere
- FileProvider未限制路徑
看到以上,大家可能會就其中涉及到的幾個點有些疑問:
- startAnyWhere是什么意思,是什么樣的能力?
- Parcel不匹配漏洞是什么原理,是如何產(chǎn)生的?
- FileProvider的作用是什么,未限制路徑又是什么問題?
- 這幾者之間存在什么關(guān)聯(lián),又會帶來哪些風(fēng)險?
二、FileProvider
2.1 功能簡介
首先我們來簡單講一下FileProvider,F(xiàn)ileProvider其實就是用來進程間共享文件的。
上方左側(cè)圖是早期的應(yīng)用間共享文件的方案,就是A應(yīng)用把文件存在外置存儲,然后把文件的物理地址給到B應(yīng)用,B應(yīng)用去這個地址去取。
那么這樣的方式存在哪些問題呢?有以下幾點:
- 權(quán)限無法控制:文件存放的位置,要保證都能訪問,這樣無法精確控制權(quán)限;
- 權(quán)限無法回收:文件一旦共享,無法撤銷;
- 目錄結(jié)構(gòu)暴露:文件共享需要公開原始的文件地址,暴露了目錄結(jié)構(gòu);
- 隱私內(nèi)容泄露:部分私有目錄文件共享存在安全隱私泄露的風(fēng)險。
基于以上問題,google基于ContentProvider設(shè)計了FileProvider,如上方右側(cè)圖,文件共享必須基于FileProvider,由AMS來管控權(quán)限,提供的協(xié)議也是定制的content協(xié)議。
2.2 使用簡介
了解了FileProvider出現(xiàn)的背景,下面介紹一下FileProvider的使用,使用FileProvider需要提供四個參數(shù):
- Uri(文件地址)
- Action(接收方信息)
- Type(文件類型)
- Flags(授予權(quán)限)
如下面代碼,最終通過startActivity來發(fā)起共享,記住這個startActivity,很重要。
Intent intent = new Intent();
intent.setAction("");
Uri uri = FileProvider.getUriForFile(getContext(), "", file);
intent.setType(getContext().getContentResolver().getType(uri));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivity(intent);
2.3 URI簡介
content URI和普通的Http協(xié)議一樣,也擁有scheme,authorities,path。
示例:content://authorities /XXX/xxx.txt。
Android提供了xml配置,如下代碼所示,把實際的路徑映射成一個虛擬的名稱,這樣的優(yōu)勢就是限制了路徑,可以把指定目錄的路徑共享出去。
看到這里,大家就可以理解未限制路徑的含義了,簡單講就是把系統(tǒng)根目錄給共享出去了,正確的做法是只共享需要使用的目錄。
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.file"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/******_paths" />
</provider>
<?xml versinotallow="1.0" encoding="utf-8"?>
<resources>
<paths>
<files-path name="test_in" path="/test/file" />
<external-path name="test_external" path="/test/file" />
</paths>
</resources>
2.4 權(quán)限簡介
FLAG_GRANT_READ_URI_PERMISSION:文件讀權(quán)限;
FLAG_GRANT_WRITE_URI_PERMISSION:文件寫權(quán)限;
FLAG_GRANT_PERSISTABLE_URI_PERMISSION:持久授權(quán),直至設(shè)備重啟或者主動調(diào)用revokeUriPermission;
FLAG_GRANT_PREFIX_URI_PERMISSION:相同前綴路徑統(tǒng)一授權(quán)。
2.5 授權(quán)方式
//第一種授權(quán)方式
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//第二種授權(quán)方式
getContext().grantUriPermission("packageName", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
2.5.1 第一種授權(quán)方式:
一次授權(quán),用完即止。
2.5.2 第二種授權(quán)方式:
- 不含持久授權(quán)flag的,權(quán)限依附于進程存活;
- 包含持久授權(quán)flag的,重啟或者主動拒絕權(quán)限才會消失。
2.6 小結(jié)
上面主要簡單介紹了一下FileProvider的設(shè)計思想、技術(shù)方案、使用方式,由此我們可以對文章開頭提出的一些疑問進行解答。
1、FileProvider的作用
答:跨進程共享文件,一般通過startActivity的方式。
2、未限制路徑
答:沒有指定需要共享的文件目錄,將系統(tǒng)根目錄共享出去了。
3、存在什么風(fēng)險,如何進行攻擊
答:單針對FileProivider來看,風(fēng)險較小,光依賴FileProvider這個問題還是沒法進行攻擊的,原因如下:
- 文件共享需要業(yè)務(wù)主動通過startActivity才能發(fā)起
- 讀寫權(quán)限交由系統(tǒng)來管理
三、startAnyWhere
接下來講一下上文中提到的startAnyWhere,顧名思義,就是應(yīng)用想打開哪個頁面就打開哪個頁面,那么在Android系統(tǒng)中,誰才有這個能力呢?
3.1 實現(xiàn)原理
能夠?qū)崿F(xiàn)startAnyWhere的只有系統(tǒng)SystemUid應(yīng)用,這類應(yīng)用在startActivity進行權(quán)限校驗的時候是直接放行的,無論Activity是否exported,都能打開,最常見的應(yīng)用比如系統(tǒng)設(shè)置。
下面是一個系統(tǒng)設(shè)置打開第三方應(yīng)用的案例,通過設(shè)置可以直接打開第三方的賬戶登錄頁。
3.2 實現(xiàn)流程
通過設(shè)置頁面的添加賬號的功能,可以直接拉起對應(yīng)應(yīng)用的界面,這個是今天漏洞的核心,我們來看一下系統(tǒng)調(diào)用流程。
如下圖,首先系統(tǒng)設(shè)置調(diào)用AccountManager的addAccount,然后通過SystemServer中的AccountManagerService,一直調(diào)用到目標(biāo)APP本身的AddAccount實現(xiàn)。
由APP本身提供一個Bundle,Bundle里面本身包含了一個intent的由設(shè)置進行打開。
這個里面其實存在一個風(fēng)險,第三方應(yīng)用可以隨意提供一個惡意Intent,系統(tǒng)會直接調(diào)用startActivity,隨之而來的風(fēng)險很大。
上圖中還存在一個第0步,即這個流程的發(fā)起方可以是三方應(yīng)用本身,不一定需要從設(shè)置進入,那么這個整個流程就閉環(huán)了,完全無需用戶介入,用戶也可以完全無感知。
不過這個風(fēng)險呢,google在Android4.4之后已經(jīng)修復(fù)了,4.4之后增加了對intent內(nèi)容的校驗。
代碼如下:
if (result != null&& (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
if (!checkKeyIntent(Binder.getCallingUid(),intent)) {
onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,"invalid intent in bundle returned");
return;
}
}
上面代碼就是取出外部傳入的KEY-INTENT進行校驗,這里面已經(jīng)出現(xiàn)了今天的主角Parcel,整個攻擊也是通過Parcel漏洞使得惡意的KEY-INTENT繞過系統(tǒng)的檢查。
四、Parcel
下面我們看一下parcel漏洞及原理。
4.1 Parcel 簡介
parcel是專門為Android提供的一個序列化的類,parcel的原理其實很簡單,就是一個嚴(yán)格的對稱讀寫,如下代碼所示。
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mSize);
}
public void readFromParcel(Parcel in) {
mSize = in.readInt();
}
同時序列化遵循基本的TLV格式,也就是Tag-Length-Value,Tag代表類型,Length代表長度,Value代表值,當(dāng)然一些特殊情況:
- Length不描述:有固定長度的類型可以不描述length,比如long,int等等;
- Tag不描述:bundle序列化時,key一定是string類型,所以不需要描述Tag。
回到對稱讀寫這一塊,如果這個代碼不對稱了會出現(xiàn)什么情況呢,google曾經(jīng)在android源碼中出現(xiàn)了很多類似不對稱的錯誤,看一下下面幾個案例。
4.2 Parcel 不對稱讀寫案例
4.2.1 案例1
如下圖,這是一個典型且明顯的不對稱,writeLog&readInt,為什么不對稱,很簡單,int和long對應(yīng)的長度不一樣。
4.2.2 案例2
這是一個比較隱晦的不對稱案例,是Android原生的WorkSource類,這個不對稱一眼無法看出,以致于最近的Android版本這個問題一直存在,這個類也是此次漏洞攻擊真正被利用的一個類。
下面簡單看一下WorkSource序列化和反序列化的流程。
序列化
如下述代碼,WorkSource序列化時,如果mChains是一個長度為0的空list,那么就會走else分支,此時序列化會連續(xù)寫兩個0。
序列化:
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mNum);
dest.writeIntArray(mUids);
dest.writeStringArray(mNames);
if (mChains == null) {
dest.writeInt(-1);
} else { // 當(dāng)mChains不為空的時候,這時候?qū)懥藘蓚€0
dest.writeInt(mChains.size());// 寫第一個0
dest.writeParcelableList(mChains, flags);// 寫第二個0
}
}
反序列化
如下述代碼,WorkSource反序列化時,當(dāng)讀到第一個0也就是numChains=0的時候,這個對應(yīng)mChains長度為0,同樣也會走else分支,此時mChains直接被置為null,但是序列化其實是寫了兩個0,這時候后面還有一個0沒有讀,這樣序列化和反序列化就造成了不對稱。
反序列化:
WorkSource(Parcel in) {
mNum = in.readInt();
mUids = in.createIntArray();
mNames = in.createStringArray();
int numChains = in.readInt(); // 讀第一個0
if (numChains > 0) {
mChains = new ArrayList<>(numChains);
in.readParcelableList(mChains, WorkChain.class.getClassLoader());
} else { // 當(dāng)讀到numChains=0的時候,這時候直接就將mChains置為null,第二個0還沒有讀
mChains = null;
}
}
當(dāng)然實際上不對稱的類還有很多,大家可以看下網(wǎng)上泄露出來的漏洞利用源碼,有很多這樣的類,這里就不列出來了,知道了漏洞的本質(zhì)是因為Parcel讀寫不對稱,我們接下來看一下其中的原理。
4.3 parcel 漏洞原理
了解parcel漏洞真正的原理之前,首先來看一下系統(tǒng)校驗intent的序列化流程。
4.3.1 系統(tǒng)校驗序列化流程
首先攻擊者手動會序列化一次需要傳給系統(tǒng)的bundle,然后系統(tǒng)會反序列化一次進行校驗,校驗完之后又會重新序列化交給設(shè)置,然后設(shè)置真正去打開頁面的時候會再次反序列化,這樣就經(jīng)歷了兩次序列化與反序列化,因為其中讀寫不對稱,所以給了攻擊者有機可趁的機會。
4.3.2 漏洞原理簡介
這個漏洞核心就是前后一共經(jīng)歷了兩次序列化和反序列化。我們以上面4.2.1案例1的不對稱舉例(readInt()對應(yīng)writeLong()),當(dāng)出現(xiàn)不對稱讀寫之后,兩次序列化與反序列化會有什么后果?如下圖所示可以看到:
第一次序列化:輸入兩個int 1;
第二次反序列化:讀的時候是readInt(),讀出兩個int 1;
第三次序列化:寫的時候是writeLong(),這是分別寫了long 1和int 1,long的長度是int長度的雙倍;
第四次反序列化:讀的時候是readInt(),第一個long 1會被分成兩個int來讀,所以就一次讀成了101。
而攻擊者也正是借助這個不對稱,導(dǎo)致實際輸入和輸出不一樣,隱藏了惡意的KEY-INTENT,從而繞過了系統(tǒng)的校驗,以此打開任意一個頁面,實現(xiàn)startAnyWhere。
4.3.3 漏洞原理實踐
因為案例1比較明顯,google早已經(jīng)修復(fù)該漏洞,而WorkSource因為比較隱晦,所以該漏洞一直存在,我們接下來看一下如何利用WorkSource來構(gòu)造攻擊實現(xiàn)。
下面一張圖帶你搞明白如何通過兩次序列化和反序列化達到我們的目的:
由上述文章可知,最終給到系統(tǒng)校驗的是一個bundle類型的數(shù)據(jù)結(jié)構(gòu),bundle是存儲key-value類型的,而我們目的就是要將惡意的KEY-INTENT隱藏起來然后繞過系統(tǒng)的校驗。接下來詳細(xì)講一下實現(xiàn)步驟:
1、手動序列化:
如上圖左側(cè)第一列,手動序列化這個bundle,這個bundle序列化時攜帶了三個key-value:
- 第一個key-value:WorkSource相關(guān)的;
- 第二個key-value:經(jīng)過精心構(gòu)造;
- 第三個key-value:隱藏惡意的KEY-INTENT。
第一次序列化后的bundle通過16進制打印出來如下圖所示:
2、系統(tǒng)進行反序列化
經(jīng)過系統(tǒng)第一次反序列化,沒有觸發(fā)不對稱,系統(tǒng)是讀不到這個惡意的KEY-INTENT的,所以自然校驗通過。
3、系統(tǒng)重新序列化
系統(tǒng)校驗完需要重新序列化,這時候由于讀寫不對稱,最終紅色區(qū)域【1,-1】兩個值變成了【0,0】。
4、setting反序列化
setting再次反序列化,上面也講到了,由于不對稱,原本兩個0只讀了1個0。
5、解析最終的key-value
- 讀第一個key-value:由于上述WorkSource的不對稱,原本兩個0只讀了1個0;
- 讀第二個key-value:由于讀第一個時少讀了一個0,剩余的0變成了第二個key-value的內(nèi)容,整體內(nèi)容錯位,由于遵循TLV的格式,錯位之后,0和13變成了第二個key-value的key,惡意KEY-INTENT前的所有值都變成了第二個key-value的value;
- 讀第三個key-value:此時真正惡意的KEY-INTENT變成我們需要的第三個key-value。
五、漏洞攻擊實戰(zhàn)
通過上面兩節(jié),我們可以看到,借助startAnyWhere和parcel漏洞,可以繞過系統(tǒng)校驗任意打開一個頁面,下面來看兩個真實案例:
5.1 實戰(zhàn)案例1
可以看到在虛擬機上,通過這個漏洞直接就打開了鎖屏密碼的設(shè)置頁面,然后可以直接繞過密碼校驗將鎖屏密碼改掉。
5.2 實戰(zhàn)案例2
案例1已經(jīng)足以反應(yīng)出問題和風(fēng)險,但是實際上國內(nèi)的手機經(jīng)過改造,基本不會存在這個問題,那么我們來看一下真機上的使用案例:
在講這個案例之前,我們要先額外講一下XXSDK中存在的一個AsistActivity,里面存在一段代碼,如下所示。
這個代碼很簡單,就是接受外部的intent的然后直接startActivity了,這里面又提到了startActivity,上面文件共享也是這樣調(diào)用的,正好符合了FileProvider的使用邏輯。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//此處省略部分代碼
Intent intent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
int intExtra = intent == null ? 0 : intent.getIntExtra("", 0);
//此處省略部分代碼
startActivityForResult(intent, intExtra);
}
借助這個類,我們便可以模擬一個完整的攻擊流程,如下圖所示:
- 第一步:攻擊APP構(gòu)造一個intent1,這個intent1的意圖是打開上述AssistActivity;intent1中攜帶了惡意的intent2,這個intent2的意圖打開攻擊APP的指定頁面,然后讓應(yīng)用共享指定文件了;
- 第二步:調(diào)用andorid系統(tǒng)添加賬號頁面;
- 第三步:業(yè)務(wù)APP中由于集成了AssistActivity,接受惡意的intent2會直接startActivity進行共享文件;
經(jīng)過以上三步,直接就把APP的一些隱私文件共享給攻擊APP,同時攻擊APP可以在惡意intent中授權(quán)直接修改文件。
5.3 惡意intent的代碼
下面看一下惡意intent的代碼:
private Intent makeFileIntent() {
Intent intent1 = new Intent().setComponent(new ComponentName("XXX", "xxx.xxx.AssistActivity")); // 打開AssistActivity
Uri uri = Uri.parse("content://xxxx/xxx_info");
Intent intent2 = new Intent(mContext, SecondActivity.class); // 打開攻擊者的頁面并且共享指定URI的文件
intent2.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent2.setType(mContext.getContentResolver().getType(uri));
intent2.setData(uri);
intent1.putExtra("key", intent2);// 惡意intent2放入intent1中
return intent;
}
5.4 惡意序列化的代碼
目前漏洞均已修復(fù),為避免風(fēng)險,不展示所有代碼。
private static Bundle makeEvilIntent(Intent intent) {
Bundle bundle = new Bundle();
Parcel obtain = Parcel.obtain();
Parcel obtain2 = Parcel.obtain();
Parcel obtain3 = Parcel.obtain();
obtain2.writeInt(3);// bundle中key-va長度
obtain2.writeString("firstKey");
obtain2.writeInt(4); //VAL_PARCELABLE
obtain2.writeString("android.os.WorkSource");
obtain2.writeInt(-1);//mNum
obtain2.writeInt(-1);//mUids
obtain2.writeInt(-1);//mNames
obtain2.writeInt(1);//mChains.length
obtain2.writeInt(-1);
...此處省略一些構(gòu)造代碼
bundle.readFromParcel(obtain);
return bundle;
}
以下是視頻演示,通過上面這一段攻擊代碼,拿到了手機上某個APP存在應(yīng)用私有目錄下的賬號信息, 同樣為了隱私,此處部分脫敏。
六、漏洞利用影響
通過上文的介紹,我們知道借助這個漏洞可以實現(xiàn)對系統(tǒng)任意文件的修改,下面列出了漏洞帶來的影響:
- 讀取用戶隱私信息;
- 安裝惡意應(yīng)用;
- 改寫動態(tài)加載的代碼;
- 改寫系統(tǒng)配置;
- 獲取特殊權(quán)限。
七、漏洞修復(fù)措施
除了發(fā)現(xiàn)問題更重要的是解決問題,下面列出了修復(fù)這個漏洞對應(yīng)的一些方案:
系統(tǒng)層:
- 修復(fù)pacel漏洞的不對稱;
- 系統(tǒng)校驗的時候,做兩次序列化與反序列化;
應(yīng)用層:
- FileProvider增加路徑限制;
- 接受intent的Activity要著重注意校驗,設(shè)置黑白名單。
八、漏洞預(yù)防措施
漏洞其實是不可避免的,下面是面對層出不窮漏洞的一些預(yù)防措施:
- 組件能不導(dǎo)出就不導(dǎo)出;
- 可導(dǎo)出的組件建議增加簽名或者包名校驗;
- 接受intent或者url參數(shù)務(wù)必校驗;
- 文件共享務(wù)必遵循最小化原則;
- 敏感內(nèi)容需要進行加密。
九、總結(jié)
接下來簡單回顧一下,本文主要講了5方面內(nèi)容:
- 第1方面:主要描述了FileProvider,闡述了其出現(xiàn)背景、設(shè)計原理、使用方式、優(yōu)缺點等;
- 第2方面:主要描述了startAnyWhere,闡述了其實現(xiàn)原理、實現(xiàn)方式;
- 第3方面:主要描述了Parcel不對稱漏洞,闡述了Parcel的設(shè)計原理、不對稱漏洞、漏洞案例、漏洞原理以及漏洞利用方案;
- 第4方面:主要描述了漏洞攻擊實戰(zhàn),從模擬器到真機,從原理到代碼,演示了通過漏洞攻擊手機、獲取用戶隱私信息的流程;
- 第5方面:主要是講了漏洞帶來的影響、漏洞的修復(fù)和預(yù)防措施。
整體來講,這個漏洞波及了所有的Android手機,無論是對用戶,對企業(yè)都造成了巨大的損失。
作為開發(fā)者的我們需要從自身做起,守護好每一個環(huán)節(jié),避免讓攻擊者有隙可乘。