引言
不同应用间有时存在功能协作场景,例如交互应用A接收用户输入的数据需要同步给后台应用B,让用户B执行具体的管控策略,此时后台应用B就可能开放数据库增删该接口给应用A,而应用B不想该功能接口被A以外的应用访问。这类为端侧特定应用提供非公开功能的场景,应用往往需要对调用方进行身份校验,以防止接口被滥用,危害自身应用安全甚至对系统安全造成影响。本文针对该类场景,从典型错误校验方式、不同组件获取包名的正确方式、校验包名/签名的正确方式等方面进行阐述,为进行端侧应用间的身份校验提供技术参考。
从威胁建模角度分析,“其他APP”与“己方APP”交互时,跨越了信任边界,存在安全威胁。
如上图所示,“其他APP”作为与“己方APP”交互的对象,属于外部实体,存在“仿冒威胁”和“抵赖威胁”。
当存在“仿冒威胁”且业务未对“其他APP”的身份进行安全校验时,恶意应用就可以伪造身份,针对性发起攻击,最终可能导致存在以下风险:
非法访问特权接口;
窃取用户敏感数据;
............
在端侧校验调用方身份时,可以选择的校验方式包括:包名校验、签名校验、权限检查等。校验方式选择不恰当或校验逻辑错误,均会导致校验失效,使得恶意应用可以调用受保护的特权接口。
若身份校验只采用验证白名单包名方式,当白名单内的应用在用户设备上未安装时,恶意应用可伪造白名单内未被安装的应用,从而绕过包名校验。
自定义权限保护级别设置过低
自定义权限可通过protectionLevel来设置权限保护级别,保护级别分为normal、dangerous、signature等,若将保护级别设置为normal,则该权限只需声明<uses-permission>即可使用,无需用户确认;
使用未定义权限风险
自定义权限有一个定义方和若干个使用方,由于权限定义应用被卸载或开发人员未定义权限等原因,导致被声明使用的权限没有定义方。此时恶意应用可主动定义该权限并设定为normal等级,导致权限失去保护作用。
<!-- 定义com.xxx.permission.xxx权限 -->
<permission
android:name="com.xxx.permission.xxx"
android:protectionLevel="signatureOrSystem" />
<!-- 声明使用com.xxx.permission.xxx权限 -->
<uses-permission android:name="com.xxx.permission.xxx" />
权限定义和声明使用案例
若未选择正确的获取包名方法,恶意应用可利用方法缺陷,绕过包名校验和依赖包名进行的签名校验。下面列举部分获取包名的错误方法:
通过getNameForUid获取包名
该方法在调用方存在sharedUserId时获取的包名为“包名:uid”,无法获取正确包名;且任意应用可通过sharedUserId伪造包名;
例如,当恶意应用声明“android:sharedUserId=”com.xxx.example”时,使用getNameForUid获取的恶意应用包名为”com.xxx.example:10xxx”(其中10xxx为恶意应用实际uid),而应用真实包名为“com.test.xxx”。
恶意应用com.test.xxx在manifest中的声明如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:sharedUserId="com.xxx.example"
xmlns:tools="http://schemas.android.com/tools">
获取processName作为包名
processName可由应用通过”android:process”自行定义,且”android:process”声明无任何限制。若使用processName作为包名,恶意应用可直接声明”android:process”为白名单内应用,绕过白名单限制。
如下为示例错误代码:
//该方法为错误的获取包名方法,只能获取进程名,进程名可伪造
int pid = Binder.getCallingPid();
ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> processInfoList = activityManager.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo processInfo : processInfoList) {
if (processInfo.pid == pid) {
return processInfo.processName;
}
}
例如:若白名单中存在”com.xxx.example”,恶意应用包名为“com.test.xxx”,恶意应用就可以在其manifest中声明” android:process=”com.xxx.example” ”,此时通过该方法获取的恶意应用的processName为”com.xxx.example”,从而绕过白名单校验。
采用startswith、endswith或String类的contains等可绕过的包名匹配方法,或匹配包名时忽略大小写,均可被绕过。
例如:校验com.xxx.function包名,绕过示例如下:
使用startswith(“com.xxx.function”):com.xxx.functiontest
使用endswith(“com.xxx.function”):com.testcom.xxx.function
使用contains(“com.xxx.function”):com.xxx.functiontest、com.testcom.xxx.function
使用equalsIgnoreCase(“com.xxx.function”):com.xxx.Function
若采用白名单方式校验应用,由于白名单内应用可能被卸载,推荐同时校验白名单内应用的包名和签名。
Activity可通过反射Activity的mReferrer方法获取调用方包名,代码示例如下:
Field referrerField = Activity.class.getDeclaredField("mReferrer");
referrerField.setAccessible(true);
String packgeName = (String)referrerField.get(this);
Provider可通过getCallingPackage方法获取调用方包名,代码示例如下:
String packageName = getCallingPackage();
通过uid获取
若通过aidl方式进行通信,可通过getpackagesforuid获取包名,此方式获取的是同uid的包名列表,虽然这种范围扩大了获取包名的范围,但该方式获取的包名列表内一定都是同签名应用,因此用通过这种方式获取的包名同样可以用作签名验证。代码示例如下:
/* 通过uid获取调用方包名,任意应用可用*/
int uid = Binder.getCallingUid();
if(uid >=10000){
//通过getPackagesForUid获取包名列表,获取的为所有该uid下的包名,不存在伪造包名
String[] pkgName = getPackageManager().getPackagesForUid(uid);
}else{
//当uid小于10000时,将获取到所有共享system uid的包名,数量较多,建议通过其他方式获取包名或根据白名单自行定义校验策略
}
若通过messenger进行通信,可通过msg.sendinguid获取调用方uid,进而通过getpackagesforuid获取同uid的包名列表,代码示例如下:
/* 通过uid获取调用方包名,任意应用可用*/
int uid = msg.sendingUid;
if(uid >=10000){
//通过getPackagesForUid获取包名列表,获取的为所有该uid下的包名,不存在伪造包名
String[] pkgName = getPackageManager().getPackagesForUid(uid);
}else{
//当uid小于10000时,将获取到所有共享system uid的包名,数量较多,建议通过其他方式获取包名或根据白名单自行定义校验策略
}
通过pid获取(需REAL_GET_TASKS权限)
获取方法代码示例如下,该方法首先获取全部进程信息,通过进程pid确定进程info,进而通过info.pkgList获取对应的同pid包名列表。
/* 通过pid获取调用方包名
* 仅系统签名应用可用,需REAL_GET_TASKS权限*/
int pid = Binder.getCallingPid();
//获取当前运行的所有进程,进而获取与pid对应的包名列表,不存在伪造包名
ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_
SERVICE);
List<ActivityManager.RunningAppProcessInfo> processInfoList = activityManager.getRun
ningAppProcesses();
for (ActivityManager.RunningAppProcessInfo processInfo : processInfoList){
if(processInfo.pid == pid){
//pkgList:调用方同pid的包名列表
String[] pkgList = processInfo.pkgList;
}
}
3.2 校验包名安全方案
包名校验均应避免使用startswith、endswith或String类的contains方法,推荐使用equals方法或ArrayList的contains方法(本质仍为equals方法)
代码示例如下:
//包名白名单
private static ArrayList<String> pkgNameWhiteList = new ArrayList<>();
static {
pkgNameWhiteList.add("com.xxx.xxx");
}
//校验包名
public boolean checkpkgName(Context context){
String pkgName = getActivityPkgName(context);
if(pkgName!=null){
return pkgNameWhiteList.contains(pkgName);
}
return false;
}
通过应用包名获取签名代码示例如下:(获取签名需确保调用方对于被调用方可见)
private static Signature[] getSignatures(Context context, String packageName) {
PackageInfo packageInfo = null;
try {
packageInfo = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
return packageInfo.signatures;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
权限校验:所有调用方声明使用共同的signature级别自定义权限,此方式必须保证权限定义方已被安装。