SVE-2021-23076漏洞分析

  • 标题: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
  • 然而即使是无法转化为代码执行,这里也同样存在路径穿越可以写入任意文件。而且从漏洞描述来看,很可能上报者是成功找到了这个私钥的。