应用身份校验安全方案
2024-3-6 16:58:50 Author: mp.weixin.qq.com(查看原文) 阅读量:6 收藏

引言

不同应用间有时存在功能协作场景,例如交互应用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.getRunningAppProcesses();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级别自定义权限,此方式必须保证权限定义方已被安装。

往期推荐:

基于复杂性理论生成高质量的LLM注入攻击对抗样本

您有一份2023vivo安全年报待查收!

vivo荣获AIIA“安全治理委员会副组长单位”

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

文章来源: https://mp.weixin.qq.com/s?__biz=MzI0Njg4NzE3MQ==&mid=2247491500&idx=1&sn=059e845f192f3de94f77c312395748e7&chksm=e9b939c0deceb0d61380dccc80bad89a1ad79ceaea7e0c61cdc8d2374847e60ecda26561507d&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh