Android 安全機制

l_serein發表於2012-05-03

1 Android 安全機制概述

Android 是一個許可權分離的系統 。 這是利用 Linux 已有的許可權管理機制,通過為每一個 Application 分配不同的 uid 和 gid , 從而使得不同的 Application 之間的私有資料和訪問( native 以及 java 層通過這種 sandbox 機制,都可以)達到隔離的目的 。 與此 同時, Android 還 在此基礎上進行擴充套件,提供了 permission 機制,它主要是用來對 Application 可以執行的某些具體操作進行許可權細分和訪問控制,同時提供了 per-URI permission 機制,用來提供對某些特定的資料塊進行 ad-hoc 方式的訪問。

1.1 uid 、 gid 、 gids

Android 的許可權分離的基礎是建立在 Linux 已有的 uid 、 gid 、 gids 基礎上的 。

UID 。 Android 在 安裝一個應用程式,就會為 它 分配一個 uid (參考 PackageManagerService 中的 newUserLP 實現)。其中普通 A ndroid 應用程式的 uid 是從 10000 開始分配 (參見 Process.FIRST_APPLICATION_UID ), 10000 以下是系統程式的 uid 。

GID 。對 於普通應用程式來說, gid 等於 uid 。由於每個應用程式的 uid 和 gid 都不相同, 因此不管是 native 層還是 java 層都能夠達到保護私有資料的作用 。

GIDS 。 gids 是由框架在 Application 安裝過程中生成,與 Application 申請的具體許可權相關。 如果 Application 申請的相應的 permission 被 granted ,而且 中有對應的 gid s , 那麼 這個 Application 的 gids 中將 包含這個 gid s 。

uid gid gids 的 詳細 設定過程:

請參考 Act i vityManagerService 中的 startProcessLocked 。在通過 zygote 來啟動一個 process 時,直接將 uid 傳給 給了 gid 。再通過zygote 來 fork 出新的程式( zygote.java 中的 forkAndSpecialize ),最終在 native 層( dalvik_system_zygote.c )中的forkAndSpecializeCommon 中通過 linux 系統呼叫來進行 gid 和 uid 和 gids 的設定。

1.2 permission

一個許可權主要包含三個方面的資訊:許可權的名稱;屬於的許可權組;保護級別。一個許可權組是指把許可權按照功能分成的不同的集合。每一個許可權組包含若干具體許可權,例如在 COST_MONEY 組中包含 android.permission.SEND_SMS , android.permission.CALL_PHONE 等和費用相關的許可權。

每個許可權通過 protectionLevel 來標識保護級別: normal , dangerous , signature , signatureorsystem 。不同的保護級別代表了程式要使用此許可權時的認證方式。 normal 的許可權只要申請了就可以使用; dangerous 的許可權在安裝時需要使用者確認才可以使用; signature 和 signatureorsystem 的許可權需要使用者的 app 和系統使用同一個數字證照。

Package 的許可權資訊主要 通過在 AndroidManifest.xml 中通過一些標籤來指定。如 <permission> 標籤, <permission-group> 標籤 <permission-tree> 等標籤。如果 package 需要申請使用某個許可權,那麼需要使用 <use-permission> 標籤來指定。

Android permission 管理機制

2.1 Framework permission 機制

2.1.1 安裝入口

permission 的初始化,是指 permission 的向系統申請,系統進行檢測並授權,並建立相應的資料結構。絕大多數的情況下 permission 都是從一個 package 中掃描所得,而這發生在 package 安裝和升級的時候。一般有如下幾種 安裝入口:

n packageInstaller , package 被下載安裝時會觸發使用。 packageInstaller 會通過 AppSecurityPermissions 來檢查 dangerous 的許可權,並對使用者給出提示。

n pm 命令 。

n adb install 。最終還是 呼叫 pm install 來安裝 apk 包。

n 拷貝即安裝。 PackageManagerService 中使用 AppDirObserver 對 /data/app/ 進行監視 ,如果有拷貝即觸發安裝。

這些安裝方式 最終都會通過呼叫 PackageManagerService 中的函式來完成程式的安裝。

2.1.2 permission 建立

第一步,從 AndroidManifest.xml 中提取 permission 資訊。主要提取如下資訊:

² shared uid

