短信验证码防泄漏安全机制逆向分析

admin 2026-04-27 04:37:35 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文通过逆向分析ColorOS的Mms.apk,详细解析了短信验证码的五层防护体系:NLP智能识别引擎通过异步线程和超时机制精准识别验证码;通知栏内容遮蔽防止锁屏偷窥;关键创新是将验证码隔离存储于私有bugledb而非Android标准sms数据库,使READSMS权限失效;结合权限控制的广播分发和7天自毁机制,形成纵深防御。该设计有效抵御恶意应用读取、SIMSwap等攻击,提升移动账户安全性。 综合评分: 85 文章分类: 移动安全,应用安全,逆向分析,安全建设,终端安全


cover_image

短信验证码防泄漏安全机制逆向分析

原创

0pen1 0pen1

白帽技术与网络安全

2026年4月2日 21:21 北京

在小说阅读器读本章

去阅读

通过 JADX 逆向Mms.apk(63MB,Android 15),完整还原 ColorOS 短信验证码的五层纵深防护体系:从 NLP 智能识别、通知内容遮蔽、ContentProvider 隔离存储、权限控制的系统广播分发,到 7 天定时自毁清理。

引言

短信验证码是移动互联网账户安全的第一道防线,却也是攻击者觊觎的高价值目标。恶意应用通过 READ_SMS 权限静默读取验证码、SIM Swap 攻击、剪贴板嗅探等手段层出不穷。

OPPO 在 ColorOS 中构建了一套完整的验证码防泄漏机制,将短信验证码从”被动明文暴露”升级为”主动安全防护”。本文基于对 OPPO Mms.apk 的逆向分析,深入剖析这套防护体系的技术实现。

防护架构总览

验证码防护并非单一策略,而是一个五层纵深防御体系:

下面逐层分析其实现细节。

第一层:NLP 智能识别引擎

1.1 识别入口

当短信到达时,系统调用 VerificationCodeUtil.d() 判断是否为验证码:

// nu.VerificationCodeUtil
publicstaticbooleand(Contextcontext, Stringbody, Stringaddress,
                        Stringtag, longtimeout) {
    if (TextUtils.isEmpty(body) ||!FeatureOption.i) {
        returnfalse;
    }
    // 预处理:清除空字符、统一换行符
    Stringcleaned=body.replace("\u0000", "")
                        .replace("\r\n", "\n")
                        .replace("\r", "\n");
    // 调用 NLP 引擎解析,带超时保护
    returnf(h(context, cleaned, TedUtils3.e(address), tag, timeout));
}

1.2 NLP 解析引擎

h() 方法将短信内容提交给 SmartDecorateManager 的 NLP 引擎进行语义分析:

protectedstaticISmsEntityh(Contextcontext, Stringbody,
                               StringserviceId, Stringtag, longtimeout) {
    if (!FeatureOption.i) returnnull;

    ExecutorServiceexecutor=Executors.newFixedThreadPool(1);
    try {
        // 异步提交 NLP 任务,带超时保护
        ISmsEntityentity=executor.submit(() -> {
            returnSmartDecorateManager.l(-1L, body, serviceId);
        }).get(timeout, TimeUnit.MILLISECONDS);
        returnentity;
    } catch (TimeoutExceptione) {
        // NLP 超时不影响短信接收
        returnnull;
    } finally {
        executor.shutdown();
    }
}

设计亮点:

  • 异步执行 + 超时机制:NLP 分析在独立线程池中运行,不阻塞短信接收链路
  • 超时降级:若 NLP 引擎响应超时,直接返回 null,短信按普通消息处理,保证可用性

1.3 验证码特征识别

NLP 引擎解析后返回 ISmsEntity 对象,其内部结构采用”气泡(Bubble)”模型:

publicstaticbooleanf(ISmsEntityentity) {
    if (entity!=null) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;List<IBubbleEntity>bubbles=entity.f();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(bubbles!=null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(IBubbleEntitybubble&nbsp;:&nbsp;bubbles) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// id="-1" 是验证码气泡的固定标识
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(bubble!=null&&"-1".equals(bubble.getId())) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returntrue;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;returnfalse;
}

NLP 引擎将短信拆解为多个 Bubble,每个 Bubble 代表一个语义单元。验证码 Bubble 使用特殊 ID "-1" 标识,其 d() 方法返回提取的验证码值,b() 方法返回关联的 Action 列表(如”复制验证码”)。

1.4 消息类型标记

识别为验证码后,系统通过 VCodeMarkMessageTypeAction 对消息打标:

// 消息类型常量(MessageData)
TYPE_NONE=0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 未分类
TYPE_FORBID_READ_VERIFICATION_CODE=1;&nbsp;// 受保护的验证码(默认)
TYPE_VERIFICATION_CODE=2; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 普通验证码
TYPE_OTHERS=100; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 通知/服务类

// 标记结果写入两处:
// 1. bugle_db → messages.message_type
// 2. content://sms → oplus_sms_type 字段
privatebooleanupdateTypeToRemoteDb(List<String>uriList,&nbsp;inttype) {
&nbsp; &nbsp;&nbsp;ContentValuesvalues=newContentValues();
&nbsp; &nbsp;&nbsp;values.put("oplus_sms_type",&nbsp;Integer.valueOf(type));
&nbsp; &nbsp;&nbsp;SqliteWrapper.f(app,&nbsp;resolver,&nbsp;Telephony.Sms.CONTENT_URI,&nbsp;values, ...);
}

TYPE_FORBID_READ_VERIFICATION_CODE(值为 1)是安全防护的核心——标记为此类型的短信将进入最严格的保护模式。

第二层:通知内容遮蔽

2.1 遮蔽策略

当验证码短信到达时,锁屏和通知栏不显示原始内容,而是替换为脱敏文案:

// VerificationCodeUtil.e() — 构建遮蔽后的通知文本
publicstaticbooleane(Contextcontext,&nbsp;ISmsEntityentity,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stringtag,&nbsp;StringBuildersb) {
&nbsp; &nbsp;&nbsp;List<IBubbleEntity>bubbles=entity.f();
&nbsp; &nbsp;&nbsp;for&nbsp;(IBubbleEntitybubble&nbsp;:&nbsp;bubbles) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;("-1".equals(bubble.getId())) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;List<IActionBase>actions=bubble.b();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(actions!=null&&!actions.isEmpty()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;StringactionText=actions.get(0).d();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 获取遮蔽提示文案
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stringhint=context.getString(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;R.string.please_click_to_view_details);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 中文格式:"验证码:XXXX,请点击查看详情"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 英文格式:"Code: XXXX, Please click to view details"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(isChinese(actionText)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(actionText.subSequence(2,&nbsp;length));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(":");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(bubble.d()); &nbsp;// 验证码值
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(",");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(hint);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(codeLabel);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(": ");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(bubble.d());
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(", ");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sb.append(hint);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returntrue;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;returnfalse;
}

2.2 效果

用户看到的通知:

途虎养车
验证码:3554,请点击查看详情

而非原始短信全文:

【途虎养车】您的登录验证码是:3554,5分钟内有效,请勿泄漏。如非本人操作,请忽略此信息。

这防止了:

  • 锁屏状态下验证码被偷窥
  • 通知栏被恶意应用通过 NotificationListenerService 截获完整内容
  • 投屏/录屏场景下验证码泄漏

2.3 区分验证码类型

系统还区分了无需特殊警告的验证码类型:

public static boolean c(ISmsEntity entity, Context context) {
&nbsp; &nbsp; List<IBubbleEntity> bubbles = entity.f();
&nbsp; &nbsp; for (IBubbleEntity bubble : bubbles) {
&nbsp; &nbsp; &nbsp; &nbsp; List<IActionBase> actions = bubble.b();
&nbsp; &nbsp; &nbsp; &nbsp; if (actions != null && actions.size() > 0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; String actionText = actions.get(0).d();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 与 "no_need_show_warn_code" 配置列表比对
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; String[] noWarnList = context.getResources()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getStringArray(R.array.no_need_show_warn_code);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for (String item : noWarnList) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (actionText.equals(item)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return true; // 此类验证码无需额外安全警告
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; return false;
}

第三层:ContentProvider 隔离存储

3.1 存储隔离机制

这是最关键的防护层。被识别为验证码的智能短信不写入 Android 标准的 content://sms,而是仅存入 Mms 应用私有的 bugle_db 数据库:

标准 Android 短信流程:
&nbsp; 短信 → TelephonyProvider → content://sms → 任何有 READ_SMS 权限的 App 可读

ColorOS 验证码流程:
&nbsp; 短信 → OPPO 云端识别 → bugle_db (私有数据库)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ↓
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; custom_messages_ext 表存储完整内容
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ↓
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content://sms ← 不写入!

3.2 数据库结构

-- bugle_db 中的存储结构

-- messages 表:消息元数据
-- 验证码短信的 sms_message_uri 字段为空(从不关联标准 sms 数据库)
SELECT _id, sms_message_uri, message_type FROM messages;
-- _id=11, sms_message_uri=NULL, message_type=1 &nbsp;← 验证码,无标准 URI

-- custom_messages_ext 表:智能短信的实际内容
SELECT messages_id, content, data_text5 FROM custom_messages_ext;
-- messages_id=11, content="【途虎养车】您的验证码是:3554..."

3.3 安全效果

这意味着即使恶意应用拥有 READ_SMS 运行时权限,也完全无法通过以下任何方式获取验证码:

// 以下查询均返回空结果
context.getContentResolver().query(
&nbsp; &nbsp; Uri.parse("content://sms"), null, null, null, "date DESC");

context.getContentResolver().query(
&nbsp; &nbsp; Uri.parse("content://mms-sms/conversations"), ...);

// caller_is_syncadapter 参数也无效
Uri.parse("content://sms")
&nbsp; &nbsp; .buildUpon()
&nbsp; &nbsp; .appendQueryParameter("caller_is_syncadapter", "true")
&nbsp; &nbsp; .build();

验证码被完全隔离在 Mms 应用的 /data/data/com.android.mms/databases/bugle_db 中,只有系统级权限(root)才能访问。

第四层:权限控制的广播分发

4.1 分发架构

验证码识别完成后,通过受权限保护的广播将验证码分发给受信任的系统组件:

// VerificationCodeUtil.l() — 分发入口
public static void l(Context context, int protocolStatus,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ISmsEntity entity, MessageData messageData) {
&nbsp; &nbsp; // 三重前置校验
&nbsp; &nbsp; if (FeatureOption.U &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 功能开关
&nbsp; &nbsp; &nbsp; &nbsp; && ProtocolDialogUtil.g() &nbsp; &nbsp; &nbsp; &nbsp;// 用户已同意隐私协议
&nbsp; &nbsp; &nbsp; &nbsp; && protocolStatus == 0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 协议状态正常
&nbsp; &nbsp; &nbsp; &nbsp; && f(entity)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 确认是验证码
&nbsp; &nbsp; &nbsp; &nbsp; // 提取验证码值
&nbsp; &nbsp; &nbsp; &nbsp; String code = b(entity, "VerificationCodeUtil");
&nbsp; &nbsp; &nbsp; &nbsp; // 触发安全分发
&nbsp; &nbsp; &nbsp; &nbsp; k(context, messageData, code);
&nbsp; &nbsp; }
}

4.2 三路分发

// VerificationCodeUtil.k() — 验证码广播的三路分发
public static void k(Context context, MessageData data, String code) {
&nbsp; &nbsp; Intent intent = a(data, code); &nbsp;// 构建携带验证码的 Intent

&nbsp; &nbsp; // 路线一:发送给 OPPO 健康应用
&nbsp; &nbsp; o(context, intent, data);
&nbsp; &nbsp; // 路线二:发送给 AutoFill 服务或系统服务
&nbsp; &nbsp; n(context, intent);
&nbsp; &nbsp; // 路线三:发送给 Metis 智能助手
&nbsp; &nbsp; m(context, data, code);
}

路线一:健康应用

// 接收方:com.heytap.health(OPPO 健康应用)
public static final String[] HEALTH_RECEIVERS = {"com.heytap.health"};

public static void o(Context context, Intent intent, MessageData data) {
&nbsp; &nbsp; intent.putExtra("message_content", data.getMessageText());
&nbsp; &nbsp; // 带 OPLUS_COMPONENT_SAFE 权限发送
&nbsp; &nbsp; j(context, intent, HEALTH_RECEIVERS);
}

private static void j(Context context, Intent intent, String[] packages) {
&nbsp; &nbsp; for (String pkg : packages) {
&nbsp; &nbsp; &nbsp; &nbsp; intent.setPackage(pkg);
&nbsp; &nbsp; &nbsp; &nbsp; intent.setAction("oplus.intent.action.sms.verify_code");
&nbsp; &nbsp; &nbsp; &nbsp; context.sendBroadcast(intent,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "oplus.permission.OPLUS_COMPONENT_SAFE");
&nbsp; &nbsp; }
}

路线二:AutoFill 服务

private static void n(Context context, Intent intent) {
&nbsp; &nbsp; if (OplusAutoFillCaller.b(context)) {
&nbsp; &nbsp; &nbsp; &nbsp; // AutoFill 服务可用 → 通过 ContentProvider 安全传递
&nbsp; &nbsp; &nbsp; &nbsp; OplusAutoFillCaller.c(context, intent);
&nbsp; &nbsp; } else {
&nbsp; &nbsp; &nbsp; &nbsp; // 降级:通过广播发送给系统核心服务
&nbsp; &nbsp; &nbsp; &nbsp; // 接收方:android(system_server), com.oplus.exsystemservice
&nbsp; &nbsp; &nbsp; &nbsp; j(context, intent, SYSTEM_RECEIVERS);
&nbsp; &nbsp; }
}

路线三:Metis 智能助手

public static void m(Context context, MessageData data, String code) {
&nbsp; &nbsp; if (OsVersionUtils.u() && SmartMessageServiceUtil.i(context)) {
&nbsp; &nbsp; &nbsp; &nbsp; Intent intent = new Intent();
&nbsp; &nbsp; &nbsp; &nbsp; intent.putExtra("app_name", context.getString(R.string.app_name));
&nbsp; &nbsp; &nbsp; &nbsp; intent.putExtra("verify_code", code);
&nbsp; &nbsp; &nbsp; &nbsp; intent.putExtra("message_content", data.getMessageText());
&nbsp; &nbsp; &nbsp; &nbsp; intent.putExtra("message_title", notificationTitle);
&nbsp; &nbsp; &nbsp; &nbsp; intent.setAction("oplus.intent.action.sms.verify_code");
&nbsp; &nbsp; &nbsp; &nbsp; intent.setPackage("com.oplus.metis");
&nbsp; &nbsp; &nbsp; &nbsp; // 使用 MMS 专属权限
&nbsp; &nbsp; &nbsp; &nbsp; context.sendBroadcast(intent, "com.oplus.permission.safe.MMS");
&nbsp; &nbsp; }
}

4.3 Intent 结构

private static Intent a(MessageData data, String code) {
&nbsp; &nbsp; Intent intent = new Intent();
&nbsp; &nbsp; intent.putExtra("verify_code", code); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 提取的验证码值
&nbsp; &nbsp; intent.putExtra("conversation_id", data.getConversationId());
&nbsp; &nbsp; intent.putExtra("message_id", data.getMessageId());
&nbsp; &nbsp; intent.putExtra("sms_send_time", data.getSentTimestamp());
&nbsp; &nbsp; intent.putExtra("sms_received_time", data.getReceivedTimestamp());
&nbsp; &nbsp; intent.putExtra("sms_received_format_time",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; data.getFormattedReceivedTimeStamp());
&nbsp; &nbsp; intent.setPackage("android"); &nbsp;// 初始目标为系统框架
&nbsp; &nbsp; return intent;
}

4.4 权限保护矩阵

| 接收方 | 权限要求 | 说明 | | — | — | — | | com.heytap.health | oplus.permission.OPLUS_COMPONENT_SAFE | OPPO 健康应用 | | android (system_server) | oplus.permission.OPLUS_COMPONENT_SAFE | 系统框架层 | | com.oplus.exsystemservice | oplus.permission.OPLUS_COMPONENT_SAFE | OPPO 系统扩展服务 | | com.oplus.metis | com.oplus.permission.safe.MMS | Metis 智能助手 |

这两个权限都需要 系统签名(signature) 才能获得,第三方应用无法注册接收这些广播。

4.5 AutoFill 安全通道

当 AutoFill 服务可用时,验证码通过更安全的 ContentProvider 调用传递:

// OplusAutoFillCaller — AutoFill 安全传递
public static boolean b(Context context) {
&nbsp; &nbsp; // 检查 AutoFill 服务是否启用
&nbsp; &nbsp; ContentProviderClient client = context.getContentResolver()
&nbsp; &nbsp; &nbsp; &nbsp; .acquireUnstableContentProviderClient(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "com.oplus.codebook.autofill.access");
&nbsp; &nbsp; if (client != null) {
&nbsp; &nbsp; &nbsp; &nbsp; Bundle result = client.call("IsAutoFillOn", null, null);
&nbsp; &nbsp; &nbsp; &nbsp; return Boolean.TRUE.equals(result.getBoolean("IsAutoFillOn"));
&nbsp; &nbsp; }
&nbsp; &nbsp; return false;
}

public static void c(Context context, Intent intent) {
&nbsp; &nbsp; // 通过 ContentProvider 的 call() 方法安全传递验证码
&nbsp; &nbsp; ContentProviderClient client = context.getContentResolver()
&nbsp; &nbsp; &nbsp; &nbsp; .acquireUnstableContentProviderClient(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "com.oplus.codebook.autofill.access");
&nbsp; &nbsp; Bundle bundle = new Bundle();
&nbsp; &nbsp; bundle.putAll(intent.getExtras());
&nbsp; &nbsp; // "SMS_code" 是 AutoFill 服务识别的方法名
&nbsp; &nbsp; client.call("SMS_code", null, bundle);
}

AutoFill 服务(com.oplus.codebook)使用 com.oplus.permission.safe.MMS 权限保护其 ContentProvider,实现了验证码到输入框的安全自动填充,用户无需手动复制。

4.6 白名单机制

部分特殊应用可以绕过验证码保护直接读取:

public static boolean g(Context context, String packageName) {
&nbsp; &nbsp; String whitelist = Settings.Global.getString(
&nbsp; &nbsp; &nbsp; &nbsp; context.getContentResolver(), "VERIFICATION_CODE_PKG");
&nbsp; &nbsp; if (TextUtils.isEmpty(whitelist)) {
&nbsp; &nbsp; &nbsp; &nbsp; // 默认白名单
&nbsp; &nbsp; &nbsp; &nbsp; whitelist = "com.android.chrome;"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; + "com.safeluck.schooltrainingorder.ningbo;"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; + "com.safety.act";
&nbsp; &nbsp; }
&nbsp; &nbsp; String[] packages = whitelist.split(";");
&nbsp; &nbsp; for (String pkg : packages) {
&nbsp; &nbsp; &nbsp; &nbsp; if (pkg.trim().equals(packageName)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return true;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; return false;
}

白名单存储在 Settings.Global(需要系统权限才能修改),默认仅包含 Chrome 浏览器和两个特定应用。

第五层:定时自毁清理

5.1 清理策略

VCodeCleanExpiredAction 实现验证码的定期自动销毁:

// 清理条件
String selection = "message_type IN ("
&nbsp; &nbsp; + TYPE_VERIFICATION_CODE + ", " &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 类型 2
&nbsp; &nbsp; + TYPE_FORBID_READ_VERIFICATION_CODE &nbsp; &nbsp; // 类型 1
&nbsp; &nbsp; + ") AND received_timestamp < ?"
&nbsp; &nbsp; + " AND custom_locked <> 1" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 未被用户锁定
&nbsp; &nbsp; + " AND deleted_timestamp = 0"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 未被手动删除

// 7 天阈值计算
public static final long getCleanupThreshold() {
&nbsp; &nbsp; return getCurrentTime() - TimeUnit.DAYS.toMillis(7);
}

5.2 双数据库同步清理

// VCodeCleanUpModule — 批量清理模块
// 实现 ICleanUpModule 接口

// 每轮查询上限 400 条,最多执行 30 轮
private static final int BATCH_SIZE = 400;
private static final int MAX_ITERATIONS = 30;

// 清理流程:
// 1. 从 bugle_db 查询过期验证码
// 2. 同时删除 bugle_db 中的记录
// 3. 通过 sms_message_uri 删除 content://sms 中的对应记录
void f() {
&nbsp; &nbsp; // 删除本地 bugle_db 记录
&nbsp; &nbsp; deleteFromBugleDb(expiredMessages);
&nbsp; &nbsp; // 删除远端 content://sms 记录(如果存在的话)
&nbsp; &nbsp; for (String uri : smsUris) {
&nbsp; &nbsp; &nbsp; &nbsp; contentResolver.delete(Uri.parse(uri), null, null);
&nbsp; &nbsp; }
}

5.3 重扫与修正

清理模块还包含重新扫描逻辑,确保早期未被正确分类的验证码也能被清理:

// VCodeCleanUpModule.t() — 重新扫描网络消息
void t() {
&nbsp; &nbsp; // 查询所有带 custom_parse_json 的网络消息
&nbsp; &nbsp; // 重新执行 NLP 识别
&nbsp; &nbsp; // 将新发现的验证码更新为 TYPE_FORBID_READ_VERIFICATION_CODE
}

用户控制:开关与协议

6.1 验证码保护开关

用户可以在短信设置中控制验证码保护功能:

// SwitchPanelStatusHelper — 开关管理
public static boolean e() {
&nbsp; &nbsp; // 读取 SharedPreferences
&nbsp; &nbsp; // key: "pref_key_forbid_read_verification_code"
&nbsp; &nbsp; // 默认值: true(默认开启保护)
&nbsp; &nbsp; return getPreference("pref_key_forbid_read_verification_code", true);
}

// 开启保护时,同时写入 SharedPreferences 和 Settings Provider
public static void k() {
&nbsp; &nbsp; // 写入 SharedPreferences
&nbsp; &nbsp; editor.putBoolean("pref_key_forbid_read_verification_code", true);
&nbsp; &nbsp; // 写入系统 Settings(供其他系统组件读取)
&nbsp; &nbsp; Settings.Global.putInt(resolver, settingKey, 1);
}

6.2 智能短信协议

验证码保护依赖用户接受智能短信服务协议:

// ProtocolDialogUtil.g() — 检查用户是否已同意协议
public static boolean g() {
&nbsp; &nbsp; // 检查智能短信协议是否已接受
&nbsp; &nbsp; // 未接受协议时,验证码保护不生效
&nbsp; &nbsp; // 这保证了用户知情权
}

6.3 关联开关

验证码保护是智能短信服务的子功能,依赖以下开关链:

智能短信服务开关 (Smart Message Service)
&nbsp; &nbsp; ├── 卡片视图开关 (Card View)
&nbsp; &nbsp; ├── 验证码保护开关 (Forbid Read Verification Code) ← 核心开关
&nbsp; &nbsp; └── 网络消息服务开关 (Network Message Service)

SwitchPanelStatusHelper.c() 检查所有三个开关均开启才返回 true。

安全分析与攻击面

7.1 防护效果

| 攻击方式 | 防护效果 | | — | — | | 恶意应用通过 READ_SMS 读取 | 完全阻断 — 验证码不在 content://sms 中 | | NotificationListenerService 截获通知 | 部分阻断 — 通知仅显示遮蔽内容 | | 剪贴板嗅探 | 未防护 — 用户复制后仍在剪贴板 | | ADB 调试获取 | 部分阻断 — 需 root 权限直接访问 bugle_db | | SIM Swap / SS7 攻击 | 不在防护范围 — 属于运营商网络层 |

7.2 仍存在的暴露面

  1. Root 设备:root 权限可直接读取 bugle_db 数据库,绕过全部防护
  2. 剪贴板泄漏:用户通过”复制验证码”按钮复制后,验证码进入系统剪贴板
  3. 广播降级:当 AutoFill 服务不可用时,降级为广播分发,增加了攻击面
  4. NLP 绕过:如果验证码格式不被 NLP 引擎识别,将按普通短信处理,不受保护
  5. 白名单滥用Settings.Global 中的白名单如果被篡改,可添加恶意应用

7.3 与 Android 原生方案对比

| 特性 | Android 原生 | ColorOS | | — | — | — | | 验证码识别 | 无 | NLP 引擎智能识别 | | 通知遮蔽 | 无(需应用层实现) | 系统级自动遮蔽 | | 存储隔离 | 无(全部在 content://sms) | 私有 bugle_db 隔离 | | 自动填充 | Android Autofill Framework | AutoFill + 安全 ContentProvider | | 定时删除 | 无 | 7 天自动销毁 | | READ_SMS 防护 | 无 | 验证码对 READ_SMS 不可见 |

总结

ColorOS 的验证码防护体系展现了一个完整的纵深防御思路:

  1. 识别层(SmartDecorateManager)— 异步 NLP 引擎,精准区分验证码与普通短信
  2. 展示层(通知遮蔽)— 锁屏/通知栏仅显示脱敏文案,防止偷窥和截屏
  3. 存储层(bugle_db 隔离)— 验证码不进入 content://sms,从根本上阻断 READ_SMS 攻击
  4. 传输层(权限广播 + AutoFill)— 系统签名权限保护,仅受信任组件可获取验证码
  5. 生命周期(7 天自毁)— 自动清理过期验证码,消除历史数据泄漏风险

这套机制虽然增加了系统复杂度,也给依赖 content://sms 的合法应用(短信备份、企业管理等)带来了兼容性问题,但从安全角度看,它有效地将验证码从一个”所有应用可读的明文字符串”升级为”系统级保护的安全凭据”,显著提升了用户账户安全水位。


分析环境:OPPO PKG110, Android 15, ColorOS, Mms.apk 63MB逆向工具:JADX + JADX-MCP-Server所有代码片段来自反编译结果,变量名经过可读性还原


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:白帽技术与网络安全 0pen1 0pen1《短信验证码防泄漏安全机制逆向分析》

几个很夯的安全Skills 网络安全文章

几个很夯的安全Skills

文章总结: 文档介绍多个针对AI编程工具的安全技能包,包括CTF解题、多语言代码审计、威胁建模等自动化工具,能辅助渗透测试和漏洞挖掘。核心发现是这些技能包支持J
评论:0   参与:  0