Android漏洞挖掘-四大组件漏洞

花了几周时间学习了一下Android的漏洞挖掘,聊聊我对Android漏洞挖掘的看法。个人认为相较于传统的混淆加壳等安全对抗研究,这个领域的难度不算很大,想要挖洞从0到1,可能只需要了解一下四大组件的生命周期以及常见的漏洞场景,比如说导出组件的非法调用,缺乏参数校验等,甚至可以直接结合MCP让AI代码审计一下反编译的java代码,就有可能出洞。所以Android漏洞挖掘想要体现技术难度更多的是在自动化工具的开发和研究中,当前自动化工具主要是静态规则扫描,污点分析,MCP+AI自动化分析等。目前看来静态规则扫描和污点分析的准确率更有保障,也是目前广泛运用的挖洞辅助方式,MCP+AI的方式上限更高,但是个人看来,当前这种方式的准确率仍有较大提升空间,后续的话可能想在漏洞自动化工具研究上继续学习一下。这里结合自己挖洞的经验,对Android四大组件漏洞进行一个总结和记录。

Android中的四大组件(一)—— Activity

Activity 是 Android 四大组件中最核心的组件,负责构建用户界面,是用户与应用交互的入口点

Activity 是什么

Activity是一个应用程序组件,提供一个屏幕让用户交互以执行某项任务。负责绘制用户界面,提供用户交互,对于一个app,用户所能见到的基本都是基于activity构成的。

Activity 的启动方式

Activity 组件有两种启动方式,一种是显示启动,一种是隐式启动。

  1. 显示启动

直接指定 Activity 的类名,明确启动特定组件。

1
2
3
4
5
6
7
8
// 启动方式一:通过Class对象
Intent intent = new Intent(this, TargetActivity.class);
startActivity(intent);

// 启动方式二:通过包名+类名(适用于跨应用启动)
Intent intent = new Intent();
intent.setClassName("com.example.targetapp", "com.example.targetapp.TargetActivity");
startActivity(intent);
  1. 隐式启动

不直接指定组件,而是声明一个操作意图(如ACTION_VIEW),系统根据 Intent-filter 匹配合适的组件。

1
2
3
4
5
6
7
8
9
// 示例:启动浏览器打开URL
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.example.com"));
startActivity(intent);

// 示例:启动拨号界面
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);

如需隐式启动,需在 AndroidMainfest.xml组件中声明 Intent-filter。

1
2
3
4
5
6
7
<activity android:name=".TargetActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity>

Activity 的生命周期

为什么要了解 Activity 的生命周期?生命周期本质上就是由一个个的回调函数组成,回调函数的触发时机,触发顺序决定了非法数据流向(外界传入的Intent等)。 Activity 完整生命周期:

  • onCreate():Activity 首次创建时调用,初始化 UI 组件和数据
  • onStart():Activity 即将可见时调用
  • onResume():Activity 获得焦点并开始与用户交互
  • onPause():Activity 失去焦点但仍部分可见(如被对话框覆盖)
  • onStop():Activity 完全不可见
  • onDestroy():Activity 即将被销毁,释放资源
  • onRestart():Activity 从停止状态重新启动

Activity 的代码实现

Activity 这部分理解最为简单,主要是 Intent 的数据的传输的过程

1
2
3
4
5
6
7
//发送数据
Intent intent = new Intent(this, SecondActivity.class);
intent.putExtra("key_username", "john_doe");
intent.putExtra("key_age", 25);startActivity(intent);
//接收数据
String userName = getIntent().getStringExtra("key_username");
int keyAge = getIntent().getIntExtra("key_age");

Activity 的常见漏洞

  1. 非法访问

对于一些有前置条件的activity,比如说重置密码的activity,如果直接导出且未添加权限,就容易被非法访问,直接进入重置密码界面进行修改密码,绕过了密码验证或者身份验证过程。

  1. 组件暴露+参数校验缺失

组件暴露 + 对 Intent 的参数未进行充分的参数校验,攻击者可通过构造特制 Intent 启动受保护组件,执行任意代码或获取敏感信息。

