首页
社区
课程
招聘
[原创][原创]iOS 云手机检测 · 设备指纹篇
发表于: 10小时前 106

[原创][原创]iOS 云手机检测 · 设备指纹篇

10小时前
106

目录

一、前言:为什么 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_accounts
WHERE 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_stats
SET 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,而是改里面的值:

正常流程:

  1. App 读取 Keychain
  2. 拿到 device_id = "abc-123"
  3. 发给服务端

    越狱插件 Hook:
  4. App 读取 Keychain
  5. 插件拦截返回值
  6. 改成 device_id = "xyz-999"
  7. 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文档以供查看


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回