詳細分析Android權限機制實現,分析APP組件、Android框架層、系統服務、原生守護進程的權限控制實現
Android APP運行在受限沙箱內,為了完成與其它APP或系統的交互,需要申請額外權限。權限在APP安裝時被授權給應用,且在APP生命周期內保持不變。權限可以被映射為Linux補充GID,用於內核在授權訪問系統資源時進行權限檢查。
Binder文中講過,APP與其它APP、系統服務間通信使用Binder的IPC機制。服務端可以通過getCallingUid()獲取APP的UID,並通過查找包管理器數據庫中保存的權限,來執行權限檢查。
與組件關聯的權限,在APP的Manifest文件中聲明,並由系統自動執行,但是APP也可以選擇額外的動態檢查。除了使用內置權限,APP還可以自定義權限,並將它們與組件相關聯,從而用於組件的訪問控制。
一、APP權限申請與管理
APP在AndroidManifest.xml文件中,使用 標簽來申請權限,通過 標簽來自定義權限。
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tianlutech.ebus"
platformBuildVersionCode="21"
platformBuildVersionName="5.0-1521886">
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS"/>
--snip--
</manifest>
上述Manifest的權限申請中,其實隱含了兩處關鍵信息:
1)權限分類
Android權限命名,是包名+permission字符串+具體權限名的形式。其中包名是android的代表系統內置權限,如上面的android.permission.READ_SMS權限。而非android包定義的則是自定義權限。自定義權限包括系統預裝及用戶安裝應用定義的權限,比如上面com.android.launcher.permission.READ_SETTINGS 權限是launcher系統應用的自定義權限。
所以Android的權限有兩種:內置權限和自定義權限
2)權限保護級別
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
(訪問Accounts Service服務中的帳戶列表)
<uses-permission android:name="android.permission.READ_SMS"/>
(讀取短信)
上述2個均為系統內置權限,但我們發現在APP安裝時,GET_ACCOUNTS的權限申請系統會默認允許,而READ_SMS權限則要用戶手動確認。這是因為這兩個權限擁有不同的權限保護級別。
Android系統上有4種不同的權限保護級別,以下分別介紹:
1、normal 級別
這是權限保護級別的默認值,它定義了訪問系統或其它APP的低風險權限。normal級別的權限無需用戶確認,會自動授權。例如上面的GET_ACCOUNTS權限。
2、dangerous 級別
dangerous 級別的權限可以訪問用戶數據或某種程序的控制用戶設備。例如READ_SMS允許應用讀取手機短信,因此在賦予dangerous 的權限前,Android會彈出一個確認對話框顯示請求信息,並由用戶手動確認。
3、signature 級別
signature 級別的權限只會授權給那些與權限聲明者使用相同證書的APP。這是最嚴格的權限級別,因為申請這個權限你得持有APP或平台的簽名私鑰。
4、signatureOrSystem 級別
這是一種折中的方案,權限可被賦予系統鏡像的部分應用或擁有相同簽名的應用。在4.4版本之后,安裝在/system/priv-app/目錄下的應用,才能被賦予這個保護權限。
拿到一個內置權限,如果查看它的保護級別呢?
我們知道Android系統內置權限定義在以android開頭的包中,這些包的集合其實就是平常說的Android 框架層。Android 框架的核心是一組由系統服務共享的類,這些類被打包成JAR文件放到/system/framework/目錄下。到這個目錄下可以發現,除了JAR文件,Android框架還包含了一個framework-res.apk的APK文件。
所有的內置權限,都在APK的AndroidManifest.xml文件中定義,搜索上面申請的2個權限GET_ACCOUNTS和READ_SMS,它們的權限保護級別分別是normal和dangerous,這也是上面一個需要用戶手動確認,一個不需要的原因:
<permission android:description="@string/permdesc_getAccounts"
android:label="@string/permlab_getAccounts"
android:name="android.permission.GET_ACCOUNTS"
android:permissionGroup="android.permission-group.ACCOUNTS"
android:protectionLevel="normal"
/>
<permission android:description="@string/permdesc_readSms"
android:label="@string/permlab_readSms"
android:name="android.permission.READ_SMS"
android:permissionGroup="android.permission-group.MESSAGES"
android:protectionLevel="dangerous"
/>
每個APP權限,是通過一個名叫包管理器的系統服務進行管理的。包管理器維護着一個已安裝APP的核心數據庫,其中包括安裝路徑、版本、簽名證書和每個包的權限,另外還包含了一個所有已定義權限列表。這個核心數據庫以XML文件的形式保存於/data/system/packages.xml。
以網易雲音樂為例,看一下packages.xml中的應用程序條目:
<package name="com.netease.cloudmusic"
codePath="/data/app/com.netease.cloudmusic-1.apk"
nativeLibraryPath="/data/app-lib/com.netease.cloudmusic-1"
flags="1588804" ft="1553ac745d0" it="1553ac75849" ut="1553ac75849"
version="72"
userId="10074">
<sigs count="1">
<cert index="15" key="3082038……" />
</sigs>
<perms>
<item name="android.permission.READ_EXTERNAL_STORAGE" />
<item name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<item name="android.permission.GET_TASKS" />
<item name="com.netease.cloudmusic.permission.OPERATOR_FREE" />
<item name="android.permission.WRITE_EXTERNAL_STORAGE" />
<item name="android.permission.ACCESS_WIFI_STATE" />
<item name="android.permission.ACCESS_COARSE_LOCATION" />
<item name="android.permission.READ_CONTACTS" />
<item name="android.permission.MODIFY_AUDIO_SETTINGS" />
<item name="android.permission.REORDER_TASKS" />
<item name="android.permission.READ_PHONE_STATE" />
<item name="android.permission.SYSTEM_ALERT_WINDOW" />
<item name="android.permission.WRITE_SETTINGS" />
<item name="android.permission.INTERNET" />
<item name="android.permission.BLUETOOTH" />
<item name="android.permission.ACCESS_FINE_LOCATION" />
<item name="android.permission.BLUETOOTH_ADMIN" />
<item name="android.permission.ACCESS_NETWORK_STATE" />
<item name="android.permission.WAKE_LOCK" />
<item name="android.permission.RECORD_AUDIO" />
</perms>
<signing-keyset identifier="3" />
</package>
每個APP包都用 標簽來表示,包含UID(userId屬性)、簽名證書( 標簽)和所授權限( 下的子標簽中)。
使用android.content.pm.PackageManager類的getPackageInfo()方法,即可獲取 標簽下所有信息,比如經常使用的APK簽名校驗。
以上基於包管理器的權限機制,對Android高層組件,如APP、framework和系統服務已經足夠,它們通過包管理器查詢APP被賦予哪些權限,並決定是否允許訪問。
對於低層的組件,如原生守護進程,通常不訪問包管理器,而依賴於進程的UID、GID和補充GID來決定是否允許訪問。而訪問系統資源,如設備文件、UNIX域套接字和網絡套接字,則由內核根據資源所有者、目標資源的訪問權限和訪問進程的UID和GID來控制。
關於上面提到的高層和低層組件,參考下圖Android架構:
二、權限到UID/GID映射及權限執行
前面介紹,低層的組件如原生守護進程,或內核控制的一些系統資源的訪問,其權限控制通常並不訪問包管理器,而是通過UID、GID和補充GID來進行授權。
這就需要將權限映射成相應的UID、GID,並不是所有的權限都會被映射,所以被映射的權限都定義在/etc/permissions/platform.xml文件的 標簽中:
<permissions>
<!-- The following tags are associating low-level group IDs with
permission names. By specifying such a mapping, you are saying
that any application process granted the given permission will
also be running with the given group ID attached to its process,
so it can perform any filesystem (read, write, execute) operations
allowed for that group. -->
<permission name="android.permission.READ_EXTERNAL_STORAGE" >
<group gid="sdcard_r" />
</permission>
--snip--
<!-- The following tags are assigning high-level permissions to specific
user IDs. These are used to allow specific core system users to
perform the given operations with the higher-level framework. For
example, we give a wide variety of permissions to the shell user
since that is the user the adb shell runs under and developers and
others should have a fairly open environment in which to
interact with the system. -->
<assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="media" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="media" />
--snip--
<library name="android.test.runner"
file="/system/framework/android.test.runner.jar" />
<library name="javax.obex"
file="/system/framework/javax.obex.jar"/>
</permissions>
注釋很關鍵,描述了映射原因。這里只有權限名到組名的映射關系,Android系統中沒有/etc/group文件,組名到GID的映射是靜態的,定義在android_filesystem_config.h文件中,可以在aosp源碼中找到,我們以網易雲音樂的READ_EXTERNAL_STORAGE權限為例,如下:
#define AID_SDCARD_R 1028 /* external storage read access */
#define AID_CLAT 1029 /* clat part of nat464 */
#define AID_LOOP_RADIO 1030 /* loop radio devices */
#define AID_MEDIA_DRM 1031 /* MediaDrm plugins */
static const struct android_id_info android_ids[] = {
……
{ "sdcard_r", AID_SDCARD_R, },
{ "clat", AID_CLAT, },
{ "loop_radio", AID_LOOP_RADIO, },
{ "mediadrm", AID_MEDIA_DRM, },
}
通過APP申請的READ_EXTERNAL_STORAGE權限找到對應組名sdcard_r,在android_ids結構中通過組名sdcard_r找到GID為AID_SDCARD_R,查看宏定義GID為1028,因此1028這個GID會作為補充GID賦予雲音樂APP。
可稍作驗證:
root@hammerhead:/ # grep "com.netease.cloudmusic" /data/system/packages.list
grep "com.netease.cloudmusic" /data/system/packages.list
com.netease.cloudmusic 10074 0 /data/data/com.netease.cloudmusic default 3002,3001,3003,1028,1015
雲音樂的包名為com.netease.cloudmusic,UID為10074,補充GID為(3002,3001,3003,1028,1015),發現GID1028在列。
APP、系統服務等高層組件,通過包管理器進行權限控制,用不到上面的進程補充GID。下面以低層組件,內核層、原生守護進程為例,說明補充GID在權限控制中的使用。
1、內核層權限執行
Android系統對普通文件、設備節點文件和本地套接字的訪問控制,和普通Linux系統一樣。但對使用網絡套接字,Android增加了特有的控制機制,那些創建網絡套接字的進程需要屬於inet組。
看一下Android內核中網絡訪問控制的實現(af_inet.c):
static int inet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
--snip--
if (!current_has_network())
return -EACCES;
--snip--
}
#ifdef CONFIG_ANDROID_PARANOID_NETWORK
#include <linux/android_aid.h>
static inline int current_has_network(void)
{
return in_egroup_p(AID_INET) || capable(CAP_NET_RAW);
}
#else
static inline int current_has_network(void)
{
return 1;
}
#endif
那些不屬於AID_INET組(GID 3003,名稱inet)的進程,不會擁有CAP_NET_RAW的權能(允許使用RAW和PACKET套接字),因而會收到一個拒絕服務的錯誤(return -EACCES)。非Android的內核不會定義CONFIG_ANDROID_PARANOID_NETWORK宏,因此沒有這個訪問控制機制。
通過前面權限到GID映射可以知道,申請了INTERNET權限的APP,inet組GID會被自動映射到其補充GID,因此具有了使用網絡套接字的權力。
可以看到內核的權限執行,使用了APP的進程補充GID,非包管理器中記錄的APP權限信息。
2、原生守護進程的權限執行
原生守護進程一般使用UNIX域套接字進行進程間通信,而非Android首先的Binder。UNIX域套接字使用文件系統上的節點來表示,因此可以使用Linux標准的文件系統權限機制進行權限控制。
大多數套接字訪問權限為只允許同用戶/組的進程訪問。而以不同UID和GID運行的客戶端,是無法連接套接字的。系統守護進程的本地套接字由init.rc定義,該文件在init進程創建時使用。
查看用於設備卷管理的守護進程vold在init.rc中的定義:
service vold /system/bin/vold
class core
socket vold stream 0660 root mount
ioprio be 2
vold聲明了一個同樣名為vold,訪問權限為0660的套接字。該套接字屬於root,所屬組為mount。vold守護進程需要以root運行,用以掛載/卸載卷設備,而mount組(AID_ MOUNT,GID 1009)的成員可以通過本地套接字向它發送指令,而無需以root用戶運行,這也減小了root提權的攻擊面。
除此之外,UNIX域套接字還提供了一種更細粒度的權限控制。
UNIX域套接字允許使用SCM_CREDENTIALS控制消息和SO_PEERCRED套接字選項,來傳遞和查詢用戶憑證。和有效UID、GID是Binder事務處理的一部分一樣,與本地套接字相關聯的憑證由內核填寫,從而無法被用戶進程偽造。
看一個例子,vold如何使用套接字客戶端憑證進行細粒度訪問控制:
//CommandListener.cpp
int CommandListener::CryptfsCmd::runCommand(SocketClient *cli,
int argc, char **argv) {
if ((cli->getUid() != 0) && (cli->getUid() != AID_SYSTEM)) {
cli->sendMsg(ResponseCode::CommandNoPermission,
"No permission to run cryptfs commands", false);
return 0;
}
--snip--
}
vold守護進程只允許以root或system運行的客戶端發送加密的容器管理指令。這里UID通過SocketClient->getUid()方法獲得,這個值是由getsocketopt(SO_PEERCRED)從客戶端獲得並初始化,這跟Binder中服務端實現的權限控制十分類似:
/* SocketClient.cpp */
void SocketClient::init(int socket, bool owned, bool useCmdNum) {
--snip--
struct ucred creds;
socklen_t szCreds = sizeof(creds);
memset(&creds, 0, szCreds);
int err = getsockopt(socket, SOL_SOCKET, SO_PEERCRED, &creds, &szCreds);
if (err == 0) {
mPid = creds.pid;
mUid = creds.uid;
mGid = creds.gid;
}
}
同時,本地套接字的連接功能封裝在了android.net.LocalSocket類中,可以被JAVA應用調用,允許高層系統服務和本地守護進程間進行通信,而無需使用JNI。例如,MountService框架類使用LocalSocket向vold進程發送指令。
三、APP 組件權限控制
基於包管理器的權限檢查,貌似也沒什么好說的。只簡單總結一下。
1、Activity
如果傳遞到Context.startActivity()或startActivityForResult()方法的intent解析到一個聲明權限的Activity時,就需要進行activity權限檢查。如果調用者未申請權限,則拋出SecurityException異常。
2、Service
與Activity一樣,只是權限檢查時機不同,Service發生在Context.startService()、stopService()和bindService()方法調用時。
3、Broadcast(廣播)
發送一個廣播時,APP可以使用Context.sendBroadcast(Intent intent, String receiverPermission)方法,來要求接收者權限。接收者需要的權限,可以在manifest文件中指定,也可以在動態注冊廣播時指定。廣播是異步的,因此權限執行發生在廣播接收器接收到廣播時。
與Activity和Service不同,廣播的權限檢查可以是雙向的,也就是廣播接收者同樣可以要求廣播發送者擁有某個特定權限,以便鎖定發送者。
4、Content Provider
Content Provider的權限執行是最復雜的。可以保護整個組件或特定的導出URI,因此擁有更細的權限控制粒度。並且可以分別為讀寫指定不同的權限。
如果為讀寫分別指定了不同權限,那讀權限控制誰可以調用目標Provider或URI的ContentResolver.query()方法,寫權限控制誰可以對目標Provider或某個暴露的URI調用ContentResolver.insert()、update()和delete()方法。當某個方法被調用時,權限檢查同步執行。
雖然權限檢查的時機不同、粒度不同,但組件權限檢查的原理都是前面講的基於包管理器的權限管理機制。