一.引言
在 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 特点
注意: 如果用户开启了“限制广告跟踪”或拒绝了 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 详细流程
-
用户访问: 用户在 iOS Safari 浏览器中访问特定的 Web 页面。
-
下载配置: 页面自动跳转下载一个
.mobileconfig文件。 -
安装描述文件: 系统弹出“配置描述文件已下载”提示,用户需前往“设置”->“已下载的描述文件”进行安装。
-
回调上报: 安装过程中,系统根据描述文件中的
DeviceAttributes配置,提取设备信息(UDID, IMEI, PRODUCT 等),并以 XML 格式 POST 回调到指定的服务器 URL。 -
重定向: 服务器接收数据后,返回一个 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 优缺点分析
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 审核明令禁止。
// 声明私有函数
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) | 无需 | 逆向分析、越狱插件 |




文章评论