1
2
3
4
5
6
7
8
9
10
//攻击代码
Intent intent = new Intent();
intent.setAction("com.example.action.EDIT"); // 目标Activity支持的action
intent.setData(Uri.parse("file:///sdcard/important_data")); // 敏感文件路径startActivity(intent);
//受害者代码
Uri fileUri = getIntent().getData();
if (fileUri != null) {
// 直接读取文件内容
readFileContent(fileUri.getPath());
}

Android中的四大组件(二)—— Service

在 Android 四大组件中,Service 是负责处理后台任务的核心组件。它没有用户界面,却能在应用退出后继续运行,承担着音乐播放、文件下载、数据同步等长时间运行的任务。

Service 是什么

初次接触 Service 时,会误以为它是 “后台线程”—— 这是一个典型的认知误区。Service 本质是运行在主线程(UI 线程)中的组件,它本身并不具备异步处理能力。如果在 Service 中执行耗时操作(如下载大文件),会直接导致主线程阻塞,引发应用无响应。

Service 的核心价值在于后台存活:即使启动它的 Activity 被销毁,Service 仍能在系统中继续运行,且受 Android 系统进程管理机制保护。真正的耗时操作需要在 Service 内部通过子线程实现。

Service 的两种核心类型

根据启动方式和生命周期的差异,Service 可分为Started Service(启动型服务)和Bound Service(绑定型服务),两者也可结合使用。

  1. Started Service:独立运行的后台任务

通过startService(Intent)启动,一旦启动后便独立于启动它的组件(如 Activity)运行,直到被主动停止或系统回收。

启动与停止流程:

  • 启动:调用Context.startService(Intent),系统会回调 Service 的onStartCommand()方法。
  • 停止:需主动调用stopService(Intent)(外部组件)或stopSelf()(服务自身),系统回调onDestroy()。
  • 特点:启动后与启动组件无直接通信能力,适合执行单一、独立的后台任务(如下载文件)。

onStartCommand () 的返回值:

该方法的返回值决定了服务被系统杀死后的重启策略,是控制 Service 生命周期的关键:

  • START_STICKY:系统内存充足时会重启服务,适合无需恢复任务状态的场景(如实时日志收集)。
  • START_REDELIVER_INTENT:系统会重启服务并重新传递最后一个Intent,适合需要恢复任务的场景(如下载中断后继续)。
  • START_NOT_STICKY:系统不会主动重启服务,适合一次性任务(如发送一条消息)。
  1. Bound Service:与组件双向通信,进程间通信

通过bindService(Intent, ServiceConnection, int)绑定到其他组件(如 Activity),两者建立持续的通信通道,组件可直接调用 Service 中的方法。

绑定与解绑流程:

绑定:调用bindService()后,系统回调 Service 的onBind()方法,返回IBinder对象(用于组件与服务通信)。

解绑:组件调用unbindService(),系统回调onUnbind();当所有绑定的组件都解绑后,Service 会自动销毁(回调onDestroy())。

特点:适合需要双向交互的场景(如音乐播放器:Activity 控制暂停 / 播放,Service 反馈当前播放进度)。

  1. 两种类型的结合使用

一个 Service 可以同时被启动和绑定:例如音乐播放器,通过startService()确保退出 APP 后继续播放(启动型),同时通过bindService()让 Activity 控制播放状态(绑定型)。此时需同时调用stopService()和unbindService()才能彻底销毁服务。

Service 的生命周期

Service 的生命周期比 Activity 更简单,但需严格区分启动型与绑定型的差异。

  1. Started Service 的生命周期
1
2
3
4
onCreate() → onStartCommand() → [运行中] → onDestroy()
onCreate():服务首次创建时调用(仅一次),用于初始化资源(如创建子线程)。
onStartCommand(Intent, int, int):每次通过startService()启动时调用,处理具体任务(需在此处开启子线程)。
onDestroy():服务销毁时调用,用于释放资源(如关闭线程、释放网络连接)。
  1. Bound Service 的生命周期
