Android短信漏洞技术分析

以下内容仅用于技术分析,如有侵权请联系删除
https://github.com/yuuouu/ColorOS-CVE-2025-10184

本文研究 CVE-2025-10184 漏洞产生原因,厂商的修复内容,以及其它解决方案的技术原理。

想象一下,手机里的短信系统像一个“大笔记本”(数据库)。这个笔记本里有很多“页”(表格),每一页负责记录一种短信或相关信息:

厂商为了做一些“额外功能”,比如在短信里插入信息流、拦截黑名单短信等,就在这个笔记本中新加了一些页。这个需求本身没问题,原则上这些新加的页不应该影响原有页的数据安全。
但是,这些新增的页没有被严格保护。攻击者可以先从新增的这些新增的弱保护页中“撬出”一部分内容。更严重的是,攻击者可以借助这些泄露页,去拿到笔记本里的其他页:“嘿,我要查这条短信、那条短信……” ——也就是说,通过漏洞页作为“跳板”,间接访问到本来应该受保护的页里的短信内容。

仓库时间线

漏洞侵入路径

  1. 逆向 com.android.providers.telephony 找到存在漏洞的数据库表
  2. 通过 adb 确认漏洞存在
  3. 使用 sql 盲注获取数据库内容
$ adb shell
$ content query --uri content://service-number/service_number --where '(SELECT COUNT(*) FROM (SELECT tbl_name FROM sqlite_master WHERE tbl_name = "sms"))>0'
# results
Row: 0 _id=123, hash_number=NULL, origin_number=NULL, source=NULL, type=NULL, update_date=NULL, extras=NULL
Row: 1 _id=999, hash_number=NULL, origin_number=NULL, source=NULL, type=NULL, update_date=NULL, extras=NULL

漏洞原因

在清单文件中写 provider 时只设置了 readPermission 读取权限,没有限制 writePermission 写入权限;再加上重写 ContentProviderupdate (Uri uri, ContentValues values, String where, String[] selectionArgs) 方法时直接将外部 where 参数放进查询语句 writableDatabase.update() 中,导致出现 SQL 注入漏洞。
漏洞的具体表现方式为:如果能够获取该数据库中任一表的 update 权限,就能通过盲注方式 1=1 AND unicode(substr((SELECT body FROM sms), 1, 1)) 去获取该数据库的全部内容。
以下是出现问题的代码:

<provider
	android:name="com.android.providers.telephony.PushMessageProvider"
    android:readPermission="android.permission.READ_SMS"
    android:exported="true"
    android:authorities="push-mms"
    android:singleUser="true"/>
