-
-
[原创][原创]iOS 云手机检测 · 设备指纹篇
-
发表于: 10小时前 106
-
目录
- 一、前言:为什么 iOS 云手机检测如此特殊
- 二、为什么 iOS 云手机检测比 Android 难做
- 三、为什么还要检测 iOS 云手机
- 四、风控能力的分层
- 五、设备指纹的核心思路转变
- 六、Keychain:iOS 的加密保险箱
- 七、实现稳定的设备 ID
- 八、服务端聚合检测
- 九、优化:时间窗口
- 十、优化:避免家庭共享误判
- 十一、对抗与反制
- 十二、设备指纹的其他维度
一、前言:为什么 iOS 云手机检测如此特殊?
在深入设备指纹检测之前,我们需要理解一个本质差异:
┌─────────────────────────────────────────────────────────────┐
│ Android 云手机 │
├─────────────────────────────────────────────────────────────┤
│ 本质:虚拟机 / 模拟器 │
│ 检测思路:找"虚拟化痕迹" │
│ ───────────────────────────────────────────────────────────│
│ • 奇怪的 CPU 型号 │
│ • build.prop 中的厂商信息 │
│ • /proc 下的异常文件 │
│ • 传感器数据虚假 │
│ • 文件系统虚拟化特征 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ iOS 云手机 │
├─────────────────────────────────────────────────────────────┤
│ 本质:真实越狱机 │
│ 检测思路:找"被滥用的真机" │
│ ───────────────────────────────────────────────────────────│
│ • 一台设备被几十个账号共享 │
│ • 老机型在批量活跃 │
│ • 硬件特征被篡改 │
│ • 时间空间不匹配 │
│ • 远程控制的行为痕迹 │
└─────────────────────────────────────────────────────────────┘
核心转变:从"识别假设备"到"识别被滥用的真机"
二、为什么 iOS 云手机检测比 Android 难做
2.1 根本原因:两者本质不同
Android 云手机
iOS 云手机
本质
虚拟机/模拟器
真实越狱机
检测思路
找"虚拟化痕迹"
找"远程控制痕迹"
难度
相对简单,特征明显
困难,本质上就是真机
Android 云手机是跑在虚拟机里的,有大量明显的虚拟化特征:奇怪的 CPU 型号、异常的传感器数据、build.prop 里的厂商信息等。
而 iOS 云手机 = 真实的 iPhone + 越狱。从设备角度看,它就是一个真机,没有"虚拟化"可言。
2.2 iOS 沙盒的限制
Android App 可以做的事,iOS App 大部分都做不了:
Android 可检测:
├── /proc/cpuinfo、/proc/meminfo
├── build.prop 系统属性
├── 进程列表、端口监听
├── 文件系统遍历
└── 传感器原始数据
iOS App(沙盒内):
├── 看不到系统进程
├── 看不到端口监听
├── 访问不了系统目录
├── 传感器数据受限
└── 只能看自己沙盒内的东西
2.3 市场规模差异
Android 云手机:国内有大量服务商(红手指、蓝叠、夜神等),市场大,研究的人多
iOS 云手机:需要真机 + 越狱 + 机房,成本极高,市场规模小一个数量级
2.4 研究门槛高
iOS 云手机检测需要:
跨域知识:网络协议、行为分析、风控模型,不只是"设备指纹"
服务器端配合:单靠设备端无法判断,需要全局视角
实战数据:需要大量真实样本才能训练模型,普通开发者拿不到
三、为什么还要检测 iOS 云手机
虽然 iOS 云手机使用量小,但仍然需要检测:
3.1 iOS 用户本身价值更高
1 2 3 4 5 6 | 同一类攻击(薅羊毛/刷量/欺诈):Android 用户 → ARPU 低,攻击者需要 1000 台设备iOS 用户 → ARPU 高,攻击者只需要 100 台设备但 100 台 iOS 云手机造成的损失 ≈ 1000 台 Android |
- 电商、金融、游戏等行业,iOS 用户的客单价/付费率通常更高
- 攻击者很精明,他们会优先攻"价值高"的目标
3.2 iOS 是风控的"薄弱环节"
很多公司的风控策略:
1 2 3 4 | Android 端:检测很完善(虚拟机、模拟器、root)iOS 端: 检测较弱(传统认为"iOS 安全") ↓攻击者会自然流向"防守更松"的一侧 |
3.3 某些业务 iOS 是主阵地
- 一些中高端 app 主打 iOS 用户
- 游戏类、社交类、付费类产品
- 苹果审核政策严,某些黑灰产手段只能走云手机批量过审
3.4 单台设备的"利用率"更高
1 2 3 4 | 一台 Android 云手机:可能同时跑 5 个脚本,质量参差不齐一台 iOS 云手机: 成本高,攻击者会用它做"高价值攻击"比如:账号养号、评分刷榜、App Store 排名 manipulation |
3.5 防御完整性的要求
大厂风控的心态:
1 2 3 | "我们 99% 的 Android 设备都能识别,但 iOS 这块有个窟窿,攻击者知道了就会集中往这里打" |
风控体系不允许有明显的短板,否则会被集中突破。
3.6 检测成本并不高
| 检测类型 | 成本 |
|---|---|
| 设备端特征采集 | 低(复用现有数据) |
| 服务端 IP/行为分析 | 低(已有风控平台) |
| 越狱检测 | 低(很多现成方案) |
大部分特征是"顺手"收集的,不需要额外投入大量成本。
四、风控能力的分层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | ┌─────────────────────────────────────────┐│ L4: 高级对抗 ││ • iOS 云手机检测 ││ • 群控/设备农场检测 ││ • 深度人机识别 ││ • 对抗性样本/模型攻防 │├─────────────────────────────────────────┤│ L3: 中级风控 ││ • Android 虚拟机/模拟器检测 ││ • Root/越狱检测 ││ • 代理/VPN 检测 ││ • 基础行为模型 │├─────────────────────────────────────────┤│ L2: 入门风控 ││ • 设备指纹 ││ • IP 黑名单/地域检测 ││ • 简单规则引擎(频率限制等) │├─────────────────────────────────────────┤│ L1: 基础安全 ││ • 账号密码 ││ • 验证码 ││ • 基础防刷 │└─────────────────────────────────────────┘ |
为什么这是"高级"
| 普通风控 | iOS 云手机检测 | |
|---|---|---|
| 攻击者 | 小黑产、脚本小子 | 专业团队、有机房资源 |
| 技术门槛 | 低,网上抄代码 | 高,需要理解底层机制 |
| 对抗性 | 低,攻击者技术有限 | 极高,是攻防军备竞赛 |
| 公开资料 | 很多,随便搜 | 极少,都是实战经验 |
| 所需知识 | 单一维度 | 网络+行为+风控+对抗 |
五、设备指纹的核心思路转变
5.1 传统设备指纹的作用
- 识别同一台设备,用于关联分析
- 识别设备是否被篡改/伪造
5.2 iOS 云手机场景下的转变
不是识别"这是假设备",而是识别"这台设备被太多账号共享"
5.3 可检测的维度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ┌─────────────────────────────────────────────────────────────┐│ 设备指纹检测维度 │├─────────────────────────────────────────────────────────────┤│ ││ 1. 设备聚合度检测(最核心) ││ • 同一台设备指纹 → 背后有多少个账号? ││ • 正常用户:1 台设备 = 1~3 个账号 ││ • 云手机: 1 台设备 = 几十个/上百个账号 ││ ││ 2. 设备年龄异常 ││ • iPhone 6/6s/7/8 在 2025 年仍在活跃 ││ • 设备发布日期与 iOS 版本不匹配 ││ • 老设备突然"批量"活跃 ││ ││ 3. 硬件特征一致性检查 ││ • 机型 → CPU → 内存 → 分辨率 的匹配性 ││ • 越狱篡改可能留下破绽 ││ ││ 4. 设备指纹稳定性异常 ││ • 频繁重置 ID(抹除数据重新养号) ││ • 时区、语言与 IP 地理位置不匹配 ││ • 同一硬件,指纹频繁变化 ││ │└─────────────────────────────────────────────────────────────┘ |
六、Keychain:iOS 的加密保险箱
6.1 什么是 Keychain?
Keychain 是 iOS 系统提供的一个加密保险箱,用于安全存储敏感数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ┌─────────────────────────────────────────────────────────────┐│ iOS 系统 ││ ││ ┌──────────┐ ┌─────────────────────────────────┐ ││ │ App A │ │ │ ││ │ │────►│ Keychain │ ││ └──────────┘ │ ┌─────────────────────────┐ │ ││ │ │ • 登录密码 / Token │ │ ││ ┌──────────┐ │ │ • 设备标识符 │ │ ││ │ App B │ │ │ • 证书 / 密钥 │ │ ││ │ │────►│ │ • 敏感配置 │ │ ││ └──────────┘ │ └─────────────────────────┘ │ │ └─────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────┘ |
6.2 Keychain vs UserDefaults
| 对比项 | UserDefaults | Keychain |
|---|---|---|
| 存储位置 | App 沙盒内 | 系统统一管理 |
| 删除 App 时 | 数据被删除 | 数据保留 ✓ |
| 加密 | 否 | 系统级加密 ✓ |
| 典型用途 | 配置、偏好设置 | 密码、Token、设备 ID |
6.3 一个生活化的类比
1 2 3 4 5 6 | UserDefaults → 你包里的笔记本删 App 就像扔掉包,笔记本一起丢了Keychain → 银行保险柜删 App 就像扔掉包,保险柜里的东西还在只有"恢复出厂设置"才会清空保险柜 |
6.4 Keychain 的隔离机制
每个 App 只能访问自己存储的数据,通过 Service + Account 组合实现隔离:
1 2 3 4 5 6 7 8 9 | App A 的数据:├── Service: "com.appA"└── Account: "device_id"App B 的数据:├── Service: "com.appB"└── Account: "device_id"即使 Account 相同,Service 不同,数据也互不干扰 |
6.5 Keychain 的数据结构
Keychain 里存的是一条一条的记录,每条记录像一个字典:
1 2 3 4 5 | Keychain 的一条记录:├── kSecClass: 类型(通常是 kSecClassGenericPassword)├── kSecAttrAccount: 账号/键名(比如 "device_id")├── kSecAttrService: 服务名(比如 "com.myapp")└── kSecValueData: 实际数据(比如 UUID 字符串) |
可以把 Keychain 想象成一张表:
| Account (键名) | Service (服务) | Data (值) |
|---|---|---|
| device_id | com.myapp | abc-123-def-456 |
| user_token | com.myapp | eyJhbGc... |
6.6 什么情况下 Keychain 会被清空?
1 2 3 4 5 6 7 | ✓ 用户删 App → Keychain 数据还在✓ 系统升级 iOS → Keychain 数据还在✓ App 强制退出 → Keychain 数据还在✗ 用户恢复出厂设置 → Keychain 被清空✗ 用户在「设置 → 密码与钥匙串」手动删除✗ 用户卸载所有同一开发商的 App(可能触发清理) |
6.7 Keychain 是官方 API 吗?
是的,Keychain 是苹果官方提供的:
- 属于 Security 框架(苹果官方 SDK)
- 所有合法 App 都可以用,不需要特殊权限
- 很多你用的 App 都在用(存登录密码、Token 等)
七、实现稳定的设备 ID
7.1 iOS 设备标识的困境
在 iOS 发展历程中,设备标识经历了多次收紧:
| 标识 | 状态 | 原因 |
|---|---|---|
| UDID | ✗ 已禁用 | 隐私问题,苹果不再提供 |
| MAC 地址 | ✗ 已禁用 | 隐私问题,iOS 7 后无法获取 |
| IDFA | 不稳定 | 用户可重置,可限制追踪(LAT) |
| IDFV | 不够稳定 | 删除所有同开发商 App 后会变 |
7.2 核心思路
既然系统不再提供稳定的设备标识,我们只能自己造一个,存在 Keychain 里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | ┌─────────────────────────────────────────────────────────────┐│ 流程 │├─────────────────────────────────────────────────────────────┤│ ││ App 启动 ──► 查询 Keychain ──► 有?──► 返回已存的 ID ││ │ ││ ▼ ││ 没有 ││ │ ││ ▼ ││ 生成新 UUID ││ │ ││ ▼ ││ 存入 Keychain ││ │ ││ ▼ ││ 返回新 ID ││ │└─────────────────────────────────────────────────────────────┘ |
7.3 存储数据
import Security
func saveToKeychain(key: String, value: String) -> Bool {
// 1. 准备数据
let data = value.data(using: .utf8)!
// 2. 构造查询字典
let query = [
kSecClass: kSecClassGenericPassword, // 类型:通用密码
kSecAttrAccount: key, // 键名
kSecAttrService: "com.myapp", // 服务名
kSecValueData: data // 实际数据
] as CFDictionary
// 3. 先删除旧数据(如果存在)
SecItemDelete(query)
// 4. 添加新数据
let result = SecItemAdd(query, nil)
return result == errSecSuccess
}
// 使用示例
saveToKeychain(key: "device_id", value: "abc-123")
7.4 读取数据
func getFromKeychain(key: String) -> String? {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key, // 要查哪个键
kSecAttrService: "com.myapp", // 服务名
kSecReturnData: true, // 返回数据本身
kSecMatchLimit: kSecMatchLimitOne // 只返回一条
] as CFDictionary
var result: AnyObject?
let status = SecItemCopyMatching(query, &result)
if status == errSecSuccess,
let data = result as? Data,
let value = String(data: data, encoding: .utf8) {
return value
}
return nil
}
// 使用示例
if let deviceID = getFromKeychain(key: "device_id") {
print("找到设备ID: \(deviceID)")
} else {
print("没有,第一次启动")
}
7.5 完整封装
class DeviceID {
private static let key = "my_app_device_id"
static func get() -> String {
// 先尝试读取
if let savedID = getFromKeychain(key: key) {
return savedID
}
// 没有,生成新的
let newID = UUID().uuidString
saveToKeychain(key: key, value: newID)
return newID
}
private static func saveToKeychain(key: String, value: String) -> Bool {
let data = value.data(using: .utf8)!
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecAttrService: "com.myapp",
kSecValueData: data
] as CFDictionary
SecItemDelete(query)
return SecItemAdd(query, nil) == errSecSuccess
}
private static func getFromKeychain(key: String) -> String? {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecAttrService: "com.myapp",
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
] as CFDictionary
var result: AnyObject?
let status = SecItemCopyMatching(query, &result)
if status == errSecSuccess,
let data = result as? Data,
let value = String(data: data, encoding: .utf8) {
return value
}
return nil
}
}
// 使用时就一行
let deviceID = DeviceID.get()
7.6 客户端上传
每次请求都带上设备 ID:
func login(account: String) {
let deviceID = DeviceID.get() // 从 Keychain 拿
let params = [
"account": account,
"device_id": deviceID, // ← 关键
"timestamp": Date().timeIntervalSince1970
]
// 发送给服务端...
}
每次请求都要带,不只是登录:
- 登录
- 刷新 Token
- 核心业务接口
这样能持续追踪。
八、服务端聚合检测
8.1 核心问题
1 2 3 4 5 6 7 8 | ┌─────────────────────────────────────────────────────────────┐│ 问题:一个设备 ID,背后有几个账号在用? │├─────────────────────────────────────────────────────────────┤│ ││ 正常用户: 1 个设备 ID = 1 ~ 3 个账号 ││ 云手机: 1 个设备 ID = 几十个甚至上百个账号 ││ │└─────────────────────────────────────────────────────────────┘ |
8.2 数据存储
device_accounts 表(记录设备与账号的关联):
| device_id | account | first_seen |
|---|---|---|
| abc-123 | user_A | 2025-01-01 |
| abc-123 | user_B | 2025-01-02 |
| abc-123 | user_C | 2025-01-03 |
| xyz-999 | user_D | 2025-01-01 |
8.3 统计查询
1 2 3 4 | -- 统计设备关联的账号数SELECT COUNT(DISTINCT account)FROM device_accountsWHERE device_id = 'abc-123'; |
8.4 使用计数表优化
为了提升性能,可以维护一个计数表:
device_stats 表:
| device_id | account_count | last_updated |
|---|---|---|
| abc-123 | 3 | 2025-01-03 |
| xyz-999 | 1 | 2025-01-01 |
每次发现新账号关联这个设备:
1 2 3 4 5 | -- 更新计数UPDATE device_statsSET account_count = account_count + 1, last_updated = NOW()WHERE device_id = 'abc-123'; |
8.5 风险判定阈值
| account_count | 风险等级 | 处理建议 |
|---|---|---|
| 1 - 3 | ???? 正常 | 无需处理 |
| 4 - 10 | ???? 可疑 | 标记,持续观察 |
| 11+ | 高风险 | 可能是云手机,加强风控 |
8.6 服务端判断逻辑
1 2 3 4 5 6 7 8 9 10 | # 伪代码def check_device(device_id): count = get_account_count(device_id) if count <= 3: return "正常" elif count <= 10: return "可疑" else: return "高风险 - 可能是云手机" |
九、优化:时间窗口
9.1 为什么要加时间窗口?
场景:一台手机用了 3 年,换过 5 个主人
2023年:用户 A、B ─┐
2024年:用户 C、D ├─ 累积 7 个账号
2025年:用户 E、F、G┘
判断:云手机?
实际:只是二手手机被转手多次
如果一直累积,会越来越不准。
9.2 解决方案
只统计最近一段时间内的账号数量:
SELECT COUNT(DISTINCT account)
FROM device_accounts
WHERE device_id = 'abc-123'
AND first_seen > DATE_SUB(NOW(), INTERVAL 30 DAY);
9.3 时间窗口选择
时间窗口 适用场景
7 天 快速检测,短期行为
30 天 平衡选择,常用
90 天 长期观察,减少误判
9.4 效果对比
无时间窗口:
abc-123 设备 3 年累积 7 个账号 → 判定可疑?
有 30 天窗口:
abc-123 设备最近 30 天只有 2 个账号 → 正常 ✓
十、优化:避免家庭共享误判
10.1 家庭共享场景
场景:一家人共用一台 iPad
爸爸的账号
妈妈的账号 → 3 个账号,1 个设备
小孩的账号
会被误判为云手机吗?
按照之前的规则,3 个账号还算正常。但如果更多呢?
10.2 解决:看行为模式
真的一家人共用设备:
• 登录时间分散(早中晚,不同人)
• 登录地点一致(都在家里)
• 行为模式不同(有人打游戏,有人刷视频)
云手机批量操作:
• 登录时间集中(短时间内大量账号登录)
• 登录地点可能异常(机房 IP)
• 行为模式相似(都是机械化操作)
10.3 行为特征对比
特征 真实家庭共享 云手机批量操作
登录时间 分散(早中晚不同人) 集中(短时间大量登录)
登录地点 一致(都在家里) 可能异常(机房 IP)
行为模式 不同(各有偏好) 相似(机械化操作)
操作间隔 不规律,有人类特征 规律,像脚本
10.4 结论
不能只看账号数量,还要看账号之间的关系和行为模式。
十一、对抗与反制
11.1 攻击者的绕过方式
既然服务端在数"一个设备几个账号",攻击者会怎么破?
方式 1:定期重置设备 ID
攻击者流程:
• 1月1日:用设备 ID = abc-123,养 10 个号
• 1月2日:清空 Keychain,生成新的 ID = xyz-999,再养 10 个号
• 1月3日:再清空,再养...
结果:每个设备 ID 只关联 10 个账号,不触发阈值
方式 2:越狱插件自动清
越狱设备上有一些插件可以批量清除指定 App 的 Keychain:
越狱插件示例:
• Keychain Cleaner
• ClearKeychain
• 或者自己写个简单的脚本
效果:一键清空某个 App 的 Keychain,不用重置手机
方式 3:修改 Keychain 里的值
更高级的是不改 Keychain,而是改里面的值:
正常流程:
- App 读取 Keychain
- 拿到 device_id = "abc-123"
- 发给服务端
越狱插件 Hook: - App 读取 Keychain
- 插件拦截返回值
- 改成 device_id = "xyz-999"
- App 发给服务端的是假的
这叫 Hook 代码注入,越狱设备上很常见。
11.2 服务端的应对
单一设备 ID 容易被绕过,需要结合更多维度:
┌─────────────────────────────────────────────────────────────┐
│ 多维设备指纹 │
├─────────────────────────────────────────────────────────────┤
│ • 设备 ID(Keychain) │
│ • IP 地址 │
│ • 设备型号、系统版本 │
│ • 电池状态、时区、语言 │
│ • 传感器特征 │
│ • 行为模式 │
└─────────────────────────────────────────────────────────────┘
即使改了设备 ID,其他特征没变,
还是能看出是同一台设备在批量操作。
11.3 多维度关联检测
12.1 设备年龄异常
云手机机房常用机型:
- iPhone 6 / 6s / 7 / 8 (成本低,容易越狱)
- iPhone SE 一代
检测点:
• 2025 年还在活跃的 iPhone 6
• 设备发布日期与当前 iOS 版本不匹配
• 老设备突然"批量"活跃
12.2 硬件特征一致性
正常设备:
• 机型 → CPU → 内存 → 屏幕分辨率 是匹配的
被篡改设备:
• 机型显示 iPhone 14
• 但分辨率、内存、CPU 特征对不上
12.3 设备指纹稳定性
正常设备:
• IDFV 相对稳定
• Keychain 存储的 ID 不变
• 系统配置(时区、语言)稳定
云手机/多账号:
• 频繁重置 ID(抹除数据重新养号)
• 时区、语言与 IP 地理位置不匹配
• 同一硬件,指纹频繁变化
12.4 能采集的设备特征
特征 采集方式 说明
设备 ID Keychain 存储 自生成 UUID
机型 UIDevice.current.model iPhone7,1 等
iOS 版本 UIDevice.current.systemVersion 15.0, 16.1 等
屏幕分辨率 UIScreen.main.bounds 宽 x 高
时区 TimeZone.current GMT+8 等
语言 Locale.current.language zh-Hans 等
电池状态 UIDevice.current.batteryLevel 电量百分比
十三、总结
13.1 核心思路转变
Android: 找"假的真机" → 虚拟化特征明显
iOS: 找"被滥用的真机" → 聚合度 + 行为异常
13.2 设备聚合检测完整流程
┌─────────────────────────────────────────────────────────────┐
│ 完整流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. iOS 端生成设备 ID,存入 Keychain │
│ 2. 每次请求携带设备 ID 上传 │
│ 3. 服务端记录 device_id ↔ account 关联 │
│ 4. 统计最近 N 天内,该设备关联的账号数 │
│ 5. 根据阈值判断风险等级 │
│ 6. 结合行为模式、IP 等多维度验证 │
│ │
└─────────────────────────────────────────────────────────────┘
13.3 关键技术点
技术点 说明
Keychain iOS 系统级加密存储,删 App 不丢
UUID 生成唯一标识符
时间窗口 只统计近期数据,避免历史累积
多维度 结合设备、IP、行为等综合判断
13.4 对抗与反制
单一设备 ID:
✓ 能识别普通用户
✗ 对抗越狱攻击者很脆弱
多维度指纹
• 设备 ID + IP + 机型 + 行为
• 即使改了 ID,其他特征仍在
• 需要综合分析才能绕过
总结:iOS 云手机检测的核心难点在于:它是真机。
我们无法像 Android 那样检测虚拟化痕迹,只能从"设备被滥用"的角度入手。设备聚合检测是其中的关键一环 —— 当一台设备被几十个账号共享时,无论它是不是虚拟机,都已经失去了"正常用户设备"的属性。
部分代码可能无法显示写成md文档以供查看