1
2
3
onCreate() → onBind() → [绑定中] → onUnbind() → onDestroy()
onBind(Intent):绑定服务时调用,返回IBinder对象(需自定义实现,封装服务的方法)。
onUnbind(Intent):所有组件解绑后调用,可返回true表示允许后续重新绑定。

Service 的代码实现

  1. 服务端

编写aidl文件定义接口,build构建后自动生成java文件,随后具体实现接口,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//IMyAidlInterface.aidl
package com.example.testservice;

interface IMyAidlInterface {
int sub(int a,int b);
}
//AIDLtest.java
package com.example.testservice;
import android.os.RemoteException;

public class AIDLtest extends IMyAidlInterface.Stub{
@Override
public int sub (int a,int b) throws RemoteException {
return a-b;
}
}

接着实现自定义的Service类,并在AndroidMainfest.xml中完成定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyService extends Service {

@Override
public void onCreate() {
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("aaa", "onStartCommand: ");

return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent){
Log.d("aaa", "onBind: ");
return new AIDLtest();
}
}
<service
android:name=".MyService"
android:exported="true"
android:enabled="true">
</service>
  1. 客户端

注意事项:从Android 11(API级别30)开始,应用默认无法看到其他应用的服务(包可见性限制)。

  1. 客户端需要做以下处理:
1
2
3
<queries>
<package android:name="com.example.testservice" />
</queries>
  1. 客户端需要启动服务,即在服务端调用onStartCommand时,调用方法startService即可
1
2
3
4
5
6
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"com.example.testservice", // 服务端包名
"com.example.testservice.MyService" // Service类全路径
));
startService(intent);
  1. 如果需要调用绑定服务调用aidl的某个接口,需要调用bindService并在onServiceConnected获取IBinder对象,通过IBinder的transact发起跨进程调用,具体过程如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
int result = 0;

try {
data.writeInterfaceToken("com.example.testservice.IMyAidlInterface"); // AIDL描述
data.writeInt(5);
data.writeInt(3);
// 2. 发起跨进程调用
binder.transact(
1, // 事务ID (值为1)
data,
reply,
0 // 同步调用标志
);

// 3. 读取结果
reply.readException(); // 检查是否有异常
result = reply.readInt();
Log.d(TAG, "result: " + result);
} catch (RemoteException e) {
e.printStackTrace();
} finally {
data.recycle();
reply.recycle();
}

}

@Override
public void onServiceDisconnected(ComponentName componentName) {

}
};
public void attack(View view) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"com.example.testservice", // 服务端包名
"com.example.testservice.MyService" // Service类全路径
));
bindService(intent, conn, Context.BIND_AUTO_CREATE);
}