public class PushMessageProvider extends ContentProvider {
    @Override // android.content.ContentProvider
    public int update(Uri uri, ContentValues contentValues, String str, String[] strArr) {
        SQLiteDatabase writableDatabase = this.mOpenHelper.getWritableDatabase();
        int iMatch = sUriMatcher.match(uri);
        int iUpdate = 0;
        boolean booleanQueryParameter = uri.getBooleanQueryParameter("bubble_update_tag", false);
        if (iMatch == 100) {
            iUpdate = writableDatabase.update(PushMessageContract.PushMMSEntry.TABLE_NAME, contentValues, str, strArr);
        } else if (iMatch == URI_PUSH_MESSAGE) {
            try {
                iUpdate = writableDatabase.update(PushMessageContract.PushMMSEntry.TABLE_NAME, contentValues, "_id = ? ", new String[]{String.valueOf(Long.parseLong(uri.getLastPathSegment()))});
            } catch (NumberFormatException unused) {
                Log.e(TAG, "Row ID must be a long.");
            }
        } else {
            throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
        if (iUpdate > 0 && !booleanQueryParameter) {
            ContentResolver contentResolver = getContext().getContentResolver();
            contentResolver.notifyChange(uri, null);
            contentResolver.notifyChange(Uri.parse("content://mms-sms/conversations/"), null);
        }
        return iUpdate;
    }
}

我猜测这个问题的出现,是因为直接复制 Android 原生里面的代码,例如下面这样,但是没注意到原生 provider 里面的 update() 方法要么返回 0,要么报错,或者限制直接写入。总结就是没有遵循 Google Developers 文档里的 Content provider 防范恶意输入指南。

<provider
    android:name="com.android.providers.telephony.SmsChangesProvider"
    android:readPermission="android.permission.READ_SMS"
    android:exported="true"
    android:multiprocess="false"
    android:authorities="sms-changes"
    android:singleUser="true"/>
    // SmsChangesProvider.java
    public int update(Uri uri, ContentValues contentValues, String str, String[] strArr) {
        return 0;
    }

官方修复内容

<!-- old -->
<provider android:readPermission="android.permission.READ_SMS" />
<!-- new -->
<provider android:permission="oplus.permission.OPLUS_COMPONENT_SAFE" />

android:permission
客户端为了读取或写入 content provider 的数据而必须具备的权限的名称。您可以使用此属性来方便地设置适用于读取和写入的单项权限。不过,readPermission、writePermission 和 grantUriPermissions 属性优先于此属性。
如果也设置了 readPermission 属性,则该属性控制对查询 content provider 的访问权限。如果设置了 writePermission 属性,则该属性控制对修改提供程序的数据的访问权限。

对比两者可以发现,此次修复修改了 provider 权限范围。根据系统定义,相当于是给这个表加入 wrire 权限,所以更新后再利用漏洞会提示无权限。

$ content query --uri content://service-number/service_number --where '(SELECT COUNT(*) FROM (SELECT tbl_name FROM sqlite_master WHERE tbl_name = "sms"))>0'
# results
Error while accessing provider:service-number
java.lang.SecurityException: Permission Denial: opening provider com.android.providers.telephony.ServiceNumberProvider from (null) (pid=27637, uid=2000) requires oplus.permission.OPLUS_COMPONENT_SAFE or oplus.permission.OPLUS_COMPONENT_SAFE

解决方案原理

方案一:骚扰拦截

此方案来自 v2ex@CoolMarket

拦截思路:根据上面的漏洞分析,我们可以发现是因为在一个数据库存某个表存在漏洞,才导致整个数据库都有问题,那么我们只要将短信存到不存在漏洞的数据库中,那么就能解决问题。这个便是骚扰拦截方案的精髓。
骚扰拦截是将普通短信和骚扰短信区分开,两者的数据库可能是同一个,也可能不是同一个。如果是存在短信列表里,后加入到骚扰拦截的短信,那么两者数据库就是同一个,漏洞还是能读取到:
对于已有短信,骚扰拦截数据是通过 mmssms.db 数据库中的 sms 表中添加字段 block_type 标识或者放入到 blocked_threads 表,没有在数据库中删除已有内容
对于新的短信,骚扰拦截数据是在数据库 /data/user/0/com.android.mms/databases/bugle_db ,通过下面的查询语句获取。

public class MessageExternalProvider extends ContentProvider {
	private Cursor k() {
       return f().p("SELECT _id, participant_normalized_destination AS oppo_recipient, sort_timestamp AS date, snippet_text AS snippet, custom_unread_count AS oppo_unread_count, custom_blocked_type AS block_type, custom_shop_id AS ted_service_id, custom_shop_name AS service_name, participant_normalized_destination AS service_number, custom_shop_logo AS service_logo, CASE WHEN (message_protocol < 256) THEN (CASE WHEN like('image/%', preview_content_type) THEN 1 WHEN like('video/%', preview_content_type) THEN 2 WHEN like('audio/%', preview_content_type) OR lower(preview_content_type) = 'application/ogg' THEN 3 WHEN lower(preview_content_type) = 'text/x-vcard' OR lower(preview_content_type) = 'text/vcard' THEN 5 ELSE 0 END) ELSE custom_preview_content_type + 100 END AS attachment_type FROM custom_blocked_conversation_list_view order by date DESC", null);
   }
}

实际效果:

经过排查,com.android.mms 清单文件里的 MessageExternalProvider 及其他 provider 写入权限或 update 是限制的,不存在出现漏洞的原因,所以此方案有效。这也是最适合普通用户的方案。
大概率当时开发这两个功能的是两拨人,如果是同一个人的话...说明哥们当时有点懒

<provider
    android:name="com.oplus.mms.datamodel.MessageExternalProvider"
    android:readPermission="oppo.permission.OPPO_COMPONENT_SAFE"
    android:writePermission="oppo.permission.OPPO_COMPONENT_SAFE"
    android:exported="true"
    android:multiprocess="false"
    android:authorities="message-external"/>

方案二:xposed 模块

作者提供的方案
通过 hook 存在漏洞的 update() 方法拦截

拦截思路:漏洞原因是三个 provider 组件存在权限漏洞,并且没有限制 update() 方法。那么只要 hook 调用 update() 方法,在返回数据前判断是哪个应用在调用就可以进行拦截。

工作原理:

