reworking my old private report on dcdevice
When creating a token from DCDevice, the method -[DCDevice generateTokenWithCompletionHandler:] is used – inside it calls DCDeviceMetadataDaemonConnection. This method creates a connection to the iPhone’s devicecheckd daemon:
NSXPCConnection *xpc_connection = (NSXPCConnection *)objc_msgSend(
objc_alloc((Class)&OBJC_CLASS___NSXPCConnection),
"initWithMachServiceName:options:",
CFSTR("com.apple.devicecheckd"),
0LL);devicecheckd
After connecting to the devicecheckd daemon via XPC, the following chain of calls starts. When a connection is received, the daemon invokes -[DCXPCListener listener:shouldAcceptNewConnection:] -> -[DCClientHandler initWithConnection:], and after the client makes an RPC call -> -[DCClientHandler fetchOpaqueBlobWithCompletion]. First of all, -[DCClientHandler fetchOpaqueBlobWithCompletion] calls:
if ( -[DCClientHandler _isSupported](self, "_isSupported") )
The value of this variable is hard-coded in DeviceIdentityIsSupported from DeviceIdentity.framework:
__int64 DeviceIdentityIsSupported_1()
{
return 1LL;
}I assume that if DCDevice is unavailable on the device, then there will be a different build of the framework where it is hard-coded to 0. After checking support, it calls -[DCClientHandler _generateAppIDFromCurrentConnection] – this method obtains . (format: ABCDE12345.com.example.myApp) using entitlements (-[NSXPCConnection valueForEntitlement:]), and if that fails, it uses SecTaskCopyTeamIdentifier and SecTaskCopySigningIdentifier. If team_id is valid and not "0000000000", the method combines the team_id and bundle_id with a dot; otherwise it uses only the bundle_id. It returns an appID (. or ):
return [appID length] ? appID : nil;
Returning to -[DCClientHandler fetchOpaqueBlobWithCompletion] (important note: I will not consider the fallback cases where the code goes into the else branch for error handling):
if ( app_id )
{
DCContext_class = objc_alloc_init((Class)&OBJC_CLASS___DCContext);
objc_msgSend(DCContext_class, "setClientAppID:", app_id); // we set our "<TeamID>.<BundleIdentifier>" in the class
// allocate DCDDeviceMetadata and initialize DCCryptoProxyImpl from DeviceCheckInternal.framework
DCDDeviceMetadata = objc_alloc((Class)&OBJC_CLASS___DCDDeviceMetadata);
DCCryptoProxyImpl = objc_alloc_init((Class)&OBJC_CLASS___DCCryptoProxyImpl);
// initialize DCDDeviceMetadata from DeviceCheckInternal.framework
init_DCDDeviceMetadata = objc_msgSend(
DCDDeviceMetadata,
"initWithContext:cryptoProxy:",
DCContext_class,
DCCryptoProxyImpl);
objc_msgSend(init_DCDDeviceMetadata, "generateEncryptedBlobWithCompletion:", v4);
}In summary: the devicecheckd daemon returns an encrypted token (opaque blob) to DeviceCheck.framework via XPC in the completionHandler block passed through -[DCDDeviceMetadata generateEncryptedBlobWithCompletion:]. This blob is later used as the token parameter in -[DCDDeviceMetadata generateTokenWithCompletionHandler:].
Breaking Down DCContext, DCDDeviceMetadata, and DCCryptoProxyImpl in fetchOpaqueBlobWithCompletion
DeviceCheckInternal.framework
The first thing that happens in -[DCClientHandler fetchOpaqueBlobWithCompletion] is the initialization of DCContext and assignment of our . or to it:
DCContext_class = objc_alloc_init((Class)&OBJC_CLASS___DCContext);
objc_msgSend(DCContext_class, "setClientAppID:", app_id);
Since DCContext has no -init method of its own, it simply inherits the implementation from NSObject. Memory is allocated for the object and a pointer to the DCContext class is set. Fields (for example _clientAppID) are zero-initialized. Then the -init selector is sent to the DCContext object. Because DCContext does not override this method, the standard -[NSObject init] is called, which simply returns self without additional logic. After that, the method -[DCContext setClientAppID:] is called, which sets self->_clientAppID to our <TeamID>.<BundleIdentifier> or <BundleIdentifier>:
id __cdecl __noreturn -[DCContext clientAppID](DCContext *self, SEL a2)
{
return objc_getProperty_33(self, a2, 8LL, 1);
}Skipping allocation of DCDDeviceMetadata, we look at:
DCCryptoProxyImpl = objc_alloc_init((Class)&OBJC_CLASS___DCCryptoProxyImpl);
Similarly, memory is allocated for the object, a pointer to the DCCryptoProxyImpl class is set, and then -[NSObject init] is called. Now our DCDDeviceMetadata (which was just allocated) is initialized:
init_DCDDeviceMetadata = objc_msgSend(
DCDDeviceMetadata,
"initWithContext:cryptoProxy:",
DCContext_class, // our class holding <TeamID>.<BundleIdentifier> or <BundleIdentifier>
DCCryptoProxyImpl_class_arg);What happens during the initialization of DCDDeviceMetadata:
// we are actually working with the instance's memory area, not with a C-structure per se
00000000 struct DCDDeviceMetadata // sizeof=0x18
00000000 {
00000000 unsigned __int8 superclass_opaque[8];
00000008 DCCryptoProxy *_cryptoProxy;
00000010 DCContext *_context;
00000018 };
id -[DCDDeviceMetadata initWithContext:cryptoProxy:](DCDDeviceMetadata *self, SEL a2, id DCContext_class_arg, id DCCryptoProxyImpl_class_arg)
{
DCDDeviceMetadata *dc_device_metadata = -[DCDDeviceMetadata init](self, "init"); // -[NSObject init]
if ( dc_device_metadata )
{
// set up fields in DCDDeviceMetadata
j__objc_storeStrong((id *)&dc_device_metadata->_cryptoProxy, DCCryptoProxyImpl_class_arg);
j__objc_storeStrong((id *)&dc_device_metadata->_context, DCContext_class_arg); // our context with our <TeamID>.<BundleIdentifier> or <BundleIdentifier>
}
return (id *)dc_device_metadata;
}Great, all classes have been initialized and now the devicecheckd daemon calls objc_msgSend(init_DCDDeviceMetadata, "generateEncryptedBlobWithCompletion:", v4); – a method that will begin generating the token.
DeviceCheckInternal.framework
Important note: almost all methods are rewritten by me to make them easier to read and understand. Our daemon invoked the method objc_msgSend(init_DCDDeviceMetadata, "generateEncryptedBlobWithCompletion:", v4); – this is the start of token creation.
void __cdecl -[DCDDeviceMetadata generateEncryptedBlobWithCompletion:](DCDDeviceMetadata *self, SEL a2, id completion_arg)
{
id v4 = objc_retain(completion_arg); // retain the completion argument
// exactly what was set up in -[DCDDeviceMetadata initWithContext:cryptoProxy:]
DCCryptoProxy *cryptoProxy = self->_cryptoProxy;
DCContext *context = self->_context; // our context with our <TeamID>.<BundleIdentifier> or <BundleIdentifier>
[cryptoProxy fetchOpaqueBlobWithContext:context
completion:^(NSData *blob, NSError *error) {
// this block corresponds to __57__DCDDeviceMetadata_generateEncryptedBlobWithCompletion___block_invoke
// take pointer to the original XPC completion block saved at the time of the call.
// if argument a2 (data) is non-zero, call completion(data, nil).
// if a2 is zero, create an NSError with code 0 and call completion(nil, error).
}];
}Excellent, the method -[DCCryptoProxy fetchOpaqueBlobWithContext:completion:] is now called, to which our context (with . or ) and the completion handler are passed. IDA decompiles the code poorly, so it will be slightly rewritten for clarity:
void __cdecl -[DCCryptoProxyImpl fetchOpaqueBlobWithContext:completion:](
DCCryptoProxyImpl *self,
SEL a2,
id DCContext_argDCContext_arg,
id completion_arg)
{
// hold onto context (<TeamID>.<BundleIdentifier> or <BundleIdentifier>) and completion
id retainedContext = [DCContext_argDCContext_arg retain];
void (^copiedCompletion)(NSData *, NSError *) = [completion_arg copy];
if (os_log_type_enabled(self.logger, OS_LOG_TYPE_DEFAULT))
{
os_log(self.logger, "Generating certificate...");
}
__block id blockContext = retainedContext;
__block void (^blockCompletion)(NSData *, NSError *) = copiedCompletion;
[self _fetchPublicKey:^(NSData *publicKey) {
DCCertificateGenerator *generator = [[DCCertificateGenerator alloc]
initWithContext:blockContext // our context with our <TeamID>.<BundleIdentifier> or <BundleIdentifier>
publicKey:publicKey]; // our key
[generator generateEncryptedCertificateChainWithCompletion:
^(NSData *encryptedChain, NSError *error)
{
blockCompletion(encryptedChain, error);
[blockCompletion release];
[blockContext release];
[generator release];
}
];
}
];
}First, the publicKey is obtained, and then it is passed on. How do we get the public key:
void __cdecl -[DCCryptoProxyImpl _fetchPublicKey:](DCCryptoProxyImpl *self, SEL a2, id completion)
{
/*
void *v4; // x20
id v5; // x19
_QWORD v6[4]; // [xsp+8h] [xbp-38h] BYREF
id v7; // [xsp+28h] [xbp-18h]
v4 = (void *)objc_claimAutoreleasedReturnValue_5(+[DCAssetFetcher sharedFetcher](&OBJC_CLASS___DCAssetFetcher, "sharedFetcher"));
v6[0] = _NSConcreteStackBlock_ptr;
v6[1] = 3221225472LL;
v6[2] = __37__DCCryptoProxyImpl__fetchPublicKey___block_invoke;
v6[3] = &unk_20A9B2838;
objc_msgSend(v4, "fetchPublicKeyAssetWithCompletion:", v6);
*/
DCAssetFetcher *fetcher = [DCAssetFetcher sharedFetcher];
[fetcher fetchPublicKeyAssetWithCompletion:^(NSData *publicKey) {
completion(publicKey);
}];
}This method calls -[DCAssetFetcher fetchPublicKeyAssetWithCompletion:].
void __cdecl -[DCAssetFetcher fetchPublicKeyAssetWithCompletion:](DCAssetFetcher *self, SEL a2, id publicKeyCompletion)
{
/*
DCAssetFetcherContext *v5 = objc_alloc_init(&OBJC_CLASS___DCAssetFetcherContext);
id v4 = objc_retain(publicKeyCompletion);
-[DCAssetFetcherContext setAllowCatalogRefresh:](v5, "setAllowCatalogRefresh:", 0LL);
-[DCAssetFetcher _fetchAssetWithContext:completionHandler:](self, "_fetchAssetWithContext:completionHandler:", v5, v4);
objc_release(v4);
objc_release(v5);
*/
DCAssetFetcherContext *context = [[DCAssetFetcherContext alloc] init];
void (^completionBlock)(NSData *) = [publicKeyCompletion retain];
[context setAllowCatalogRefresh:NO]; // self->_allowCatalogRefresh = NO;
[self _fetchAssetWithContext:context
completionHandler:completionBlock];
}For now I won't comment further; we continue down the chain:
void __cdecl -[DCAssetFetcher _fetchAssetWithContext:completionHandler:](
DCAssetFetcher *self,
SEL a2,
(DCAssetFetcherContext *)context, // another context, without team or bundle ID
id (void (^)(NSData *assetData, NSError *error))completion)
{
DCAssetFetcherContext *retainedContext = [context retain];
void (^completionBlock)(NSData *, NSError *) = [completion retain];
if (os_log_type_enabled(self.logger, OS_LOG_TYPE_DEFAULT)) {
os_log(self.logger, "Querying...");
}
[self _queryMetadataWithContext:retainedContext
completion:completionBlock];
[completionBlock release];
[retainedContext release];
}Now things get interesting – _queryMetadataWithContext:
void __cdecl __noreturn -[DCAssetFetcher _queryMetadataWithContext:completion:](
DCAssetFetcher *self,
SEL a2,
(DCAssetFetcherContext *)context, // another context, without team or bundle ID
completion:(void (^)(NSData *assetData, NSError *error))completion)
{
DCAssetFetcherContext *retainedContext = [context retain];
void (^completionBlock)(NSData *, NSError *) = [completion retain];
if (os_log_type_enabled(self.logger, OS_LOG_TYPE_DEFAULT))
{
os_log(self.logger,
"Starting to fetch asset with context: %@",
retainedContext);
}
id assetQuery = [[self _assetQuery] retain]; // claimAutoreleasedReturnValue
NSUInteger resultCode = [assetQuery queryMetaDataSync]; // this is all from libobjc.A
// Branch: skip cache or absent (ignoreCachedMetadata || resultCode == 2)
if ([retainedContext ignoreCachedMetadata] || resultCode == 2)
{
[self _handleMissingMetadataWithContext:retainedContext
completion:completionBlock];
} else {
if (resultCode != 0)
{
// Error: generate NSError and return immediately
NSError *error = [NSError errorWithDomain:@"com.apple.twobit.fetcherror"
code:0xFFFF_FFFF_FFFF_F448
userInfo:nil];
completionBlock(nil, error);
[assetQuery release];
return;
}
// Success: pass data upward
[self _handleSuccessForQuery:assetQuery
completion:completionBlock];
}
[assetQuery release];
[completionBlock release];
[retainedContext release];
}.........
- (void)_handleMissingMetadataWithContext:(DCAssetFetchContext *)context
completion:(void (^)(DCAsset *asset, NSError *error))completion {
NSLog(@"[DCAssetFetcher] Query sync result indicated missing asset catalog");
if (context.allowCatalogRefresh) {
context.allowCatalogRefresh = NO;
context.ignoreCachedMetadata = NO;
// trigger asset catalog refresh and retry the request after completion
[self->_assetQuery refreshCatalogWithCompletion:^{
[self _queryMetadataWithContext:context completion:completion];
}];
return;
}
NSError *error = [NSError errorWithDomain:@"com.apple.twobit.fetcherror"
code:-3101
userInfo:nil];
completion(nil, error);
}....
- (void)_handleSuccessForQuery:(DCAssetQuery *)query
completion:(void (^)(DCAsset *asset, NSError *error))completion {
NSArray *results = [query results];
if (results.count == 0) {
NSError *error = [NSError errorWithDomain:@"com.apple.twobit.fetcherror"
code:-3100
userInfo:nil];
completion(nil, error);
return;
}
if (results.count > 1) {
NSLog(@"[DCAssetFetcher] Warning: more than one asset found, using first one");
}
NSDictionary *mobileAsset = results.firstObject;
NSError *validationError = nil;
DCAsset *asset = [self _validateAsset:mobileAsset error:&validationError];
if (!asset) {
completion(nil, validationError);
return;
}
[[DCXPCActivityController sharedInstance] updateActivityScheduleWithAsset:asset];
completion(asset, nil);
}......
- (DCAsset *)_validateAsset:(NSDictionary *)mobileAsset error:(NSError **)error {
DCAsset *asset = [DCAsset assetWithMobileAsset:mobileAsset];
if (!asset) {
if (error) {
*error = [NSError errorWithDomain:@"com.apple.twobit.fetcherror"
code:-3200
userInfo:nil];
}
return nil;
}
return asset;
}......
+ (DCAsset *)assetWithMobileAsset:(NSDictionary *)mobileAsset {
NSNumber *version = mobileAsset[@"com.apple.MobileAsset.AssetVersion"];
if (![version isKindOfClass:[NSNumber class]] || version.integerValue != 1) {
NSLog(@"[DCAsset] Unknown asset version: %@", version);
return nil;
}
NSData *pubKeyData = mobileAsset[@"com.apple.devicecheck.pubvalue"]; // assetProperty:
if (![pubKeyData isKindOfClass:[NSData class]] || pubKeyData.length == 0) {
NSLog(@"[DCAsset] No public key found in asset");
return nil;
}
DCAsset *asset = [[DCAsset alloc] init];
asset.version = 1;
asset.publicKey = pubKeyData;
NSNumber *refreshInterval = mobileAsset[@"com.apple.devicecheck.refreshtimer"]; // assetProperty:
if ([refreshInterval isKindOfClass:[NSNumber class]]) {
asset.publicKeyRefreshInterval = refreshInterval.doubleValue;
}
return asset;
}....
- (void)updateActivityScheduleWithAsset:(DCAsset *)asset {
if (asset.publicKeyRefreshInterval <= 0) return;
NSDictionary *criteria = @{
XPC_ACTIVITY_INTERVAL : @(asset.publicKeyRefreshInterval),
XPC_ACTIVITY_REPEATING : @YES,
XPC_ACTIVITY_REQUIRE_NETWORK : @YES
};
xpc_activity_register("com.apple.devicecheck.notify", criteria, ^(xpc_activity_t activity) {
[[DCAssetFetcher sharedInstance] performMetadataRefreshForActivity];
});
}And the funniest part – none of this was needed and it’s the wrong path: the correct key is in __37__DCCryptoProxyImpl__fetchPublicKey___block_invoke:
+[NSData dataWithBytes:length:](&OBJC_CLASS___NSData, "dataWithBytes:length:", &fallback_server_pubkey, 65LL);
It is hard-coded and the same in all versions of macOS, iOS, and iPadOS:
0450d934fa67bcf6f2dfbf96629e0a7238e9205d75f28cfcd84f35a6592bbe058a9c0f8edbca2acb67efb774971ca45f7d856a694fb1b9c40b94fb2e7a5a9498b0
This is 130 bytes of key – the key itself is 65 characters:
╭─ ~ ······························································································································· ✔ at 12:04:28
╰─ unhex 0450d934fa67bcf6f2dfbf96629e0a7238e9205d75f28cfcd84f35a6592bbe058a9c0f8edbca2acb67efb774971ca45f7d856a694fb1b9c40b94fb2e7a5a9498b0
P�4�g���߿�b�
r8� ]u���O5�Y+������*�g�t��_}�jiO���
��.zZ���%
and that’s our public key.
DeviceCheckInternal.framework
Important note: almost all methods are rewritten by me to make them easier to read and understand. Returning to fetchOpaqueBlobWithContext:
void __cdecl -[DCCryptoProxyImpl fetchOpaqueBlobWithContext:completion:](
DCCryptoProxyImpl *self,
SEL a2,
id DCContext_argDCContext_arg,
id completion_arg)
{
// hold onto context (<TeamID>.<BundleIdentifier> or <BundleIdentifier>) and completion
id retainedContext = [DCContext_argDCContext_arg retain];
void (^copiedCompletion)(NSData *, NSError *) = [completion_arg copy];
if (os_log_type_enabled(self.logger, OS_LOG_TYPE_DEFAULT))
{
os_log(self.logger, "Generating certificate...");
}
__block id blockContext = retainedContext;
__block void (^blockCompletion)(NSData *, NSError *) = copiedCompletion;
[self _fetchPublicKey:^(NSData *publicKey) // here we receive our key
{
DCCertificateGenerator *generator = [[DCCertificateGenerator alloc]
initWithContext:blockContext // our context with our <TeamID>.<BundleIdentifier> or <BundleIdentifier>
publicKey:publicKey]; // our key
[generator generateEncryptedCertificateChainWithCompletion:
^(NSData *encryptedChain, NSError *error)
{
blockCompletion(encryptedChain, error);
[blockCompletion release];
[blockContext release];
[generator release];
}
];
}
];
}Now let’s look at the initialization of generator:
00000000 struct DCCertificateGenerator // sizeof=0x18
00000000 {
00000000 unsigned __int8 superclass_opaque[8];
00000008 NSData *_publicKey;
00000010 DCContext *_context;
00000018 };
id __cdecl -[DCCertificateGenerator initWithContext:publicKey:](DCCertificateGenerator *self, SEL a2, id context_arg, id publicKey_arg)
{
DCCertificateGenerator *v9 = -[DCCertificateGenerator init](self, "init"); // -[NSObject init]
if ( v9 )
{
j__objc_storeStrong((id *)&v9->_publicKey, publicKey_arg); // our obtained public key
j__objc_storeStrong((id *)&v9->_context, context_arg); // our context with our <TeamID>.<BundleIdentifier> or <BundleIdentifier>
}
return (id *)v9;
}After successful initialization, generateEncryptedCertificateChainWithCompletion is called – creation of the token:
void __cdecl -[DCCertificateGenerator generateEncryptedCertificateChainWithCompletion:](
DCCertificateGenerator *self,
SEL a2,
id a3)
{
id v3; // x20
_QWORD v4[4]; // [xsp+0h] [xbp-40h] BYREF
DCCertificateGenerator *v5; // [xsp+20h] [xbp-20h]
id v6; // [xsp+28h] [xbp-18h]
v4[0] = _NSConcreteStackBlock_ptr;
v4[1] = 3221225472LL;
v4[2] = __74__DCCertificateGenerator_generateEncryptedCertificateChainWithCompletion___block_invoke;
v4[3] = &unk_20A9B2860;
v5 = self;
v6 = objc_retain(a3);
v3 = objc_retain(v6);
-[DCCertificateGenerator _generateCertificateChainWithCompletion:](v5, "_generateCertificateChainWithCompletion:", v4);
objc_release(v6);
objc_release(v3);
}First – we need to get the certificate via _generateCertificateChainWithCompletion, and then pass it to __74__DCCertificateGenerator_generateEncryptedCertificateChainWithCompletion___block_invoke – where it will be passed to _encryptData, the place where the token is created. In short, all this is about obtaining two root certificates from the keychain – in theory this can be reversed or the logic replicated (which I very much doubt, since it involves the keychain), but obtaining the certificate happens in __DeviceIdentityIssueClientCertificateWithCompletion_block_invoke. Here are example certificates from the argument:
-----BEGIN CERTIFICATE-----
MIIDPjCCAuWgAwIBAgIGAZhghIx7MAoGCCqGSM49BAMCMFMxJzAlBgNVBAMMHkJh
c2ljIEF0dGVzdGF0aW9uIFVzZXIgU3ViIENBMTETMBEGA1UECgwKQXBwbGUgSW5j
LjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yNTA3MzAxMjQ1NTZaFw0yNjA3MTQy
MDE2NTZaMIGRMUkwRwYDVQQDDEA5ZjE2ZTNiNDU3OWY3Y2Q4MzBjMWFjNTNhM2Y2
MDk2MmFlZWIyMDgzMTgwNzI4Y2UzNTMyNmM2Y2JhZDdlOTIzMRowGAYDVQQLDBFC
QUEgQ2VydGlmaWNhdGlvbjETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwK
Q2FsaWZvcm5pYTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOY3WyNGuRO+wcrp
t2Yb6ARssM0g5GFCy302nZQ3p/DPxR3cG4wqLK73zYEiKjUXU1Uv0bgbV61CmTSv
Pkd0KmejggFkMIIBYDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIE8DCCAT4G
CSqGSIb3Y2QKAQSCAS8EggErMYIBJ/+EmqGSUA0wCxYEQ0hJUAIDAIAV/4SqjZJE
ETAPFgRFQ0lEAgcKTVwAaAAu/4aTtcJjGzAZFgRibWFjBBFkNDphMzozZDozNDo2
YTowMP+Gy7XKaRkwFxYEaW1laQQPMzU5NDA0MDgyMDM4NzA1/4ebydxtFjAUFgRz
cm5tBAxGSzJWTUREVUpDTDj/h6uR0mQyMDAWBHVkaWQEKDU1MzcwZjUyYWQwOWRi
YmEyYjZhYTcwZDM4ZjZmMjc5NzExYmYxNmX/h7u1wmMbMBkWBHdtYWMEEWQ0OmEz
OjNkOjM0OjY5OmRm/4ebldJkOjA4FgRzZWlkBDAwNDI0MzEyQjFFNDM4MDAxNzIx
OTE0MTgyNTk0Mjg0NDdCRjZFMzJCQzNCN0YwQ0YwCgYIKoZIzj0EAwIDRwAwRAIg
LNjRS4pu3925qkoourOxUM3L+8hwVYbT/35bIW9q5KQCICf4Xpvuvdx/DMBbr5Wp
9GXn0bxP69/8S99Z9x+t0ow0
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICIzCCAaigAwIBAgIIeNjhG9tnDGgwCgYIKoZIzj0EAwIwUzEnMCUGA1UEAwwe
QmFzaWMgQXR0ZXN0YXRpb24gVXNlciBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTE3MDQyMDAwNDIwMFoXDTMyMDMy
MjAwMDAwMFowUzEnMCUGA1UEAwweQmFzaWMgQXR0ZXN0YXRpb24gVXNlciBTdWIg
Q0ExMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMBZ
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABOY3WyNGuRO+wcrpt2Yb6ARssM0g5GFC
y302nZQ3p/DPxR3cG4wqLK73zYEiKjUXU1Uv0bgbV61CmTSvPkd0KmejggFkMIIB
YDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIE8DCCAT4GCSqGSIb3Y2QKAQSC
AS8EggErMYIBJ/+EmqGSUA0wCxYEQ0hJUAIDAIAV/4SqjZJEETAPFgRFQ0lEAgcK
TVwAaAAu/4aTtcJjGzAZFgRibWFjBBFkNDphMzozZDozNDo2YTowMP+Gy7XKaRkw
FxYEaW1laQQPMzU5NDA0MDgyMDM4NzA1/4ebydxtFjAUFgRzcm5tBAxGSzJWTURE
VUpDTDj/h6uR0mQyMDAWBHVkaWQEKDU1MzcwZjUyYWQwOWRiYmEyYjZhYTcwZDM4
ZjZmMjc5NzExYmYxNmX/h7u1wmMbMBkWBHdtYWMEEWQ0OmEzOjNkOjM0OjY5OmRm
/4ebldJkOjA4FgRzZWlkBDAwNDI0MzEyQjFFNDM4MDAxNzIxOTE0MTgyNTk0Mjg0
NDdCRjZFMzJCQzNCN0YwQ0YwCgYIKoZIzj0EAwIDRwAwRAIgLNjRS4pu3925qko
ourOxUM3L+8hwVYbT/35bIW9q5KQCICf4Xpvuvdx/DMBbr5Wp9GXn0bxP69/8S99
Z9x+t0ow0
-----END CERTIFICATE-----
or:
-----BEGIN CERTIFICATE-----
MIIDYTCCAwegAwIBAgIGAZY9utjzMAoGCCqGSM49BAMCMFMxJzAlBgNVBAMMHkJh
c2ljIEF0dGVzdGF0aW9uIFVzZXIgU3ViIE5BMTETMBEGA1UECgwKQXBwbGUgSW5j
LjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yNTA0MTUwODMyNTdaFw0yNjA0MTAx
NTAwNTdaMIGRMUkwRwYDVQQDDEA1ODk5YWNjNTY4YWI2YjJiOThjYzdmMmRjMTYy
YWJlNTkzZjJlMDM0YjJlNDAyMjA2Y2MzOWMwMmFkNTQzNzcxMRowGAYDVQQLDBFC
QUEgQ2VydGlmaWNhdGlvbjETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwK
Q2FsaWZvcm5pYTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEfuSX2/VaqQkTcU
M3wLH7/5zIkiO/oJIsW08lKB5jGhL+v+pcqsYqt9yQsbxPS3axgjbqoCOcP/V3zY
Fy8kDm82jggGGMIIBgjAMBggqhkjOPQQDAgUAA0gAMEUCIHLplLqirOgMmrPkMSQa
DOpl/MAyEYejw/otUrfGGITiAiEAuLs1MYHGWuUdhyofXfY0S45GsSYXA/g8ombH
MkcU54A=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICIzCCAaigAwIBAgIIeNjhG9tnDGgwCgYIKoZIzj0EAwIwUzEnMCUGA1UEAwwe
QmFzaWMgQXR0ZXN0YXRpb24gVXNlciBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTE3MDQyMDA0MjAwMFoXDTMyMDMy
MDAwMDAwMFowUzEnMCUGA1UEAwweQmFzaWMgQXR0ZXN0YXRpb24gVXNlciBSb290
IENBMTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTBZ
MBMGByqGSM49AgEGCCqGSM49AwEHA0gAMEUCIHLplLqirOgMmrPkMSQaDOpl/MAy
EYejw/otUrfGGITiAiEAuLs1MYHGWuUdhyofXfY0S45GsSYXA/g8ombHMkcU54A=
-----END CERTIFICATE-----
We skip all of these details and go to _encryptData – the creation of the token. Let’s create the token:
- (NSData *)_encryptData:(NSData *)data // certificates
serverSyncedDate:(NSDate *)serverDate
error:(NSError **)error {
// Log start of encryption
os_log_t log = os_log_create("com.apple.devicecheck", "DeviceCheck");
if (os_log_type_enabled(log, OS_LOG_TYPE_DEFAULT)) {
os_log(log, "Encrypting data...");
}
// Get client App ID as UTF-8 data
NSString *clientAppID = [self.context clientAppID];
NSData *clientAppData = [clientAppID dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger clientAppLen = clientAppData.length;
const void *clientAppBytes = clientAppData.bytes;
// Prepare input data
NSUInteger inputLen = data.length;
const void *inputBytes = data.bytes;
// Current timestamp from server-synced date
NSTimeInterval timestamp = [serverDate timeIntervalSince1970];
// AES-GCM mode descriptor from CommonCrypto
const struct ccmode_gcm *gcm = ccaes_gcm_encrypt_mode();
// Allocate buffers: output and plaintext payload
size_t payloadLen = inputLen + clientAppLen + 81;
size_t outputLen = inputLen + clientAppLen + 235;
uint8_t *outBuf = calloc(1, outputLen);
if (!outBuf) {
if (error) *error = [NSError errorWithDomain:@"DeviceCheckError"
code:-1
userInfo:nil];
return nil;
}
uint8_t *payloadBuf = calloc(1, payloadLen);
if (!payloadBuf) {
free(outBuf);
if (error) *error = [NSError errorWithDomain:@"DeviceCheckError"
code:-1
userInfo:nil];
return nil;
}
// Write header/type (value 2) and payload length in outBuf
*((uint32_t *)outBuf) = 2;
*((uint32_t *)(outBuf + 150)) = (uint32_t)payloadLen;
// Copy device's static public key into outBuf at offset 5
NSData *devicePubKeyData = self.publicKey; // NSData containing the public key bytes
memcpy(outBuf + 5, devicePubKeyData.bytes, devicePubKeyData.length);
// Assemble payload: [timestamp (8 bytes), inputLen (4 bytes), clientAppLen (4 bytes), inputBytes, clientAppBytes]
*((uint32_t *)(payloadBuf + 73)) = (uint32_t)inputLen;
memcpy(payloadBuf + 81, inputBytes, inputLen);
*((uint32_t *)(payloadBuf + 77)) = (uint32_t)clientAppLen;
memcpy(payloadBuf + 81 + inputLen, clientAppBytes, clientAppLen);
*((uint64_t *)(payloadBuf + 65)) = (uint64_t)timestamp;
// Log system call (no-op with 0) as seen in disassembly
DCLogSystem(0);
// Create ephemeral ECDH key using the keybag
id keybag = [self keybagHandle];
uint64_t refKey = 0;
int aksErr = aks_ref_key_create((__int64)keybag, 11, 4, 0, 0, &refKey);
if (aksErr != 0) {
DCLogSystem(aksErr);
free(outBuf);
free(payloadBuf);
if (error) *error = [NSError errorWithDomain:@"DeviceCheckError"
code:aksErr
userInfo:nil];
return nil;
}
// Get ephemeral public key (should be 65 bytes) and copy it
size_t ecdhPubLen = 0;
const uint8_t *ecdhPub = (const uint8_t *)aks_ref_key_get_public_key(refKey, &ecdhPubLen);
if (ecdhPubLen != 65) {
DCLogSystem(ecdhPubLen);
free(outBuf);
free(payloadBuf);
if (error) *error = [NSError errorWithDomain:@"DeviceCheckError"
code:-2
userInfo:nil];
return nil;
}
memcpy(outBuf + 85, ecdhPub, ecdhPubLen);
// Compute ECDH shared secret with device's static public key
const uint8_t *devicePubBytes = devicePubKeyData.bytes;
size_t devicePubLen = devicePubKeyData.length;
int ecdhErr = aks_ref_key_compute_key(refKey, 0, 0, (__int64)devicePubBytes, devicePubLen);
if (ecdhErr != 0) {
DCLogSystem(ecdhErr);
free(outBuf);
free(payloadBuf);
if (error) *error = [NSError errorWithDomain:@"DeviceCheckError"
code:ecdhErr
userInfo:nil];
return nil;
}
// The shared secret is at refKey; skip first 2 bytes (per disassembly analysis)
uint8_t *sharedSecret = (uint8_t *)refKey + 2;
size_t sharedLen = devicePubLen - 2;
// Derive key material with HKDF-SHA256 (44 bytes: 32-byte key + 12-byte IV)
uint8_t hkdfOut[44];
cchkdf(ccsha256_di(), sharedLen, sharedSecret, 0, NULL, 0x2C /*44 bytes*/, hkdfOut);
uint8_t *aesKey = hkdfOut; // first 32 bytes
uint8_t *aesIV = hkdfOut + 32; // next 12 bytes
// Encrypt payloadBuf with AES-GCM; tag-> outBuf+1, ciphertext-> outBuf+154
int gcmErr = ccgcm_one_shot(gcm,
32, aesKey,
12, aesIV,
0, NULL,
payloadLen, payloadBuf,
outBuf + 154,
16, outBuf + 1);
if (gcmErr != 0) {
DCLogSystem(gcmErr);
free(outBuf);
free(payloadBuf);
if (error) *error = [NSError errorWithDomain:@"DeviceCheckError"
code:gcmErr
userInfo:nil];
return nil;
}
// Build NSData for the encrypted payload
NSData *encryptedData = [NSData dataWithBytes:outBuf length:outputLen];
// Log base64 payload if logging is enabled
if (os_log_type_enabled(log, OS_LOG_TYPE_DEFAULT)) {
NSData *b64 = [encryptedData base64EncodedDataWithOptions:0];
NSString *payloadStr = [[NSString alloc] initWithData:b64
encoding:NSUTF8StringEncoding];
os_log(log, "\nPayload (base64):\n%{public}s\n\n", payloadStr.UTF8String);
}
free(outBuf);
free(payloadBuf);
return encryptedData;
}In this repository I attempted to show the full path of generating a DeviceCheck token: from calling DCDevice in the app, through XPC and the devicecheckd daemon, to the final encryption of the payload in _encryptData. Key stages are covered – initializing the context with TeamID.BundleID, obtaining the public key (including the fallback constant), assembling the certificate chain, and forming the encrypted blob sent to the server. I hope this report made it a bit clearer why DeviceCheck is such a pain. In theory it can be bypassed (at least by using your own obtained certificates), but I'm too lazy to do that.