其中Parcel用于数据传输,在这里负责参数和返回值的获取,writeInterfaceToken用于写入接口标识(验证服务端是否支持该接口),至于事务ID 为什么是1,这个就需要在aidl build之后的java文件中找到答案了,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public static abstract class Stub extends android.os.Binder implements com.example.testservice.IMyAidlInterface
{
/** Construct the stub at attach it to the interface. */
@SuppressWarnings("this-escape")
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.example.testservice.IMyAidlInterface interface,
* generating a proxy if needed.
*/
public static com.example.testservice.IMyAidlInterface asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.example.testservice.IMyAidlInterface))) {
return ((com.example.testservice.IMyAidlInterface)iin);
}
return new com.example.testservice.IMyAidlInterface.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
java.lang.String descriptor = DESCRIPTOR;
if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
data.enforceInterface(descriptor);
}
if (code == INTERFACE_TRANSACTION) {
reply.writeString(descriptor);
return true;
}
switch (code)
{
case TRANSACTION_sub:
{
int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();
int _result = this.sub(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
break;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
return true;
}
private static class Proxy implements com.example.testservice.IMyAidlInterface
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
@Override public int sub(int a, int b) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(a);
_data.writeInt(b);
boolean _status = mRemote.transact(Stub.TRANSACTION_sub, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_sub = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
}

该生成的java文件中包含了Binder进一步的数据传输代码,onTransact处理了方法的分发,根据ID=1分发到sub函数,ID为什么是1,有如下定义

1
static final int TRANSACTION_sub = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);

其中FIRST_CALL_TRANSACTION 在IBinder.class定义如下

1
2
3
4
5
6
7
8
int DUMP_TRANSACTION = 1598311760;
int FIRST_CALL_TRANSACTION = 1;
int FLAG_ONEWAY = 1;
int INTERFACE_TRANSACTION = 1598968902;
int LAST_CALL_TRANSACTION = 16777215;
int LIKE_TRANSACTION = 1598835019;
int PING_TRANSACTION = 1599098439;
int TWEET_TRANSACTION = 1599362900;

因此,默认情况下aidl定义的接口调用编号从1开始,然后就是2,3,4等

Service 的常见漏洞

  1. 导出组件和权限不足导致的Binder接口被恶意调用,获取隐私权限或者打开受保护组件等

Android中的四大组件(三)—— Provider

在 Android 四大组件中,Activity负责用户交互,Service处理后台任务,BroadcastReceiver实现跨组件通信,而ContentProvider则是专为数据共享而生的核心组件。它打破了 Android 应用的 “沙箱隔离” 机制,让不同应用能够安全、规范地访问彼此的数据,是实现跨应用数据交互的标准化解决方案。

ContentProvider 是什么

Android 系统为了保障应用安全,采用 “沙箱机制”:每个应用的数据默认仅对自身可见,其他应用无法直接访问(如私有目录下的文件、数据库等)。但实际开发中,跨应用数据共享的需求普遍存在 —— 例如:读取系统联系人、获取相册图片、甚至自定义应用间的数据交互(如社交应用共享用户信息给第三方工具)。

ContentProvider正是为解决这一问题而设计的组件:它封装了数据访问逻辑,提供统一的接口(类似 “数据网关”),允许其他应用通过标准化的方式(如增删改查)访问其管理的数据,同时通过权限控制确保数据安全。

ContentProvider 的核心原理

ContentProvider 的工作机制可概括为 “URI 标识数据,ContentResolver 统一访问”,具体流程如下:

  1. 数据的唯一标识:URI

ContentProvider 通过URI(统一资源标识符) 来标识其管理的数据,格式如下: content://authority/path/[id]

  • content://:固定前缀,表明这是 ContentProvider 的 URI。
  • authority:唯一标识 ContentProvider 的字符串(通常用应用包名,如com.example.app.provider),避免不同应用的 ContentProvider 冲突。
  • path:数据路径,用于区分不同类型的数据(如/users表示用户表,/messages表示消息表)。
  • id:可选,指定某条具体数据的 ID(如/users/1表示 ID 为 1 的用户)。

例如,系统联系人的 URI 可能为:content://com.android.contacts/contacts(所有联系人)或content://com.android.contacts/contacts/1(ID 为 1 的联系人)。

  1. 访问入口:ContentResolver

客户端应用不会直接操作 ContentProvider,而是通过ContentResolver对象调用方法。ContentResolver 是 Android 系统提供的 “中介”,它会根据 URI 找到对应的 ContentProvider,再执行具体操作(用于解耦客户端与服务端)。

获取 ContentResolver 的方式:

1
ContentResolver resolver = getContentResolver();
  1. 跨进程通信的封装

ContentProvider 底层依赖Binder****机制实现跨进程通信,但开发者无需手动处理 IPC(跨进程通信)细节 ——ContentProvider 已封装了这一过程,客户端只需通过 ContentResolver 调用方法,即可与其他进程的 ContentProvider 交互。

ContentProvider 的核心类与方法

  1. ContentProvider 类

自定义 ContentProvider 需继承ContentProvider,并实现以下核心方法(这些方法会被 ContentResolver 调用):

  • onCreate():初始化 ContentProvider(如创建数据库),在进程启动时调用,返回true表示初始化成功。
  • query(Uri, String[], String, String[], String):查询数据,参数对应 SQL 的SELECT语句(URI、查询列、where 条件、where 参数、排序),返回Cursor结果集。
  • insert(Uri, ContentValues):插入数据,返回新插入数据的 URI(包含 ID)。
  • update(Uri, ContentValues, String, String[]):更新数据,返回受影响的行数。
  • delete(Uri, String, String[]):删除数据,返回受影响的行数。
  1. 辅助工具类
  • UriMatcher:用于匹配 URI,将不同的 URI 映射到整数标识(类似 “方法 ID”),便于在 CRUD 方法中区分操作的数据表。 示例:
1
2
3
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);// 匹配用户列表URI:content://com.example.provider/users
matcher.addURI("com.example.provider", "users", USER_LIST);// 匹配单个用户URI:content://com.example.provider/users/1
matcher.addURI("com.example.provider", "users/#", USER_ITEM);
  • ContentUris:用于处理 URI 中的 ID,提供withAppendedId(Uri, long)(给 URI 添加 ID)和parseId(Uri)(从 URI 中解析 ID)等方法。