  1. 监听调用三个存在漏洞的 provider.update() 方法:PushMessageProviderPushShopProviderServiceNumberProvider
  2. 如果不是系统应用调用该方法,则拦截返回空数据
  3. 弹窗提示用户,将调用链数据记录到日志中

安装模块后需要重启,是因为 com.android.providers.telephony 无法被手动重启,在清单文件中可以看到 android:process="com.android.phone",表明该应用依附于 com.android.phone 进程启动,除非重启,不然此进程不会被销毁。
具体内容可见源码,目的是揪出调用此漏洞应用,提高漏洞调用成本。如有发现,将立刻公诸于众!

方案三:禁用 provider 组件

基于 v2ex@Dawnnnnnn 提供的方案思路
使用 Blocker 工具通过 rootShizuku 禁用漏洞组件

拦截思路:由于这个漏洞是因为三个未加写入权限的 provider 组件造成的,那只要禁用掉相应组件就能够堵住这个漏洞。
这个过程会出现很多问题,比如:禁用系统组件会造成什么后果?没有 root 权限如何禁用系统应用内的组件?
我将通过以下抽丝剥茧的讲解,深度认识 Android 系统运行的一个截面。如有错误,恳请斧正。

Root 工作原理

// com.merxury.blocker.core.controllers.root.command
internal class RootController @Inject constructor(  
    @ApplicationContext private val context: Context,  
) : IController {
	override suspend fun switchComponent(
       component: ComponentInfo,
       state: Int,
    ): Boolean {
       val packageName = component.packageName
       val componentName = component.name
       val comm: String = when (state) {
           PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> removeEscapeCharacter(
               String.format(
                   ENABLE_COMPONENT_TEMPLATE, context.userId, packageName, componentName,
               ),
           )

           PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> removeEscapeCharacter(
               String.format(
                   DISABLE_COMPONENT_TEMPLATE, context.userId, packageName, componentName,
               ),
           )

           else -> return false
       }
       Timber.d("command:$comm, componentState is $state")
       return withContext(Dispatchers.IO) {
           try {
               val commandOutput = comm.exec()
               return@withContext commandOutput.isSuccess
           } catch (e: Exception) {
               throw e
           }
       }
    }
	companion object {
        private const val DISABLE_COMPONENT_TEMPLATE = "pm disable --user %s %s/%s"
        private const val ENABLE_COMPONENT_TEMPLATE = "pm enable --user %s %s/%s"
    }
}

// com.merxury.blocker.core.extension
suspend fun String.exec(dispatcher: CoroutineDispatcher = Dispatchers.IO): ShellResult = withContext(dispatcher) {  
    val rootGranted = PermissionUtils.isRootAvailable(dispatcher)  
    if (!rootGranted) {  
        throw RuntimeException("Root unavailable")  
    }  
    val result = Shell.cmd(this@exec).exec()  
    return@withContext ShellResult(  
        result.out,  
        result.err,  
        result.code,  
        result.isSuccess,  
    )  
}

可以看出在 root 的情况下,Blocker 直接执行 Shell 命令,从而禁用 provider 组件
adb shell su pm disable/enable --user 0 com.android.providers.telephony/.ServiceNumberProvider

Shizuku 工作原理

先从 Blocker → Shizuku → PackageManager 的调用链开始

