Content Provider安全场景和危害
在Android系统中,Content Provider作为应用程序四大组件之一,它起到在应用程序之间共享数据的作用,通过Binder进程间通信机制以及匿名共享内存机制来实现。
然而有些数据是应用自己的核心数据,需要有保护地进行开放。
虽然Binder进程间通信机制突破了以应用程序为边界的权限控制,但是它是安全可控的,因为数据的访问接口是由数据的所有者来提供的,就是数据提供方可以在接口层来实现安全控制,决定哪些数据是可以读,哪些数据可以写。
很多开发者不能恰当的使用,导致攻击者可访问到应用本身不想共享的数据。虽然Content Provider组件本身也提供了读写权限控制,但是它的控制粒度是比较粗的。
Content Provider漏洞分类
- 信息泄漏
- SQL注入
- 目录遍历
信息泄露漏洞
content URI是一个标志provider中的数据的URI。Content URI中包含了整个provider的以符号表示的名字(它的authority)和指向一个表的名字(一个路径)。当你调用一个客户端的方法来操作一个,provider中的一个表,指向表的contentURI是参数之一,如果对Content Provider的权限没有做好控制,就有可能导致恶意的程序通过这种方式读取APP的敏感数据。
<provider android:name=".providers.YouNiProvider" android:process="com.snda.youni.mms" android:authorities="com.snda.youni.providers.DataStructs"/>
private void getyouni(){
int i = 0;
ContentResolver contentresolver=getContentResolver();
String[] projection={"* from contacts--"};
Uri uri =Uri.parse("content://com.snda.youni.providers.DataStructs/message_ex");
Cursor cursor=contentresolver.query(uri.projection,null,null,null);
String text="";
while(cursor.moveToNext()){
text+=cursor.getString(cursor.getColumnIndex("display_name"))+"\n";
}
Log.i("TEST",text);
}
信息泄漏漏洞 防护
1.minSdkVersion不低于9
2.不向外部app提供数据的私有content provider显示设置exported=”false”,避免组件暴露(编译api小于17时更应注意此点)
3.内部app通过content provid交换数据时,设置protectionLevel=”signature”验证签名
4.公开的content provider确保不存储敏感数据
针对权限保护绕过防御措施:
1.使用Context.checkCallingPermission()和Context.enforceCallingPermission()来确保调用者拥有相应的权限,防止串谋攻击(confused deputy)。
2.可以使用如下函数,获取应用的permission保护级别是否与系统中已定义的permission保护级别一致。如果不一致,则抛出异常。
public void definedPermissionsSecurityOk(Context con){
PackageManager pm =con.getPackageManager();
try{
PackageInfo myPackageInfo=pm.getPackageInfo(con.getPackageName(),PackageManager.GET_PERMISSIONS);
PermissionInfo[] definedPermissions=myPackageInfo.permissions;
for(int i=0;i<definedPermissions.length;i++){
int protLevelReportedBySystem = pm.getPermissionInfo(definedPermissions[i].name,0).protectionLevel;
if(definedPermissions[i].protectionLevel!=protLevelReportedBySystem){
throw new SecurityException("protectionLevel mismatch for"+definedPermissions[i].name);
}
}
}catch(NameNotFoundException e){
e.printStackTrace();
}
}
SQL注入漏洞
对Content Provider进行增删改查操作时,程序没有对用户的输入进行过滤,未采用参数化查询的方式,可能导致sql注入攻击。
所谓的SQL注入攻击指的是攻击者可以精心构造selection参数、projection参数以及其他有效的SQL语句组成部分,实现在未授权的情况下从Content Provider获取更多信息。
应该避免使用SQLiteDatabase.rawQuery()进行查询,而应该使用编译好的参数化语句。使用预编译好的语句比如SQLiteStatement,不仅可以避免SQL注入,而且操作性能也大幅提高,因为其不用每次执行都进行解析。
另外一种方式是使用query(),insert(),update(),和delete()方法,因为这些函数也提供了参数化的语句。
预编译的参数化语句,问号处可以插入或者使bindString()绑定值。从而避免SQL注入攻击。
INSERT VALUES INTO [table name](?,?,?,?...)
SQL注入漏洞 防护
1.实现健壮的服务端校验
2.使用参数化查询语句,比如SQLiteStatement。
3.避免使用rawQuery()。
4.过滤用户的输入。
目录遍历漏洞
使用ContentProvider.openFile()可以实现应用间共享数据,如果这个方法使用不当将会导致目录遍历漏洞。因此在使用Content Provider实现数据交换时,应该对传递的路径进行过滤。
private static String IMAGE_DIRECTORY=localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri,String paramString);
throws FileNotFoundException{
File file=new File(IMAGE_DIRECTORY,paramUri.getLastPathSegment());
return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);
}
这段代码使用android.net.Uri.getLastPathSegment()从paramUri中获取文件名,然后将其放置在预定义好的目录IMAGE_DIRECTORY中,如果该URL是encoded编码后的,那么将可能导致目录遍历漏洞。
Android4.3开始,Uri.getLastPathSegment()内部实现调用Uri.getPathSegments()。
public String getLastPathSegment(){
List<String> segments=getPathSegments();
int size=segments.size();
if(size==0){
return null;
}
return segments.get(size-1);
}
Uri.getPathSegments()部分代码片段:
PathSegments getPathSegments(){
if(pathSegments!=null){
return pathSegments;
}
String path = getEncoded();
if(path==null){
return pathSegments = PathSegments.EMPTY;
}
PathSegmentsBuilder segmentBuilder=new PathSegmentsBuilder();
int previous =0;
int current;
while((current=path.indexOf('/',previous))>-1){
if(previous<current){
String decodedSegment=decode(path.substring(previous,current));
segmentBuilder.add(decodedSegment);
}
previous=current+1;
}
if(previous<path.length()){
segmentBuilder.add(decode(path.substring(preyious)));
}
return pathSegments=segmentBuilder.build();
}
Uri.getPathSegments首先会通过getEncoded()获取一个路径,然后以”/“为分隔符将path分成片段,最后调用decode()方法解码。
了解了函数内部处理流程,那么假使我们传递一个encoded编码后的url给getLastPathSegment(),编码后的分隔符就变成了%2F,绕过了内部的分割规则,那么返回的就可能不是真正想要的文件了。这是API设计方面的问题,直接导致了目录遍历漏洞。
为了避免这种情况导致的目录遍历漏洞,开发者应该在传递给getLastPathSegment()之前解码。
有的开发者了解上面描述这种漏洞代码,采用调用两次getLastPathSegment()方法的方式,第一次调用是为了解码,第二次调用期望得到正确的值。
private static String IMAGE_DIRECTORY=localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri,String paramString) throws FileNotFoundException{
File file=new File(IMAGE_DIRECTORY,Uri.parse(paramUri.getLastPathSegment()).getLastPathSegment());
return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);
}
这个编码后的URL: ..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml
第一次调用getLastPathSegment(),会返回../../../data/data/com.example.android.app/shared_prefs/Example.xml。
第二次调用getLastPathSegment()会返回Example.xml
然而攻击者可以采用一种叫做"Double Encoding"的技术,使得第一次调用getLastPathSegment()后无法解码。
比如下面经过double encoded后的string就可以绕过上面这种防御
%252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml
第一次解码后: %2E%2E%2F%2E%2E%2F%2E%2E%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml
第二次解码后: ../../../data/data/com.example.android.app/shared_prefs/Example.xml
仍会导致目录遍历。所以简单的解码后再传人也是不够的,仍然需要严格校验以确保path是期望的路径。
目录遍历漏洞:防护
首先对paramUri解码,文件创建后再通过调用File.getCanonicalPath()来对path的格式进行规范化,最后校验其是否在预定义的目录IMAGE_DIRECTORY。
File.getCanonicalPath()函数实现是这样的,它会将path规范化,得到一个唯一的绝对路径。这通常涉及到从路径名中移除多余的名称(比如”.”和”..”)、分析符号连接(对于UNIX平台),以及将驱动器名称转换成标准大小写形式(对于Microsoft Windows平台)。
private static String IMAGE_DIRECTORY=localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri,String paramString) throws FileNotFoundException{
String decodedUriString = Uri.decode(paramUri.toString());
File file=new File(IMAGE_DIRECTORY,Uri.parse(decodedUriString).getLastPathSegment());
if(file.getCanonicalPath().indexOf(localFile.getCanonnicalPath())!=0){
throw new IllegalArgumentException();
}
return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);
}