ContentProvider 的代码实现

  1. 服务端

AndroidMainfest.xml定义provider组件,设置基本的className和ContentProvider唯一标识符authorities,并设置为导出组件方便客户端调用。

1
2
3
4
5
<provider
android:name=".StudentProvider"
android:authorities="com.example.app.provider"
android:exported="true" >
</provider>

实现自定义的StudentProvider类,继承自ContentProvider,在内部分别实现onCreate,query等所需函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class StudentProvider extends ContentProvider {
private SQLiteDatabase db;
public static final String AUTHORITY = "com.example.app.provider";
public static class Entry implements BaseColumns {
public static final String TABLE_NAME = "students";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_GRADE = "grade";
}
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

static {
uriMatcher.addURI(AUTHORITY, "students", 1); // 查询所有
uriMatcher.addURI(AUTHORITY, "students/#", 2); // 按ID查询
}

@Override
public boolean onCreate() {
// 初始化数据库
SQLiteOpenHelper helper = new SQLiteOpenHelper(getContext(), "school.db", null, 1) {
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + Entry.TABLE_NAME + " (" +
_ID + " INTEGER PRIMARY KEY, " +
Entry.COLUMN_NAME + " TEXT, " +
Entry.COLUMN_GRADE + " INTEGER)");
}

@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

}
};
db = helper.getWritableDatabase();
return true;
}

// 核心方法:查询
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {

switch (uriMatcher.match(uri)) {
case 1: // 查询所有学生
return db.query(Entry.TABLE_NAME,
projection, selection, selectionArgs,
null, null, sortOrder);

case 2: // 按ID查询
String id = uri.getLastPathSegment();
return db.query(Entry.TABLE_NAME,
projection, _ID + "=?",
new String[]{id}, null, null, null);

default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}

@Nullable
@Override
public String getType(@NonNull Uri uri) {
return "";
}

// 插入数据
@Override
public Uri insert(Uri uri, ContentValues values) {
long id = db.insert(Entry.TABLE_NAME, null, values);
return ContentUris.withAppendedId(uri, id);
}

@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}

@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
return 0;
}
}
  1. 客户端

客户端需确保CONTENT_URI(表)的路径正确并利用ContentResolver的query,insert等函数与服务端通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static final String AUTHORITY = "com.example.app.provider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/students");

public static class Entry implements BaseColumns {
public static final String COLUMN_NAME = "name";
public static final String COLUMN_GRADE = "grade";
}

public void attack(View view) {
// 插入新学生
ContentValues values = new ContentValues();
values.put(Entry.COLUMN_NAME, "张三");
values.put(Entry.COLUMN_GRADE, 90);

getContentResolver().insert(
CONTENT_URI,
values
);

// 查询所有学生
Cursor cursor = getContentResolver().query(
CONTENT_URI,
null, null, null, null
);

if (cursor != null) {
while (cursor.moveToNext()) {
@SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex(Entry.COLUMN_NAME));
@SuppressLint("Range") int grade = cursor.getInt(cursor.getColumnIndex(Entry.COLUMN_GRADE));
Log.d("Student", name + ": " + grade);
}
cursor.close();
}

