vivo千镜安全实验室 2024年09月11日
应用身份校验安全方案
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文针对端侧特定应用提供非公开功能的场景,从典型错误校验方式、不同组件获取包名的正确方式、校验包名/签名的正确方式等方面进行阐述,为进行端侧应用间的身份校验提供技术参考。

🤔 **威胁分析:** 当存在“仿冒威胁”且业务未对“其他APP”的身份进行安全校验时,恶意应用就可以伪造身份,针对性发起攻击,最终可能导致非法访问特权接口、窃取用户敏感数据等风险。

⚠️ **典型错误:** 仅校验包名、自定义权限使用不当、校验函数使用不当等错误方式会导致校验失效,使得恶意应用可以调用受保护的特权接口。

🛡️ **应用身份校验安全方案:** 推荐同时校验白名单内应用的包名和签名,并使用equals方法或ArrayList的contains方法进行包名校验,避免使用startswith、endswith或String类的contains方法。

🔐 **获取调用方包名安全方案:** 通过Activity、Provider、Service获取调用方包名,并提供相应的代码示例。

🔒 **校验签名安全方案:** 通过应用包名获取签名,并提供相应的代码示例。

🔑 **权限校验方案:** 所有调用方声明使用共同的signature级别自定义权限,确保权限定义方已被安装。

💪 **其他安全措施:** 除了身份校验外,还需要采取其他安全措施,例如数据加密、访问控制等,来进一步提升应用安全性。

💡 **总结:** 端侧应用间身份校验是保障应用安全的重要措施,需要选择合适的校验方式并进行正确的实现。

🚀 **建议:** 在进行端侧应用间身份校验时,应仔细分析威胁模型,选择合适的校验方式,并进行充分的测试,确保校验逻辑正确,防止恶意应用绕过校验。


引言

不同应用间有时存在功能协作场景,例如交互应用A接收用户输入的数据需要同步给后台应用B,让用户B执行具体的管控策略,此时后台应用B就可能开放数据库增删该接口给应用A,而应用B不想该功能接口被A以外的应用访问。这类为端侧特定应用提供非公开功能的场景,应用往往需要对调用方进行身份校验,以防止接口被滥用,危害自身应用安全甚至对系统安全造成影响。本文针对该类场景,从典型错误校验方式、不同组件获取包名的正确方式、校验包名/签名的正确方式等方面进行阐述,为进行端侧应用间的身份校验提供技术参考。



01 威胁分析


从威胁建模角度分析,“其他APP”与“己方APP”交互时,跨越了信任边界,存在安全威胁。

如上图所示,“其他APP”作为与“己方APP”交互的对象,属于外部实体,存在“仿冒威胁”和“抵赖威胁”。

当存在“仿冒威胁”且业务未对“其他APP”的身份进行安全校验时,恶意应用就可以伪造身份,针对性发起攻击,最终可能导致存在以下风险:

    非法访问特权接口;

    窃取用户敏感数据;

    ............



02  典型错误


在端侧校验调用方身份时,可以选择的校验方式包括:包名校验、签名校验、权限检查等。校验方式选择不恰当或校验逻辑错误,均会导致校验失效,使得恶意应用可以调用受保护的特权接口。

2.1 校验方式错误

① 仅校验包名

若身份校验只采用验证白名单包名方式,当白名单内的应用在用户设备上未安装时,恶意应用可伪造白名单内未被安装的应用,从而绕过包名校验。

② 自定义权限使用不当

    自定义权限保护级别设置过低

自定义权限可通过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" />

权限定义和声明使用案例

2.2 校验函数使用不当

① 获取包名方法错误

若未选择正确的获取包名方法,恶意应用可利用方法缺陷,绕过包名校验和依赖包名进行的签名校验。下面列举部分获取包名的错误方法:

    通过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



03  应用身份校验安全方案


若采用白名单方式校验应用,由于白名单内应用可能被卸载,推荐同时校验白名单内应用的包名和签名。

3.1 获取调用方包名安全方案

① Activity获取调用方包名(Android 11及以上版本可用)

Activity可通过反射Activity的mReferrer方法获取调用方包名,代码示例如下:

Field referrerField = Activity.class.getDeclaredField("mReferrer");
referrerField.setAccessible(true);
String packgeName = (String)referrerField.get(this);

② Provider获取调用方包名

Provider可通过getCallingPackage方法获取调用方包名,代码示例如下:

String packageName = getCallingPackage();

③ Service获取调用方包名

    通过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;
}

3.3  校验签名安全方案

通过应用包名获取签名代码示例如下:(获取签名需确保调用方对于被调用方可见)

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;
}

3.4 权限校验方案

权限校验:所有调用方声明使用共同的signature级别自定义权限,此方式必须保证权限定义方已被安装。






关注我们,了解更多安全内容!

?发表于:中国 广东

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

应用安全 身份校验 端侧安全 包名校验 签名校验 权限校验 安全方案
相关文章