安裝一個(gè)apk引起的無(wú)法開(kāi)機(jī)

本文代碼部分基于Android 11.0(Android R)

故事的開(kāi)始

今天老大急沖沖的跑過(guò)來(lái)說(shuō):xx,你幫我看看這手機(jī)咋回事,突然開(kāi)不了機(jī)。

我心想:我最近也沒(méi)提過(guò)代碼,應(yīng)該不是我的問(wèn)題吧。(甩鍋.

把電腦插上手機(jī)后,我看到下面這段報(bào)錯(cuò)一直在loop

12-31 16:08:49.603 21899 21899 E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: main
12-31 16:08:49.603 21899 21899 E AndroidRuntime: java.lang.IllegalStateException: Signature|privileged permissions not in privapp-permissions whitelist: {com.xxx.xxx.xxxxx (/data/app/BR9Kz0rmscIpqqvqBf8jwg==/com.xxx.xxx.xxxxx-fLGzzHKkZaTB5_DLxgo_Fg==): android.permission.BACKUP, com.xxx.xxx.xxxxx (/data/app/BR9Kz0rmscIpqqvqBf8jwg==/com.xxx.xxx.xxxxx-fLGzzHKkZaTB5_DLxgo_Fg==): android.permission.UPDATE_DEVICE_STATS}
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.server.pm.permission.PermissionManagerService.systemReady(PermissionManagerService.java:4688)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.server.pm.permission.PermissionManagerService.access$500(PermissionManagerService.java:181)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.server.pm.permission.PermissionManagerService$PermissionManagerServiceInternalImpl.systemReady(PermissionManagerService.java:4771)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.server.pm.PackageManagerService.systemReady(PackageManagerService.java:22183)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.server.SystemServer.startOtherServices(SystemServer.java:2305)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.server.SystemServer.run(SystemServer.java:624)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.server.SystemServer.main(SystemServer.java:440)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
12-31 16:08:49.603 21899 21899 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

看完之后我立馬回答:老大,這個(gè)我知道,一定是系統(tǒng)應(yīng)用組的同學(xué)忘記在privapp-permissions-platform.xml文件下面加權(quán)限聲明了。

老大:不會(huì)呀,這個(gè)手機(jī)我一直在用,后面突然就變成這樣了。

此刻的我一臉懵逼:還有如此神奇之事。

故事的開(kāi)始便是這樣,接下來(lái)我就開(kāi)始調(diào)查這個(gè)神奇的現(xiàn)象。

萬(wàn)惡之源

《眾 所 周 知》

手機(jī)開(kāi)機(jī)會(huì)檢查priv-app的權(quán)限是否和/etc/permissions/privapp-permissions-platform.xml(有可能在別的文件夾下,例如vendor/etc/permissions,也有可能叫其他名字,因?yàn)橹灰獂ml的節(jié)點(diǎn)是對(duì)的就行,pm中的SystemConfig會(huì)對(duì)這類文件夾的所有xml進(jìn)行掃描)所聲明的權(quán)限是否一樣,不一樣則無(wú)法開(kāi)機(jī),就會(huì)一直loop上面的crash。

先從log中的PermissionManagerService#systemReady方法入手

private void systemReady() {
    mSystemReady = true;
    // 萬(wàn)惡之源
    if (mPrivappPermissionsViolations != null) {
        throw new IllegalStateException("Signature|privileged permissions not in "
                                        + "privapp-permissions whitelist: " + mPrivappPermissionsViolations);
    }
    // ...
}

只要mPrivappPermissionsViolations這個(gè)數(shù)組中有數(shù)據(jù),我們就永遠(yuǎn)無(wú)法開(kāi)機(jī),看看這個(gè)數(shù)組是怎么填充的。

private boolean grantSignaturePermission(String perm, AndroidPackage pkg,
            PackageSetting pkgSetting, BasePermission bp, PermissionsState origPermissions) {
        // ...
        if (!privappPermissionsDisable && privilegedPermission && pkg.isPrivileged()
                && !platformPackage && platformPermission) {
            if (!hasPrivappWhitelistEntry(perm, pkg)) {
                if (!mSystemReady
                        && !pkgSetting.getPkgState().isUpdatedSystemApp()) {
                    ApexManager apexMgr = ApexManager.getInstance();
                    String apexContainingPkg = apexMgr.getActiveApexPackageNameContainingPackage(
                            pkg);

                    if (apexContainingPkg == null || apexMgr.isFactory(
                            apexMgr.getPackageInfo(apexContainingPkg, MATCH_ACTIVE_PACKAGE))) {
                        // ... 進(jìn)行和xml聲明的權(quán)限進(jìn)行對(duì)比... 發(fā)現(xiàn)在黑名單找到這個(gè)perm的或者在白名單找不到這個(gè)perm
                        // 一律加到開(kāi)機(jī)防火墻(我自己想的名字0.0)中
                        if (permissionViolation) {
                            Slog.w(TAG, "Privileged permission " + perm + " for package "
                                    + pkg.getPackageName() + " (" + pkg.getCodePath()
                                    + ") not in privapp-permissions whitelist");

                            if (RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE) {
                                if (mPrivappPermissionsViolations == null) {
                                    mPrivappPermissionsViolations = new ArraySet<>();
                                }
                                mPrivappPermissionsViolations.add(
                                        pkg.getPackageName() + " (" + pkg.getCodePath() + "): "
                                                + perm);
                            }
                        } else {
                            return false;
                        }
                    }
                    // ....          
                }
                }
            }
        }
}