  1. Blocker

// com.merxury.blocker.core.controllers.shizuku
internal class ShizukuController @Inject constructor(  
    @ApplicationContext private val context: Context,  
) : IController {  
    override suspend fun switchComponent(  
        component: ComponentInfo,  
        state: Int,  
    ): Boolean {  
        val packageName = component.packageName  
        val componentName = component.name  
        // 0 means kill the application  
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {  
            pm.setComponentEnabledSetting(  
                ComponentName(packageName, componentName), state, 0, context.userId, context.packageName,  
            )  
        } else {  
            pm.setComponentEnabledSetting(  
                ComponentName(packageName, componentName), state, 0, context.userId,  
            )  
        }  
        return true  
    }
}
// android.content.pm
public interface IPackageManager extends IInterface {
	@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
	void setComponentEnabledSetting(ComponentName componentName, int newState, int flags, int userId, String callingPackage)
        throws RemoteException;
    void setComponentEnabledSetting(ComponentName componentName, int newState, int flags, int userId)
        throws RemoteException;
}
  1. Shizuku
// moe.shizuku.manager.ktx
fun PackageManager.setComponentEnabled(componentName: ComponentName, enabled: Boolean) {  
    val oldState = getComponentEnabledSetting(componentName)  
    val newState = if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED  
    if (newState != oldState) {  
        val flags = PackageManager.DONT_KILL_APP  
        setComponentEnabledSetting(componentName, newState, flags)  
    }  
}  
  
fun PackageManager.isComponentEnabled(componentName: ComponentName, defaultValue: Boolean = true): Boolean {  
    return when (getComponentEnabledSetting(componentName)) {  
        PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false  
        PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true  
        PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> defaultValue  
        else -> false  
    }  
}
  1. PackageManager 中实际工作的是 PackageManagerService以下部分
                if (callingUid == Process.SHELL_UID
                        && (pkgSetting.getFlags() & ApplicationInfo.FLAG_TEST_ONLY) == 0) {
                    // Shell只能在ENABLED和DISABLED_USER状态之间切换整个包,除非该包拥有 ApplicationInfo.FLAG_TEST_ONLY 属性
                    final int oldState = pkgSetting.getEnabled(userId);
                    final int newState = setting.getEnabledState();
                    if (!setting.isComponent()
                            &&
                            (oldState == COMPONENT_ENABLED_STATE_DISABLED_USER
                                    || oldState == COMPONENT_ENABLED_STATE_DEFAULT
                                    || oldState == COMPONENT_ENABLED_STATE_ENABLED)
                            &&
                            (newState == COMPONENT_ENABLED_STATE_DISABLED_USER
                                    || newState == COMPONENT_ENABLED_STATE_DEFAULT
                                    || newState == COMPONENT_ENABLED_STATE_ENABLED)) {
                        // 如果是直接切换包状态则允许
                    } else {
	                    // 否则报错 SecurityException
                        throw new SecurityException(
                                "Shell cannot change component state for "
                                        + setting.getComponentName() + " to " + newState);
                    }
                }
                pkgSettings.put(packageName, pkgSetting);
    

从以上完整的调用链可以看出,Blocker 调用了 Shizuku 的提供 setComponentEnabled 能力,真正起作用的是 PackageManager.setComponentEnabledSetting(componentName, newState, flags) 方法。
PackageManagerService 中可以看到 Shell 只能在 ENABLED 和 DISABLED_USER 状态之间切换整个包,除非该包拥有 ApplicationInfo.FLAG_TEST_ONLY 属性,这也正是 SecurityException错误来源。如果没有这个限制的话,adb 命令可以对任意禁用系统组件,这是及其危险的操作。

为什么 Shizuku 只能对 testOnly="true" 属性的应用修改

Shizuku 本质上是在本机调用 adb 调试,实际起作用的是 Shell,Google 对声明 testOnly="true" 属性的应用提供了一些开放接口。
(pkgSetting.getFlags() & ApplicationInfo.FLAG_TEST_ONLY) == 0 就是其中一种,如果声明了该属性,将允许对该包的特定组件进行修改;未声明此属性,则 Shell 只能对应用进行整体限制,对单个组件进行修改会弹出 SecurityException 错误。相当于是给你一个工具箱,你可以选择开启/关闭这个箱子,不能对工具箱内的某个工具进行限制,而测试工具箱允许你自由开关箱子内的任意工具。
所以 adb shell 权限对于应用的更改,只能是带有 testOnly="true" 属性的才能修改,除非 root。

public static final int AndroidManifestApplication_testOnly

Option to indicate this application is only for testing purposes. For example, it may expose functionality or data outside of itself that would cause a security hole, but is useful for testing. This kind of application can not be installed without the INSTALL_ALLOW_TEST flag, which means only through adb install.

Must be a boolean value, either "true" or "false".
This may also be a reference to a resource (in the form "@[package:]type:name") or theme attribute (in the form "?[package:][type:]name") containing a value of this type.

This corresponds to the global attribute resource symbol testOnly.
Constant Value: 15 (0x0000000f)

这个属性在:升级、替换、权限策略、安全性、稳定性都可能存在问题,很好奇为什么会在系统应用上声明这个属性。经过我手里的包排查发现,com.android.providers.telephony 在 ColorOS 7-14 中的 3.3.0(29)、12.0.50(31)、13.0.110(33)、14.20.20(34) 均未声明 testOnly="true" 属性;仅在 ColorOS 15 15.40.10(35) 中声明了该属性。很奇怪的一个点,不会是测试完了忘记删,打包进 ROM 里面吧?

是不是可以通过 adb 禁用

经过上面的理解,我们完全可以通过 adb 命令来实现一样的功能

# 禁用
adb shell pm disable --user 0 com.android.providers.telephony/.ServiceNumberProvider
adb shell pm disable --user 0 com.android.providers.telephony/.PushMessageProvider
adb shell pm disable --user 0 com.android.providers.telephony/.PushShopProvider
# results
Component {com.android.providers.telephony/com.android.providers.telephony.ServiceNumberProvider} new state: disabled

# 启用
adb shell pm enable --user 0 com.android.providers.telephony/.ServiceNumberProvider
adb shell pm enable --user 0 com.android.providers.telephony/.PushMessageProvider
adb shell pm enable --user 0 com.android.providers.telephony/.PushShopProvider
# results
Component {com.android.providers.telephony/com.android.providers.telephony.ServiceNumberProvider} new state: enabled

为什么关闭组件,重启手机后,重新开启组件不是立刻生效

在关闭组件后,系统中的 ContentProvider 实例就会被卸载掉,应用层再访问会得到:java.lang.SecurityException: Permission Denial: opening provider ...
但是对于收发短信服务来说,其工作并不依靠每次获取数据库实例,而是在其启动阶段就持有 mmssms.db 数据库句柄,后续收发短信通过句柄操作数据库。所以哪怕组件关闭了也能正常收发短信。这也正是这个现象的原因:恶意应用无法读取,短信正常收发的效果能维持到下次重启前。换句话说是维持到 收发短信服务(com.android.phone) 重启前。
在重新启动后,由于三个数据库表被卸载,导致收发短信服务无法正常启动,拿不到数据库。这时候哪怕重新开启了组件,但是因为无法手动重启收发短信服务,也就无法重走数据库持有流程,导致开启组件后依然无法正常收发短信,必须在开启组件后再重启一次才能正常收发。

方案四:限制 mmssms.db 数据库读取权限

基于酷安@kkkkkcc 提供的方案思路
去除 /data/data/com.android.providers.telephony/databases/mmssms.db 文件的读取权限

拦截思路:限制存在漏洞的数据库读取权限。漏洞最终是要对数据库进行读写操作,那么只要限制数据库的读取权限,就能拦截漏洞。
禁用数据库读取权限是 root 级别的文件管理限制,在限制之后任何想要读取此数据库的应用都会返回无权限,包括已经拿到数据库句柄的收发短信服务。这种限制只限制读取数据库,不限制写入数据库,而且是实时生效的,不依赖系统重启。
对于能收到短信的现象,是因为收到短信插入数据库之后会通过 notifyChange(notifyIfNotDefault, url, callerPkg) 机制将内容更新到短信列表,不需要查询完整数据库。
对于无法发送短信的现象,根据下面的崩溃日志和代码可以看出,com.android.mms 无法打开数据库执行 insert 操作导致崩溃。

崩溃日志:

Error inserting subject=null thread_id=9 address=1008611 sub_id=5 date_sent=1760245512842 body=1 date=1760245512842 read=1 seen=1 type=2 creator=com.android.mms                android.database.sqlite.SQLiteCantOpenDatabaseException: unknown error (code 14 SQLITE_CANTOPEN): Could not open database
AndroidRuntime      E  FATAL EXCEPTION: IntentService[ActionService] (Ask Gemini)
Process: com.android.mms, PID: 16833
java.lang.NullPointerException: Uri must not be null
	at android.os.Parcel.createException(Parcel.java:2093)
	at android.os.Parcel.readException(Parcel.java:2055)
	at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:188)
	at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
	at android.content.ContentProviderProxy.insert(ContentProviderNative.java:481)
	at android.content.ContentResolver.insert(ContentResolver.java:1848)
	at com.android.mms.sms.g.a(MmsUtils.java:15)
	at com.android.mms.sms.g.e0(MmsUtils.java:3)

