- 标题: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>
- 而FilterProvider我们只能申请读权限
<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; }
- Handler处理消息的代码
@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; } }
- 密钥和IV的值
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
- 然而即使是无法转化为代码执行,这里也同样存在路径穿越可以写入任意文件。而且从漏洞描述来看,很可能上报者是成功找到了这个私钥的。