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