看完這里,我有一個(gè)疑問(wèn),一個(gè)普通的App怎么會(huì)有如此大的影響(小身材大力量?),可以看到上面各種條件判斷才能走到這個(gè)校檢權(quán)限聲明的碼塊里。而這其中的一個(gè)條件引起了我的注意---pkg.isPrivileged(),這個(gè)為true則說(shuō)明這是一個(gè)priv-app,而priv-app一般都是內(nèi)置在系統(tǒng)內(nèi)作為系統(tǒng)軟件,還能在外部安裝?(可能是我見(jiàn)識(shí)太少了 丟人~~)。與之關(guān)聯(lián)的是一個(gè)scanFlag叫SCAN_AS_PRIVILEGED,這個(gè)在開(kāi)機(jī)掃描的時(shí)候就經(jīng)常使用,對(duì)一個(gè)指定的系統(tǒng)內(nèi)部路徑scan的時(shí)候加上SCAN_AS_PRIVILEGED,這個(gè)路徑下面所有的包都是priv-app。果不其然,當(dāng)我找這個(gè)flag的使用時(shí)候,發(fā)現(xiàn)PMS的其他地方也會(huì)給一個(gè)包附上這個(gè)flag,就在PMS#adjustScanFlags方法中。

// 對(duì)和priv-app進(jìn)行sharedUser的應(yīng)用也要像priv-app一樣掃描
final boolean skipVendorPrivilegeScan = ((scanFlags & SCAN_AS_VENDOR) != 0)
    && getVendorPartitionVersion() < 28;
if (((scanFlags & SCAN_AS_PRIVILEGED) == 0)
    && !pkg.isPrivileged()
    && (pkg.getSharedUserId() != null)
    && !skipVendorPrivilegeScan) {
    SharedUserSetting sharedUserSetting = null;
    try {
        sharedUserSetting = mSettings.getSharedUserLPw(pkg.getSharedUserId(), 0,
                                                       0, false);
    } catch (PackageManagerException ignore) {
    }
    if (sharedUserSetting != null && sharedUserSetting.isPrivileged()) {
        // 豁免使用平臺(tái)密鑰簽名的SharedUsers。 
        // TODO(b / 72378145)修復(fù)此豁免。
        // 強(qiáng)制簽名應(yīng)用程序像其他priv-apps一樣將其特權(quán)許可列入白名單。
        synchronized (mLock) {
            PackageSetting platformPkgSetting = mSettings.mPackages.get("android");
            if ((compareSignatures(platformPkgSetting.signatures.mSigningDetails.signatures,
                                   pkg.getSigningDetails().signatures)
                 != PackageManager.SIGNATURE_MATCH)) {
                scanFlags |= SCAN_AS_PRIVILEGED;
            }
        }
    }
}

而在PMS#adjustScanFlags方法中,給和priv-app sharedUser的應(yīng)用 append 上SCAN_AS_PRIVILEGED,讓其也要強(qiáng)制加入到開(kāi)機(jī)檢查的白名單列表,所以google這是故意而為之。既然作為一個(gè)priv-app,開(kāi)機(jī)檢查權(quán)限也就成了理所當(dāng)然了。所以我們?yōu)閜riv-app預(yù)置到系統(tǒng)的時(shí)候,要確定該priv-app有沒(méi)有其他的sharedUserId應(yīng)用,如果有的話,也要在權(quán)限白名單privapp-permissions-platform.xml中聲明這個(gè)沒(méi)有預(yù)置到系統(tǒng)的App申請(qǐng)的權(quán)限,否則裝上去之后重啟會(huì)造成loop crash。

為什么SharedUser的應(yīng)用能夠共享權(quán)限

《 眾 所 周 知》 * 2

所有檢查權(quán)限的最后步驟都是通過(guò)調(diào)用PackageSettings中的getPermissionsState()獲取App的權(quán)限獲取狀態(tài),代碼如下:

@Override
public PermissionsState getPermissionsState() {
    return (sharedUser != null)
        ? sharedUser.getPermissionsState()
        : super.getPermissionsState();
}

