news 2026/10/11 4:04:25

手机被偷只能认栽?我给安卓装了“反向告警“:小偷换卡的瞬间,一条短信暴露他自己

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手机被偷只能认栽?我给安卓装了“反向告警“:小偷换卡的瞬间,一条短信暴露他自己

PhoneGuard 开源系列第六篇。前五篇我们聊了应用锁、ADB 取证、图标伪装、设备管理器自动锁机和 SIM 哨兵,这一篇补上防盗闭环里最解气的一环:手机被入侵时,让手机自己往外发短信报案。

一、传统防盗的三板斧,全都盲

手机丢了,常规操作就三招:远程锁定、定位追踪、挂失 SIM 卡。

但仔细想想,这三招有个共同的死穴——全都依赖"你还能连上那台手机" 。

小偷不是傻子。断网、拔卡、关机、进 Recovery 双清,三板斧瞬间全瞎。你看着查找手机页面上最后出现在某小区的定位点,然后就没有然后了。

换个角度想:小偷拿到手机后,一定会做某些动作——换自己的 SIM 卡、连电脑导数据、连猜密码。这些动作拦不住,但它们本身就是"活信号":能不能让手机在被动的瞬间,主动把消息送出去?

这就是 Avast、Lookout 这类老牌防盗软件的 SIM Change Alert 思路,也是我这篇要讲的实现:换卡告警。

二、最妙的一环:告警短信是"用小偷的卡"发出去的

先说清楚这个设计的精妙之处。

手机被换卡后,机里插的已经是入侵者的 SIM 卡。此时程序发出一条告警短信,这条短信走的是对方的号码、记在对方的运营商账单上、出现在对方的实名信息里。

也就是说:收件人只要收到这条短信,顺手回拨或截图,入侵者的手机号码就到手了。 这不是定位,是自投罗网。

在 PhoneGuard 里,这个能力做成了三个触发源共用一套告警管道:

表格

触发源 监听方式 场景

SIM 卡被换 SIM_STATE_CHANGED 广播 + ICCID 绑定对比 小偷拔卡换卡

连接电脑/USB USB 设备接入广播 + ADB 状态 小偷连电脑想导数据

密码连猜 设备管理器失败计数回调 小偷暴力试密码

任何一个触发,都会向机主预设的"紧急号码"发一条告警短信。下面拆开讲实现。

三、第一步:监听"换卡"这个动作

Android 提供了系统广播 android.intent.action.SIM_STATE_CHANGED,SIM 卡每次插拔、上电、加载都会广播。但注意,它是个状态机,不能一收到广播就喊"换卡了"——正常开机也会触发。

关键在 ss 这个 extra 里的两个状态:

kotlin

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

class SimChangeReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {

if (intent.action == "android.intent.action.SIM_STATE_CHANGED") {

val state = intent.getStringExtra("ss")

if (state == "ABSENT" || state == "LOADED") {

// ABSENT = 卡被拔出;LOADED = 新卡加载完成

// LOADED 时启动前台服务做进一步比对

if (state == "LOADED") {

val serviceIntent = Intent(context, SimGuardService::class.java)

context.startForegroundService(serviceIntent)

}

}

}

}

}

LOADED 意味着新卡已经就绪,这时候才轮到第二步:这张卡是不是原来那张?

四、第二步:ICCID 绑定对比——这里有个 API 坑

思路很简单:第一次使用时把当前卡的 ICCID(SIM 卡唯一序列号)存下来,之后每次 LOADED 就拿新卡的 ICCID 和存档比对,不一致 = 换卡。

但这里埋着一个很多文章不提的坑:从 Android 10(Q)开始,普通应用调 getSimSerialNumber() 已经拿不到 ICCID 了——没有特权权限只会返回 null 或直接抛 SecurityException。

我的处理是按系统版本降级到 simCarrierId(不需要特殊权限):

kotlin

1

2

3

4

5

6

7

8

9

10

11

12

13

14

