Denial of App - Google Bug 13416059 分析
0x01 背景
Soot作者Eric Bodden所在的實驗室, Secure Software Engineering最近宣佈他們將在SPSM'14上講述名為Denial-of-App-Attack的Android系統漏洞,影響4.4.3之前的機型,並給出了poc和對應的google commit id.
這個在googlecode上對應的連結是 https://code.google.com/p/android/issues/detail?id=65790
POC:https://github.com/secure-software-engineering/denial-of-app-attack
該問題可以導致攻擊者可以指定應用使其無法安裝在手機上,除非有root許可權或者factory reset手機。可以被木馬用來佔位拒絕防毒軟體的安裝,或者佔位拒絕競品安裝。下面是根據commit diff和poc給出的漏洞具體分析。
0x02 問題現象:
下載安裝這個POC,可以看到其實就是指定一個packagename,例如com.taobao.taobao,然後生成了一個malformed的APK並執行安裝,由於該APK的dex是非法的,安裝的時候會報告INSTALL_FAILED_DEXOPT並安裝失敗。但如果隨後安裝真正的com.taobao.taobao時,即使指定了重新安裝選項(pm install -r),卻會報INSTALL_FAILED_UID_CHANGED,導致後續安裝失敗,而在被佔位的手機上已安裝應用中卻找不到com.taobao.taobao,自然也無法清除掉佔位的幽靈,造成真正的淘寶應用完全無法安裝,推而廣之可以用在360等防毒軟體上。
0x03 問題本質:
Google的diff對此問題的描述是:
We'd otherwise leave the data dirs & native libraries lying around. This will leave the app permanently broken because the next install of the app will fail with INSTALL_FAILED_UID_CHANGED.
Also remove an unnecessary instance variable.
Cherry-pick from master Bug 13416059
透過觀察可以發現,第一次安裝(所謂“佔位”)結束的時候,在/data/data/目錄下已經有了com.taobao.taobao目錄並分配了一個uid,例如u70(10070),但第二次安裝的時候,PackageManager卻出現了UID_CHANGED的error,而沒有複用u70,這是為什麼?
INSTALL_FAILED_DEXOPT和UID_CHANGED是在如下程式碼塊中:
#!java
3622 private PackageParser.Package scanPackageLI(PackageParser.Package pkg,
3623 int parseFlags, int scanMode, long currentTime, UserHandle user) {
//....
4141 if ((scanMode&SCAN_NO_DEX) == 0) {
4142 if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0)
4143 == DEX_OPT_FAILED) {
4144 mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
4145 return null;
4146 }
4147 }
scanPackageLI函式流程大概如下:
#!java
/**/
//檢查是否系統應用
/**/
//檢查Package是否重複,否則丟擲PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE
// Initialize package source and resource directories
3686 File destCodeFile = new File(pkg.applicationInfo.sourceDir);
3687 File destResourceFile = new File(pkg.applicationInfo.publicSourceDir);
//...
// Just create the setting, don't add it yet. For already existing packages
3812 // the PkgSetting exists already and doesn't have to be created.
3813 pkgSetting = mSettings.getPackageLPw(pkg, origPackage, realName, suid, destCodeFile,
3814 destResourceFile, pkg.applicationInfo.nativeLibraryDir,
3815 pkg.applicationInfo.flags, user, false);
//在這之後uid已經被指定了
/**/
//檢查簽名
//檢查Provider許可權
//開始建立目錄
final long scanFileTime = scanFile.lastModified();
3926 final boolean forceDex = (scanMode&SCAN_FORCE_DEX) != 0;
3927 pkg.applicationInfo.processName = fixProcessName(
3928 pkg.applicationInfo.packageName,
3929 pkg.applicationInfo.processName,
3930 pkg.applicationInfo.uid);
3931
3932 File dataPath;
3933 if (mPlatformPackage == pkg) {
//omit
3937 } else {
3938 // This is a normal package, need to make its data directory.
3939 dataPath = getDataPathForPackage(pkg.packageName, 0);
3940
3941 boolean uidError = false;
3942
3943 if (dataPath.exists()) {
3944 int currentUid = 0;
3945 try {
3946 StructStat stat = Libcore.os.stat(dataPath.getPath());
3947 currentUid = stat.st_uid;
3948 } catch (ErrnoException e) {
3949 Slog.e(TAG, "Couldn't stat path " + dataPath.getPath(), e);
3950 }
3951
3952 // If we have mismatched owners for the data path, we have a problem.
3953 if (currentUid != pkg.applicationInfo.uid) {
3954 boolean recovered = false;
3955 if (currentUid == 0) {
3956 //omit...
3969 }
3970 if (!recovered && ((parseFlags&PackageParser.PARSE_IS_SYSTEM) != 0
3971 || (scanMode&SCAN_BOOTING) != 0)) {
3972 // If this is a system app, we can at least delete its
3973 // current data so the application will still work.
3974 //omit...
4001 } else if (!recovered) {
4002 // If we allow this install to proceed, we will be broken.
4003 // Abort, abort!
4004 mLastScanError = PackageManager.INSTALL_FAILED_UID_CHANGED;
4005 return null;
4006 }
} else {//目錄不存在,新建立
4029 if (DEBUG_PACKAGE_SCANNING) {
4030 if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
4031 Log.v(TAG, "Want this data dir: " + dataPath);
4032 }
4033 //invoke installer to do the actual installation
4034 int ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid);//建立目錄
4035 if (ret < 0) {
4036 // Error from installer
4037 mLastScanError = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
4038 return null;
4039 }
4040
4041 if (dataPath.exists()) {
4042 pkg.applicationInfo.dataDir = dataPath.getPath();
4043 } else {
4044 Slog.w(TAG, "Unable to create data directory: " + dataPath);
4045 pkg.applicationInfo.dataDir = null;
4046 }
4047 }
//omit...
//複製nativeLibrary
//omit...
//進行DexOpt
4141 if ((scanMode&SCAN_NO_DEX) == 0) {
4142 if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0)
4143 == DEX_OPT_FAILED) {
4144 mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
4145 return null;
4146 }
4147 }
那麼漏洞的原理就很清楚了,第一次佔位安裝時,故意讓PMS在資料目錄已分配uid並寫入了/data/data/下之後走到dexopt時使其報錯,導致安裝異常終止,此時已放置的資料目錄卻沒有被清除掉。第二次安裝的時候package被分配了新的的uid,但此時已有同名卻不同uid的資料目錄存在,導致uid_changed錯誤,安裝失敗。
為什麼第二次安裝的時候就會被分配不同的uid?關鍵在於 mSettings.getPackageLPw,輾轉ref到/frameworks/base/services/java/com/android/server/pm/Settings.java
#!java
private PackageSetting getPackageLPw(String name, PackageSetting origPackage,
359 String realName, SharedUserSetting sharedUser, File codePath, File resourcePath,
360 String nativeLibraryPathString, int vc, int pkgFlags,
361 UserHandle installUser, boolean add, boolean allowInstall) {
//omit...
} else {
423 p = new PackageSetting(name, realName, codePath, resourcePath,
424 nativeLibraryPathString, vc, pkgFlags);
425 p.setTimeStamp(codePath.lastModified());
426 p.sharedUser = sharedUser;
427 // If this is not a system app, it starts out stopped.
428 if ((pkgFlags&ApplicationInfo.FLAG_SYSTEM) == 0) {
429 if (DEBUG_STOPPED) {
430 RuntimeException e = new RuntimeException("here");
431 e.fillInStackTrace();
432 Slog.i(PackageManagerService.TAG, "Stopping package " + name, e);
433 }
434 List<UserInfo> users = getAllUsers();
435 if (users != null && allowInstall) {
436 for (UserInfo user : users) {
437 // By default we consider this app to be installed
438 // for the user if no user has been specified (which
439 // means to leave it at its original value, and the
440 // original default value is true), or we are being
441 // asked to install for all users, or this is the
442 // user we are installing for.
443 final boolean installed = installUser == null
444 || installUser.getIdentifier() == UserHandle.USER_ALL
445 || installUser.getIdentifier() == user.id;
446 p.setUserState(user.id, COMPONENT_ENABLED_STATE_DEFAULT,
447 installed,
448 true, // stopped,
449 true, // notLaunched
450 null, null);
451 writePackageRestrictionsLPr(user.id);
452 }
453 }
454 }
455 if (sharedUser != null) {
456 p.appId = sharedUser.userId;
457 } else {
458 // Clone the setting here for disabled system packages
459 PackageSetting dis = mDisabledSysPackages.get(name);
460 if (dis != null) {
//omit..
484 } else {
485 // Assign new user id
486 p.appId = newUserIdLPw(p);//關鍵點
487 }
488 }
繼續檢視newUserIdLPw
#!java
private int newUserIdLPw(Object obj) {
2360 // Let's be stupidly inefficient for now...
2361 final int N = mUserIds.size();
2362 for (int i = 0; i < N; i++) {
2363 if (mUserIds.get(i) == null) {//檢查空位
2364 mUserIds.set(i, obj);
2365 return Process.FIRST_APPLICATION_UID + i;
2366 }
2367 }
2368
2369 // None left?
2370 if (N > (Process.LAST_APPLICATION_UID-Process.FIRST_APPLICATION_UID)) {
2371 return -1;
2372 }
2373
2374 mUserIds.add(obj);
2375 return Process.FIRST_APPLICATION_UID + N;
2376 }
mUserIds是一個PackageSettings的陣列狀結構,維護了當前的userid,並在安裝時遍歷進行分配。在第一次惡意的佔位安裝中, mUserIds這個array狀結構已經被新增了一個PackageSettings進去,形成類似於
#!java
[PackageSetting{(10001, bla)}, ..., PackageSetting{(10070, com.taobao.taobao)}]
的結構,但在dexopt failed的時候最末尾一項沒有被移除。隨後再安裝時,newUserIdLPw會遍歷mUserIds,發現沒有空位,就會在末尾重新新增一個,就會形成
#!java
[PackageSetting{(10001, bla)},...,PackageSetting{(10070, com.taobao.taobao)},PackageSetting{(10071, com.taobao.taobao)}]
的結構,導致兩次安裝分配的UID不同,觸發INSTALL_FAILED_UID_CHANGED。
但值得注意的是,這時候mUserIds並沒有被固化在packages.xml和packages.list中。
0x04 進一步思考
那麼這樣肯定會想到,如果殺掉system_server(軟重啟),讓其重新掃描並建立mUserIds陣列不就能修復這個問題了?
理論上來說,如果在重啟前沒有安裝過其他應用的話,那麼這還真是可行的。因為重啟後重新建立的uid陣列是[(10001, bla),...,()10069, haha)],那麼重新安裝的com.taobao.taobao剛好能佔到10070的位置,皆大歡喜。
但如果在重啟後又安裝了其他應用,那麼其就會佔掉10070的位置,導致taobao再安裝的時候以10071及之後的uid就拿不回原來應該屬於它的/data/data/com.taoba.taobao了... what a pity.
以上在stock rom(Genymotion, SDK)和小米2、Nexus等上驗證透過。
所以現在看來,原作者說只有root或者reset才能清除這個問題的說法似乎不準確,至少從給出的poc和google的diff來看實驗結果某些情況下重啟就能fix。具體還有什麼細節就只能等待SPSM的paper了。總體來說,這是一個比較好玩的trick類漏洞,而且從issuelink來看,應該還有一些其他型別的同樣效果的漏洞存在。
0x05 修復
Google對此的修復:
Google的diff主要是新增了SCAN_DELETE_DATA_ON_FAILURES的flag,在設定了該flag的時候安裝失敗時會刪除遺留掉的檔案。
#!java
@@ -4644,6 +4643,10 @@
if ((scanMode&SCAN_NO_DEX) == 0) {
if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
== DEX_OPT_FAILED) {
+ if ((scanMode & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+ removeDataDirsLI(pkg.packageName);
+ }
+
mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
return null;
}
@@ -4721,6 +4724,10 @@
PackageParser.Package clientPkg = clientLibPkgs.get(i);
if (performDexOptLI(clientPkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
== DEX_OPT_FAILED) {
+ if ((scanMode & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+ removeDataDirsLI(pkg.packageName);
+ }
+
mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
return null;
}
如何fix某個佔位攻擊:
root下刪除該資料目錄即可,非root。。。那隻能reset了。
相關文章
- Hyperscan is generally vulnerable to regular expression denial of service (ReDoS)2024-05-17Express
- 分析表時遇到BUG2019-07-21
- Google分析language垃圾資訊2018-07-03Go
- text-size-adjust bug 分析2019-03-04
- App 出海 —— Google 結算系統面面觀2022-04-29APPGo
- 給Chrome“捉蟲”16000個,Google開源bug自檢工具2019-02-10ChromeGo
- Google Chrome發現新Bug CPU使用率飆升至100%2018-12-24GoChrome
- 如何使用DTM將App事件傳送到Google Analytics2022-03-15APP事件Go
- 安卓Bug 17356824 BroadcastAnywhere漏洞分析2020-08-19安卓AST
- 【原始碼】Redis exists命令bug分析2022-02-08原始碼Redis
- 使用系統TabLayout的app快來修Bug2021-09-09TabLayoutAPP
- 測出Bug就完了?從4個方面教你Bug根因分析2023-12-15
- 全球Google Play市場分析報告2020-04-07Go
- 上傳APP到Google Play許可權問題2018-08-09APPGo
- Google Play ASO之安卓APP頁面優化2018-04-25Go安卓APP優化
- iOS APP效能分析2018-05-16iOSAPP
- lombok1.16.20序列化bug分析2019-04-14Lombok
- Google DNS劫持背後的技術分析2020-08-19GoDNS
- Think With Google:YouTube影片廣告性別分析2019-12-17Go
- 新一代資料分析利器:Google Dremel原理分析KL2022-03-21GoREM
- 【譯】使用 Google TWA 技術將 PWA 打包成 Android App2019-06-03GoAndroidAPP
- 線上BUG:MySQL死鎖分析實戰2021-07-04MySql
- iOS APP包分析工具2023-11-24iOSAPP
- 移動APP測試之怎麼避免Bug漏測?2022-06-30APP
- Google Play 放置遊戲產品型別分析2019-10-08Go遊戲型別
- google protocol buffer——protobuf的基本使用和模型分析2020-08-16GoProtocol模型
- Google收購安全分析軟體廠商Zynamics2019-05-10Go
- 分散式資料庫Google Spanner原理分析KP2022-03-21分散式資料庫Go
- 在你的 Android App 中使用 Flutter | Google開發者大會2018-09-25AndroidAPPFlutterGo
- 2020年App Store訂閱收益為Google Play近4倍2021-02-12APPGo
- Google Play & App Store 年度最佳應用及遊戲盤點2020-12-10GoAPP遊戲
- Google Play App Store API 採集谷歌安卓應用商城app的資料介面 - 2024最新2024-04-23GoAPPAPI谷歌安卓
- 房天下APP競品分析2019-03-04APP
- 菜鳥裹裹App分析系列-產品分析2018-07-26APP
- Sensor Tower:2020年App Store和Google Play全球收入1110億美元2021-01-25APPGo
- WordPress區塊鏈專案首碼圈APP前端無BUG原始碼2021-09-15區塊鏈APP前端原始碼
- 軟硬體--智慧穿戴常見BUG及原因分析2023-05-17
- 高質量的缺陷分析:讓自己少寫 bug2020-11-09