如果Package的PackageSettings的sharedUser不為空,則默認(rèn)用sharedUser的權(quán)限。所以SharedUser的應(yīng)用能互相共享大家已有的權(quán)限,且一個(gè)權(quán)限授予了,在這個(gè)SharedUser的其他包都會(huì)默認(rèn)授予這個(gè)權(quán)限。做系統(tǒng)應(yīng)用的同學(xué)可能比較了解,只要在manifest.xml中聲明android:sharedUserId="android.uid.system",就能獲取并使用其他同屬于android.uid.systemSharedUser組的其他權(quán)限。

SharedUser怎樣被賦值到PackageSettings上

我們先來(lái)看看PMS#scanPackageNewLI方法中的這個(gè)地方,也就值sharedUser賦值的地方。

SharedUserSetting sharedUserSetting = null;
if (parsedPackage.getSharedUserId() != null) {
    // SIDE EFFECTS; may potentially allocate a new shared user
    sharedUserSetting = mSettings.getSharedUserLPw(parsedPackage.getSharedUserId(),
                                                   0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true /*create*/);
    if (DEBUG_PACKAGE_SCANNING) {
        if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
            Log.d(TAG, "Shared UserID " + parsedPackage.getSharedUserId()
                  + " (uid=" + sharedUserSetting.userId + "):"
                  + " packages=" + sharedUserSetting.packages);
    }
}

我們可以看到這里根據(jù)parsedPackage.getSharedUserId這個(gè)String類型的SharedUserId,就去pm.Settings中尋找屬于這個(gè)package的SharedUserSetting了,也沒(méi)有校驗(yàn)這個(gè)包的sharedUserId是否符合某些條件,例如簽名之類的條件。直到最后的PMS#commitPackageSettings(持久化PackageSettings到packages.xml文件中,安裝包數(shù)據(jù)不再發(fā)生變化步驟),也沒(méi)有對(duì)這個(gè)SharedUserSetting進(jìn)行重新賦值,所以到這步我覺(jué)得應(yīng)該是SharedUserId的問(wèn)題。機(jī)智的我把想法就放在了這個(gè)SharedUserId的賦值流程上面了,一通查找之下,我找到了SharedUserId賦值的地方:ParsingPackageUtils#parseSharedUser

private static ParseResult<ParsingPackage> parseSharedUser(ParseInput input,                             
        ParsingPackage pkg, TypedArray sa) {                                                             
    String str = nonConfigString(0, R.styleable.AndroidManifest_sharedUserId, sa);                       
    if (TextUtils.isEmpty(str)) {                                                                        
        return input.success(pkg);                                                                       
    }                                                                                                    
                                                                                                         
    if (!"android".equals(pkg.getPackageName())) {                                                       
        ParseResult<?> nameResult = validateName(input, str, true, true);                                
        if (nameResult.isError()) {                                                                      
            return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID,                   
                    "<manifest> specifies bad sharedUserId name \"" + str + "\": "                       
                            + nameResult.getErrorMessage());                                             
        }                                                                                                
    }                                                                                                    
                                                                                                         
    return input.success(pkg                                                                             
            .setSharedUserId(str.intern())                                                               
            .setSharedUserLabel(resId(R.styleable.AndroidManifest_sharedUserLabel, sa)));                
}                                                                                                        

上面這段代碼很簡(jiǎn)單,大概的意思就是讀取R.styleable.AndroidManifest_sharedUserId這個(gè)attribute中的內(nèi)容,然后直接給它賦值到這個(gè)String類型的SharedUserId上面,也沒(méi)有做合規(guī)檢查。這時(shí)候不禁就有個(gè)疑問(wèn)了,那我豈不是隨便聲明一個(gè)android:sharedUserId="xxx"就能獲取到SharedUser的其他權(quán)限?這時(shí)候我打開(kāi)了測(cè)試的test demo,給它加上android:sharedUserId屬性,但沒(méi)有成功安裝成功,且報(bào)了INSTALL_FAILED_SHARED_USER_INCOMPATIBLE異常,往PMS那么一搜,啪的一下,很快就找到辣(但其實(shí)是找錯(cuò)了 因?yàn)閳?bào)錯(cuò)不是在PMS中產(chǎn)生)。因?yàn)檎义e(cuò)了的原因,我把安裝的堆棧打了出來(lái),發(fā)現(xiàn)報(bào)錯(cuò)其實(shí)是在PackageManagerServiceUtils#verifySignatures。

先把堆棧打出來(lái)

