免责声明:本文只作为系统研究与学习请勿用于任何商业用途。
BB两句,做这件事情完全是兴趣使然。研究时间在2023年6月附近,请各位看官带着问题看不然很容易迷茫,在分析这个安全组件的时候其实国内没有很细的文章都是零零碎碎的知识点,这个文章当作一个分析与总结(老逼灯的逆向终章)。
Q1:DeviceCheck Token 这个安全组件到底干什么用的?
Q2:大厂的基本应用与Token的使用场景,以及谁在用
从各个交流群社区看,这个玩意应该有人已经分析过了,只是没人发而已交流的时候已经能感觉到各位大佬做过这件事了。带着这些问题开始我们的逆向分析之旅。
##环境&基础知识储备
本次分析流程的主要环境
- 静态分析工具IDA pro
- 设备:iPhone7p
- 系统:iOS 13.4
- 动态分析工具:frida 版本任意
- 最好有Xcode环境:因为要写例子做交叉测试
##开工
逆向就是一个推理过程,如果你有正向开发的经验会事半功倍,这个开发经验到达了解的阶段就可以对我们的逆向分析工作有很大的帮助。
DeviceCheck Token是什么安全组件以及使用方法具体解释请参见这里:传送门,我的理解就是苹果在收紧安全访问权限(应用层无法访问设备唯一ID)后提供的替代方案。
importDeviceCheck
let device=DCDevice.current
ifdevice.isSupported {
device.generateToken { data, errorin
iflet token=data?.base64EncodedString() {
send token to your server
}
}
}
代码看上去很简单就是生成了一个token,这个token有个特点每次生成都不一致但是,这token在应用方(app的开发者)他们上传给业务开发者服务器后,再次传递给苹果提供的接口查询与验证,苹果贴心的为每台提供2bit的状态存储,这个token只有苹果能验证其他人无法验证唯一性(想想他们是怎么做到的),2bit最多能表示4种状态:00、01、10、11、这些状态就可以为app的业务方提供支撑可以用来做什么呢?
- 新设备标识:(如果这个token,没有被设置过任何状态那好,发新人券)占用:
00 - 老设备标识:占用:
01 - 违规设备标识: 占用:
10 - other:占用:
11在我们对这个安全组件进行了初步的认识以后我们要做的是写例子也好,直接调试目标APP也好,就是找到这个token的生成来源做定位与分析,这里要提前交代一下DDeviceCheckToken在iOS 15系统以后进行迭代与更新,本次分析仅仅适用于13.x-14.x,废话不多少直接上具体frida调试脚本,这里使用的是自己写的例子避免token感染,因为我自己写的不会去上传和变更状态,如果是想做0元购的同学我劝你自己写例子。
Interceptor.attach(ObjC.classes['DCDevice']['- generateTokenWithCompletionHandler:'].implementation, {
onEnter: function (args) {
console.warn('\n generateToken..... \n Called from:\n'+Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n')+'\n'+"end ----------");
},
onLeave: function (retval) {
}
});
generateToken.....
Called from :
0x18e4bca20 Foundation!__NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__
0x18dbaa330 libxpc.dylib!_xpc_malloc
0x18db9d068 libxpc.dylib!_xpc_dictionary_insert
0x18dba4f98 libxpc.dylib!_xpc_mach_send_deserialize
0x18db9c290 libxpc.dylib!_xpc_dictionary_deserialize_into
0x18ddab5b4 libsystem_kernel.dylib!kdebug_trace
0x18db9c0e0 libxpc.dylib!_xpc_dictionary_create_from_received_message
0x1b7f010b0 DeviceCheck! - [DCDevice _isSupportedReturningError:]
0x1b7f01280 DeviceCheck! - [DCDevice isSupported]
0x104e01aac DCDeviceCheckExample! - [ViewController viewDidLoad]
0x18dc40434 libdispatch.dylib!_dispatch_block_invoke_direct$VARIANT$mp
0x18ddab5b4 libsystem_kernel.dylib!kdebug_trace
0x104e020f8 DCDeviceCheckExample!main
end - - - - - - - - - -
xpc - - - - - - - - - - - - - > com.apple.devicecheckd port: 2055
这里请注意看,在我们获取token的时候他会去调用系统的xpc服务,这个服务就是 com.apple.devicecheckd,端口是 port:2055,这里要普及一个知识,就是iOS系统内进程之间通讯用的是xpc服务调用的方式,这个可以自行了解。经过一番搜索和gpt问答在13.x 系统内有一个devcheckd 守护进程用来服务获取 ddtoken 的生成工作,在root的手机上自行ps -ef | grep devicecheckd 就能查询到所在的位置,打开以后你会发现这个进程对应mach-o文件为什么这么小啊,这是得益于苹果的dyld缓存机制所有公用的库都会被整合进dyld_cache文件中如果想完整看到 devicecheckd 生成token流程完整解析是必须的,如果是局部解析也可以就是会缺省很多变量的识别而已。局部解析请自行解析 devicecheckd查看import表中对应库,这里要多BB两句就是Apple内部在写项目的时候这种系统的服务都是wapper项目所有核心代码都在缓存中,挺好的项目结构清晰。至此痛苦面具开始展开
我的完整解析目前是20g左右,因为在定位和分析的过程中无不的卡,按x看引用都会卡的飞起,核心跟踪思路就是一层一层找函数没有技巧可言。耐心就够了,考虑到大家的体验我会把脚本直接放出还有具体的为代码方便大家减少痛苦。

Interceptor.attach(ObjC.classes['DCCertificateGenerator']['- _encryptData:error:'].implementation, {
onEnter: function (args) {
let input_data=new ObjC.Object(args[2]);
let byte_ptr=input_data.bytes();
let byte_size=input_data.length();
console.warn(``-[DCCertificateGenerator _encryptData:${buf2hex(byte_ptr.readByteArray(byte_size))} error:]);
},
onLeave: function (retval) {
let output_data=new ObjC.Object(retval);
let byte_ptr=output_data.bytes();
let byte_size=output_data.length();
console.warn(``-[DCCertificateGenerator _encryptData:error:]return->${buf2hex(byte_ptr.readByteArray(byte_size))});
}
});
/***
*buf convert hex_str
*@parambufferbufferisan ArrayBuffer
*@returns {string}
*/
function buf2hex(buffer) {
return[...new Uint8Array(buffer)].map(x=> x.toString(16).padStart(2,'0')).join('');
}
从上边代码不难看出核心生成代码就在:devicecheckd进程的 -[DCCertificateGenerator _encryptData:error:] 函数中这个函数的伪代码如下,由于我解析的比较完整部分变量我已经重命名了。
/***
*id__cdecl-[DCCertificateGenerator _encryptData:error:](DCCertificateGenerator*self, SEL a2,ida3,id*a4)
*{
*idv5;//x21
*void*v6;//x0
*NSObject*v7;//x19
*NSString*v8;//x0
*NSString*v9;//x19
*NSData*v10;//x0
*NSData*v11;//x20
*unsignedintclientAppId_length;//w28
*NSDate*alloc_data;//x0
*NSDate*v14;//x24
*unsignedintinput_data_length_v15;//w20
*idv16;//x21
*idinput_data_bytes;//x19
*_DWORD*v18;//x0
*_DWORD*v19_output_buf;//x22
*void*v20;//x21
*idv21_publickey_bytes;//x23
*idv22_public_keylen;//x0
*char*v23;//x21
*__int64 v24;//d0
*idv25;//x0
*__int64 v26;//x0
*NSDate*v27;//x28
*__int64 v28;//x19
*void*v29;//x0
*NSObject*v30;//x23
*void*v31;//x0
*_OWORD*public_key_1;//x0
*__int128 v33;//q0
*__int128 v34;//q1
*__int128 v35;//q2
*__int64 i;//x20
*void**v37;//x19
*idv38;//x20
*idv39;//x0
*__int64 v40;//x0
*__int64 v41;//x19
*void*v42;//x0
*void*v43;//x0
*NSData*v44;//x24
*__int64 v46;//x19
*__int64 v47;//x20
*unsigned __int8*v48;//x23
*unsignedintv49;//t1
*__int64 v50;//x0
*__int64 v51;//x19
*void*v52;//x0
*__int64 j;//x23
*__int64 k;//x20
*__int64 v55;//x0
*__int64 v56;//x19
*void*v57;//x0
*__int64 m;//x20
*NSData*v59;//x0
*void*v60;//x0
*idv61;//x19
*idv62;//x0
*idv63;//x20
*idv64;//x0
*idv65;//x19
*idv66;//x0
*__int64 v67_output_len;//[xsp+20h] [xbp-E0h]
*__int64 v68;//[xsp+28h] [xbp-D8h]
*idclientappid_bytes__src;//[xsp+30h] [xbp-D0h]
*NSData*v70;//[xsp+38h] [xbp-C8h]
*size_t __n;//[xsp+40h] [xbp-C0h] BYREF
*__int64 v72;//[xsp+48h] [xbp-B8h]
*__int64 v73;//[xsp+50h] [xbp-B0h]
*void**v74;//[xsp+58h] [xbp-A8h] BYREF
*uint8_t v75[4];//[xsp+60h] [xbp-A0h] BYREF
*idv76;//[xsp+64h] [xbp-9Ch]
*uint8_t buf[2];//[xsp+70h] [xbp-90h] BYREF
*_BYTE v78[16];//[xsp+90h] [xbp-70h] BYREF
*
*v5=j__objc_retain_765(a3);
*v6=(void*)_DCLogSystem(v5);
*v7=j__objc_retainAutoreleasedReturnValue_706(v6);
*if( j__os_log_type_enabled_718(v7, OS_LOG_TYPE_DEFAULT) )
*{
**(_WORD*)buf=0;
*j___os_log_impl_652(&dword_1ACFED000, v7, OS_LOG_TYPE_DEFAULT,"Encrypting data...", buf,2u);
*}
*j__objc_release_785(v7);
*v8=-[DCContext clientAppID](self->_context,"clientAppID");
*v9=j__objc_retainAutoreleasedReturnValue_706(v8);
*v10=-[NSString dataUsingEncoding:](v9,"dataUsingEncoding:",4LL);
*v11=j__objc_retainAutoreleasedReturnValue_706(v10);
*j__objc_release_785(v9);
*clientAppId_length=(unsignedint)j__objc_msgSend_796(v11,"length");
*v70=j__objc_retainAutorelease_520(v11);
*clientappid_bytes__src=j__objc_msgSend_796(v70,"bytes");
*alloc_data=+[NSDate date](&OBJC_CLASS___NSDate,"date");
*v14=j__objc_retainAutoreleasedReturnValue_706(alloc_data);
*input_data_length_v15=(unsignedint)j__objc_msgSend_796(v5,"length");
*v16=j__objc_retainAutorelease_520(v5);
*input_data_bytes=j__objc_msgSend_796(v16,"bytes");
*j__objc_release_785(v16);
*v73=0LL;
*v74=0LL;
*v72=0LL;
*v68=j__ccaes_gcm_encrypt_mode_10();
*v18=j__calloc_348(1uLL, input_data_length_v15+clientAppId_length+235LL);
*if( !v18 )
*{
*v27=v14;
*v31=(void*)_DCLogSystem(0LL);
*v30=j__objc_retainAutoreleasedReturnValue_706(v31);
*if( j__os_log_type_enabled_718(v30, OS_LOG_TYPE_ERROR) )
*-[DCCertificateGenerator _encryptData:error:].cold.1(v30);
*v19_output_buf=0LL;
*v23=0LL;
*goto LABEL_18;
*}
*v19_output_buf=v18;
*v67_output_len=input_data_length_v15+clientAppId_length+235LL;
**(_DWORD*)((char*)v18+150)=input_data_length_v15+clientAppId_length+81;
**v18=2;
*v20=v18+5;
*v21_publickey_bytes=j__objc_msgSend_796(self->_publicKey,"bytes");
*v22_public_keylen=j__objc_msgSend_796(self->_publicKey,"length");
*j__memcpy_479(v20, v21_publickey_bytes, (size_t)v22_public_keylen);
*v23=(char*)j__calloc_348(1uLL,*(unsignedint*)((char*)v19_output_buf+150));
**(_DWORD*)(v23+73)=input_data_length_v15;
*j__memcpy_479(v23+81, input_data_bytes, input_data_length_v15);
**(_DWORD*)(v23+77)=clientAppId_length;
*j__memcpy_479(&v23[*(unsignedint*)(v23+73)+81], clientappid_bytes__src, clientAppId_length);
*-[NSDate timeIntervalSince1970](v14,"timeIntervalSince1970");
**(_QWORD*)(v23+65)=v24;
*v25=-[DCCertificateGenerator keybagHandle](self,"keybagHandle");
*v26=aks_ref_key_create_1((__int64)v25,11,4u,0LL,0LL, &v74);
*v27=v14;
*if( (_DWORD)v26 )
*{
*v28=v26;
*v29=(void*)_DCLogSystem(v26);
*v30=j__objc_retainAutoreleasedReturnValue_706(v29);
*if( j__os_log_type_enabled_718(v30, OS_LOG_TYPE_ERROR) )
*-[DCCertificateGenerator _encryptData:error:].cold.6(v28, v30);
*LABEL_18:
*v44=0LL;
*goto LABEL_19;
*}
*public_key_1=(_OWORD*)aks_ref_key_get_public_key_1(v74, (__int64*)&__n);
*if( __n !=65)
*{//日志输出不用关心
*v43=(void*)_DCLogSystem(public_key_1);
*v30=j__objc_retainAutoreleasedReturnValue_706(v43);
*if( j__os_log_type_enabled_718(v30, OS_LOG_TYPE_ERROR) )
*-[DCCertificateGenerator _encryptData:error:].cold.5(&__n, v30);
*goto LABEL_18;
*}
**(_OWORD*)((char*)v19_output_buf+85)=*public_key_1;
*v33=public_key_1[1];
*v34=public_key_1[2];
*v35=public_key_1[3];
**((_BYTE*)v19_output_buf+149)=*((_BYTE*)public_key_1+64);
**(_OWORD*)((char*)v19_output_buf+133)=v35;
**(_OWORD*)((char*)v19_output_buf+117)=v34;
**(_OWORD*)((char*)v19_output_buf+101)=v33;
*j__memcpy_479(v23, public_key_1, __n);
*j__printf_133("%-25.25s = ","random_pubkey");
*for( i=85LL; i !=150;++i )
*j__printf_133("%02x",*((unsigned __int8*)v19_output_buf+i));
*j__putchar_35(10);
*v37=v74;
*v38=j__objc_msgSend_796(self->_publicKey,"bytes");
*v39=j__objc_msgSend_796(self->_publicKey,"length");
*v40=aks_ref_key_compute_key_0(v37,0LL,0LL, (__int64)v38, (__int64)v39);
*if( (_DWORD)v40 )
*{
*v41=v40;
*v42=(void*)_DCLogSystem(v40);
*v30=j__objc_retainAutoreleasedReturnValue_706(v42);
*if( j__os_log_type_enabled_718(v30, OS_LOG_TYPE_ERROR) )
*-[DCCertificateGenerator _encryptData:error:].cold.4(v41, v30);
*goto LABEL_18;
*}
*v46=v73;
*v47=v72-2;
*j__printf_133("%-25.25s = ","ECDH shared key");
*if( v47 )
*{
*v48=(unsigned __int8*)(v46+2);
*do
*{
*v49=*v48++;
*j__printf_133("%02x", v49);
*--v47;
*}
*while( v47 );
*}
*j__putchar_35(10);
*v50=j__cchkdf_6(ccsha256_ltc_di, v72-2, v73+2,0LL,0LL,0LL,0LL,0x2CuLL, (char*)buf);
*if( (_DWORD)v50 )
*{
*v51=v50;
*v52=(void*)_DCLogSystem(v50);
*v30=j__objc_retainAutoreleasedReturnValue_706(v52);
*if( j__os_log_type_enabled_718(v30, OS_LOG_TYPE_ERROR) )
*-[DCCertificateGenerator _encryptData:error:].cold.3(v51, v30);
*goto LABEL_18;
*}
*j__printf_133("%-25.25s = ","HKDF derived key");
*for( j=0LL; j !=32;++j )
*j__printf_133("%02x", buf[j]);
*j__putchar_35(10);
*j__printf_133("%-25.25s = ","HKDF derived iv");
*for( k=0LL; k !=12;++k )
*j__printf_133("%02x", (unsigned __int8)v78[k]);
*j__putchar_35(10);
*v55=j__ccgcm_one_shot_1(
*v68,
*32LL,
*(__int64)buf,
*12LL,
*(__int64)v78,
*0LL,
*0LL,
**(unsignedint*)((char*)v19_output_buf+150),
*(__int64)v23,
*(__int64)v19_output_buf+154,
*16LL,
*(__int64)(v19_output_buf+1));
*if( (_DWORD)v55 )
*{
*v56=v55;
*v57=(void*)_DCLogSystem(v55);
*v30=j__objc_retainAutoreleasedReturnValue_706(v57);
*if( j__os_log_type_enabled_718(v30, OS_LOG_TYPE_ERROR) )
*-[DCCertificateGenerator _encryptData:error:].cold.2(v56, v30);
*goto LABEL_18;
*}
*j__printf_133("%-25.25s = ","tag");
*for( m=4LL; m !=20;++m )
*j__printf_133("%02x",*((unsigned __int8*)v19_output_buf+m));
*j__putchar_35(10);
*j__fprintf_202((FILE*)__stderrp_0,"encrypted_data_len: %d\n",*(unsignedint*)((char*)v19_output_buf+150));
*v59=j__objc_msgSend_796(&OBJC_CLASS___NSData,"dataWithBytes:length:", v19_output_buf, v67_output_len);
*v44=j__objc_retainAutoreleasedReturnValue_706(v59);
*v60=(void*)_DCLogSystem(v44);
*v30=j__objc_retainAutoreleasedReturnValue_706(v60);
*if( j__os_log_type_enabled_718(v30, OS_LOG_TYPE_DEFAULT) )
*{
*v61=j__objc_alloc_700((Class)&OBJC_CLASS___NSString);
*v62=j__objc_msgSend_796(v44,"base64EncodedDataWithOptions:",1LL);
*v63=j__objc_retainAutoreleasedReturnValue_706(v62);
*v64=j__objc_msgSend_796(v61,"initWithData:encoding:", v63,4LL);
*v65=j__objc_retainAutorelease_520(v64);
*v66=j__objc_msgSend_796(v65,"UTF8String");
**(_DWORD*)v75=136315138;
*v76=v66;
*j___os_log_impl_652(&dword_1ACFED000, v30, OS_LOG_TYPE_DEFAULT,"\nPayload (base64):\n%s\n\n", v75,0xCu);
*j__objc_release_785(v65);
*j__objc_release_785(v63);
*}
*v19_output_buf=0LL;
*LABEL_19:
*j__objc_release_785(v30);
*j__free_627(v19_output_buf);
*j__free_627(v23);
*j__objc_release_785(v27);
*j__objc_release_785(v70);
*returnj__objc_autoreleaseReturnValue_646(v44);
*}
*
*
*/
这个函数的入参就是两本x509证书,这个证书的来源就是我们apple设备在激活的时候的证书,证书的扩展信息内详细记录了我们的设备型号,和具体硬件参数信息,这也是为什么苹果能通过token进行验证设备唯一性。其次要关注的就是 -[DCContext clientAppID] 这里返回的是,具体应用的bundleid,如果大家要想伪造的时候自己hook好这里,就可以实现一个自主生成的服务了,这个不需要你去打开应用就可以生成,避免了token感染的问题。这里伪代码也直接放出了有能力硬刚的同学可以自己实现这些通用算法(这些都是通用算法),我目前的方式就是用python实现了一套这样的token生成,有个算法还不行这里要考虑一个问题就是证书,这个证书我们特地去研究激活逻辑,就用了一些取巧方案,后面我会详细介绍生态和我构建的整个生态系统。
##后记&总结
分析流程看上去平平无奇简单,实则是一个很痛苦的过程,很多人问为什么分析这个东西,因为在和小伙伴们在群里吹牛逼的时候总会有人提到这个东西,某某厂用来封设备我就很好奇通过我这篇文章大概能了解这个token在iOS生态下的作用,一般都会被大厂用在0元购资格上这也是为什么市面上很多改机在iOS生态下搞不了0元购的问题解除了我心中的疑惑。再谈谈我自己用来批量伪造的方案吧。我相信大家更愿意听我讲这些具体方案如下
- 硬改
说一下硬改,在逛国外社区的时候无意间发现了,基于checkm8漏洞的硬改软件这个软件很神奇的就是我在不越狱(影响范围比较小)的情况下,通过checkm8引导进PongoOS后进行硬件参数改造以实现改机目的,这个改的相当彻底任何参数都能给你改一遍任何厂商都防不住不信你们自己去尝试,而且我还不越狱你拿我没办法就是成本比较高而已,和ddtoken有鸡毛关系当然有,这个硬改软件改出来的设备可以完美通过各种厂商的校验测试。但是我用的是商业版硬改他贴心的为我伪造了正确设备参数,因为不正确的参数设备根本激活不过去,这个正确参数有限,敏锐的我发现了我在买了这个软件每天就干一件事情就是硬改刷机提取证书,不到俩月这个商业软件就倒闭了我总共刷了2000套证书。 - 生态链条
产业生态链这里我要解释一下就是目前可以提取证书来做算法服务但是,你哪里来这么多证书呢,我自己的套路就是我主动联系了很多在华强北做二手手机的贩子(这里指的是老款手机)和他们谈合作,手机我提取一套参数我给20元,他们由于收到二手设备都会刷机,这样无非就是帮他吧们刷机和整机成本降低很多愿意干,我刚开始让我同学去华强北联系的时候给10元的时候他们不鸟我,然后加到20元的时候才有部分商家进行合作。健康的生态链套就这么搞了起来,但是我现在更看好东南亚和阿三的iPhone市场。这个就当闲扯了。
#后记&感想
- 写这边文章的目的是把方法和思路告诉大家,因为我知道今年爬虫圈子和逆向圈子太沉寂了着对于行业发展不好,其次就是我在清明前后就拿了大礼包在家里闲置,也面试了一些公司很多都是朋友认识,一聊都是方案上啊过不去,都是互相学习式招聘。
- 其次就是我对于逆向这种无法健康持续发展下去的行业失去了信心,我本身不想做任何带颜色的业务,现在逆向对于我来说作为一个爱好去发展,第二方面就是我本身硬件不足,无学历,年龄也稍大对于企业来说都是负担,我偶尔投投大厂面试一下都是本着技术交流的态度去的,发现自己还是有很多需要学习的地方。
- 现在目前的就是在家学习一些其他的谋生技巧,其次做一做逆向(纯爱好研究),最近在研究某节的产品很让人着迷,很值得细细品味也学习。研究的方向如下:
1)编译器:字节码复用逆向展开
2)汇编大模型:这个灵感来源于 luna服务和腾讯科恩实验室,brrayai项目,后续绝对是一大主流逆向辅助做字节码推导,很有用省去了很多麻烦,其次就是把所有c项目都编译一遍去学习,想想这项目就好玩。
3)各种花指令研究,:由于ida pro我没钱也没能力搞新版去尝试,最近在朋友的帮助转战binary ninja平台来做逆向分析。
#完结
这篇文章内没有开放xpc trace脚本,还有就是需要ida解析文件或者要打赏