- 标题:SVE-2021-23076 (CVE-2021-25510, CVE-2021-25511): Camera privilege escalation and arbitrary file write in FilterProvider (system_app) in Samsung Device
- 描述:An improper validation vulnerability in FilterProvider prior to SMR Dec-2021 Release 1 allows local privilege escalation. The patch adds proper validation logic to prevent privilege escalation.
- 二进制的位置在
/system/app/FilterProvider/FilterProvider.apk
- 所有组件中唯一能直接进来的就是这个PackageIntentReceiver
<receiver android:exported="true" android:name="com.samsung.android.provider.filterprovider.PackageIntentReceiver">
<intent-filter>
<action android:name="com.samsung.android.provider.filterprovider.INTENT_PACKAGE_ADDED"/>
<action android:name="com.samsung.android.provider.filterprovider.INTENT_PACKAGE_REMOVED"/>
</intent-filter>
<intent-filter>
<action android:name="com.samsung.filterinstaller.INSTALL_FILTER"/>
<data android:scheme="package" android:sspPrefix="com.candycamera.android.filter"/>
<data android:scheme="package" android:sspPrefix="com.ucam.filters.forsamsung"/>
<data android:scheme="package" android:sspPrefix="com.pinguo.camera360filter"/>
<data android:scheme="package" android:sspPrefix="com.linecorp.aillis.filter"/>
<data android:scheme="package" android:sspPrefix="com.linecorp.b612.filter"/>
<data android:scheme="package" android:sspPrefix="com.seerslab.filter"/>
<data android:scheme="package" android:sspPrefix="com.samsung.android.filter.effect"/>
</intent-filter>
</receiver>
<provider android:authorities="com.samsung.android.provider.filterprovider" android:exported="true" android:name="com.samsung.android.provider.filterprovider.FilterProvider" android:readPermission="com.samsung.android.provider.filterprovider.permission.READ_FILTER" android:writePermission="com.samsung.android.provider.filterprovider.permission.WRITE_FILTER"/>
<permission android:description="@string/permission_read_filter_desc" android:label="@string/permission_read_filter" android:name="com.samsung.android.provider.filterprovider.permission.READ_FILTER" android:protectionLevel="normal"/>
<permission android:description="@string/permission_write_filter_desc" android:label="@string/permission_write_filter" android:name="com.samsung.android.provider.filterprovider.permission.WRITE_FILTER" android:protectionLevel="signature"/>
- onReceiver之后,先进行一番权限的检查,权限检查通过就启动FilterPackageService服务
@Override // android.content.BroadcastReceiver
public void onReceive(Context arg16, Intent arg17) {
String v13;
if(arg17.getDataString() == null) {
Log.i(PackageIntentReceiver.TAG, "DataString is null : " + arg17.getAction());
return;
}
String v3 = arg17.getAction();
String v0 = arg17.getDataString().substring(8);
if("com.samsung.filterinstaller.INSTALL_FILTER_LIST".equals(v3)) {
//...
}
Log.i(PackageIntentReceiver.TAG, "action : " + v3);
Log.i(PackageIntentReceiver.TAG, "pkgName : " + v0);
if(("com.samsung.android.provider.filterprovider.INTENT_PACKAGE_ADDED".equals(v3)) || ("com.samsung.filterinstaller.INSTALL_FILTER".equals(v3))) {
try {
PackageManager v11_1 = arg16.getPackageManager();
if(v11_1 == null) {
return;
}
PackageInfo v0_5 = v11_1.getPackageInfo(v0, 0x1000); // package name validation bypass
if(v0_5 == null || v0_5.requestedPermissions == null) {
Log.e(PackageIntentReceiver.TAG, "This package does not have permission");
return;
}
if(!Arrays.asList(v0_5.requestedPermissions).contains("com.sec.android.camera.permission.USE_EFFECT_FILTER")) { // request permissions validation bypass
Log.e(PackageIntentReceiver.TAG, "This package is not a effect filter");
return;
Log.e(PackageIntentReceiver.TAG, "This package does not have permission");
return;
}
}
catch(PackageManager.NameNotFoundException v0_4) {
Log.e(PackageIntentReceiver.TAG, "onReceive NameNotFoundException : " + v0_4.toString());
}
}
try {
Intent v0_7 = new Intent(arg16, FilterPackageService.class);
if(("com.samsung.android.provider.filterprovider.INTENT_PACKAGE_ADDED".equals(v3)) || ("com.samsung.android.provider.filterprovider.INTENT_PACKAGE_REMOVED".equals(v3)) || ("com.samsung.filterinstaller.INSTALL_FILTER".equals(v3)) || ("com.samsung.filterinstaller.INSTALL_FILTER_LIST".equals(v3))) {
v0_7.fillIn(arg17, 2);
arg16.startService(v0_7);
return;
}
}
catch(Exception v0_6) {
Log.e(PackageIntentReceiver.TAG, "onReceive exception : " + v0_6.toString());
return;
}
}
- 这个校验写的是让我开幕雷击,这里面至少能想到两个绕过的思路
- 首先是包名字段直接受控于dataString这个外部传入的数据,可以直接伪造sspPrefix里面指定的几个包名就可以了
- 其次requestedPermissions只代表AndroidManifest.xml中定义的权限,和实际应用是否被授予了这个权限无关
- 所以这里面的绕过思路就是,首先配置成白名单的URI和包名,然后再在AndroidManifest.xml定义一下权限
com.sec.android.camera.permission.USE_EFFECT_FILTER
即可。 - 测试代码以及输出
try {
PackageManager pm = getPackageManager();
PackageInfo pi = pm.getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
Log.d(TAG, "pi.requestedPermissions.length = " + pi.requestedPermissions.length);
for (String requestPermission : pi.requestedPermissions) {
Log.d(TAG, "requestedPermission: " + requestPermission);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
D/STest: pi.requestedPermissions.length = 1
D/STest: requestedPermission: com.sec.android.camera.permission.USE_EFFECT_FILTER
- 扩展思考:其实这个地方想做权限控制非常简单,直接用checkCallingPermission就完了,不知道三星这是在搞什么。
- 启动FilterPackageService服务之后,是发了一个Handler消息
@Override // android.app.Service
public int onStartCommand(Intent arg5, int arg6, int arg7) {
if(arg5 == null) {
Log.i(FilterPackageService.TAG, "onStartCommand(null)");
return 2;
}
String v0 = arg5.getAction();
this.mStartId = arg7;
Message v7 = this.mServiceHandler.obtainMessage();
Log.v(FilterPackageService.TAG, "onStartCommand(" + v0 + ")");
if("com.samsung.android.provider.filterprovider.INTENT_PACKAGE_ADDED".equals(v0)) {
v7.arg1 = 1;
v7.obj = arg5.getData().toString();
this.mServiceHandler.sendMessage(v7);
return 1;
}
if("com.samsung.android.provider.filterprovider.INTENT_PACKAGE_REMOVED".equals(v0)) {
v7.arg1 = 2;
v7.obj = arg5.getData().toString();
this.mServiceHandler.sendMessage(v7);
return 1;
}
if("com.samsung.filterinstaller.INSTALL_FILTER".equals(v0)) {
v7.arg1 = 1;
v7.setData(arg5.getExtras());
v7.obj = arg5.getData().toString();
this.mServiceHandler.sendMessage(v7);
return 1;
}
if("com.samsung.filterinstaller.INSTALL_FILTER_LIST".equals(v0)) {
v7.arg1 = 3;
v7.obj = arg5.getParcelableArrayListExtra("filter_package_list");
this.mServiceHandler.sendMessage(v7);
return 1;
}
this.stopSelfResult(this.mStartId);
return 1;
}
@Override // android.os.Handler
public void handleMessage(Message arg8) {
String v0_1;
if(FilterPackageService.VERBOSE_LOGGING) {
Log.v(FilterPackageService.TAG, "handleMessage " + arg8.arg1);
}
PackageManager v0 = FilterPackageService.this.getPackageManager();
if(v0 == null) {
Log.e(FilterPackageService.TAG, "There is no packageManager returned");
return;
}
if(v0.getApplicationEnabledSetting("com.samsung.android.provider.filterprovider") == 2) {
Log.e(FilterPackageService.TAG, "FilterProvider application is not enabled");
return;
}
if(arg8.arg1 == 3) {
v0_1 = null;
}
else {
//...
}
int v4 = arg8.arg1;
if(v4 == 1) {
new FilterInstaller(FilterPackageService.this.getApplicationContext(), v0_1).installFilters();
}
else if(v4 == 2) {
new FilterInstaller(FilterPackageService.this, v0_1).uninstallFilters();
}
else if(v4 == 3) {
Log.d(FilterPackageService.TAG, "ACTION_LIST_ADDED");
if(arg8.obj != null) {
ArrayList v8 = (ArrayList)arg8.obj;
int v0_2;
for(v0_2 = 0; v0_2 < v8.size(); ++v0_2) {
Log.d(FilterPackageService.TAG, ((String)v8.get(v0_2)));
new FilterInstaller(FilterPackageService.this.getApplicationContext(), ((String)v8.get(v0_2))).installFilters(true);
}
}
FilterNotifyUtil.NotifyChange(FilterPackageService.this.getApplicationContext(), 2, null);
}
int v0_3 = FilterPackageService.this.mStartId;
FilterPackageService.this.stopSelfResult(v0_3);
}
- 可以看到1号消息是安装一个Filters,2号消息是卸载Filters,而3号消息是安装一个Filters列表,这里我们就关注1号消息
void installFilters(boolean arg4) {
Log.i(FilterInstaller.TAG, "installFilters");
this.createDownloadFilterDirectory();
try {
Bundle v0 = this.mServiceContext.getPackageManager().getApplicationInfo(this.mPackageName, 0x80).metaData;
if(v0 != null && v0.getInt("version") == 3) {
this.installSelFilter(this.mServiceContext, v0, this.mPackageName, ((boolean)(((int)arg4))));
return;
}
this.installLibFilter(this.mServiceContext, this.mPackageName, ((boolean)(((int)arg4))));
}
catch(Exception v4) {
Log.e(FilterInstaller.TAG, "installFilters error : " + v4.toString());
}
}
- 这里面的this.mPackageName也是我们可以控制的,因为在FilterInstaller的构造函数里面发现他就是根据构造时传入的第二参数进行设置的,而第二参数是来源于消息对象
FilterInstaller(Context arg1, String arg2) {
this.mPackageName = arg2;
this.mServiceContext = arg1;
}
- 此外还取了目标包名应用Manifest文件里面的metaData字段,这些我们也是可以控制的
- 在installSelFilter中存在写入文件的操作,而文件的来源居然是我们应用的assets
private void installSelFilter(Context arg20, Bundle arg21, String arg22, boolean arg23) {
Resources v0_5;
Cursor v10;
Log.i(FilterInstaller.TAG, "installSelFilter");
String v3 = arg21.getString("title");
String v5 = arg21.getString("vendor");
int v7 = arg21.getInt("version");
int v9 = arg21.getInt("title_id");
ContentResolver v15 = arg20.getContentResolver();
int v16 = 0;
try {
v10 = v15.query(Constants.URI_FILTERS, null, "package_name", new String[]{arg22}, null);
}
catch(Exception v0) {
goto label_63;
}
//...
label_74:
String v10_1 = null;
try {
v0_5 = arg20.getPackageManager().getResourcesForApplication(arg22); // Read our application resources
}
catch(PackageManager.NameNotFoundException v0_4) {
Log.e(FilterInstaller.TAG, "installSelFilter, getResourcesForApplication : " + v0_4.toString());
v0_5 = null;
}
AssetManager v0_6 = v0_5.getAssets(); // Read our application assets
String v11 = v3 + ".sel";
String v12 = this.renameFilterName(v11, arg22);
String v13 = FilterInstaller.FILTER_STORAGE_SEL + "/" + v12;
Bitmap v0_7 = this.decryptSelFilter(v0_6, "sel/" + v11);
if(v0_7 != null) {
v10_1 = this.saveBitmapFile(v0_7, v13, true);
Log.i(FilterInstaller.TAG, "copied file path : " + v10_1);
}
if(v10_1 != null) {
ContentValues v0_8 = new ContentValues();
v0_8.put("package_name", arg22);
v0_8.put("name", v3);
v0_8.put("filename", v12);
v0_8.put("vendor", v5);
v0_8.put("version", String.valueOf(v7));
v0_8.put("title_id", Integer.valueOf(v9));
v0_8.put("filter_type", "SINGLE");
v0_8.put("category", Integer.valueOf(2));
v0_8.put("preload_filter", Integer.valueOf(0));
v0_8.put("vendor_package_name", arg22);
Log.i(FilterInstaller.TAG, "installSelFilter - insert DB");
v15.insert(Constants.URI_FILTERS, v0_8);
if(arg23) {
v16 = 3;
}
FilterNotifyUtil.NotifyChange(arg20, v16, v0_8);
}
}
- 改名函数是把文件名改为
[package_name].[title].sel
,由于title可使用任意字符所以存在路径穿越问题 - 解密过程的密钥和IV值全部为硬编码,可以直接拿来进行加密
private Bitmap decodeAES(String arg6) {
try {
byte[] v6_1 = Base64.decode(arg6, 0);
IvParameterSpec v1 = new IvParameterSpec(FilterInstaller.IV_BYTES);
SecretKeySpec v2 = new SecretKeySpec(FilterInstaller.SECRET_KEY.getBytes("UTF-8"), "AES");
Cipher v3 = Cipher.getInstance("AES/CBC/PKCS5Padding");
v3.init(2, v2, v1);
return BitmapFactory.decodeByteArray(v3.doFinal(v6_1), 0, v3.doFinal(v6_1).length);
}
catch(Exception v6) {
Log.e(FilterInstaller.TAG, "decodeAES error : " + v6.toString());
return null;
}
}
private Bitmap decryptSelFilter(AssetManager arg4, String arg5) {
Log.i(FilterInstaller.TAG, "decryptSelFilter source : " + arg5);
try {
Scanner v4_1 = new Scanner(arg4.open(arg5)).useDelimiter("\\A");
String v4_2 = v4_1.hasNext() ? v4_1.next() : "";
return this.decodeAES(v4_2);
}
catch(Exception v4) {
Log.e(FilterInstaller.TAG, "decryptSelFilter error : " + v4.toString());
return null;
}
}
FilterInstaller.IV_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
FilterInstaller.SECRET_KEY = "[3%250947@#73985$903*&)7";
- 这样一来我们可以构造一个合法的Sel文件并且写入到下面的目录中
FilterInstaller.FILTER_STORAGE_ROOT_DIR = Environment.getDataDirectory().getPath();
FilterInstaller.FILTER_STORAGE_SEL = FilterInstaller.FILTER_STORAGE_ROOT_DIR + "/DownFilters";
- 而在installLibFilter函数中,也存在类似的逻辑
private void installLibFilter(Context arg11, String arg12, boolean arg13) {
Cursor v1_2;
Resources v0;
Log.i(FilterInstaller.TAG, "install filter for Lib");
try {
v0 = arg11.getPackageManager().getResourcesForApplication(arg12);
}
catch(PackageManager.NameNotFoundException v11) {
Log.e(FilterInstaller.TAG, "installLibFilter, resource error, package name : " + arg12 + ", exception : " + v11.toString());
return;
}
ContentResolver v1 = arg11.getContentResolver();
int v11_1 = 0;
try {
v1_2 = v1.query(Constants.URI_FILTERS, null, "package_name", new String[]{arg12}, null);
}
catch(Exception v1_1) {
goto label_67;
}
//...
label_70:
AssetManager v1_4 = v0.getAssets();
String[] v2_1 = new String[0];
try {
v2_1 = v1_4.list("so");
}
catch(IOException v3_1) {
Log.e(FilterInstaller.TAG, "installLibFilter, asset lib list exception : " + v3_1.toString());
}
if(v2_1.length <= 0) {
Log.e(FilterInstaller.TAG, "asset lib list length is small than 0");
return;
}
int v3_2 = v2_1.length;
int v4;
for(v4 = 0; v4 < v3_2; ++v4) {
String v5 = v2_1[v4];
Log.i(FilterInstaller.TAG, "filterFile : " + v5);
if(this.storeLibFilters(v1_4, v0, v5, v5.split("\\.")[0]) == -1L && (v5.endsWith(".so"))) {
Log.e(FilterInstaller.TAG, "storeLibFilters fail");
}
}
ContentValues v0_1 = new ContentValues();
v0_1.put("package_name", arg12);
Context v12 = this.mServiceContext;
if(arg13) {
v11_1 = 3;
}
FilterNotifyUtil.NotifyChange(v12, v11_1, v0_1);
}
- 也是从我们应用的assets中取文件,storeLibFilters的逻辑如下
private long storeLibFilters(AssetManager arg17, Resources arg18, String arg19, String arg20) {
String v8;
String v15 = null;
String v13 = null;
Log.i(FilterInstaller.TAG, "storeLibFilters - title : " + arg20 + ", file name : " + arg19);
String v6 = this.renameFilterName(arg19, this.mPackageName);
Log.i(FilterInstaller.TAG, "newFileName : " + v6);
String v7 = this.copyFileFromAssets(arg17, "so/" + arg19, FilterInstaller.FILTER_STORAGE_LIB + "/" + v6, true);
if(v7 == null) {
Log.e(FilterInstaller.TAG, "lib copy failed : " + v6);
return -1L;
}
try {
if(arg17.list("so_arm64").length > 0) {
v8 = this.copyFileFromAssets(arg17, "so_arm64/" + arg19, FilterInstaller.FILTER_STORAGE_LIB64 + "/" + v6, true);
if(v8 == null) {
Log.e(FilterInstaller.TAG, "lib64 copy failed : " + v6);
return -1L;
}
}
else {
goto label_98;
}
goto label_99;
}
catch(IOException v0) {
Log.e(FilterInstaller.TAG, "Exception from check 64bit library so file : " + v0.toString());
return -1L;
}
label_98:
v8 = null;
try {
label_99:
String[] v15_1 = arg17.list("tex");
if(v15_1.length > 0) {
v15 = this.renameFilterName(v15_1[0], this.mPackageName);
v13 = this.copyFileFromAssets(arg17, "tex/" + v15_1[0], FilterInstaller.FILTER_STORAGE_TEXTURE + "/" + v15, true);
if(v13 == null) {
Log.e(FilterInstaller.TAG, "texture copy failed : " + v15);
return -1L;
}
}
else {
goto label_148;
}
}
catch(IOException v0_1) {
Log.e(FilterInstaller.TAG, "installLibFilter, asset texture list exception : " + v0_1.toString());
}
goto label_154;
label_148:
v13 = null;
v15 = null;
label_154:
Log.i(FilterInstaller.TAG, "success copy lib32 : " + v7);
Log.i(FilterInstaller.TAG, "success copy lib64 : " + v8);
Log.i(FilterInstaller.TAG, "success copy texture : " + v13);
if((arg19.endsWith(".so")) && !this.checkSoSignature(v6)) {
Log.e(FilterInstaller.TAG, "Signature check failed.");
return -1L;
}
if(!arg19.endsWith(".sig")) {
ContentResolver v8_1 = this.mServiceContext.getContentResolver();
ContentValues v9 = new ContentValues();
try {
int v0_3 = arg18.getIdentifier(arg20 + "_title", "string", this.mPackageName);
int v7_1 = arg18.getIdentifier(arg20 + "_vendor", "string", this.mPackageName);
int v4 = arg18.getIdentifier(arg20 + "_version", "string", this.mPackageName);
Log.e(FilterInstaller.TAG, "title = " + arg18.getString(v0_3) + ", vendor = " + arg18.getString(v7_1) + ", version = " + arg18.getString(v4));
v9.put("name", arg18.getString(v0_3));
v9.put("filename", v6);
v9.put("version", arg18.getString(v4));
v9.put("vendor", arg18.getString(v7_1));
v9.put("package_name", this.mPackageName);
v9.put("title_id", Integer.valueOf(v0_3));
v9.put("preload_filter", Integer.valueOf(0));
v9.put("filter_type", "SINGLE");
v9.put("category", Integer.valueOf(2));
v9.put("vendor_package_name", this.mPackageName);
if(v13 != null) {
v9.put("texture", v15);
}
}
catch(Resources.NotFoundException v0_2) {
Log.e(FilterInstaller.TAG, "Resource is not defined properly! " + v0_2.toString());
return -1L;
}
Uri v0_4 = v8_1.insert(Constants.URI_FILTERS, v9);
if(v0_4 == null) {
if(FilterPackageService.VERBOSE_LOGGING) {
Log.v(FilterInstaller.TAG, "Installation failure: " + arg19);
}
return -1L;
}
return (long)Long.valueOf(v0_4.getLastPathSegment());
}
return -1L;
}
- 里面大概的逻辑就是,取so文件夹下的32位ELF和so_arm64下的64位ELF,以及tex下的texture文件,并且对于so结尾的文件存在签名校验
- 这个签名校验是使用的一个硬编码的公钥进行校验的,如果无法获得私钥是没办法绕过的。不过由于这个东西看起来像一个业务特性,可以尝试逆向那些本来就在白名单里的应用看能否找到私钥,也就是下面这些URI所对应的应用
com.candycamera.android.filter
com.ucam.filters.forsamsung
com.pinguo.camera360filter
com.linecorp.aillis.filter
com.linecorp.b612.filter
com.seerslab.filter
com.samsung.android.filter.effect
- 然而即使是无法转化为代码执行,这里也同样存在路径穿越可以写入任意文件。而且从漏洞描述来看,很可能上报者是成功找到了这个私钥的。