private fun getCurrentIccid(): String {

return try {

val tm = getSystemService(TELEPHONY_SERVICE) as TelephonyManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

tm.simCarrierId.toString() // Q+ 的替代方案

} else {

@Suppress("DEPRECATION")

tm.simSerialNumber ?: ""

}

} catch (e: SecurityException) {

""

}

}

carrierId 是运营商分配的数字 ID,同一张卡值不变,做"是否换卡"的比对够用了。防御式写法上,任何异常都返回空串,比对逻辑只在两边都非空时才判定,避免权限异常导致误报。

五、第三步:把短信发出去——SmsManager 的三个细节

确认换卡后,就是发短信。SmsManager 的调用本身只有几行,但有三个细节决定这功能是"能用"还是"稳用":

细节一:API 31 之后的获取方式变了。 getDefault() 已废弃,Android 12 起推荐 getSystemService(SmsManager::class.java),且可能返回 null(无 SIM 模块设备),要接住。

细节二:短信超长要手动拆。 告警正文带机型、时间、行动指引,很容易超 70 个汉字的单条上限。divideMessage() + sendMultipartTextMessage() 是标配:

kotlin

1

2

3

4

5

6

7

8

9

10

11

12

val sm: SmsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

getSystemService(SmsManager::class.java) ?: return

} else {

@Suppress("DEPRECATION") SmsManager.getDefault()

}

val parts = sm.divideMessage(body)

if (parts.size <= 1) {

sm.sendTextMessage(phone, null, body, null, null)

} else {

sm.sendMultipartTextMessage(phone, null, parts, null, null)

}

细节三:SEND_SMS 是危险权限,必须静默降级。 用户没点授权时,程序绝不能崩,也不能反复弹窗骚扰——我的做法是检查权限,未授权就写一条日志提示"请在 App 内授权发短信",流程安静地结束:

kotlin

1

2

3

4

5

6

if (ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS)

!= PackageManager.PERMISSION_GRANTED) {

logger.logEvent(..., message = "未授权短信权限,告警未发出")

return

}

发送动作放在子线程,sendTextMessage 本身不保证同步返回结果,失败要捕获异常记录日志,方便用户在 App 内的日志页看到"为什么没收到短信"。

六、防轰炸:告警短信不能变成骚扰短信

轮询式检测有个天然问题:换卡后"新卡 ≠ 绑定卡"这个状态是持续成立的,如果每次轮询命中都发短信,小偷兜里那台手机会以 10 秒一条的频率轰炸紧急号码——电话费烧穿,告警反而被当成垃圾短信拉黑。

所以两个节流层是必须的:

SimGuard 服务层:10 秒轮询 + 30 分钟告警窗口,窗口内重复命中直接跳过;

通用告警层:五件套共用的 SmsAlertHelper 内置 10 分钟节流,同一个 Helper 无论被几个触发源调用,短信都不会刷屏。

kotlin

1

2

3

4

5

6

7

8

9

10

11

12

13

14

object SmsAlertHelper {

private var lastSmsAt = 0L

private const val SMS_INTERVAL_MS = 10 * 60 * 1000L

fun sendIfNeeded(context: Context, phone: String, body: String, ...) {

if (phone.isBlank()) return

if (未授权) { onResult("未授权"); return }

val now = System.currentTimeMillis()

if (now - lastSmsAt < SMS_INTERVAL_MS) return // 节流

lastSmsAt = now

Thread { ...发送... }.start()

}

}

七、短信正文怎么写?它是发给"未来的你"的

正文不是越长越好,是越可用越好。收到短信的人可能是机主自己(手机还在附近)、家人、或者捡到手机的好心人。我的模板:

plaintext

1

2

3

【PhoneGuard】您的手机SIM卡已被更换!设备:Pixel 6,时间:10-10 02:30。

若非本人操作,手机可能已被盗,请立即远程锁定、挂失SIM卡并报警。

三个要素:机型(多设备用户一眼认出是哪台)、时间(锁定案发窗口)、行动指引(不用临场想接下来干嘛)。

八、说两句边界

这套东西有用,但别神化:

它提供的是线索,不是证据。 短信暴露的号码是运营商实名信息,最终认定要靠警察,程序员不要把自己当执法者。

SEND_SMS 权限在国内应用商店上架会被严格审查,所以 PhoneGuard 的定位一直是侧载/个人使用——对个人用户来说,权限自己批,功能自己控,反而干净。

换卡告警依赖"小偷会换卡使用"这个行为假设,配合前几篇讲的设备管理器自动锁机(让刷机变难)和 ADB 取证(连电脑就留证据)一起用,才是完整的防御纵深。

九、完整源码

三个触发源 + 告警管道的全部实现都在这个开源仓库里,Kotlin + Jetpack Compose,六个独立工程按需取用:

GitHub:https://github.com/hnpdsyn/PhoneGuard1

apps/SimGuard:换卡告警(本文主角)

apps/AdbGuard:USB/ADB 接入告警

apps/Intrusion:暴力破解检测告警

apps/LoginGuard:登录失败退避 + 静默取证

apps/AppLock:应用锁 + 隐私保险箱

apps/SecurityLog:六件套统一安全日志

下一篇拆六件套的 "黑匣子"——SecurityLog 统一安全日志:怎么用一个 JSONL 文件记下全部入侵事件,既不拖慢前台,又能事后还原案发全程。

觉得有用的话点个收藏,仓库点个 Star,我们下篇见。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 4:04:16

Source Code Pro 代码字体选型与配置全攻略:从原理到团队落地

简介&#xff1a;Source Code Pro 是一款专为程序员设计的编程字体&#xff0c;字符边缘清晰、辨识度高&#xff0c;相比系统默认字体能显著提升代码可读性&#xff0c;适合在 Visual Studio Code、IntelliJ IDEA、终端等场景中长时间阅读与编写代码。字体家族覆盖 Light、Regu…

作者头像 李华
网站建设 2026/10/11 4:03:46

高效降压电路布局与铺铜优化技巧学习笔记

多路DC降压模块电路。在PCB中布局完成后如图所示。对该驱动板进行铺铜。在顶层给局部铺铜&#xff0c;使VIN形成大电流通路。如图所示&#xff0c;对VIN区域进行铺铜&#xff0c;避开GND铺铜区域。对类似电感元件进行禁止铺铜的处理。1.电感漏磁场会耦合到下方平面&#xff0c;…

作者头像 李华
网站建设 2026/10/11 4:03:08

TLS 与 SSL 有什么区别?你应该使用哪一个?

一份面向网站站长、开发者与运维人员的深度技术指南&#xff0c;从协议演变、工作原理、安全漏洞、性能对比到升级实战&#xff0c;全面解析 SSL 为何已被废弃、TLS 1.3 为何成为现代标准&#xff0c;以及今天你到底该用哪一个。1. 开头导语 自 1994 年 Netscape 推出 SSL 协议…

作者头像 李华
网站建设 2026/10/11 3:58:32

RT-Thread—STM32—环境搭建

RT-Thread——STM32——环境搭建 概述 本教程主要根据官方推荐的教程进行环境搭建&#xff0c;但是在打包方面按照自己的习惯进行了打包。 RT-Thread官网有特别详细的教程&#xff0c;这儿就不详细说明RT-Thread官网 软件准备 MDK528a (Keil5)CubeMx_v5-2-0STM32CubeMx的支持…

作者头像 李华
网站建设 2026/10/11 3:57:30

超酷!!!成功使用doxygen+Graphviz+HtmlHelp 自动生成函数调用关系图

使用DoxygenGraphvizHtmlHelp 生成函数调用关系图 在写这篇博客之前&#xff0c;首先感谢一下另外一篇博主的文章&#xff0c;Windows平台下DoxygenGraphVizHtmlHelp自动生成函数调用关系图。 附上原文链接&#xff1a;Windows平台下DoxygenGraphVizHtmlHelp自动生成函数调用关…

作者头像 李华
网站建设 2026/10/11 3:57:23

车载传感器与执行器工程实践手册:故障根因分析与系统级设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华