这几篇将围绕ContentProvider进行安全漏洞分析。
关于ContentProvider,网上有不少资料,但都零零碎碎,而且样例时间也较老,因此我整合了网上可靠资源,并结合自己实践给出一篇可靠的博客。
这篇将详细介绍ContentProvider的实现原理以及实例。
1. 概述
ContentProvider用于提供数据的统一访问格式,封装底层的具体实现。对于数据使用者来说,无需知晓数据的来源是数据库,文件,或者网络等,只需简单地使用ContentProvider提供的数据操作接口,即增(insert),删(delete),改(update),查(query)四个(下面统称CRUD)方法。
ContentProvider主要用于在不同的应用程序之间实现数据共享的功能【也可以在单应用中用】,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供器是Android实现跨程序共享数据的标准方式。
1.1 ContentProvider类
ContentProvider是Android四大组件之一,但生命周期简单,只有onCreate过程。ContentProvider是一个抽象类,如果要实现自己的ContentProvider类,只需继承于ContentProvider,并实现以下6个abstract方法:
1.2 ContentResolver类
其他app或者进程想要操作ContentProvider,则需要先获取ContentResolver,再利用ContentResolver类来完成对数据的CRUD。
外部程序通过ContentResolver接口可以访问ContentProvider提供的数据。ContentResolver提供的接口和ContentProvider中需要实现的接口对应,具体method可以查询Android官方 API。
如果在一个Activity中,可以通过以下语句获取ContentResolver对象:
ContentResolver cr = getContentResolver();
然后使用这个ContentResolver的method通过uri和任何ContentProvider交互。
1.3 Uri
上面说到ContentResolver是通过指定Uri对象来和特定ContentProvider对象通信,下面先说下URI。
URI通用格式如下:
[scheme:][//authority][path][?query][#fragment]
在ContentProvider中:
1. scheme为固定“content”
2. authority用于唯一标识这个ContentProvider,外部调用者可根据该标识找到它
3. path:用来表示要操作的数据,路径的构建要根据业务而定
如contentprovider指定操作数据库,则要操作xxx表中id为10的记录,可以构建路径:/xxx/10
例如content://com.eric.project/android/3
如果要把一个字符串转换成Uri,可以使用Uri类中的parse方法,如下:
Uri uri = Uri.parse("content://cn.itcast.provider.personprovider/person") |
2. ContentResolver与ContentProvider通信流程
然后看下ContentResolver与ContentProvider是如何用Uri通信的,首先给ContentResolver对象的CRUD方法传入指定Uri对象,然后系统会通过这个Uri的authority查找指定ContentProvider对象,然后通过ContentProvider中的同名CRUD方法调用相应数据操作类,从而执行具体的CRUD操作。整个流程如下:
3. 实例详解
这里给出ContentProvider结合sqlite的四个基本操作CRUD样例。主要实现在一个app(TestCp)中进行CRUD,在另一个app(TestCpCall)中显示的简单功能。
效果如下:
Query:
Insert:
Delete:
Update:
原本的gif失效了,下面是在oppo coloros12.1上新测的
实验环境:实验机Nexus 6P,架构是armv8-aarch64。
首先要建立数据库,这里选用sqlite,但手机没预装sqlite3,参考网上,
法一:源码手动编译,法二:android编码
这里选用法二。
接着给出ContentProvider的6个abstract方法:
- insert(Uri, ContentValues):插入新数据;
- delete(Uri, String, String[]):删除已有数据;
- update(Uri, ContentValues, String, String[]):更新数据;
- query(Uri, String[], String, String[], String):查询数据;
- onCreate():执行初始化工作;
- getType(Uri):获取数据MIME类型。
上述就是要override的method,从而调用sqlite。
3.1 创建sqlite
主要有两种方法:
(1) 继承实现SQLiteOpenHelper:这是Android提供的数据库操作类,需要重写onCreate以及onUpgrade抽象方法,分别在数据库初次创建以及数据库版本更新时用。同时要重构构造函数。
(2) 直接调用SQLiteDataBase类的openOrCreateDatabase():具体见官方API。
这里选用方法一,代码如下:
public class MySQlOpenHelper extends SQLiteOpenHelper {
/***重载构造***/
public MySQlOpenHelper(Context context, String name,SQLiteDatabase.CursorFactory factory,int version){
super(context,name,factory,version);
// Log.d("cv","openhelper");
}
/***重写onCreate***/
/***创建数据表:stuInfo,数据库初次创建时调用***/
@Override
public void onCreate(SQLiteDatabase db){
// Log.d("cv","create table");
String createTable="create table "+Constant.tableName +
"(id integer primary key," +
"name text)";
db.execSQL(createTable);
}
/***重写onUpgrade**/
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
Log.i("--------", oldVersion + "------->" + newVersion);
}
}
3.2 编写ContentProvider实现
有了Sqlite,就要继承ContentProvider类来调用Sqlite方法,针对前面说的6个override method,分别说明。
一般在onCreate中执行创建数据库,这里不赘述。
对Sqlite的操作分别有SQLiteDatabase.execSQL(String SQL语句) 以及 特定的SQLiteDatabase.insert/delete/query/update方法,这里选用特定方法。
3.2.1 insert
在app TestCp中,方法如下:
@Override
public Uri insert(Uri uri, ContentValues values){
// Log.d("cv","insert");
long newUserId=db.insert(Constant.tableName,null,values);
return null;
}
在app TestCpCall中,方法如下:
public void btnInsert(View v){
String name=editText.getText().toString();
ContentValues cv=new ContentValues();
cv.put("id",idCnt++); //int
cv.put("name",name);
getContentResolver().insert(uri,cv);
// Log.d("cv","Callinsertfinish");
}
很简单,就是按key-value形式通过ContentResolver调用ContentProvider进而插入数据到Sqlite。
3.2.2 delete
在app TestCp中,代码如下
@Override
public int delete(Uri uri,String selection,String[] selectionArgs){
int deleteInt=0;
deleteInt=db.delete(Constant.tableName,selection,selectionArgs);
Log.d("cv","deleteLine"+deleteInt);
return deleteInt; //操作成功返回行数,失败返回0
}
在app TestCpCall中,代码如下
public void btnDelete(View v){
String name=editText.getText().toString();
int deleteInt=getContentResolver().delete(uri,"name=?",new String[]{name});
if(deleteInt==0) Toast.makeText(this,"no such record",Toast.LENGTH_SHORT).show(); //容错
}
delete方法返回的是操作成功的删除行数,若失败则为0;
3.2.3 update
在app TestCp中,代码如下
@Override
public int update(Uri uri,ContentValues values,String selection,
String[] selectionArgs){
int updateInt=0;
updateInt=db.update(Constant.tableName,values,selection,selectionArgs);
return updateInt;
}
在app TestCpCall中,代码如下
public void btnUpdate(View v){
String updateSrc=editTextSrc.getText().toString();
String updateTar=editTextTar.getText().toString();
ContentValues values=new ContentValues();
values.put("name",updateTar);
int updateInt=getContentResolver().update(uri,values,"name=?",new String[]{updateSrc});
if(updateInt==0) Toast.makeText(this,"no such record",Toast.LENGTH_SHORT).show(); //容错
}
其中update的返回值是更新的行数。
3.2.4 query
在app TestCp中,代码如下
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs,String sortOrder){
// Log.d("cv","query");
Cursor cursor=db.query(Constant.tableName,projection,selection,selectionArgs,null,null,sortOrder);
return cursor;
}
在appTestCall中,代码如下
public void btnQuery(View v){
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
boolean isExist=cursor.moveToFirst();
try {
do {
int id = cursor.getInt(cursor.getColumnIndex("id"));
String name = cursor.getString(cursor.getColumnIndex("name"));
Log.d("cv", "Callquery " + "id:" + String.valueOf(id) + " " + "name:" + name);
} while (cursor.moveToNext());
}catch(Exception e){
Toast.makeText(this,"Table is Empty",Toast.LENGTH_SHORT).show();
// Log.d("cv","database null");
}
cursor.close();
}
SqliteDataBase的query返回的是查询成功数据的cursor,这里直接查询全部。
3.2.5 getType(Uri)
这个函数按字面意思较难理解,我们先看下它的作用,
参考源码定义
/**
* Implement this to handle requests for the MIME type of the data at the
* given URI. The returned MIME type should start with
* <code>vnd.android.cursor.item</code> for a single record,
* or <code>vnd.android.cursor.dir/</code> for multiple items.
* This method can be called from multiple threads, as described in
* <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
* and Threads</a>.
*
* <p>Note that there are no permissions needed for an application to
* access this information; if your content provider requires read and/or
* write permissions, or is not exported, all applications can still call
* this method regardless of their access permissions. This allows them
* to retrieve the MIME type for a URI when dispatching intents.
*
* @param uri the URI to query.
* @return a MIME type string, or {@code null} if there is no type.
*/
public abstract @Nullable String getType(@NonNull Uri uri);
即根据给定的Uri返回一个MIME类型的数据,
若Uri表单条数据,那MIME返回值应为vnd.android.cursor.item/#格式;
若Uri表多条数据,那MIME返回值应为vnd.android.cursor.dir/#格式。
同时如果应用没有访问该ContentProvider的权限(read/write,or is not exported),getType也是可以被调用的。
上面说到MIME,按照百科的解释,MIME为多用途互联网邮件扩展(MIME,Multipurpose Internet Mail Extensions),是一个互联网标准,简单说浏览器识别不同类型文本(HTML,XML,GIF,FLASH等)就是通过MIME Type,
如html,看Response Header有如下
Javascript类型有如下
其中content-type就表明了数据MIME类型,具体可以看w3c的解释。
然后回过来看getType,参考一些信息,以指定的两种方式开头,android可以识别出是单条数据还是多条数据,即上面所说的单和多数据返回格式,从而在进行其他CRUD操作时,系统能直接识别出是单条还是多条数据,从而不同再去分析了,提高系统性能。
以上代码简单实现了Android中使用ContentProvider与Sqlite来进行CRUD操作。源码见:
ContentProvider简单易用实例,含容错处理,基于AndroidStudio3,gradle-Android代码类资源-优快云下载
下一章将介绍sqlite中注入,渗透相关技术。
参考:
1】 CRUD:https://zh.wikipedia.org/wiki/%E5%A2%9E%E5%88%AA%E6%9F%A5%E6%94%B9
2】 官网
1》 https://developer.android.com/guide/topics/providers/content-providers
2》 https://developer.android.com/guide/topics/providers/content-provider-basics?hl=zh-cn#ClientProvider
3】 第一行代码v2