代码:

public class MmsUtils {
	class a implements Runnable {
       a() {
       }
       @Override // java.lang.Runnable
       public void run() {
           int i2;
           int iR;
           DatabaseWrapper databaseWrapperQ = DataModel.m().q();
           databaseWrapperQ.a();
           int iR2 = 0;
           try {
               try {
                   ContentValues contentValues = new ContentValues();
                   contentValues.put("message_status", (Integer) 106);
                   iR = databaseWrapperQ.r("messages", contentValues, "message_status IN (?, ?)", new String[]{Integer.toString(105), Integer.toString(103)}) + 0;
                   try {
                       contentValues.clear();
                       contentValues.put("message_status", (Integer) 8);
                       iR2 = databaseWrapperQ.r("messages", contentValues, "message_status IN (?, ?)", new String[]{Integer.toString(5), Integer.toString(6)});
                       databaseWrapperQ.q();
                   } catch (Exception e2) {
                       e = e2;
                       i2 = iR2;
                       iR2 = iR;
                       OplusLogUtil.c("MessagingApp", "fixupMessageStatus has an error." + e);
                       databaseWrapperQ.c();
                       iR = iR2;
                       iR2 = i2;
                       LogUtil2.f("MessagingApp", "fixupMessageStatus Send failed - " + iR2 + " Download failed - " + iR);
                   }
               } catch (Exception e3) {
                   e = e3;
                   i2 = 0;
               }
               LogUtil2.f("MessagingApp", "fixupMessageStatus Send failed - " + iR2 + " Download failed - " + iR);
           } finally {
               databaseWrapperQ.c();
           }
       }
   	}
	public static boolean E0(Context context, Uri uri, int i2, long j2) {
	    ContentResolver contentResolver;
	    ContentValues contentValues;
	    if (uri == null) {
	        return false;
	    }
	    try {
	        contentResolver = context.getContentResolver();
	        contentValues = new ContentValues(2);
	        contentValues.put("type", Integer.valueOf(i2));
	        contentValues.put("date", Long.valueOf(j2));
	    } catch (SQLiteException e2) {
	        LogUtil2.d("MessagingApp", "MmsUtils: update sms message failure " + e2);
	    } catch (IllegalArgumentException e3) {
	        LogUtil2.d("MessagingApp", "MmsUtils: update sms message failure " + e3);
	    }
	    return contentResolver.update(uri, contentValues, null, null) == 1;
	}
}

