Android系统中的组件是应用的基本构建块,用于实现应用的功能和展现界面。组件可以被其他应用调用或访问,这可以通过设置组件的android:exported属性来控制。然而,如果exported属性设置不当,可能会导致严重的安全漏洞,例如数据泄露、隐私侵犯甚至恶意代码执行。
Android 安全开发之 exported 组件安全
- 一、安全渗透案例
- 二、解决方案
-
- [2.1 设置组件 exported false](#2.1 设置组件 exported false)
- [2.1 设置组件 exported true](#2.1 设置组件 exported true)
- [三、 exported 组件的安全最佳实践](#三、 exported 组件的安全最佳实践)
一、安全渗透案例
应用多个 exported 组件可绕过私密保险箱密码,直接读取私有目录下的私密文件。
1)DocViewActivity读取私密保险箱密码文件
bash
adb shell am start -n com.xxx.filemanager/com.xxx.safe.view.preview.DocViewActivity --es android.intent.extra.TITLE test_title --es safe_file_path /data/data/com.xxx.filemanager/shared_prefs/safe_record.xml
2)DocViewActivity、VideoPlayActivity、AudioPlayActivity、ImageViewActivity 在设置隐私保险箱密码后,可绕过密码直接播放保险箱中的文件
可以播放私密保险箱中的视频
bash
adb shell am start -n com.xxx.filemanager/com.xxx.safe.view.preview.VideoPlayActivity --es android.intent.extra.TITLE test_title --es safe_file_path /data/data/com.xxx.filemanager/app_private/4a992493f68406c22ae6a6af2e19be89
可以播放私密保险箱中的音频
bash
adb shell am start -n com.xxx.filemanager/com.xxx.safe.view.preview.AudioPlayActivity --es android.intent.extra.TITLE test_title --es safe_file_path /data/data/com.xxx.filemanager/app_private/5e98efcd742f79aa4eb5928f12bab778
可以播放私密保险箱中的图片
bash
adb shell am start -n com.xxx.filemanager/com.xxx.safe.view.preview.ImageViewActivity --es android.intent.extra.TITLE test_title --es safe_file_path /data/data/com.xxx.filemanager/app_private/e10dec177fc877e1ccb4b2b47d0596f8
修复方案:
- 需要确保私密保险箱秘密验证正确后,才能读app_private下的文件
- 限制应用沙箱文件的读取路径,只能读取/data/data/com.xxx.filemanager/app_private下的文件
二、解决方案
2.1 设置组件 exported false
如果此类预览文件 Activity 仅自己使用,可以将其设置exported="false"
,其他应用将无法访问。
xml
<activity android:name="com.xxx.safe.view.preview.AudioPlayActivity"
android:exported="false"
android:label="@string/audio_preview"
android:theme="@style/AppTheme.Dialog"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
<intent-filter>
<action android:name="com.xxx.intent.action.filemanager.VIEW" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="audio/*" />
</intent-filter>
</activity>
<activity android:name="com.xxx.safe.view.preview.VideoPlayActivity"
android:exported="false"
android:theme="@style/AppTheme.NoActionBar"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
<intent-filter>
<action android:name="com.xxx.intent.action.filemanager.VIEW" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<activity android:name="com.xxx.safe.view.preview.ImageViewActivity"
android:exported="false"
android:theme="@style/AppTheme.NoActionBar"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
<intent-filter>
<action android:name="com.xxx.intent.action.filemanager.VIEW" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity android:name="com.xxx.safe.view.preview.DocViewActivity"
android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="com.xxx.intent.action.filemanager.VIEW" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/*" />
<data android:mimeType="text/html"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="application/xhtml+xml"/>
<data android:mimeType="application/vnd.wap.xhtml+xml"/>
</intent-filter>
</activity>
由于设置exported="false"
,应用自身调用时候若使用隐式Intent也将无法跳转,需要指定包名PackageName
或组件名ComponentName
,才可以成功跳转。
java
private void openSelectedFile(EncryptionFileInfo info, String file) {
String originalPath = info.getOriginalPath();
String originName = SafeUtils.getOriginalFileName(originalPath);
int mediaType = info.getMediaType();
if (!mIsDoubleClick) {
mIsDoubleClick = true;
Intent intent = new Intent();
intent.setAction(SafeConstants.ACTION_SAFE_FILE_VIEW);
switch (mediaType) {
case MediaFile.AUDIO_TYPE:
intent.setType("audio/*");
break;
case MediaFile.VIDEO_TYPE:
intent.setType("video/*");
break;
case MediaFile.IMAGE_TYPE:
intent.setType("image/*");
break;
case MediaFile.DOC_TYPE:
MediaFileType mediaFileType = MediaFile.getFileType(originalPath);
intent.setType(mediaFileType == null ? "text/*" : mediaFileType.mimeType);
break;
default:
break;
}
intent.putExtra(SafeConstants.SAFE_FILE_PATH, file);
intent.putExtra(Intent.EXTRA_TITLE, originName);
intent.setPackage(mActivity.getPackageName());
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
mIsDoubleClick = false;
Toast.makeText(mActivity, R.string.open_fail, Toast.LENGTH_SHORT).show();
}
}
}
如果将组件设置属性exported="false"
,adb命令启动Activity则会抛出SecurityException
异常,无法启动Activity预览文件。
bash
server@dev-fj-srv:~/Desktop/$ adb shell am start -n com.xxx.filemanager/com.xxx.safe.view.preview.DocViewActivity --es android.intent.extra.TITLE test_title --es safe_file_path /data/data/com.xxx.filemanager/shared_prefs/safe_record.xml
Starting: Intent { cmp=com.xxx.filemanager/com.xxx.safe.view.preview.DocViewActivity (has extras) }
Exception: java.lang.SecurityException: Permission Denial: starting Intent { flg=0x10000000 cmp=com.xxx.filemanager/com.xxx.safe.view.preview.DocViewActivity (has extras) } from null (pid=15578, uid=2000) not exported from uid 10193
java.lang.SecurityException: Permission Denial: starting Intent { flg=0x10000000 cmp=com.xxx.filemanager/com.xxx.safe.view.preview.DocViewActivity (has extras) } from null (pid=15578, uid=2000) not exported from uid 10193
at com.android.server.wm.ActivityTaskSupervisor.checkStartAnyActivityPermission(ActivityTaskSupervisor.java:1164)
at com.android.server.wm.ActivityStarter.executeRequest(ActivityStarter.java:1194)
at com.android.server.wm.ActivityStarter.execute(ActivityStarter.java:833)
at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1371)
at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1301)
at com.android.server.am.ActivityManagerService.startActivityAsUserWithFeature(ActivityManagerService.java:3348)
at com.android.server.am.ActivityManagerShellCommand.runStartActivity(ActivityManagerShellCommand.java:731)
at com.android.server.am.ActivityManagerShellCommand.onCommand(ActivityManagerShellCommand.java:240)
at com.android.server.am.UnisocActivityManagerShellCommand.onCommand(UnisocActivityManagerShellCommand.java:92)
at com.android.modules.utils.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:97)
at android.os.ShellCommand.exec(ShellCommand.java:38)
at com.android.server.am.UnisocActivityManagerServiceImpl.onShellCommand(UnisocActivityManagerServiceImpl.java:430)
at android.os.Binder.shellCommand(Binder.java:1068)
at android.os.Binder.onTransact(Binder.java:888)
at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:5348)
at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2812)
at android.os.Binder.execTransactInternal(Binder.java:1344)
at android.os.Binder.execTransact(Binder.java:1275)
2.1 设置组件 exported true
如果需要通过部分应用使用,组件必须可导出exported="true"
,可以添加鉴权校验或者包名过滤,以免数据泄漏。
java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 进行鉴权校验
boolean isAuthorized = performAuthorizationCheck();
if (isAuthorized) {
// 鉴权通过,执行正常的处理逻辑
setContentView(R.layout.activity_main);
// ...
} else {
// 鉴权失败,执行相应的处理逻辑,比如显示错误提示或跳转到其他界面
showAuthorizationFailedMessage();
finish();
}
}
在上述代码中,首先调用performAuthorizationCheck()方法进行鉴权校验。如果鉴权通过,就继续执行正常的处理逻辑,比如设置布局、初始化界面等。如果鉴权失败,你可以根据需求执行相应的处理逻辑,例如显示错误提示或跳转到其他界面,并在最后调用finish()方法关闭当前Activity。
java
private boolean performAuthorizationCheck() {
// 自定义的鉴权逻辑
if (customAuthorizationLogic()) {
return true;
} else {
return false;
}
}
在performAuthorizationCheck()方法中,你可以根据你的应用程序的需求和安全策略实现相应的鉴权逻辑。以下是一些常见的鉴权处理方式:
- 用户权限验证:检查当前用户是否具有执行该操作的权限。这可能涉及到用户身份验证、角色检查或权限列表的验证。
- 用户登录状态验证:检查用户是否已登录或会话是否有效。你可以根据你的应用逻辑,验证用户的登录状态和会话令牌的有效性。
- 设备标识验证:验证启动请求是否来自于一个受信任的设备。你可以使用设备标识信息(如IMEI、Android ID等)与预先存储的信任设备列表进行比对。
三、 exported 组件的安全最佳实践
为了降低exported组件的安全风险,开发者应该遵循以下最佳实践:
1.最小化exported组件:只将必要的组件设置为exported,尽量减少暴露在外部的组件数量。
2.严格控制权限:只授予exported组件所需的最小权限,避免授予不必要的权限。
3.输入验证:对所有输入数据进行严格的验证,防止恶意数据注入。
4.输出过滤:对所有输出数据进行过滤,防止敏感数据泄露。
5.加密敏感数据:对敏感数据进行加密存储,防止被窃取。
6.使用安全的编码实践:遵循安全的编码实践,避免编写易受攻击的代码。
7.定期进行安全测试:定期对exported组件进行安全测试,发现并修复潜在的安全漏洞。
相关参考: