5.2.3 權限和保護級别 進階話題
原書: Android Application Secure Design/Secure Coding Guidebook 譯者: 飛龍 協定: CC BY-NC-SA 4.0
5.2.3.1 繞過自定義簽名許可的 Android 作業系統特性及其對策
自定義簽名權限是一種權限,實作使用相同開發人員密鑰簽名的應用之間的應用間通信。 由于開發人員密鑰是私鑰,不能公開,是以隻有在内部應用互相通信的情況下,才有權使用簽名權限進行保護。
首先,我們将描述在 Android 的開發者指南(
http://developer.android.com/guide/topics/security/security.html)中解釋的自定義簽名權限的基本用法。 但是,後面将解釋,存在繞過許可方面的問題。 是以,本指南中描述的對策是必要的。
以下是自定義簽名權限的基本用法。
在提供方應用的
AndroidManifest.xml
中定義内部簽名權限。(權限定義)
例如:
<permission android:name="xxx" android:protectionLevel="signature" />
AndroidManifest.xml
中,使用要保護的元件的權限屬性強制執行權限。 (執行權限)
<activity android:permission="xxx" ... >...</activity>
在每個使用者方應用的
AndroidManifest.xml
中,使用
uses-permission
标簽聲明内部定義的簽名權限,來通路要保護的元件。 (使用權限聲明)
<uses-permission android:name="xxx" />
使用相同的開發人員密鑰,對所有互相通信的應用的 APK 進行簽名。
實際上,如果滿足以下條件,這種方法會存在漏洞,可以繞過簽名權限。
為了便于說明,我們将受自定義簽名權限保護的應用稱為
ProtectedApp
,并且
AttackerApp
是已由不同于
ProtectedApp
的開發人員密鑰簽名的應用。 繞過簽名權限的漏洞意味着,即使
AttackerApp
的簽名不比對,也有可能通路
ProtectedApp
的元件。
條件 1:
AttackerApp
也定義了正常權限,與
ProtectedApp
所定義的簽名權限名稱相同(嚴格來說,簽名權限也是可以接受的)。
<permission android:name=" xxx" android:protectionLevel="normal" />
條件 2:
AttackerApp
使用
uses-permission
聲明了自定義的正常權限。
<uses-permission android:name="xxx" />
條件 3:
AttackerApp
安裝在
ProtectedApp
之前。
滿足條件 1 和條件 2 所需的權限名稱,很容易從 APK
AndroidManifest.xml
檔案中取出,被攻擊者知道。 攻擊者也可以用一定的努力滿足條件 3(例如欺騙使用者)。
如果隻采用基本用法,就有自定義簽名權限的繞過風險,需要采取防範此類漏洞的對策。 具體而言,你可以通過使用“5.2.2.4 驗證内部定義的簽名權限是否由内部應用定義”中描述的方法來發現如何解決上述問題。
5.2.3.2 使用者僞造的 AndroidManifest.xml
AndroidManifest.xml
我們已經談到,自定義權限的保護級别可能會被改變。 為了防止由于這種情況導緻的故障,需要在 Java 的源代碼一側實施某些對策。 從
AndroidManifest.xml
僞造的角度來看,我們将讨論在源代碼方面要采取的對策。 我們将示範一個可以檢測僞造的簡單安裝案例。 但請注意,對于出于犯罪目的而僞造的專業黑客來說,這些對策效果甚微。
這部分内容關于應用僞造和惡意使用者。 盡管這本來不屬于指導手冊的範圍,但由于這與權限有關,并且這種僞造的工具作為 Android 應用公開提供,是以我們決定将其稱為“針對業餘黑客的簡單對策”。
必須記住的是,可以從市場安裝的應用,是可以在沒有 root 權限的情況下,被僞造的應用。原因是應用可以重建和簽署
AndroidManifest.xml
檔案。通過使用這些應用,任何人都可以删除已安裝應用的任何權限。
舉個例子,似乎有些情況下重建的 APK 具有不同的簽名,
AndroidManifest.xml
發生改變,并删除了
INTERNET
權限,來使應用中附加的廣告子產品失效。有些使用者稱贊這些類型的工具,因為任何個人資訊沒有被洩漏到任何地方。由于這些附加在應用中的廣告停止運作,此類行為會對依靠廣告收入的開發者造成金錢損失。而且相信大多數使用者沒有任何反感。
在下面的代碼中,我們展示了一個實作的執行個體,一個使用
uses-permission
聲明了
INTERNET
權限的應用,驗證
INTERNET
權限是否在運作時在
AndroidManifest.xml
檔案中描述。
public class CheckPermissionActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Acquire Permission defined in AndroidManifest.xml
List<String> list = getDefinedPermissionList();
// Detect falsification
if( checkPermissions(list) ){
// OK
Log.d("dbg", "OK.");
}else{
Log.d("dbg", "manifest file is stale.");
finish();
}
}
/**
* Acquire Permission through list that was defined in AndroidManifest.xml
* @return
*/
private List<String> getDefinedPermissionList(){
List<String> list = new ArrayList<String>();
list.add("android.permission.INTERNET");
return list;
}
/**
* Verify that Permission has not been changed Permission
* @param permissionList
* @return
*/
private boolean checkPermissions(List<String> permissionList){
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(
getPackageName(), PackageManager.GET_PERMISSIONS);
String[] permissionArray = packageInfo.requestedPermissions;
if (permissionArray != null) {
for (String permission : permissionArray) {
if(! permissionList.remove(permission)){
// Unintended Permission has been added
return false;
}
}
}
if(permissionList.size() == 0){
// OK
return true;
}
} catch (NameNotFoundException e) { }
return false;
}
}
5.2.3.3 APK 僞造的檢測
我們在“5.2.3.2 使用者僞造的
AndroidManifest.xml
”中,解釋了使用者對權限的僞造檢測。但是,應用僞造并不僅限于權限,在許多其他情況下,應用在沒有任何源代碼更改的情況下被占用。例如,隻是通過将資源替換為自己的應用,他們将其他開發人員的應用(僞造)分發到市場中,就好像它們是自己的應用一樣。在這裡,我們将展示一個更通用的方法,來檢測 APK 檔案的僞造。
為了僞造 APK,需要将 APK 檔案解碼為檔案夾和檔案,修改其内容,然後将其重建為新的 APK 檔案。由于僞造者沒有原始開發者的密鑰,他必須用他自己的鑰匙簽署新的 APK 檔案。由于 APK 的僞造不可避免地會産生簽名(證書)的變化,是以可以通過比較 APK 中的證書,和源代碼中嵌入的開發人員證書,在運作時檢測 APK 是否被僞造。
以下是示例代碼。另外,如果使用這個實作示例,專業黑客将能夠輕松繞過僞造檢測。請注意這是一個簡單的實作示例,請将此示例代碼應用于你的應用。
要點:
- 在開始主要操作之前,驗證應用的證書是否屬于開發人員。
SignatureCheckActivity.java
package org.jssec.android.permission.signcheckactivity;
import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.Utils;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;
public class SignatureCheckActivity extends Activity {
// Self signed certificate hash value
private static String sMyCertHash = null;
private static String myCertHash(Context context) {
if (sMyCertHash == null) {
if (Utils.isDebuggable(context)) {
// Certificate hash value of "androiddebugkey" of debug.
sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
} else {
// Certificate hash value of "my company key" of keystore
sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
}
}
return sMyCertHash;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// *** POINT 1 *** Verify that an application's certificate belongs to the developer before major processing is started
if (!PkgCert.test(this, this.getPackageName(), myCertHash(this))) {
Toast.makeText(this, "Self-sign match NG", Toast.LENGTH_LONG).show();
finish();
return;
}
Toast.makeText(this, "Self-sign match OK", Toast.LENGTH_LONG).show();
}
}
PkgCert.java
package org.jssec.android.shared;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
public class PkgCert {
public static boolean test(Context ctx, String pkgname, String correctHash) {
if (correctHash == null) return false;
correctHash = correctHash.replaceAll(" ", "");
return correctHash.equals(hash(ctx, pkgname));
}
public static String hash(Context ctx, String pkgname) {
if (pkgname == null) return null;
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
if (pkginfo.signatures.length != 1) return null; // Will not handle multiple signatures.
Signature sig = pkginfo.signatures[0];
byte[] cert = sig.toByteArray();
byte[] sha256 = computeSha256(cert);
return byte2hex(sha256);
} catch (NameNotFoundException e) {
return null;
}
}
private static byte[] computeSha256(byte[] data) {
try {
return MessageDigest.getInstance("SHA-256").digest(data);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
private static String byte2hex(byte[] data) {
if (data == null) return null;
final StringBuilder hexadecimal = new StringBuilder();
for (final byte b : data) {
hexadecimal.append(String.format("%02X", b));
}
return hexadecimal.toString();
}
}
5.2.3.4 權限重委托問題
通路聯系人或 GPS,它們帶有受 Android OS 保護的資訊和功能時,應用必須聲明使用權限。當所需的權限被授予時,權限被委托給應用,應用将能夠通路受權限保護的資訊和功能。
根據程式的設計方式,被授予權限的應用可以擷取受權限保護的資料。此外,應用可以向另一個應用提供受保護資料,而不必強制確定相同的權限,這無異于,沒有權限的應用可以通路受權限保護的資料。這實際上是重新授權,稱為權限重新授權問題。是以,隻有 Android 的權限機制的規範,才能夠管理從來自用程式的,保護資料的直接通路的權限。
圖 5.2-9 展示了一個具體的例子。 中心的應用表明,已聲明
android.permission.READ_CONTACTS
的應用使用它來讀取聯系人,然後将它們存儲到其自己的資料庫中。 當已經存儲的資訊通過内容供應器,提供給另一個應用,而沒有任何限制時,就會發生重新授權問題。
作為一個類似的例子,聲明了
android.permission.CALL_PHONE
的應用,使用它從另一個應用接收電話号碼(可能是使用者輸入的),它未聲明相同權限。如果該号碼在未經使用者驗證的情況下被呼叫,那麼也存在重新授權問題。
在某些情況下,通過權限獲得的,幾乎完整的資訊或功能資産,需要由其他應用二次提供。在這些情況下,供應方應用必須要求相同權限,才能保持原始的保護級别。此外,在僅以間接方式提供資訊和功能資産的一部分的情況下,根據資訊或功能資産的一部分的損害程度,需要适當保護。由“4.1.1.1 建立/使用私有活動”或“4.1.1.4 建立/使用私有活動”,我們可以使用類似于前者的保護措施,驗證使用者的同意,并設定目标應用的活動限制,以及其他。
這種重新授權問題不僅限于 Android 權限。對于 Android 應用,應用從不同的應用,網絡和存儲媒體中擷取必要的資訊/功能,這是常見的。在很多情況下,通路它們需要一些權限和限制。例如,如果提供者來源的 Android 應用,則它是權限;如果它是網絡,那麼它是登入機制;如果它是存儲媒體,則會存在通路限制。是以,在仔細考慮後,需要對應用實作這些措施,因為資訊/功能不是以與使用者意圖相反的方式使用的。以間接方式将獲得的資訊/功能提供給另一應用,或轉移到網絡或存儲媒體時,這一點尤其重要。根據需要,你必須強制確定權限或限制使用權限,如 Android 權限。詢問使用者的同意是解決方案的一部分。
在以下代碼中,我們示範了一個情況,使用
READ_CONTACTS
權限,從聯系人資料庫中擷取清單的應用,對資訊的目标強制確定相同的
READ_CONTACTS
權限。
強制確定提供者的相同權限。
AndroidManifest.xml
<?xml version="1.0" encofing="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.permission.transferpermission"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".TransferPermissionActivity"
android:label="@string/title_activity_transfer_permission" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name=".TransferPermissionContentProvider"
<!-- *** Point1 *** Enforce the same permission that the provider does. -->
android:authorities="org.jssec.android.permission.transferpermission"
android:enabled="true"
android:exported="true"
android:readPermission="android.permission.READ_CONTACTS" >
</provider>
</application>
</manifest>
當一個應用確定多個權限時,上述方法不會解決它。 通過使用源代碼中的
Context#checkCallingPermission()
或
PackageManager#checkPermission()
,它驗證調用者應用是否在清單中,使用
uses-permission
聲明了所有權限。
在活動中:
public void onCreate(Bundle savedInstanceState) {
[...]
if (checkCallingPermission("android.permission.READ_CONTACTS") == PackageManager.PERMISSION_GRANTED
&& checkCallingPermission("android.permission.WRITE_CONTACTS") == PackageManager.PERMISSION_GRANTED) {
// Processing during the time when an invoker is correctly declaring to use
return;
}
finish();
}
5.2.3.5 自定義權限的簽名檢查機制(Android 5.0 及以上)
在 Android 5.0(API Level 21)及更高版本中,如果滿足以下條件,則無法安裝定義其自定義權限的應用。
- 在裝置上已經安裝了另一個應用,用相同名稱定義了自定義權限。
- 應用使用不同的密鑰簽名
當具有受保護函數(元件)的應用,和使用該函數的應用,定義了具有相同名稱的自定義權限,并且使用相同密鑰簽名時,上述機制将防止安裝定義了自定義權限的其他公司的應用同名。 但是,如“5.2.2.3 你自己的簽名權限必須僅在提供方應用中定義(必需)”中所述,該機制對于檢查自定義權限是否由你自己的公司定義是行不通的,因為權限 如果多個應用定義相同的權限,在你自己不知道的情況下,可能通過解除安裝應用來使其失效。
總而言之,在 Android 5.0(API Level 21)和更高版本中,當你的應用定義你自己的簽名權限時,你還需要遵守兩個規則:“5.2.2.3 你自己的簽名權限隻能在提供方應用上定義(必需) “和”5.2.2.4 驗證内部定義的簽名權限是否由内部應用定義(必需)“。
5.2.3.6 Android 版本 6.0 和更高版本中對權限模型規範的修改
Android 6.0(API Level 23)引入了權限模型的修改規範,這些規範影響了應用的設計和規範。 在本節中,我們将概述 Android 6.0 及更高版本中的權限模型。
權限授予和拒絕的時機
如果應用聲明使用需要使用者确認的權限(危險權限)【請參見“5.2.2.1 Android 系統危險權限必須僅用于保護使用者資産(必需)”一節】,Android 5.1(API 級别 22)和更早的版本,要求在安裝應用時顯示這些權限的清單,并且使用者必須授予所有權限才能繼續安裝。 此時,應用聲明的所有權限(包括危險權限以外的權限)均已授予該應用;一旦這些權限被授予應用,它們就會一直有效,直到應用從終端上解除安裝。
但是,在 Android 6.0 及更高版本的規範中,應用執行時會授予權限。 在安裝應用時不會發生權限授予和使用者的權限确認。 當應用執行需要危險權限的過程時,需要檢查是否已将這些權限提前授予應用;如果沒有,則必須在 Android 作業系統中顯示确認視窗,來請求使用者的同意 [25]。如果使用者從确認視窗授予權限,則将權限授予應用。 但是,使用者授予應用的權限(危險權限)可以随時通過設定菜單撤銷(圖 5.2-10)。 出于這個原因,必須實作适當的過程,來確定應用不會産生不規則的行為,即使在因為未授予權限,而無法通路所需的資訊或功能的情況下。
[25] 由于正常權限和簽名權限是由 Android OS 自動授予的,是以不需要擷取使用者對這些權限的确認。
權限授予和拒絕的機關
根據與之相關的功能和資訊類型,可以将多個權限組合在一起稱為權限組。 例如,讀取月曆資訊所需的權限
android.permission.READ_CALENDAR
以及寫入月曆資訊所需的權限
android.permission.WRITE_CALENDAR
都關聯權限組
android.permission-group.CALENDAR
。
在 Android 6.0 及更高版本的新權限模型中,權限的授予和撤銷可以使用權限組統一執行。 是以,當一個應用在運作時請求
android.permission.READ_CALENDAR
并且使用者同意該請求時,Android OS 的行為就像
android.permission.READ_CALENDAR
和
android.permission.WRITE_CALENDAR
都已被授權一樣。 如果随後請求
android.permission.WRITE_CALENDAR
權限,則作業系統不會向使用者顯示對話框,而是直接授予權限。
權限組分類的更多資訊,請參閱開發人員參考(
http://developer.android.com/intl/ja/guide/topics/security/permissions.html#perm-groups)。
修改後的規範的影響範圍
應用在運作時需要權限請求的情況,僅限于終端運作 Android 6.0 或更高版本,并且應用的
targetSDKVersion
為 23 或更高的情況。 如果終端運作的是 Android 5.1 或更低版本,或者應用的
targetSDKVersion
為 22 或更低,則安裝時會完全請求和授予權限,這與傳統情況相同。 但是,如果終端運作的是 Android 6.0 或更高版本,則即使應用的
targetSDKVersion
低于 23,使用者在安裝時授予的權限也可能随時被使用者撤銷。 這會造成應用意外終止的可能性。 開發人員必須遵守修改後的規範,或将應用的
maxSDKVersion
設定為 22 或更低版本,來確定該應用不能安裝在運作 Android 6.0(API Level 23)或更高版本(表 5.2-1)的終端上。
表.2-1
Android OS 終端版本 | 應用的 | 應用被授予權限的時機 | 使用者是否能控制權限 |
---|---|---|---|
= 6.0 | = 23 | 執行時 | 是 |
| < 23 | 安裝時 | 是(需要快速響應) |
<= 5.1 | | 否 | |
但是,應該注意,
maxSdkVersion
的影響是有限的。 當
maxSdkVersion
的值設定為 22 或更低時,Android 6.0(API Level 23)和更高版本的裝置,不再被列為 Google Play 中目标應用的可安裝裝置。 另一方面,由于未在 Google Play 以外的市場中檢查
maxSdkVersion
的值,是以可能會在 Android 6.0(API Level 23)或更高版本中安裝目标應用。 由于
maxSdkVersion
的效果有限,Google 不建議使用
maxSdkVersion
,是以建議開發人員立即遵守修改後的規範。
在 Android 6.0 及更高版本中,以下網絡通信權限的保護級别從危險更改為正常。 是以,即使應用聲明使用這些權限,也不需要獲得使用者的顯式統一,是以修改後的規範在此情況下不會産生影響。
-
android.permission.BLUETOOTH
-
android.permission.BLUETOOTH_ADMIN
-
android.permission.CHANGE_WIFI_MULTICAST_STATE
-
android.permission.CHANGE_WIFI_STATE
-
android.permission.CHANGE_WIMAX_STATE
-
android.permission.DISABLE_KEYGUARD
-
android.permission.INTERNET
-
android.permission.NFC