verifySignatures:689, PackageManagerServiceUtils (com.android.server.pm)
reconcilePackagesLocked:16947, PackageManagerService (com.android.server.pm)
installPackagesLI:17373, PackageManagerService (com.android.server.pm)
installPackagesTracedLI:16696, PackageManagerService (com.android.server.pm)
lambda$processInstallRequestsAsync$22$PackageManagerService:14802, PackageManagerService (com.android.server.pm)
run:-1, -$$Lambda$PackageManagerService$9znobjOH7ab0F1jsW2oFdNipS-8 (com.android.server.pm)
handleCallback:938, Handler (android.os)
dispatchMessage:99, Handler (android.os)
loop:223, Looper (android.os)
run:67, HandlerThread (android.os)
run:44, ServiceThread (com.android.server)

這也很符合邏輯,SharedUser中的其他包:你說(shuō)你是我兄弟,那你就是我兄弟?拿你的簽名跟我的對(duì)比一下,一樣的才算我兄弟。畢竟在包管理中,一個(gè)包的簽名相當(dāng)于這個(gè)包的DNA了,只有經(jīng)過(guò)同一個(gè)x509.pem和.pk8文件簽名的apk的簽名才會(huì)相同。

可以看到這里校驗(yàn)簽名不匹配直接拋出了throw new PackageManagerException,終止安裝流程,根本不給你安裝成功的機(jī)會(huì)。所以并不需要置空SharedUserSetting或者SharedUserId,只要安裝成功的Package且它們的SharedUserSetting不為空,都是合法的。

/**
 * Verifies that signatures match.
 * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}.
 * @throws PackageManagerException if the signatures did not match.
 */
public static boolean verifySignatures(PackageSetting pkgSetting,
                                       PackageSetting disabledPkgSetting, PackageParser.SigningDetails parsedSignatures,
                                       boolean compareCompat, boolean compareRecover)
    throws PackageManagerException {
    final String packageName = pkgSetting.name;
    boolean compatMatch = false;
    // ...
    // 檢查是否匹配SharedUser的簽名
    if (pkgSetting.getSharedUser() != null
        && pkgSetting.getSharedUser().signatures.mSigningDetails
        != PackageParser.SigningDetails.UNKNOWN) {

        // 已經(jīng)存在的軟件包。確保簽名匹配。在簽署證書(shū)輪換的情況下,
        // 帶有較新證書(shū)的軟件包必須與較舊版本的sharedUserId保持一致。
        // 我們檢查是否新軟件包是由較舊的證書(shū)簽名的,可以使用當(dāng)前的sharedUser簽名,
        // 還是由較新的證書(shū)簽名,以及是否與現(xiàn)有的簽名證書(shū)作為sharedUser簽名,則可以。
        boolean match =
            parsedSignatures.checkCapability(
            pkgSetting.getSharedUser().signatures.mSigningDetails,
            PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID)
            || pkgSetting.getSharedUser().signatures.mSigningDetails.checkCapability(
            parsedSignatures,
            PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID);
        // 如果sharedUserId功能檢查失敗,
        // 則可能是由于這是sharedUserId中到目前為止唯一的包,
        // 并且繼承被更新以拒絕繼承中先前密鑰的sharedUserId功能。
        if (!match && pkgSetting.getSharedUser().packages.size() == 1
            && pkgSetting.getSharedUser().packages.valueAt(0).name.equals(packageName)) {
            match = true;
        }
        if (!match && compareCompat) {
            match = matchSignaturesCompat(
                packageName, pkgSetting.getSharedUser().signatures, parsedSignatures);
        }
        if (!match && compareRecover) {
            match =
                matchSignaturesRecover(packageName,
                                       pkgSetting.getSharedUser().signatures.mSigningDetails,
                                       parsedSignatures,
                                       PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID)
                || matchSignaturesRecover(packageName,
                                          parsedSignatures,
                                          pkgSetting.getSharedUser().signatures.mSigningDetails,
                                          PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID);
            compatMatch |= match;
        }
        // 不配對(duì) 則拋異常 安裝失敗
        if (!match) {
            throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
                                              "Package " + packageName
                                              + " has no signatures that match those in shared user "
                                              + pkgSetting.getSharedUser().name + "; ignoring!");
        }
        // ...
    }
    return compatMatch;
}

最后,請(qǐng)大家注意好手機(jī)備份和軟件來(lái)源的確認(rèn),如果有其他手機(jī)廠商“修復(fù)”了這個(gè)問(wèn)題可以在評(píng)論區(qū)告知,AndroidQ和R都會(huì)存在這個(gè)問(wèn)題,至于前面的版本就沒(méi)一一確認(rèn)了。我已經(jīng)用這個(gè)apk搞壞了一臺(tái)Google Pixel(淚目),普通用戶大概也只能恢復(fù)出廠設(shè)置了,高級(jí)用戶可以用串口打開(kāi)USB調(diào)試,然后把這個(gè)包卸載掉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容