指定與其它 package 共享同一個 uid 。

² permission

提取 permissions 標籤指定屬性。它使用 permissionInfo 來描述一個許可權的基本資訊。需要指定 protectedLevel 資訊,並指定所屬 group資訊。它將被新增到這個 package 的 permissions 這個 list 結構中。

² permission-tree

提取 permissions-tree 標籤屬性。 permissions-tree 也通過 permissionInfo 來描述,並被新增到 package 的 permissions 這個 list 結構中。 permission-tree 只是一個名字空間,用來向其中動態新增一些所謂 Dynamic 的 permission ,這些 permission 可以動態修改。這些permission 名稱要以 permission-tree 的名稱開頭。它本身不是一種許可權,沒有 protectedLevel 和所屬 group 。只是儲存了所屬的 packge和許可權名(帶有 package 字首的)。

² permission-group

定義 permission 組資訊,用 PermissionGroup 表示。本身不代表一個許可權,會新增進入 package 的 permissionGroups 這個 list 中。

² uses-permission

定義了 package 需要申請的許可權名。將許可權名新增到 package 的 requestedPermissions 這個 list 中。

² adopt-permissions

將該標籤指定的 name 存入 package 的 mAdoptPermissions 這個 list 中。 Name 指定了這個 package 需要從 name 指定的 package 進行許可權領養。在 system package 進行升級時使用。

第二步。獲取 Package 中的證照,驗證,並將簽名資訊儲存在 Package 結構中。

1. 如果該 package 來自 system img (系統 app ),那麼只需要從該 Package 的 AndroidManifest.xml 中獲取簽名資訊,而無需驗證其完整性。但是如果這個 package 與其它 package 共享一個 uid ,那麼這個共享 uid 對應的 sharedUser 中儲存的簽名與之不一致,那麼簽名驗證失敗。

2. 如果是普通的 package ,那麼需要提取證照和簽名資訊,並對檔案的完成性進行驗證。

第三步。如果是普通的 package ,那麼清除 package 的 mAdoptPermissions 欄位資訊(系統 package 升級才使用)。

第四步。如果在 AndroidManifest.xml 中指定了 shared user ,那麼先檢視全域性 list 中( mSharedUsers )是否該 uid 對應的 SharedUserSetting 資料結構,若沒有則新分配一個 uid ,建立 SharedUserSetting 並儲存到全域性全域性 list ( mSharedUsers )中。

mUserIds 儲存了系統中已經分配的 uid 對應的 SharedUserSetting 結構。每次分配時總是從第一個開始輪詢,找到第一個空閒的位置 i ,然後加上 FIRST_APPLICATION_UID 即可。

第五步。建立 PackageSettings 資料結構。並將 PackageSettings 與 SharedUserSetting 進行繫結。其中 PackageSettings 儲存了 SharedUserSetting 結構;而 SharedUserSetting 中會使用 PackageSettings 中的簽名資訊填充自己內部的簽名資訊,並將 PackageSettings 新增到一個佇列中,表示 PackageSettings 為其中的共享者之一。

在建立時,首先會以 packageName 去全域性資料結構 mPackages 中查詢是否已經有對應的 PackageSettings 資料結構存在。如果已經存在PackageSettings 資料結構(比如這個 package 已經被 uninstall ,但是還沒有刪除資料,此時 package 結構已經被釋放)。那麼比較該package 中的簽名資訊(從 AndroidManifest 中掃描得到)與 PackageSettings 中的簽名資訊是否匹配。如果不匹配但是為 system package ,那麼信任此 package ,並將 package 中的簽名資訊更新到已有的 PackageSettings 中去,同時如果這個 package 與其它package 共享了 uid ,而且 shared uid 中儲存的簽名資訊與當前 package 不符,那麼簽名也驗證失敗。

第六步。如果 mAdoptPermissions 欄位不為空,那麼處理 permission 的領養(從指定的 package 對應的 PackageSettings 中,將許可權的擁有者修改為當前 package ,一般在 system app 升級的時候才發生,在此之前需要驗證當被領養的 package 已經被解除安裝,即檢查 package 資料結構是否存在)。

