IosBX's Blog

IosbX's Blog
记录自己的每一次进步
  1. 首页
  2. iOS
  3. 正文

iOS身份验证的那些事

2026年 2月 11日 339点热度 2人点赞 0条评论

一.引言

在 iOS 开发中,准确识别设备或用户对于个性化服务、广告归因和安全风控至关重要。随着 Apple 对用户隐私保护的不断加强(尤其是 iOS 14 引入的 ATT 框架),获取设备唯一标识符变得越来越规范和严格,本文将站在正向开发和逆向开发的角度详细介绍我所知道的 iOS 中的身份标识,不足的地方还请谅解。

二. UIDevice

1. IDFV (Identifier for Vendor)

IDFV (Identifier for Vendor) 是供应商标识符,用于标识属于同一开发商(Vendor)的所有应用。

1.1 特点

  • 厂商隔离: 同一设备上,来自不同开发者账号的 App 获取到的 IDFV 是不同的。

  • 厂商共享: 同一设备上,来自同一开发者账号的不同 App 获取到的 IDFV 是相同的。

  • 隐私友好: 不需要用户授权即可获取。

1.2 生命周期

如果用户卸载了该 Vendor 下的所有 App,再次安装时,IDFV 会重置。如果设备上至少保留了一个该 Vendor 的 App,则 IDFV 保持不变。

1.3 代码示例

// 获取 IDFV
NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
NSLog(@"获取到的IDFV: %@", idfv);

1.4 Hook 修改

#import <objc/runtime.h>

@implementation UIDevice (Hook)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(identifierForVendor);
        SEL swizzledSelector = @selector(hook_identifierForVendor);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (NSUUID *)hook_identifierForVendor {
    // 返回自定义的 UUID
    return [[NSUUID alloc] initWithUUIDString:@"IosBX666-0000-0000-0000-000000000000"];
}

@end

2. IDFA (Identifier for Advertisers)

IDFA (Identifier for Advertisers) 是广告标识符,用于跨应用追踪用户,主要服务于广告投放和归因分析。

2.1 特点

  • 全局唯一: 在同一台设备上,所有 App 获取到的 IDFA 理论上是相同的。

  • 可重置: 用户可以在设置中重置广告标识符。

  • 需授权: 从 iOS 14.5 开始,必须通过 AppTrackingTransparency (ATT) 框架请求用户授权,否则只能获取到全 0 字符串。

  • Info.plist 配置: 必须添加 NSUserTrackingUsageDescription 键,描述获取权限的用途。

注意: 如果用户开启了“限制广告跟踪”或拒绝了 ATT 授权,获取到的 IDFA 将是 00000000-0000-0000-0000-000000000000。

2.2 代码示例

// 需导入头文件
#import <AdSupport/AdSupport.h>
#import <AppTrackingTransparency/AppTrackingTransparency.h>

// 请求权限并获取 IDFA
if (@available(iOS 14, *)) {
    [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
        if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
            NSString *idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
            NSLog(@"获取到的IDFA: %@", idfa);
        } else {
            NSLog(@"用户拒绝了追踪权限");
        }
    }];
} else {
    // iOS 14 以下直接获取
    NSString *idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
    NSLog(@"获取到的IDFA: %@", idfa);
}

2.3 Hook 修改

#import <AdSupport/AdSupport.h>
#import <objc/runtime.h>

@implementation ASIdentifierManager (Hook)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(advertisingIdentifier);
        SEL swizzledSelector = @selector(hook_advertisingIdentifier);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (NSUUID *)hook_advertisingIdentifier {
    // 返回自定义的 IDFA
    return [[NSUUID alloc] initWithUUIDString:@"IosBX666-1234-1234-1234-1234567890AB"];
}

@end

效果展示

测试设备 iOS26.2

授权之后

三. MobileConfig

UDID (Unique Device Identifier) 是设备的唯一硬件标识符,Apple 早就禁止 App 直接获取。但在 Safari 浏览器中,可以通过利用 MDM (Mobile Device Management) 协议,引导用户安装 MobileConfig 描述文件 来间接获取。