需要注意的是,在Android 11之后,客户端访问服务端的Provider时需要声明所需要的Provider,否则访问不到服务端的Provider。示例如下:

1
2
3
4
<!-- 声明需要访问的Provider -->
<queries>
<provider android:authorities="com.example.app.provider" />
</queries>

ContentProvider 的常见漏洞

  1. 权限配置不当:未授权访问敏感数据

ContentProvider 的安全依赖于AndroidManifest.xml中的权限配置和exported属性:

  • exported="true"表示允许其他应用访问该 Provider;
  • readPermission/writePermission用于限制读写权限。

若配置存在以下问题,会导致未授权访问:

  1. exported="true"但未设置权限:其他应用无需任何权限即可访问 Provider,直接暴露数据;
  2. 权限定义过于宽松:如使用系统默认权限(如android.permission.READ_EXTERNAL_STORAGE)而非自定义权限,导致权限被滥用;

某金融类 App 的 ContentProvider 用于存储用户交易记录,开发者在 Manifest 中声明时未设置readPermission,且因添加了intent-filter导致exported默认为true

1
2
3
4
5
6
7
8
<provider
android:name=".TransactionProvider"
android:authorities="com.finance.transaction"
android:exported="true"> <!-- 或因intent-filter默认true -->
<intent-filter>
<action android:name="com.finance.READ_TRANSACTION" />
</intent-filter>
</provider>

恶意应用可直接通过ContentResolver查询该 Provider,获取用户银行卡号、交易金额等敏感信息:

1
2
3
ContentResolver resolver = getContentResolver();
Uri uri = Uri.parse("content://com.finance.transaction/records");
Cursor cursor = resolver.query(uri, null, null, null, null); // 无需权限即可查询

修复建议:

  • 显式设置exported属性:非必要共享的 Provider 设为exported="false"
  • 为共享 Provider 配置自定义权限(如com.example.provider.READ),并设置readPermission/writePermission
  1. 数据访问控制不严:越权操作与数据篡改

ContentProvider 的核心逻辑(query/insert/update/delete)若未对访问范围进行严格校验,会导致越权操作:

  • URI 校验缺失:未使用UriMatcher严格匹配合法 URI,允许恶意 URI 访问非预期数据(如访问其他用户的记录、系统表等);
  • 调用者身份未校验:未检查访问者的包名 / 签名,导致有权限的恶意应用越权操作(如 A 应用仅允许 B 应用访问,但未校验 B 的签名,被 C 应用伪造权限访问);
  • 参数过滤不严:对传入的selection(查询条件)、values(插入数据)未过滤,允许恶意参数篡改数据(如修改他人信息、删除关键记录)。

某社交 App 的 ContentProvider 允许查询用户资料,但其query方法未校验 URI 路径,导致恶意应用通过构造 URI 访问所有用户的隐私数据:

1
2
3
4
5
6
7
8
9
// 漏洞代码:未严格校验URI,仅简单判断路径
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
if (uri.getPath().contains("user")) { // 宽松匹配,可被绕过
return db.query("user", projection, selection, selectionArgs, null, null, sortOrder);
}
return null;
}
//恶意应用构造 URI content://com.social.provider/user/../all_users(通过路径遍历),即可查询all_users表中的所有用户数据(包括手机号、住址等)。

修复建议:

  • 使用UriMatcher严格匹配合法 URI,拒绝未定义的 URI 访问;
  • 在 Provider 方法中校验调用者身份:通过getCallingPackage()获取包名,结合PackageManager验证签名(仅允许信任的应用访问);
  • 对查询条件、插入数据进行过滤,禁止 SQL 关键字(如DROP、DELETE)或敏感字段(如admin)的恶意参数。
  1. 文件路径遍历:泄露应用私有文件

部分 ContentProvider 用于提供文件访问(如读取应用内的图片、文档),若未限制文件路径,允许通过../等符号遍历到应用的私有目录(如/data/data/包名/),会导致私有文件被泄露。

某教育 App 的 ContentProvider 允许访问应用内的课件文件,但其未限制文件路径:

1
2
3
4
5
6
7
// 漏洞代码:直接使用URI中的路径访问文件
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
String filePath = getContext().getFilesDir() + uri.getPath(); // 拼接路径,未过滤
return ParcelFileDescriptor.open(new File(filePath), ParcelFileDescriptor.MODE_READ_ONLY);
}
//恶意应用构造 URI content://com.education.provider/files/../databases/user.db,通过路径遍历访问到应用的私有数据库user.db,获取所有用户的账号密码。

修复建议:

  • 直接使用FileProvider共享文件,Android 支持库提供,自动限制路径。

Android中的四大组件(四)—— Receiver

在 Android 四大组件中,Broadcast Receiver(广播接收器)是负责响应系统或应用发出的 “广播消息” 的组件。它如同现实中的 “信号接收器”,能跨组件、跨应用传递事件,是实现松散耦合通信的核心工具。

Receiver 是什么

Receiver 的核心作用是接收并响应广播事件。这些事件可以是系统级的(如网络连接变化、电池电量低),也可以是应用级的(如数据更新、用户操作完成)。其设计理念源于 “发布 - 订阅模式”:发送者(Publisher)发布事件,Receiver(Subscriber)订阅并处理事件,两者无需直接关联,实现了解耦。

例如:

  • 系统在网络从 4G 切换到 WiFi 时,会发送android.net``.conn.CONNECTIVITY_CHANGE广播;
  • 应用完成文件下载后,可发送自定义广播com.example.DOWNLOAD_COMPLETE,通知其他组件更新 UI。

Receiver 的两种核心类型

根据广播的传递机制,可分为标准广播和有序广播,两者的行为差异直接影响使用场景。

  1. 标准广播(Normal Broadcast)
  • 特点:完全异步的广播,所有 Receiver 几乎同时收到事件,无法被拦截或修改。
  • 传递流程:发送者调用sendBroadcast(Intent)后,系统将广播同时分发给所有注册的 Receiver,不存在优先级顺序。
  • 适用场景:无需优先级处理、无需拦截的事件(如 “应用数据更新” 通知)。
  • 示例:
1
2
3
// 发送标准广播
Intent intent = new Intent("com.example.NORMAL_BROADCAST");
sendBroadcast(intent);
  1. 有序广播(Ordered Broadcast)
  • 特点:同步传递的广播,按 Receiver 的优先级依次接收,高优先级 Receiver 可拦截广播或修改数据。
  • 传递流程:
    • 发送者调用sendOrderedBroadcast(Intent, String),指定权限(可选);
    • 系统按 Receiver 声明的优先级(android:priority,值越大优先级越高)排序;
    • 高优先级 Receiver 先接收,可通过abortBroadcast()终止广播,低优先级 Receiver 将无法收到;
    • Receiver 可通过setResultData()修改数据,后续 Receiver 通过getResultData()获取修改后的数据。
  • 适用场景:需要优先级处理或拦截的事件(如短信拦截、电话黑名单)。
  • 示例:短信拦截场景中,安全应用的 Receiver 优先级高于系统短信应用,可拦截垃圾短信。

Receiver 的两种注册方式

Receiver 需要 “注册” 才能接收广播,根据注册时机分为静态注册和动态注册,两者适用场景截然不同。

  1. 静态注册:Manifest 中声明(全局生效)

AndroidManifest.xml中声明 Receiver,由系统负责管理,即使应用未启动也能接收广播。

  • 注册格式:
1
2
3
4
5
6
7
8
9
10
11
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="false" // 是否允许其他应用发送广播到该Receiver
android:priority="1000"> // 仅有序广播有效,优先级值(-1000~1000)
<intent-filter>
<!-- 接收的广播类型 -->
<action android:name="android.intent.action.BOOT_COMPLETED" /> <!-- 开机完成 -->
<action android:name="com.example.MY_ACTION" /> <!-- 自定义广播 -->
</intent-filter>
</receiver>
  • 特点:
    • 优点:应用未启动时也能响应(如开机启动、时区变化);
    • 缺点:灵活性低,无法动态注销,且受 Android 系统版本限制(见下文 “版本适配”)。
  1. 动态注册:代码中注册(随组件生命周期

在 Activity/Fragment/Service 等组件中通过代码注册,与组件生命周期绑定,组件销毁前需手动注销。

  • 注册与注销流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {
private MyReceiver myReceiver;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1. 创建Receiver实例
myReceiver = new MyReceiver();
// 2. 定义要接收的广播
IntentFilter filter = new IntentFilter();
filter.addAction("android.net.conn.CONNECTIVITY_CHANGE"); // 网络变化
filter.setPriority(1000); // 有序广播优先级
// 3. 注册Receiver
registerReceiver(myReceiver, filter);
}

@Override
protected void onDestroy() {
super.onDestroy();
// 4. 必须注销,否则内存泄漏
unregisterReceiver(myReceiver);
}
}
  • 特点:
    • 优点:灵活(可动态开启 / 关闭),不受静态广播限制,适合与组件生命周期关联的场景(如页面监听网络状态);
    • 缺点:应用未启动或组件销毁后无法接收广播。

Receiver 的生命周期

Receiver 的生命周期是四大组件中最简单的,核心仅一个方法:

1
onReceive(Context context, Intent intent) → [销毁]
  • onReceive():广播到达时调用,接收intent中的数据(如intent.getAction()获取事件类型,intent.getStringExtra()获取附加数据)。
    • 注意:此方法运行在主线程(UI 线程),执行时间不能超过 10 秒,否则会触发 ANR(应用无响应)。
    • 若需处理耗时操作(如下载文件),需通过IntentServiceJobIntentService(Android O+)处理,不能直接在onReceive()中开线程(可能被系统杀死)。
  • 生命周期结束:onReceive()执行完毕后,Receiver 实例会被系统立即销毁,无法长期持有状态。

Receiver 的代码实现

  1. 静态注册->接收端

静态注册比较简单,在AndroidMainfest中定义Receiver并在类中实现onReceive函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 静态注册广播接收器 -->
<receiver
android:name=".SimpleReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="com.example.simple.BROADCAST_MESSAGE" />
<!-- Android 12+ 必须添加 -->
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
public class SimpleReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String message = intent.getStringExtra("message");
Log.d("Eee", "onReceive: " + message);
}
}
  1. 动态注册->接收端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MainActivity extends AppCompatActivity {
private static final String TAG = "BroadcastDemo";
private static final String CUSTOM_ACTION = "com.example.simple.BROADCAST_MESSAGE";
private BroadcastReceiver dynamicReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView infoText = findViewById(R.id.info_text);
infoText.setText("此应用作为广播接收方运行");
// 初始化广播接收器
dynamicReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String message = intent.getStringExtra("message");
Log.d(TAG, "动态接收器收到消息: " + message);
}
};
registerReceiver();
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private void registerReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(CUSTOM_ACTION);
// Android 8.0+ 需要明确指定接收器是否导出
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerReceiver(dynamicReceiver, filter, Context.RECEIVER_EXPORTED);
}else {
registerReceiver(dynamicReceiver, filter);
}
Log.d(TAG, "广播接收器已注册");
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(dynamicReceiver);
Log.d(TAG, "广播接收器已注销");
}
}
  1. 发送端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
// 创建广播Intent
Intent intent = new Intent(BROADCAST_ACTION);
// 指定接收方包名,Android 8.0 (API 26)后必须显示广播或者动态注册
intent.setPackage(RECEIVER_PACKAGE);
// 添加消息内容
intent.putExtra("message", "你好,接收方!这是来自发送方的广播");
intent.putExtra("timestamp", System.currentTimeMillis());
// 发送广播
sendBroadcast(intent);

Toast.makeText(this, "广播已发送", Toast.LENGTH_SHORT).show();
} catch (SecurityException e) {
Toast.makeText(this, "发送失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
}

Receiver 的常见漏洞

  1. android:exported="true且未设置权限保护时,任何应用均可发送广播触发接收器。
  2. 使用隐式Intent发送广播时,可能被恶意应用注册相同Action的接收器截获数据。