第七步。新增自定義許可權。將 package 中定義的 permissionGroup 新增到全域性的列表 mPermissionGroups 中去;將 package 中定義的 permissions 新增到全域性的列表中去(如果是 permission-tree 型別,那麼新增到 mSettings.mPermissionTrees ,如果是一般的 permission 新增到 mSettings.mPermissions 中)。

第八步。清除不一致的 permission 資訊。

1. 清除不一致的 permission-tree 資訊。如果該 permission-tree 的 packageSettings 欄位為空,說明還未對該 package 進行過解析(若程式碼執行到此處時 packageSettings 肯定已經被建立過),將其 remove 掉。如果 packageSettings 不為空,但是對應的 package 資料結構為空(說明該 package 已經被解除安裝,但資料還有保留),或者 package 資料結構中根本不含有這個 permission-tree ,那麼將這個permission-tree 清除。

2. 清除不一致的 permission 資訊。如果 packageSettings 或者 package 結構為空(未解析該 package 或者被解除安裝,但資料有保留),或者package 中根本沒有定義該 permission ,那麼將該 permission 清除。

第九步。對每一個 package 進行輪詢,並進行 permission 授權。

1. 對申請的許可權進行檢查,並更新 grantedPermissions 列表

2. 如果其沒有設定 shared user id ,那麼將其 gids 初始化為 mGlobalGids ,它從 permission.xml 中讀取。

3. 遍歷所有申請的許可權,進行如下檢查

1 )如果是該許可權是 normal 或者 dangerous 的。通過檢查。

2 )如果許可權需要簽名驗證。如果簽名驗證通過。還需要進行如下檢查

* 如果程式升級,而且是 system package 。那麼是否授予該許可權要看原來的 package 是否被授予了該許可權。如果被授予了,那麼通過檢查,否則不通過。

* 如果是新安裝的。那麼檢查通過。

4. 如果 3 中檢查通過,那麼將這個 permission 新增到 package 的 grantedPermissions 列表中,表示這個 permission 申請成功( granted)。申請成功的同時會將這個申請到的 permission 的 gids 新增到這個 package 的 gids 中去。

5. 將 permissionsFixed 欄位標準為 ture ,表示這個 packge 的 permission 進行過修正。後續將禁止對非 system 的 app 的許可權進行再次修正。

2.1.3 Dynamic permission 的管理

PackageManagerService 提供了 addPermission/ removePermission 介面用來動態新增和刪除一些許可權。但是這些許可權必須是所謂的動態許可權( BasePermission.TYPE_DYNAMIC )。

一個 Package 如果要新增 Dynamic permissions ,首先必須要在 manifest 中申明 <permission-tree> 標籤,它實際上是一個許可權的名字空間(例如,“ com.foo.far ”這個許可權就是 permission-tree “com.foo ”的成員),本身不是一個許可權。一個 Package 只能為自己的 permission-tree 或者擁有相同的 uid 的 package 新增或者刪除許可權。

Package 不能夠通過這種介面去修改在 manifest 中靜態申請的許可權,否則丟擲異常。

首先查詢這個 permission 在全域性 permission 列表 mSettings.mPermissions 中是否存在。如果存在,而且型別為BasePermission.TYPE_DYNAMIC 那麼根據傳入的許可權資訊修改全域性表中的許可權資訊,並觸發 permissions.xml 的持久化。

如果在全域性的 permission 列表 mSettings.mPermissions 中沒有找到,先找到這個 permission 所在 permissionTree ,然後新增到全域性permission 列表 mSettings.mPermissions 中去,並觸發 permissions.xml 的持久化。

2.1.4 Uri permission 的管理

下面兩個 介面 主要用於 Uri permission 的管理 (其實現在 ActivityManagerService 中)。

// 為指定的 uid 和 targetPkg 新增對某個 content Uri 的讀或者寫許可權。

public void grantUriPermission(IApplicationThread caller, String targetPkg, Uri uri, int mode) throws RemoteException;

// 清除所有通過 grantUriPermission 對某個 Uri 授予的許可權。

public void revokeUriPermission(IApplicationThread caller, Uri uri, int mode) throws RemoteException;

grantUriPermission 主要的實現過程分析。

grantUriPermission 分析:

1. 驗證 caller 的 ProcessRecord 和 targetPkg 不為空。否則檢測不通過。