3.1 详细流程

  1. 用户访问: 用户在 iOS Safari 浏览器中访问特定的 Web 页面。

  2. 下载配置: 页面自动跳转下载一个 .mobileconfig 文件。

  3. 安装描述文件: 系统弹出“配置描述文件已下载”提示,用户需前往“设置”->“已下载的描述文件”进行安装。

  4. 回调上报: 安装过程中,系统根据描述文件中的 DeviceAttributes 配置,提取设备信息(UDID, IMEI, PRODUCT 等),并以 XML 格式 POST 回调到指定的服务器 URL。

  5. 重定向: 服务器接收数据后,返回一个 HTTP 301 重定向,将用户带回 Safari,并在 URL 参数中附带 UDID,从而实现 Web 端获取设备 UDID。

3.2 MobileConfig 文件结构示例

这是一个标准的未签名 .mobileconfig XML 文件内容。注意 DeviceAttributes 字段定义了需要获取的设备信息。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <dict>
        <key>URL</key>
        <!-- 接收设备信息回调的服务器接口 -->
        <string>https://your-server.com/receive_udid</string>
        
        <key>DeviceAttributes</key>
        <array>
            <string>UDID</string>
            <string>IMEI</string>
            <string>ICCID</string>
            <string>VERSION</string>
            <string>PRODUCT</string>
        </array>
    </dict>
    
    <key>PayloadOrganization</key>
    <string>Your Organization</string>
    <key>PayloadDisplayName</key>
    <string>查询设备 UDID</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
    <key>PayloadUUID</key>
    <string>9CF421B3-9853-4454-BC8A-9821D3F0F5E7</string>
    <key>PayloadIdentifier</key>
    <string>com.your.udid.profile</string>
    <key>PayloadDescription</key>
    <string>本文件仅用于获取设备 UDID,安装后可自动移除。</string>
    <key>PayloadType</key>
    <string>Profile Service</string>
</dict>
</plist>

3.3 服务器端处理

当用户点击安装后,Apple 服务器会将设备信息以 application/x-apple-aspen-config 格式 POST 到上述 XML 中定义的 URL。服务端需要解析 XML 数据,提取 UDID。

// PHP 简单示例
$data = file_get_contents('php://input');

// 解析 plist XML
$plist = new CFPropertyList();
$plist->parse($data);
$array = $plist->toArray();

$udid = $array['UDID'];
$imei = $array['IMEI'];

// 重定向回前端页面,带上 UDID
header("HTTP/1.1 301 Moved Permanently");
header("Location: https://your-website.com/result?udid=" . $udid);
exit();

3.4 优缺点分析

  • 优点: 能获取到最准确、不可变更的硬件 ID (UDID, IMEI)。

  • 缺点:

    • 交互流程繁琐,需跳转“设置”安装。

    • 需要申请 SSL 证书。

3.5 Hook 网络请求或抓包修改

四. libMobileGestalt 私有API

libMobileGestalt.dylib 是 iOS 系统的一个私有动态库,存储了大量设备硬件参数。通过调用其导出函数 MGCopyAnswer,可以获取包括序列号、设备型号、地区码等在内的数百项设备信息。

警告: 使用私有 API (Private API) 是 App Store 审核的红线。此方法仅限用于逆向分析、越狱插件或企业内部应用,需要用 ldid 提升对应权限。

4.1 特点

  • 数据详尽: 几乎包含所有设备硬件信息(如 SerialNumber, WifiAddress, BluetoothAddress, RegionCode 等)。
  • 系统级: 直接读取底层硬件配置,准确性极高。
  • 高风险: 属于 Private API,App Store 审核明令禁止。

4.2代码示例

// 声明私有函数
extern CFStringRef MGCopyAnswer(CFStringRef key);

