花了几周时间学习了一下Android的漏洞挖掘,聊聊我对Android漏洞挖掘的看法。个人认为相较于传统的混淆加壳等安全对抗研究,这个领域的难度不算很大,想要挖洞从0到1,可能只需要了解一下四大组件的生命周期以及常见的漏洞场景,比如说导出组件的非法调用,缺乏参数校验等,甚至可以直接结合MCP让AI代码审计一下反编译的java代码,就有可能出洞。所以Android漏洞挖掘想要体现技术难度更多的是在自动化工具的开发和研究中,当前自动化工具主要是静态规则扫描,污点分析,MCP+AI自动化分析等。目前看来静态规则扫描和污点分析的准确率更有保障,也是目前广泛运用的挖洞辅助方式,MCP+AI的方式上限更高,但是个人看来,当前这种方式的准确率仍有较大提升空间,后续的话可能想在漏洞自动化工具研究上继续学习一下。这里结合自己挖洞的经验,对Android四大组件漏洞进行一个总结和记录。
Android中的四大组件(一)—— Activity
Activity 是 Android 四大组件中最核心的组件,负责构建用户界面,是用户与应用交互的入口点
Activity 是什么
Activity是一个应用程序组件,提供一个屏幕让用户交互以执行某项任务。负责绘制用户界面,提供用户交互,对于一个app,用户所能见到的基本都是基于activity构成的。
Activity 的启动方式
Activity 组件有两种启动方式,一种是显示启动,一种是隐式启动。
- 显示启动
直接指定 Activity 的类名,明确启动特定组件。
1 2 3 4 5 6 7 8
| Intent intent = new Intent(this, TargetActivity.class); startActivity(intent);
Intent intent = new Intent(); intent.setClassName("com.example.targetapp", "com.example.targetapp.TargetActivity"); startActivity(intent);
|
- 隐式启动
不直接指定组件,而是声明一个操作意图(如ACTION_VIEW
),系统根据 Intent-filter 匹配合适的组件。
1 2 3 4 5 6 7 8 9
| 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 的常见漏洞
- 非法访问
对于一些有前置条件的activity,比如说重置密码的activity,如果直接导出且未添加权限,就容易被非法访问,直接进入重置密码界面进行修改密码,绕过了密码验证或者身份验证过程。
- 组件暴露+参数校验缺失
组件暴露 + 对 Intent 的参数未进行充分的参数校验,攻击者可通过构造特制 Intent 启动受保护组件,执行任意代码或获取敏感信息。
1 2 3 4 5 6 7 8 9 10
| Intent intent = new Intent(); intent.setAction("com.example.action.EDIT"); intent.setData(Uri.parse("file:///sdcard/important_data"));
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(绑定型服务),两者也可结合使用。
- 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:系统不会主动重启服务,适合一次性任务(如发送一条消息)。
- Bound Service:与组件双向通信,进程间通信
通过bindService(Intent, ServiceConnection, int)绑定到其他组件(如 Activity),两者建立持续的通信通道,组件可直接调用 Service 中的方法。
绑定与解绑流程:
绑定:调用bindService()后,系统回调 Service 的onBind()方法,返回IBinder对象(用于组件与服务通信)。
解绑:组件调用unbindService(),系统回调onUnbind();当所有绑定的组件都解绑后,Service 会自动销毁(回调onDestroy())。
特点:适合需要双向交互的场景(如音乐播放器:Activity 控制暂停 / 播放,Service 反馈当前播放进度)。
- 两种类型的结合使用
一个 Service 可以同时被启动和绑定:例如音乐播放器,通过startService()确保退出 APP 后继续播放(启动型),同时通过bindService()让 Activity 控制播放状态(绑定型)。此时需同时调用stopService()和unbindService()才能彻底销毁服务。
Service 的生命周期
Service 的生命周期比 Activity 更简单,但需严格区分启动型与绑定型的差异。
- Started Service 的生命周期
1 2 3 4
| onCreate() → onStartCommand() → [运行中] → onDestroy() onCreate():服务首次创建时调用(仅一次),用于初始化资源(如创建子线程)。 onStartCommand(Intent, int, int):每次通过startService()启动时调用,处理具体任务(需在此处开启子线程)。 onDestroy():服务销毁时调用,用于释放资源(如关闭线程、释放网络连接)。
|
- Bound Service 的生命周期
1 2 3
| onCreate() → onBind() → [绑定中] → onUnbind() → onDestroy() onBind(Intent):绑定服务时调用,返回IBinder对象(需自定义实现,封装服务的方法)。 onUnbind(Intent):所有组件解绑后调用,可返回true表示允许后续重新绑定。
|
Service 的代码实现
- 服务端
编写aidl文件定义接口,build构建后自动生成java文件,随后具体实现接口,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.example.testservice;
interface IMyAidlInterface { int sub(int a,int b); }
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>
|
- 客户端
注意事项:从Android 11(API级别30)开始,应用默认无法看到其他应用的服务(包可见性限制)。
- 客户端需要做以下处理:
1 2 3
| <queries> <package android:name="com.example.testservice" /> </queries>
|
- 客户端需要启动服务,即在服务端调用onStartCommand时,调用方法startService即可
1 2 3 4 5 6
| Intent intent = new Intent(); intent.setComponent(new ComponentName( "com.example.testservice", "com.example.testservice.MyService" )); startService(intent);
|
- 如果需要调用绑定服务调用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"); data.writeInt(5); data.writeInt(3); binder.transact( 1, data, reply, 0 );
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" )); 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 { @SuppressWarnings("this-escape") public Stub() { this.attachInterface(this, DESCRIPTOR); }
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; }
@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 的常见漏洞
- 导出组件和权限不足导致的Binder接口被恶意调用,获取隐私权限或者打开受保护组件等
Android中的四大组件(三)—— Provider
在 Android 四大组件中,Activity
负责用户交互,Service
处理后台任务,BroadcastReceiver
实现跨组件通信,而ContentProvider则是专为数据共享而生的核心组件。它打破了 Android 应用的 “沙箱隔离” 机制,让不同应用能够安全、规范地访问彼此的数据,是实现跨应用数据交互的标准化解决方案。
ContentProvider 是什么
Android 系统为了保障应用安全,采用 “沙箱机制”:每个应用的数据默认仅对自身可见,其他应用无法直接访问(如私有目录下的文件、数据库等)。但实际开发中,跨应用数据共享的需求普遍存在 —— 例如:读取系统联系人、获取相册图片、甚至自定义应用间的数据交互(如社交应用共享用户信息给第三方工具)。
ContentProvider
正是为解决这一问题而设计的组件:它封装了数据访问逻辑,提供统一的接口(类似 “数据网关”),允许其他应用通过标准化的方式(如增删改查)访问其管理的数据,同时通过权限控制确保数据安全。
ContentProvider 的核心原理
ContentProvider 的工作机制可概括为 “URI 标识数据,ContentResolver 统一访问”,具体流程如下:
- 数据的唯一标识: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 的联系人)。
- 访问入口:ContentResolver
客户端应用不会直接操作 ContentProvider,而是通过ContentResolver对象调用方法。ContentResolver 是 Android 系统提供的 “中介”,它会根据 URI 找到对应的 ContentProvider,再执行具体操作(用于解耦客户端与服务端)。
获取 ContentResolver 的方式:
1
| ContentResolver resolver = getContentResolver();
|
- 跨进程通信的封装
ContentProvider 底层依赖Binder****机制实现跨进程通信,但开发者无需手动处理 IPC(跨进程通信)细节 ——ContentProvider 已封装了这一过程,客户端只需通过 ContentResolver 调用方法,即可与其他进程的 ContentProvider 交互。
ContentProvider 的核心类与方法
- 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[]):删除数据,返回受影响的行数。
- 辅助工具类
- UriMatcher:用于匹配 URI,将不同的 URI 映射到整数标识(类似 “方法 ID”),便于在 CRUD 方法中区分操作的数据表。 示例:
1 2 3
| UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); matcher.addURI("com.example.provider", "users", USER_LIST); matcher.addURI("com.example.provider", "users/#", USER_ITEM);
|
- ContentUris:用于处理 URI 中的 ID,提供
withAppendedId(Uri, long)
(给 URI 添加 ID)和parseId(Uri)
(从 URI 中解析 ID)等方法。
ContentProvider 的代码实现
- 服务端
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); }
@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: 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; } }
|
- 客户端
客户端需确保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
| <queries> <provider android:authorities="com.example.app.provider" /> </queries>
|
ContentProvider 的常见漏洞
- 权限配置不当:未授权访问敏感数据
ContentProvider 的安全依赖于AndroidManifest.xml
中的权限配置和exported
属性:
exported="true"
表示允许其他应用访问该 Provider;
readPermission
/writePermission
用于限制读写权限。
若配置存在以下问题,会导致未授权访问:
exported="true"
但未设置权限:其他应用无需任何权限即可访问 Provider,直接暴露数据;
- 权限定义过于宽松:如使用系统默认权限(如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> <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
;
- 数据访问控制不严:越权操作与数据篡改
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
| @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; }
|
修复建议:
- 使用UriMatcher严格匹配合法 URI,拒绝未定义的 URI 访问;
- 在 Provider 方法中校验调用者身份:通过getCallingPackage()获取包名,结合PackageManager验证签名(仅允许信任的应用访问);
- 对查询条件、插入数据进行过滤,禁止 SQL 关键字(如DROP、DELETE)或敏感字段(如admin)的恶意参数。
- 文件路径遍历:泄露应用私有文件
部分 ContentProvider 用于提供文件访问(如读取应用内的图片、文档),若未限制文件路径,允许通过../等符号遍历到应用的私有目录(如/data/data/包名/),会导致私有文件被泄露。
某教育 App 的 ContentProvider 允许访问应用内的课件文件,但其未限制文件路径:
1 2 3 4 5 6 7
| @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); }
|
修复建议:
- 直接使用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 的两种核心类型
根据广播的传递机制,可分为标准广播和有序广播,两者的行为差异直接影响使用场景。
- 标准广播(Normal Broadcast)
- 特点:完全异步的广播,所有 Receiver 几乎同时收到事件,无法被拦截或修改。
- 传递流程:发送者调用
sendBroadcast(Intent)
后,系统将广播同时分发给所有注册的 Receiver,不存在优先级顺序。
- 适用场景:无需优先级处理、无需拦截的事件(如 “应用数据更新” 通知)。
- 示例:
1 2 3
| Intent intent = new Intent("com.example.NORMAL_BROADCAST"); sendBroadcast(intent);
|
- 有序广播(Ordered Broadcast)
- 特点:同步传递的广播,按 Receiver 的优先级依次接收,高优先级 Receiver 可拦截广播或修改数据。
- 传递流程:
- 发送者调用
sendOrderedBroadcast(Intent, String)
,指定权限(可选);
- 系统按 Receiver 声明的优先级(
android:priority
,值越大优先级越高)排序;
- 高优先级 Receiver 先接收,可通过
abortBroadcast()
终止广播,低优先级 Receiver 将无法收到;
- Receiver 可通过
setResultData()
修改数据,后续 Receiver 通过getResultData()
获取修改后的数据。
- 适用场景:需要优先级处理或拦截的事件(如短信拦截、电话黑名单)。
- 示例:短信拦截场景中,安全应用的 Receiver 优先级高于系统短信应用,可拦截垃圾短信。
Receiver 的两种注册方式
Receiver 需要 “注册” 才能接收广播,根据注册时机分为静态注册和动态注册,两者适用场景截然不同。
- 静态注册: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 系统版本限制(见下文 “版本适配”)。
- 动态注册:代码中注册(随组件生命周期)
在 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); myReceiver = new MyReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction("android.net.conn.CONNECTIVITY_CHANGE"); filter.setPriority(1000); registerReceiver(myReceiver, filter); }
@Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(myReceiver); } }
|
- 特点:
- 优点:灵活(可动态开启 / 关闭),不受静态广播限制,适合与组件生命周期关联的场景(如页面监听网络状态);
- 缺点:应用未启动或组件销毁后无法接收广播。
Receiver 的生命周期
Receiver 的生命周期是四大组件中最简单的,核心仅一个方法:
1
| onReceive(Context context, Intent intent) → [销毁]
|
- onReceive():广播到达时调用,接收
intent
中的数据(如intent.getAction()
获取事件类型,intent.getStringExtra()
获取附加数据)。
- 注意:此方法运行在主线程(UI 线程),执行时间不能超过 10 秒,否则会触发 ANR(应用无响应)。
- 若需处理耗时操作(如下载文件),需通过
IntentService
或JobIntentService
(Android O+)处理,不能直接在onReceive()
中开线程(可能被系统杀死)。
- 生命周期结束:
onReceive()
执行完毕后,Receiver 实例会被系统立即销毁,无法长期持有状态。
Receiver 的代码实现
- 静态注册->接收端
静态注册比较简单,在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" /> <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 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); 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| try { Intent intent = new Intent(BROADCAST_ACTION); 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 的常见漏洞
- 当
android:exported="true
且未设置权限保护时,任何应用均可发送广播触发接收器。
- 使用隐式Intent发送广播时,可能被恶意应用注册相同Action的接收器截获数据。