2. 驗證所請求的 mode 為 Intent.FLAG_GRANT_READ_URI_PERMISSION 或者為 Intent.FLAG_GRANT_WRITE_URI_PERMISSION ,否則不通過。

3. 確保引數 Uri 是一個 content Uri 。否則,則檢測不通過。

4. 通過 Uri 得到目標 ContentProvider ,如果不存在,則檢測不通過。

5. 從 PackageManagerService 中獲得 targetPkg 對應的 uid 。

6. 檢查 target uid 所對應的 package 是否真正需要這個許可權?

先判斷要申請的是讀還是寫許可權,然後檢視對應的 ContentProvider 中對應的 readPermission writePermission 欄位是否儲存了許可權名稱。如果該欄位不為空,則以 target uid 和該許可權名去PackageManagerService 中去查詢該 uid 是否被 granted 了該許可權。如果已經獲得了該許可權,那麼無需再去為這個 Activity 去申請這個 Uri 許可權了,返回。否者繼續執行如下操作。

7. 檢查這個 ContentProvider 的 grantUriPermissions 開關變數,是否允許對其它 package 進行許可權的 grant 操作。如果禁止,那麼丟擲異常。

8. 檢查這個 ContentProvider 是否設定了 Uri 的過濾型別 uriPermissionPatterns ,如果設定了過濾型別,則將需要申請許可權的 Uri 與之匹配。匹配不同過,則丟擲異常。

9. 檢查呼叫者自己是否有許可權訪問這個 Uri 。如果沒有,丟擲異常。

10. 從 mGrantedUriPermissions 中取得 target uid 對應的 HashMap<Uri, UriPermission> 資料結構。用 target uid 和 Uri 生成UriPermission 並儲存在 mGrantedUriPermissions 中。

revokeUriPermission 實現分析。

找到該 Uri 對應的 ContentProvider ,然後刪除 mGrantedUriPermissions 中與 Uri 對應的所有許可權。

2.2 permission 的動態檢查

這裡的動態檢查是指是 package 在程式執行過程中進行某些操作或者資料訪問時才進行的 check ,與之對應的是應用程式安裝或者升級時 PackageManagerService 通過掃描包中的靜態許可權資訊相對應。

系統與許可權 檢查 相關的機制的實現主要集中在 PackageManagerService 和 ActivityManagerService 中。 ActivityManagerService 主要負責的是底層的 uid 層次的身份檢查; PackageManagerService 則維護了 uid 到自己擁有的和被授予的許可權的一張表。在通過 ActivityManagerService 的身份檢查後, PackageManagerService 根據請求者的 uid 來檢視這張表,判斷其是否具有相應的許可權。

除此之外, per-URI permission 機制的實現也需要一張表,它維護在 ActivityManagerService 中,它建立了從 content URI 到被授權訪問這個 URI 的 component 之間的對映。但是它也需要藉助 PackageManagerService 的機制來輔助實現。

2.2.1 framework 提供的介面

Android framework 中提供了一些介面用來對外來的訪問(包括自己)進行許可權檢查 。 這些介面 主要通過 ContextWrapper 提供,具體實現在 ContextImpl 中 。如果 package 接受到外來訪問者的操作請求,那麼可以呼叫這些介面進行許可權檢查。一般情況下可以把這些介面的檢查介面分為兩種,一種是返回錯誤,另一種是丟擲異常。

主要包含如下幾組:

n permission 和 uid 檢查 API

下面這一組介面主要用來檢查某個呼叫(或者是其它 package 或者是自己)是否擁有訪問某個 permission 的許可權。引數中 pid 和 uid 可以指定,如果沒有指定,那麼 framework 會通過 Binder 來獲取呼叫者的 uid 和 pid 資訊,加以填充。返回值為 PackageManager.PERMISSION_GRANTED 或者 PackageManager.PERMISSION_DENIED 。

public int checkPermission(String permission, int pid, int uid) // 檢查某個 uid 和 pid 是否有 permission 許可權

public int checkCallingPermission(String permission) // 檢查呼叫者是否有 permission 許可權,如果呼叫者是自己那麼返回PackageManager.PERMISSION_DENIED

public int checkCallingOrSelfPermission(String permission) // 檢查自己或者其它呼叫者是否有 permission 許可權

下面這一組和上面類似,如果遇到檢查不通過時,會丟擲異常,列印訊息 。