// 调用示例
- (void)getDeviceParameters {
    // 获取设备序列号 (Serial Number)
    NSString *serialNumber = (__bridge_transfer NSString *)MGCopyAnswer(CFSTR("SerialNumber"));
    NSLog(@"序列号: %@", serialNumber);
    
    // 获取蓝牙地址
    NSString *bluetoothAddress = (__bridge_transfer NSString *)MGCopyAnswer(CFSTR("BluetoothAddress"));
    NSLog(@"蓝牙地址: %@", bluetoothAddress);
}

4.3 Hook 修改

直接 hook MGCopyAnswer 函数会导致进程立即崩溃 invalid instruction,因为在 iOS 的 libMobileGestalt.dylib 中,导出的 MGCopyAnswer 实际上只是一个 跳板函数(Trampoline) ,它内部直接跳转(Branch)到了另一个未导出的内部函数(通常称为 MGCopyAnswer_internal )。

需要在 MGCopyAnswer 函数的前 8 个字节内搜索 ARM64 的无条件跳转指令( b 指令),计算出它通过 PC 相对寻址要跳转到的绝对地址,这个地址就是真正的 MGCopyAnswer_internal。

#import <substrate.h>

static unsigned long long step64(const uint8_t *buf, unsigned long long start, size_t length, uint32_t what, uint32_t mask) {
	unsigned long long end = start + length;
	while (start < end) {
		uint32_t x = *(uint32_t *)(buf + start);
		if ((x & mask) == what) {
			return start;
		}
		start += 4;
	}
	return 0;
}

static unsigned long long find_branch64(const uint8_t *buf, unsigned long long start, size_t length) {
	return step64(buf, start, length, 0x14000000, 0xFC000000);
}

static unsigned long long follow_branch64(const uint8_t *buf, unsigned long long branch) {
	long long w;
	w = *(uint32_t *)(buf + branch) & 0x3FFFFFF;
	w <<= 64 - 26; w >>= 64 - 26 - 2;
	return branch + w;
}

static CFPropertyListRef (*orig_MGCopyAnswer_internal)(CFStringRef property, uint32_t *outTypeCode);
CFPropertyListRef new_MGCopyAnswer_internal(CFStringRef property, uint32_t *outTypeCode) {
	NSString *key = (__bridge NSString *)property;
	CFPropertyListRef result = nil;
	
	if ([key isEqualToString:@"UniqueDeviceID"]) {
		result = (__bridge_retained CFStringRef)@"FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF";
	} else {
		result = orig_MGCopyAnswer_internal(property, outTypeCode);
	}
	return result;
}

%ctor {
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
		NSLog(@"[IosBX] 正在加载 libMobileGestalt Hook");

		MSImageRef libGestalt = MSGetImageByName("/usr/lib/libMobileGestalt.dylib");
		if (libGestalt) {
			void *MGCopyAnswerFn = MSFindSymbol(libGestalt, "_MGCopyAnswer");
			NSLog(@"[IosBX] _MGCopyAnswer 地址: %p", MGCopyAnswerFn);
			const uint8_t *MGCopyAnswer_ptr = (const uint8_t *)MGCopyAnswer;
			unsigned long long branch = find_branch64(MGCopyAnswer_ptr, 0, 8);
			unsigned long long branch_offset = follow_branch64(MGCopyAnswer_ptr, branch);
			MSHookFunction(((void *)((const uint8_t *)MGCopyAnswerFn + branch_offset)), (void *)new_MGCopyAnswer_internal, (void **)&orig_MGCopyAnswer_internal);
		}
	});
}

八. 总结与对比

为了更直观地选择合适的方案,我们将上述几种方式进行了对比:

方案 持久性 (卸载重装) 合规性 (App Store) 用户授权 适用场景
IDFV 变 (若Vendor App全卸载) 合规 无需 公司内部 App 数据互通
IDFA 不变 (除非用户重置) 合规 需要 (ATT) 广告投放、跨 App 归因
MobileConfig (UDID) 永久不变 合规 (仅限 Safari/Web) 需安装描述文件 测试分发、企业内部设备管理
libMobileGestalt 永久不变 违规 (私有API) 无需 逆向分析、越狱插件
本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2026年 2月 12日

IosBX

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2026 IosBX's Blog. ALL RIGHTS RESERVED.