结语

感谢提供各种解决方案的热心网友,还是 Android 玩机内容丰富,这就是开源带来的生态影响。不过这个漏洞也正是 Android 开源但又不够限制 OEM 导致口碑难调的问题。总的来说,希望开源生态蓬勃发展,这也能促使普通用户在官方修复前找到合适的解决方案。
坚决反对通过漏洞牟利,创建此库并对外分享的初心是希望能让更多受到实质影响的用户能了解这个漏洞的实质内容,而不是对着新闻恐慌却又无从下手,我不喜欢无力感,所以尽我所能第一时间让更多人能真正的看到漏洞的影响。我并不是漏洞的发现者,只是有一点点专业能力能让更多人了解并尝试用户自救方案,很高兴能帮到大家。
最后的最后:谨慎使用 Shell 应用,包括开发者模式与无线调试功能,不要轻易给未知来源授权调试请求;网上那些 USB 连上设备就能读取手机数据的内容并不是空穴来风,以这次的漏洞举例,在有 adb 的情况下非常容易导出短信内容, Shell 应用拥有 Read_SMS 权限;另外,针对某一款应用做数据库破解也不是特别困难的事,再次建议大家谨慎使用 adb 调试功能,非必要情况建议关闭。

bad 彩蛋

经过实测,除 OPPO 外还有一家厂商在最新系统上存在此类型漏洞。厂商回应正在处理中,暂不公布。其它厂商暂🈚️设备实测。

版权说明

仓库解决方案可随意转载,此文章可小部分截图带 yuuouu@github 署名转载,禁止全文转载。如需转载请联系:t.me/yuuo_u