public void enforcePermission(String permission, int pid, int uid, String message)

public void enforceCallingPermission(String permission, String message)

public void enforceCallingOrSelfPermission(String permission, String message)

n per-URI 檢查 API

為某個 package 新增訪問 content Uri 的讀或者寫許可權。

public void grantUriPermission(String toPackage, Uri uri, int modeFlags)

public void revokeUriPermission(Uri uri, int modeFlags)

檢查某個 pid 和 uid 的 package 是否擁有 uri 的讀寫許可權,返回值表示是否被 granted 。

public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags)

public int checkCallingUriPermission(Uri uri, int modeFlags)

public int checkCallingOrSelfUriPermission(Uri uri, int modeFlags)

public int checkUriPermission(Uri uri, String readPermission,String writePermission, int pid, int uid, int modeFlags)

檢查某個 pid 和 uid 的 package 是否擁有 uri 的讀寫許可權,如果失敗則丟擲異常,列印訊息 。

public void enforceUriPermission(Uri uri, int pid, int uid, int modeFlags, String message)

public void enforceCallingUriPermission(Uri uri, int modeFlags, String message)

public void enforceCallingOrSelfUriPermission(Uri uri, int modeFlags, String message)

public void enforceUriPermission(Uri uri, String readPermission, String writePermission,int pid, int uid, int modeFlags, String message)

2.2.2 實現分析

ContextImpl.java 中提供的 API ,其實都是由 ActivityManagerService 中的如下幾個介面進行的封裝。

public int checkPermission(String permission, int pid, int uid) throws RemoteException; // 主要用於一般的 permission 檢查

public int checkUriPermission(Uri uri, int pid, int uid, int mode) throws RemoteException; // 主要用於 Content Uri 的 permission 檢查

n checkPermission 的實現分析

1. 如果傳入的 permission 名稱為 null ,那麼返回 PackageManager.PERMISSION_DENIED 。

2. 判斷呼叫者 uid 是否符合要求 。

1 ) 如果 uid 為 0 ,說明是 root 許可權的程式,對許可權不作控制。

2 ) 如果 uid 為 system server 程式的 uid ,說明是 system server ,對許可權不作控制。

3 ) 如果是 ActivityManager 程式本身,對許可權不作控制。

4 )如果呼叫者 uid 與引數傳入的 req uid 不一致,那麼返回 PackageManager.PERMISSION_DENIED 。

3. 如果通過 2 的檢查後,再 呼叫 PackageManagerService.checkUidPermission ,判斷 這個 uid 是否擁有相應的許可權,分析如下 。

1 ) 首先它通過呼叫 getUserIdLP ,去 PackageManagerService.Setting.mUserIds 陣列中,根據 uid 查詢 uid (也就是 package )的許可權列表。一旦找到,就表示有相應的許可權。

2 ) 如果沒有找到,那麼再去 PackageManagerService.mSystemPermissions 中找。這些資訊是啟動時,從/system/etc/permissions/platform.xml 中讀取的。這裡記錄了一些系統級的應用的 uid 對應的 permission 。

3 )返回結果 。

n 同樣 checkUriPermission 的實現 主要在 ActivityManagerService 中,分析如下:

1. 如果 uid 為 0 ,說明是 root 使用者,那麼不控制許可權。

2. 否則,在 ActivityManagerService 維護的 mGrantedUriPermissions 這個表中查詢這個 uid 是否含有這個許可權,如果有再檢查其請求的是讀還是寫許可權。

3 Android 簽名機制

關於簽名機制,其實分兩個階段。

包掃描階段需要進行完整性和證照的驗證。普通 package 的簽名和證照是必須要先經過驗證的。具體做法是對 manifest 下面的幾個檔案進行完整性檢查。完整性檢查包括這個 jar 包中的所有檔案。如果是系統 package 的話,只需要使用 AndroidMenifest.xml 這個檔案去提取簽名和驗證資訊就可以了。

在許可權建立階段。如果該 package 來自 system img (系統 app ),那麼 trust it ,而且使用新的簽名資訊去替換就的資訊。前提是如果這個 package 與其它 package 共享一個 uid ,那麼這個共享 uid 對應的 sharedUser 中儲存的簽名與之不一致,那麼簽名驗證失敗。有些時候系解除安裝一個 app ,但是不刪除資料,那麼其 PackageSettings 資訊會保留,其中會儲存簽名資訊。這樣再安裝是就會出現不一致。

