引言
不同应用间有时存在功能协作场景,例如交互应用A接收用户输入的数据需要同步给后台应用B,让用户B执行具体的管控策略,此时后台应用B就可能开放数据库增删该接口给应用A,而应用B不想该功能接口被A以外的应用访问。这类为端侧特定应用提供非公开功能的场景,应用往往需要对调用方进行身份校验,以防止接口被滥用,危害自身应用安全甚至对系统安全造成影响。本文针对该类场景,从典型错误校验方式、不同组件获取包名的正确方式、校验包名/签名的正确方式等方面进行阐述,为进行端侧应用间的身份校验提供技术参考。
01 威胁分析
从威胁建模角度分析,“其他APP”与“己方APP”交互时,跨越了信任边界,存在安全威胁。
如上图所示,“其他APP”作为与“己方APP”交互的对象,属于外部实体,存在“仿冒威胁”和“抵赖威胁”。
当存在“仿冒威胁”且业务未对“其他APP”的身份进行安全校验时,恶意应用就可以伪造身份,针对性发起攻击,最终可能导致存在以下风险:
非法访问特权接口;
窃取用户敏感数据;
............
02 典型错误
在端侧校验调用方身份时,可以选择的校验方式包括:包名校验、签名校验、权限检查等。校验方式选择不恰当或校验逻辑错误,均会导致校验失效,使得恶意应用可以调用受保护的特权接口。
若身份校验只采用验证白名单包名方式,当白名单内的应用在用户设备上未安装时,恶意应用可伪造白名单内未被安装的应用,从而绕过包名校验。
自定义权限可通过protectionLevel来设置权限保护级别,保护级别分为normal、dangerous、signature等,若将保护级别设置为normal,则该权限只需声明即可使用,无需用户确认;
自定义权限有一个定义方和若干个使用方,由于权限定义应用被卸载或开发人员未定义权限等原因,导致被声明使用的权限没有定义方。此时恶意应用可主动定义该权限并设定为normal等级,导致权限失去保护作用。
<!-- 定义com.xxx.permission.xxx权限 -->
权限定义和声明使用案例
若未选择正确的获取包名方法,恶意应用可利用方法缺陷,绕过包名校验和依赖包名进行的签名校验。下面列举部分获取包名的错误方法:
该方法在调用方存在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"?>
processName可由应用通过”android:process”自行定义,且”android:process”声明无任何限制。若使用processName作为包名,恶意应用可直接声明”android:process”为白名单内应用,绕过白名单限制。
如下为示例错误代码:
//该方法为错误的获取包名方法,只能获取进程名,进程名可伪造
例如:若白名单中存在”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 应用身份校验安全方案
若采用白名单方式校验应用,由于白名单内应用可能被卸载,推荐同时校验白名单内应用的包名和签名。
Activity可通过反射Activity的mReferrer方法获取调用方包名,代码示例如下:
Field referrerField = Activity.class.getDeclaredField("mReferrer");
Provider可通过getCallingPackage方法获取调用方包名,代码示例如下:
String packageName = getCallingPackage();
若通过aidl方式进行通信,可通过getpackagesforuid获取包名,此方式获取的是同uid的包名列表,虽然这种范围扩大了获取包名的范围,但该方式获取的包名列表内一定都是同签名应用,因此用通过这种方式获取的包名同样可以用作签名验证。代码示例如下:
/* 通过uid获取调用方包名,任意应用可用*/
若通过messenger进行通信,可通过msg.sendinguid获取调用方uid,进而通过getpackagesforuid获取同uid的包名列表,代码示例如下:
/* 通过uid获取调用方包名,任意应用可用*/
获取方法代码示例如下,该方法首先获取全部进程信息,通过进程pid确定进程info,进而通过info.pkgList获取对应的同pid包名列表。
/* 通过pid获取调用方包名
3.2 校验包名安全方案
包名校验均应避免使用startswith、endswith或String类的contains方法,推荐使用equals方法或ArrayList的contains方法(本质仍为equals方法)
代码示例如下:
//包名白名单
通过应用包名获取签名代码示例如下:(获取签名需确保调用方对于被调用方可见)
private static Signature[] getSignatures(Context context, String packageName) {
权限校验:所有调用方声明使用共同的signature级别自定义权限,此方式必须保证权限定义方已被安装。
往期推荐:
关注我们,了解更多安全内容!