文章总结: 本文通过逆向分析ColorOS的Mms.apk,详细解析了短信验证码的五层防护体系:NLP智能识别引擎通过异步线程和超时机制精准识别验证码;通知栏内容遮蔽防止锁屏偷窥;关键创新是将验证码隔离存储于私有bugledb而非Android标准sms数据库,使READSMS权限失效;结合权限控制的广播分发和7天自毁机制,形成纵深防御。该设计有效抵御恶意应用读取、SIMSwap等攻击,提升移动账户安全性。 综合评分: 85 文章分类: 移动安全,应用安全,逆向分析,安全建设,终端安全
短信验证码防泄漏安全机制逆向分析
原创
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) {
List<IBubbleEntity>bubbles=entity.f();
if (bubbles!=null) {
for (IBubbleEntitybubble : bubbles) {
// id="-1" 是验证码气泡的固定标识
if (bubble!=null&&"-1".equals(bubble.getId())) {
returntrue;
}
}
}
}
returnfalse;
}
NLP 引擎将短信拆解为多个 Bubble,每个 Bubble 代表一个语义单元。验证码 Bubble 使用特殊 ID "-1" 标识,其 d() 方法返回提取的验证码值,b() 方法返回关联的 Action 列表(如”复制验证码”)。
1.4 消息类型标记
识别为验证码后,系统通过 VCodeMarkMessageTypeAction 对消息打标:
// 消息类型常量(MessageData)
TYPE_NONE=0; // 未分类
TYPE_FORBID_READ_VERIFICATION_CODE=1; // 受保护的验证码(默认)
TYPE_VERIFICATION_CODE=2; // 普通验证码
TYPE_OTHERS=100; // 通知/服务类
// 标记结果写入两处:
// 1. bugle_db → messages.message_type
// 2. content://sms → oplus_sms_type 字段
privatebooleanupdateTypeToRemoteDb(List<String>uriList, inttype) {
ContentValuesvalues=newContentValues();
values.put("oplus_sms_type", Integer.valueOf(type));
SqliteWrapper.f(app, resolver, Telephony.Sms.CONTENT_URI, values, ...);
}
TYPE_FORBID_READ_VERIFICATION_CODE(值为 1)是安全防护的核心——标记为此类型的短信将进入最严格的保护模式。
第二层:通知内容遮蔽
2.1 遮蔽策略
当验证码短信到达时,锁屏和通知栏不显示原始内容,而是替换为脱敏文案:
// VerificationCodeUtil.e() — 构建遮蔽后的通知文本
publicstaticbooleane(Contextcontext, ISmsEntityentity,
Stringtag, StringBuildersb) {
List<IBubbleEntity>bubbles=entity.f();
for (IBubbleEntitybubble : bubbles) {
if ("-1".equals(bubble.getId())) {
List<IActionBase>actions=bubble.b();
if (actions!=null&&!actions.isEmpty()) {
StringactionText=actions.get(0).d();
// 获取遮蔽提示文案
Stringhint=context.getString(
R.string.please_click_to_view_details);
// 中文格式:"验证码:XXXX,请点击查看详情"
// 英文格式:"Code: XXXX, Please click to view details"
if (isChinese(actionText)) {
sb.append(actionText.subSequence(2, length));
sb.append(":");
sb.append(bubble.d()); // 验证码值
sb.append(",");
sb.append(hint);
} else {
sb.append(codeLabel);
sb.append(": ");
sb.append(bubble.d());
sb.append(", ");
sb.append(hint);
}
}
returntrue;
}
}
returnfalse;
}
2.2 效果
用户看到的通知:
途虎养车
验证码:3554,请点击查看详情
而非原始短信全文:
【途虎养车】您的登录验证码是:3554,5分钟内有效,请勿泄漏。如非本人操作,请忽略此信息。
这防止了:
- 锁屏状态下验证码被偷窥
- 通知栏被恶意应用通过 NotificationListenerService 截获完整内容
- 投屏/录屏场景下验证码泄漏
2.3 区分验证码类型
系统还区分了无需特殊警告的验证码类型:
public static boolean c(ISmsEntity entity, Context context) {
List<IBubbleEntity> bubbles = entity.f();
for (IBubbleEntity bubble : bubbles) {
List<IActionBase> actions = bubble.b();
if (actions != null && actions.size() > 0) {
String actionText = actions.get(0).d();
// 与 "no_need_show_warn_code" 配置列表比对
String[] noWarnList = context.getResources()
.getStringArray(R.array.no_need_show_warn_code);
for (String item : noWarnList) {
if (actionText.equals(item)) {
return true; // 此类验证码无需额外安全警告
}
}
}
}
return false;
}
第三层:ContentProvider 隔离存储
3.1 存储隔离机制
这是最关键的防护层。被识别为验证码的智能短信不写入 Android 标准的 content://sms,而是仅存入 Mms 应用私有的 bugle_db 数据库:
标准 Android 短信流程:
短信 → TelephonyProvider → content://sms → 任何有 READ_SMS 权限的 App 可读
ColorOS 验证码流程:
短信 → OPPO 云端识别 → bugle_db (私有数据库)
↓
custom_messages_ext 表存储完整内容
↓
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 ← 验证码,无标准 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(
Uri.parse("content://sms"), null, null, null, "date DESC");
context.getContentResolver().query(
Uri.parse("content://mms-sms/conversations"), ...);
// caller_is_syncadapter 参数也无效
Uri.parse("content://sms")
.buildUpon()
.appendQueryParameter("caller_is_syncadapter", "true")
.build();
验证码被完全隔离在 Mms 应用的 /data/data/com.android.mms/databases/bugle_db 中,只有系统级权限(root)才能访问。
第四层:权限控制的广播分发
4.1 分发架构
验证码识别完成后,通过受权限保护的广播将验证码分发给受信任的系统组件:
// VerificationCodeUtil.l() — 分发入口
public static void l(Context context, int protocolStatus,
ISmsEntity entity, MessageData messageData) {
// 三重前置校验
if (FeatureOption.U // 功能开关
&& ProtocolDialogUtil.g() // 用户已同意隐私协议
&& protocolStatus == 0 // 协议状态正常
&& f(entity)) { // 确认是验证码
// 提取验证码值
String code = b(entity, "VerificationCodeUtil");
// 触发安全分发
k(context, messageData, code);
}
}
4.2 三路分发
// VerificationCodeUtil.k() — 验证码广播的三路分发
public static void k(Context context, MessageData data, String code) {
Intent intent = a(data, code); // 构建携带验证码的 Intent
// 路线一:发送给 OPPO 健康应用
o(context, intent, data);
// 路线二:发送给 AutoFill 服务或系统服务
n(context, intent);
// 路线三:发送给 Metis 智能助手
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) {
intent.putExtra("message_content", data.getMessageText());
// 带 OPLUS_COMPONENT_SAFE 权限发送
j(context, intent, HEALTH_RECEIVERS);
}
private static void j(Context context, Intent intent, String[] packages) {
for (String pkg : packages) {
intent.setPackage(pkg);
intent.setAction("oplus.intent.action.sms.verify_code");
context.sendBroadcast(intent,
"oplus.permission.OPLUS_COMPONENT_SAFE");
}
}
路线二:AutoFill 服务
private static void n(Context context, Intent intent) {
if (OplusAutoFillCaller.b(context)) {
// AutoFill 服务可用 → 通过 ContentProvider 安全传递
OplusAutoFillCaller.c(context, intent);
} else {
// 降级:通过广播发送给系统核心服务
// 接收方:android(system_server), com.oplus.exsystemservice
j(context, intent, SYSTEM_RECEIVERS);
}
}
路线三:Metis 智能助手
public static void m(Context context, MessageData data, String code) {
if (OsVersionUtils.u() && SmartMessageServiceUtil.i(context)) {
Intent intent = new Intent();
intent.putExtra("app_name", context.getString(R.string.app_name));
intent.putExtra("verify_code", code);
intent.putExtra("message_content", data.getMessageText());
intent.putExtra("message_title", notificationTitle);
intent.setAction("oplus.intent.action.sms.verify_code");
intent.setPackage("com.oplus.metis");
// 使用 MMS 专属权限
context.sendBroadcast(intent, "com.oplus.permission.safe.MMS");
}
}
4.3 Intent 结构
private static Intent a(MessageData data, String code) {
Intent intent = new Intent();
intent.putExtra("verify_code", code); // 提取的验证码值
intent.putExtra("conversation_id", data.getConversationId());
intent.putExtra("message_id", data.getMessageId());
intent.putExtra("sms_send_time", data.getSentTimestamp());
intent.putExtra("sms_received_time", data.getReceivedTimestamp());
intent.putExtra("sms_received_format_time",
data.getFormattedReceivedTimeStamp());
intent.setPackage("android"); // 初始目标为系统框架
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) {
// 检查 AutoFill 服务是否启用
ContentProviderClient client = context.getContentResolver()
.acquireUnstableContentProviderClient(
"com.oplus.codebook.autofill.access");
if (client != null) {
Bundle result = client.call("IsAutoFillOn", null, null);
return Boolean.TRUE.equals(result.getBoolean("IsAutoFillOn"));
}
return false;
}
public static void c(Context context, Intent intent) {
// 通过 ContentProvider 的 call() 方法安全传递验证码
ContentProviderClient client = context.getContentResolver()
.acquireUnstableContentProviderClient(
"com.oplus.codebook.autofill.access");
Bundle bundle = new Bundle();
bundle.putAll(intent.getExtras());
// "SMS_code" 是 AutoFill 服务识别的方法名
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) {
String whitelist = Settings.Global.getString(
context.getContentResolver(), "VERIFICATION_CODE_PKG");
if (TextUtils.isEmpty(whitelist)) {
// 默认白名单
whitelist = "com.android.chrome;"
+ "com.safeluck.schooltrainingorder.ningbo;"
+ "com.safety.act";
}
String[] packages = whitelist.split(";");
for (String pkg : packages) {
if (pkg.trim().equals(packageName)) {
return true;
}
}
return false;
}
白名单存储在 Settings.Global(需要系统权限才能修改),默认仅包含 Chrome 浏览器和两个特定应用。
第五层:定时自毁清理
5.1 清理策略
VCodeCleanExpiredAction 实现验证码的定期自动销毁:
// 清理条件
String selection = "message_type IN ("
+ TYPE_VERIFICATION_CODE + ", " // 类型 2
+ TYPE_FORBID_READ_VERIFICATION_CODE // 类型 1
+ ") AND received_timestamp < ?"
+ " AND custom_locked <> 1" // 未被用户锁定
+ " AND deleted_timestamp = 0"; // 未被手动删除
// 7 天阈值计算
public static final long getCleanupThreshold() {
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() {
// 删除本地 bugle_db 记录
deleteFromBugleDb(expiredMessages);
// 删除远端 content://sms 记录(如果存在的话)
for (String uri : smsUris) {
contentResolver.delete(Uri.parse(uri), null, null);
}
}
5.3 重扫与修正
清理模块还包含重新扫描逻辑,确保早期未被正确分类的验证码也能被清理:
// VCodeCleanUpModule.t() — 重新扫描网络消息
void t() {
// 查询所有带 custom_parse_json 的网络消息
// 重新执行 NLP 识别
// 将新发现的验证码更新为 TYPE_FORBID_READ_VERIFICATION_CODE
}
用户控制:开关与协议
6.1 验证码保护开关
用户可以在短信设置中控制验证码保护功能:
// SwitchPanelStatusHelper — 开关管理
public static boolean e() {
// 读取 SharedPreferences
// key: "pref_key_forbid_read_verification_code"
// 默认值: true(默认开启保护)
return getPreference("pref_key_forbid_read_verification_code", true);
}
// 开启保护时,同时写入 SharedPreferences 和 Settings Provider
public static void k() {
// 写入 SharedPreferences
editor.putBoolean("pref_key_forbid_read_verification_code", true);
// 写入系统 Settings(供其他系统组件读取)
Settings.Global.putInt(resolver, settingKey, 1);
}
6.2 智能短信协议
验证码保护依赖用户接受智能短信服务协议:
// ProtocolDialogUtil.g() — 检查用户是否已同意协议
public static boolean g() {
// 检查智能短信协议是否已接受
// 未接受协议时,验证码保护不生效
// 这保证了用户知情权
}
6.3 关联开关
验证码保护是智能短信服务的子功能,依赖以下开关链:
智能短信服务开关 (Smart Message Service)
├── 卡片视图开关 (Card View)
├── 验证码保护开关 (Forbid Read Verification Code) ← 核心开关
└── 网络消息服务开关 (Network Message Service)
SwitchPanelStatusHelper.c() 检查所有三个开关均开启才返回 true。
安全分析与攻击面
7.1 防护效果
| 攻击方式 | 防护效果 |
| — | — |
| 恶意应用通过 READ_SMS 读取 | 完全阻断 — 验证码不在 content://sms 中 |
| NotificationListenerService 截获通知 | 部分阻断 — 通知仅显示遮蔽内容 |
| 剪贴板嗅探 | 未防护 — 用户复制后仍在剪贴板 |
| ADB 调试获取 | 部分阻断 — 需 root 权限直接访问 bugle_db |
| SIM Swap / SS7 攻击 | 不在防护范围 — 属于运营商网络层 |
7.2 仍存在的暴露面
- Root 设备:root 权限可直接读取 bugle_db 数据库,绕过全部防护
- 剪贴板泄漏:用户通过”复制验证码”按钮复制后,验证码进入系统剪贴板
- 广播降级:当 AutoFill 服务不可用时,降级为广播分发,增加了攻击面
- NLP 绕过:如果验证码格式不被 NLP 引擎识别,将按普通短信处理,不受保护
- 白名单滥用:
Settings.Global中的白名单如果被篡改,可添加恶意应用
7.3 与 Android 原生方案对比
| 特性 | Android 原生 | ColorOS | | — | — | — | | 验证码识别 | 无 | NLP 引擎智能识别 | | 通知遮蔽 | 无(需应用层实现) | 系统级自动遮蔽 | | 存储隔离 | 无(全部在 content://sms) | 私有 bugle_db 隔离 | | 自动填充 | Android Autofill Framework | AutoFill + 安全 ContentProvider | | 定时删除 | 无 | 7 天自动销毁 | | READ_SMS 防护 | 无 | 验证码对 READ_SMS 不可见 |
总结
ColorOS 的验证码防护体系展现了一个完整的纵深防御思路:
- 识别层(SmartDecorateManager)— 异步 NLP 引擎,精准区分验证码与普通短信
- 展示层(通知遮蔽)— 锁屏/通知栏仅显示脱敏文案,防止偷窥和截屏
- 存储层(bugle_db 隔离)— 验证码不进入 content://sms,从根本上阻断 READ_SMS 攻击
- 传输层(权限广播 + AutoFill)— 系统签名权限保护,仅受信任组件可获取验证码
- 生命周期(7 天自毁)— 自动清理过期验证码,消除历史数据泄漏风险
这套机制虽然增加了系统复杂度,也给依赖 content://sms 的合法应用(短信备份、企业管理等)带来了兼容性问题,但从安全角度看,它有效地将验证码从一个”所有应用可读的明文字符串”升级为”系统级保护的安全凭据”,显著提升了用户账户安全水位。
分析环境:OPPO PKG110, Android 15, ColorOS, Mms.apk 63MB逆向工具:JADX + JADX-MCP-Server所有代码片段来自反编译结果,变量名经过可读性还原
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:白帽技术与网络安全 0pen1 0pen1《短信验证码防泄漏安全机制逆向分析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论