3.1 Android Package 簽名原理

android 中系統和 app 都是需要簽名的。可以自己通過 development/tools/make_key 來生成公鑰和私鑰。

android 原始碼中提供了工具 ./out/host/linux-x86/framework/signapk.jar 來進行手動簽名。簽名的主要作用在於限制對於程式的修改僅限於同一來源。系統中主要有兩個地方會檢查。如果是程式升級的安裝,則要檢查新舊程式的簽名證照是否一致,如果不一致則會安裝失敗;對於申請許可權的 protectedlevel 為 signature 或者 signatureorsystem 的,會檢查許可權申請者和許可權宣告者的證照是否是一致的。簽名相關檔案可以從 apk 包中的 META-INF 目錄下找到。

signapk.jar 的原始碼在 build/tools/signapk ,簽名主要有以下幾步:

l 將除去 CERT.RSA , CERT.SF , MANIFEST.MF 的所有檔案生成 SHA1 簽名

首先將除了 CERT.RSA , CERT.SF , MANIFEST.MF 之外的所有非目錄檔案分別用 SHA-1 計算摘要資訊,然後使用 base64 進行編碼,存入MANIFEST.MF 中。 如果 MANIFEST.MF 不存在,則需要建立。存放格式是 entry name 以及對應的摘要

l 根據 之前計算的 SHA1 摘要資訊,以及 私鑰生成 一系列的 signature 並寫入 CERT.SF

對 整個 MANIFEST.MF 進行 SHA1 計算,並將摘要資訊存入 CERT.SF 中 。然後對之前計算的所有摘要資訊使用 SHA1 再次計算數字簽名,並寫入 CERT.SF 中。

l 把公鑰和簽名資訊寫入 CERT.RST

把之前整個的簽名輸出檔案 使用私有金鑰計算簽名。同時將簽名結果,以及之前聲稱的公鑰資訊寫入 CERT.RSA 中儲存。

3.2 Package 的簽名驗證

安裝時對一個 package 的簽名驗證的主要邏輯在 JarVerifier.java 檔案的 verifyCertificate 函式中實現。 其主要的思路是通過提取 cert.rsa 中的證照和簽名資訊,獲取簽名演算法等資訊,然後按照之前對 apk 簽名的方法進行計算,比較得到的簽名和摘要資訊與 apk 中儲存的匹配。

第一步。提取證照資訊,並對 cert.sf 進行完整性驗證。

1. 先找到是否有 DSA 和 RSA 檔案 ,如果找到則對其進行 decode ,然後讀取其中的所有的證照列表(這些證照會被儲存在 Package 資訊中,供後續使用)。

2. 讀取這個檔案中的簽名資料資訊塊列表,只取第一個簽名資料塊。讀取其中的釋出者和證照序列號。

3. 根據證照序列號,去匹配之前得到的所有證照,找到與之匹配的證照。

4. 從之前得到的簽名資料塊中讀取簽名演算法和編碼方式等資訊

5. 讀取 cert.sf 檔案,並計算整個的簽名,與資料塊中的簽名(編碼格式的)進行比較,如果相同則完整性校驗成功。

第二步。使用 cert.sf 中的摘要資訊,驗證 MANIFEST.MF 的完整性。

在 cert.sf 中提取 SHA1-Digest-Manifest 或者 SHA1-Digest 開頭的簽名 資料塊 ( -Digest-Manifest 這個是整個 MANIFEST.MF 的摘要 資訊,其它的是 jar 包中其它檔案的摘要資訊 ), 並逐個對這些資料塊 進行驗證。驗證的方法是,現將 cert.sf 看做是很多的 entries ,每個entries 包含了一些基本資訊,如這個 entry 中使用的摘要演算法( SHA1 等),對 jar 包中的哪個檔案計算了摘要,摘要結果是什麼。 處理時先找到每個摘要資料開中的檔案資訊,然後從 jar 包中讀取,然後使用 -Digest 之前的摘要演算法進行計算,如果計算結果與摘要資料塊中儲存的資訊的相匹配,那麼就完成驗證。

相關文章