2. 框架層
因為 Android 6.0 之前組件不能在運行時改變權限,所以系統的權限檢查執行過程是靜態的。這個情況下,組件的角色和權限的等安全屬性會被放置在元數據中,即 AndroidManifest.xml 文件中,而不是組件的本身。系統包管理器會負責記錄組件的權限,所以靜態權限檢查可以從包管理器拿到權限,由運行環境或容器來執行權限檢查,這樣子可以把業務邏輯和安全決策分離開來,但是靈活性不足。
那 Android 組件可不可以不預先聲明權限在 AndroidManifest.xml 中呢?答案是:可以的。Android 的動態權限執行,可以讓組件自身執行權限檢查,而不是運行環境。
所以接下來我們將深入了解框架層的動態和靜態權限執行的原理。
動態權限執行
動態權限執行,最典型的場景,就是 IPC。Android 的核心系統服務統一會註冊到服務管理器,任何應用,只要知道服務的註冊名稱,就可以拿到對應的 Binder引用,就可使用 Binder IPC 機制調用服務。因為 Binder 沒有內置的訪問控制機制,所以每個系統服務需要自己實現訪問控制機制。
系統服務可以直接檢查調用者的 UID,通過限定 UID 來控制訪問權限,這種方式簡單直接,但是對於非固定UID的應用,就比較棘手了。而且大部分服務,並不關心調用者的 UID,只需要檢查調用者是否被賦予特定的權限即可。所以這種方式,比較適合只允許以 root(UID:0) 或 system(UID:1000) 運行的進程訪問的服務檢查。
那換一種方式,服務怎麼拿到調用者的權限列表?我們知道,大部分 UID 都是和包一一對應的,除了共享 UID。(共享 UID 後面再詳細解釋)
使用 Binder.getCallingUid() 和 Binder.getCallingPid() 獲取調用者的 UID 和 PID,通過 UID 在包管理器中查詢到對應應用的權限。android.content.Context 類中就有 checkPermission(String permission, int pid, int uid) 方法。實質上會調用到 PMS 中的 checkUidPermission(String perName, int uid),如下:
Android 6.0 以下 PMS 中的 checkUidPermission(String perName, int uid)
public int checkUidPermission(String permName, int uid) {
synchronized (mPackages) {
Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
if (obj != null) {
GrantedPermissions gp = (GrantedPermissions)obj;
if (gp.grantedPermissions.contains(permName)) {
return PackageManager.PERMISSION_GRANTED;
}
} else {
HashSet<String> perms = mSystemPermissions.get(uid);
if (perms != null && perms.contains(permName)) {
return PackageManager.PERMISSION_GRANTED;
}
}
}
return PackageManager.PERMISSION_DENIED;
}
Android 6.0 以下的 checkUidPermission() 方法比較簡單,首先,基於入參 uid 獲取應用的 appId,拿到權限列表對象(也就是 packages.xml 裡的 <package> 映射),如果 GrantedPermissions 類中的 grantedPermissions 集合包含目標權限,則檢查通過。
如果沒有該 GrantedPermissions 對象,則檢查目標權限是否可以被自動授予,實際上 mSystemPermissions 就是 platform.xml 文件中的 <assing-permission> 標籤映射緩存,記錄了一些系統級應用的 uid 對應的 permission。例:
...
<assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="cameraserver" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="cameraserver" />
<assign-permission name="android.permission.UPDATE_DEVICE_STATS" uid="cameraserver" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="graphics" />
...
Android 6.0 及以上 PMS 中的 checkUidPermission(String perName, int uid)
@Override
public int checkUidPermission(String permName, int uid) {
final int callingUid = Binder.getCallingUid();
final int callingUserId = UserHandle.getUserId(callingUid);
final boolean isCallerInstantApp = getInstantAppPackageName(callingUid) != null;
final boolean isUidInstantApp = getInstantAppPackageName(uid) != null;
final int userId = UserHandle.getUserId(uid);
if (!sUserManager.exists(userId)) {
return PackageManager.PERMISSION_DENIED;
}
synchronized (mPackages) {
Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
if (obj != null) {
...
final SettingBase settingBase = (SettingBase) obj;
final PermissionsState permissionsState = settingBase.getPermissionsState();
if (permissionsState.hasPermission(permName, userId)) {
if (isUidInstantApp) {
BasePermission bp = mSettings.mPermissions.get(permName);
if (bp != null && bp.isInstant()) {
return PackageManager.PERMISSION_GRANTED;
}
} else {
return PackageManager.PERMISSION_GRANTED;
}
}
...
} else {
ArraySet<String> perms = mSystemPermissions.get(uid);
if (perms != null) {
if (perms.contains(permName)) {
return PackageManager.PERMISSION_GRANTED;
}
...
}
}
}
return PackageManager.PERMISSION_DENIED;
}
可以注意到,6.0 之後 checkPermission() 方法有所改變。多了從 mSettings.mPermissions 去查詢權限列表。關鍵就在於這個 mSettings 裡面保存的這個 SettingBase 對象,它記錄了 PermissionsState 也就是權限的授予情況。
public boolean hasPermission(String name, int userId) {
enforceValidUserId(userId);
if (mPermissions == null) {
return false;
}
PermissionData permissionData = mPermissions.get(name);
return permissionData != null && permissionData.isGranted(userId);
}
所以檢查權限的流程是本來就有的,6.0 之後差異僅在於:危險級別權限可以動態修改授權情況,也就是修改 PermissionState 的 mGranted 值,所以每次權限執行,都會查詢下 mGranted 值。
靜態權限執行
靜態權限執行的典型場景,是跨應用組件交互。我們使用隱式 Intent 來表達意圖,搜索匹配的組件,如果有多個,彈出選擇框,目標組件被選定後,會由 ActivityManagerService 執行權限檢查,檢查目標組件是否有相應的權限要求,如果有,則把權限檢查的工作交給 PMS,去檢查調用者有沒有被授權這些權限。
接下來的總體的流程和動態執行流程大致相同:Binder.getCallingUid()和Binder.getCallingPid()獲取調用者的 UID 和 PID,然後利用 UID 映射包名,再獲得相關權限集合。如果權限集合中含有所需權限即啟動,否則拋出 SecurityException 異常。靜態權限執行這裡,我們可以詳細了解下,每種組件的權限檢查時機和具體順序是怎麼樣的。
組件權限執行
思考一下,什麼時候會執行對調用者的權限檢查?那肯定是在目標組件被調用的時候,去解析目標組件聲明的權限,如果有,就執行權限檢查。
Activity 和 Service。Activity 顯而易見,會在 startActivity() 和 startActivityForResult() 裡解析到聲明權限的 Activity 時,就執行權限檢查。而 Service startService()、stopService() 和 bindService(),這 3 個方法被調用時都會進行權限檢查。
廣播。我們注意到,發送廣播除了常用的 sendBroadcast(Intent intent),還有個 sendBroadcast(Intent intent, String receiverPermission),該方法可以要求廣播接受者具備特定的權限,但是,調用 sendBroadcast 是不會進行權限檢查的,因為廣播是異步的,所以權限檢查會在 intent 傳遞到已註冊的廣播接受者時進行,如果接收者不具備特定的權限,則不會接收到該廣播,也不會收到 SecurityException 異常。
反過來,接收者可以要求廣播發送者必須具備的權限,所要求的權限在 manifest 文件中設置 <receiver> 標籤的 permission 屬性,或者動態註冊時指定 registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler),權限檢查也是在廣播傳遞時執行。
所以,收發廣播可以分開指定權限。值得一提的是,一些系統廣播被聲明為 protected,並且只能由系統進程發送,比如 PACKAGE_INSTALLED。只能由系統進程發送,這個限制會在內核層進行檢查,對調用者的 UID 進行匹配,只能是 SYSTEM_UID、PHONE_UID、SHELL_UID、BLUETOOTH_UID 或 root。如果其他 UID 的進程試圖發送系統廣播,則會收到 SecurityException 異常。
想了解所有的系統廣播,可以打開/system/framework/framework-res.apk 中的 AndroidManifest.xml <protected-broadcast> 標籤詳細了解。ContentProvider。ContentProvider 可以為讀寫分別指定不同的權限,即:調用目標 provider、query() 方法 和 insert()、update()、delete() 都會進行權限檢查。
綜上所述,Android 的權限的檢查會在各個層次上實施。
高層的組件,例如應用和系統服務,通過包管理器查詢應用程式被賦予的權限,並決定是否準予訪問。
低層的組件,通常不訪問包管理器,比如本地守護進程,依賴於進程的 UID、GID 和補充 GID 來決定賦予。
訪問系統資源時,如設備文件、UNIX 域套接字和網絡套接字,則由內核根據所有者、目標資源的訪問權限和訪問進程的進程屬性或者 packages.list 來進行控制。
最後簡單說下共享 UID,填一下前面挖的坑。雖說 Android 會為每一個應用分配唯一的 UID,但如果應用使用相同的密鑰籤發,就可以使用相同 UID 運行,也就是運行在同一個進程中。
這個特性被系統應用和核心框架服務廣泛使用,比如:Google Play 和 Google 定位服務,請求同一進程內的 Google 登錄服務,從而達到靜默自動同步用戶數據的體驗。
值得注意的是:Android 不支持將一個已安裝的應用,從非共享 UID 切換到共享狀態,因為改變了已安裝應用的 UID,會導致應用失去對自己文件的訪問權限(在一些早期 Android 版本中),所以如果使用共享 UID 必須從一開始就設計好。