一、背景
由于此次一个项目,我们打包后,总是有崩溃,经过分析,是由于用户名密码没有取到,导致用户名密码为空,而APP 的开发者又没有对用户密码判空,最终导致崩溃。经过进一步分析,用户名密码是存储在keychain 中,经过我们重签名后,从keychain 中取不出数据。
二、问题
1)keychain 是什么
2)keychain 如何存取数据
3)为什么会取不出数据
4)要如何修改,才能让APP 正常取出数据
1、keychain 是什么
iOS APP的Keychain数据是存储在应用沙箱外面的,各APP的keychain数据内容为逻辑隔离,由系统进程securityd实施访问控制。为了在多个APP间能够共享Keychain信息,Apple引入了Keychain访问组概念:拥有相同Keychain访问组标识符的应用,可以共享Keychain数据。
2、如何存取数据
- (BOOL)addItemWithService:(NSString *)service account:(NSString *)account password:(NSString *)password {
NSMutableDictionary *queryDict = [NSMutableDictionary dictionary];
queryDict[(__bridge id)kSecAttrAccessGroup] = @"com.uusafe.keychainDemo";
[queryDict setObject:service forKey:(__bridge id)kSecAttrService];
[queryDict setObject:account forKey:(__bridge id)kSecAttrAccount];
[queryDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
OSStatus status = -1;
CFTypeRef result = NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)queryDict, &result);
if (status == errSecItemNotFound) {
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[queryDict setObject:passwordData forKey:(__bridge id)kSecValueData];
status = SecItemAdd((__bridge CFDictionaryRef)queryDict, NULL);
NSLog(@"插入成功%d",status);
}else if(status == errSecSuccess) {
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithDictionary:queryDict];
[dict setObject:passwordData forKey:(__bridge id)kSecValueData];
status = SecItemUpdate((__bridge CFDictionaryRef)queryDict, (__bridge CFDictionaryRef)dict);
}
return (status == errSecSuccess);
}
注意,这里有个kSecAttrAccessGroup 后面用的到。
我们一个一个来解释这些key。这里有一个图,来自网上:
- kSecClassInternetPassword
属于该类别的条目往往用来存储上网登录密码,远程服务器密码等 - kSecClassGenericPassword
存储一些通用的密码,比如数据库密码,***连接的密码等等 - kSecClassCertificate
- kSecClassKey
- kSecClassIdentity
这三类条目往往用于建立基于证书,秘钥和公钥系统的安全连接。 - kSecReturnData
返回条目所存储的数据,返回值类型是CFDataRef
设置所属类别
[queryDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]
设置所属服务
[queryDict setObject:service forKey:(__bridge id)kSecAttrService];
设置用户名和密码
[queryDict setObject:account forKey:(__bridge id)kSecAttrAccount];
[queryDict setObject:passwordData forKey:(__bridge id)kSecValueData];
注意:kSecAttrService 和 kSecAttrAccount 不可以都重复,共同组成主键
返回条目数量,是所有还是只查询一个
[queryDict setObject:(__bridge id)kSecMatchLimit forKey:(__bridge id)kSecMatchLimitAll];
另外,我们知道,keychain中的数据是可以迁移到其他设备的。这就涉及到iOS 的数据保护机制:存储在Keychain中的数据被另一层与用户密码(passcode)相关联的加密机制保护着。数据保护加密密钥(保护类密钥-protection class keys)是由一个设备硬件密钥和一个由用户密码衍生的密钥共同产生的。可以通过向Keychain接口的SecItemAdd或SecItemUpdate方法的kSecAttrAccessible属性提供一个可访问常量来启用Keychain的数据保护。该可访问常量值决定一个Keychain条目在何时可被应用访问,同时也决定某个Keychain条目是否允许移动到另一台设备上。 以下是keychain 条目的数据可访问常量列表:
- kSecAttrAccessibleWhenUnlocked
Keychain条目只能在设备被解锁后被访问,此时用于解密Keychain条目的数据保护类密钥只在设备被解锁后才会被加载到内存中,并且当设备锁定后,加密密钥将在10s钟内自动清除。 - kSecAttrAccessibleAfterFirstUnlock
Keychain条目可以在设备第一次解锁到重启过程中被访问,此时用于解密Keychain条目的数据保护类密钥只有在用户重启并解锁设备后才会被加载到内存中,并且该密钥将一直保留在内存中,直到下一次设备重启。 - kSecAttrAccessibleAlways
Keychain条目即使在设备锁定时也可被访问,此时用于解密Keychain条目的数据保护类密钥一直被加载在内存中。 - kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly keychian 条目在设置了密码的情况下,并且设备解锁了,应用在前台才能访问,并且无法迁移到其他设备,也无法被分到新设备,这个属性在没有密码的设备上不生效
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly Keychain条目只能在设备被解锁后被访问,并且无法在设备间移动。
- kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly Keychain条目可在设备第一次解锁后访问,但无法在设备间移动
- kSecAttrAccessibleAlwaysThisDeviceOnly
Keychain条目即使在设备锁定时也可被访问,但无法在设备间移动
Keychain条目的数据保护可访问常量被映射到各Keychain表(genp、inet..)中的pdmn列(protection domain)。下表显示了keychain数据保护可访问常量和pdmn值之间的映射关系。
当然keychain也不是绝对安全的。在越狱机器上可以使用Keychain_dumper 将所有keychain 条目导出。
我们可以用 Keychain_dumper 导出来看看
keychain 是一个用有限访问权限的sqlite 数据库,(据说是aes256 加密,但是可以直接导出来看到是明文的),在iPhone 上,Keychain存放在“/private/var/Keychains/keychain-2.db” SQLite数据库。可以将sqlite 数据库导出来打开看看
从这里可以看出,每个类别就有一张表,这里就有我们刚刚看到的一切信息。可以看到每个字段信息。
以上就是苹果暴露给我们的。
接下来,要真正了解如何插入keychain 信息的,那么稍微了解一点iOS IPC 通信。
XPC 通信
我们知道IPC通信的基本方式有APP group,剪切板,keychain,这些都是我们平时开发用到的。但是最基本也是最复杂的通信方式XPC 通信。通过xpc,APP 可以与一些系统服务进行通信。
在iOS 系统中,每个APP 都是一个独立的进程,当APP 要与其他进程通信时,都会建立一个xpc 连接,每个连接都是一对一的,意味着服务在不同的连接进行操作时,都会调用xpc_connection_create就会创建一个新的链接
xpc_connection_t c = xpc_connection_create("com.example.service", NULL);
xpc_connection_set_event_handler(c, ^(xpc_object_t event) {
// ...
});
xpc_connection_resume(c);
可以看到,每个连接都会有一个handler,可以理解为回调
当一个消息发送到xpc连接,将自动将消息派发到一个由runtime 管理的消息队列中。当连接的远端一单开启的时候,消息讲出对并被发送。
每个消息就是一个字典,字符窜key 和强类型值:
xpc_dictionary_t message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_uint64(message, "foo", 1);
xpc_connection_send_message(c, message);
xpc_release(message)
关于xpc 通信的资料太少,网上有人称为天使与恶魔😈
3、为什么会取不出数据
要回答这个问题,必须得对keychain的增删改查的原理弄清楚。
在keychain中,我们调用SecItemAdd
时,APP会与系统服务securityd 建立一个xpc 连接,在securityd 中,当有消息发送过来是,就会调用:
void __fastcall securityd_xpc_dictionary_handler(__int64 a1, __int64 a2)
在这个函数中会有一系列操作,包括获取及决定keychain-access-group
、增删改查等。
1、先来看看如何获取及决定keychain-access-group
v9 = CFDataCreate(v12, &v248, 32LL);
pthread_setspecific(taskThreadKey, v10);
v8 = SecTaskCopyAccessGroups((__int64)v10);
v7 = 0LL;
if ( (v11 & 0xFFFFFFFFFFFFFFFELL) == 26 )
v7 = SecTaskCopyArrayOfStringsForEntitlement(v10, CFSTR(“com.apple.developer.associated-domains"));
重点关注SecTaskCopyAccessGroups
我们点进去看看:
__int64 __fastcall SecTaskCopyAccessGroups(__int64 a1)
{
__int64 v1; // x21
__int64 v2; // x19
__int64 v3; // x20
__int64 v4; // x21
__int64 v5; // x24
__int64 v6; // x23
signed __int64 v7; // x8
__int64 v8; // x0
__int64 v9; // x22
v1 = a1;
v2 = SecTaskCopyArrayOfStringsForEntitlement(a1, CFSTR("keychain-access-groups"));
v3 = SecTaskCopyArrayOfStringsForEntitlement(v1, CFSTR("com.apple.security.application-groups"));
v4 = SecTaskCopyApplicationIdentifier(v1);
if ( v2 )
v5 = CFArrayGetCount(v2);
else
v5 = 0LL;
if ( v3 )
v6 = CFArrayGetCount(v3);
else
v6 = 0LL;
if ( v4 )
v7 = v5 + 1;
else
v7 = v5;
if ( v7 + v6 )
{
v8 = CFArrayCreateMutable(kCFAllocatorDefault);
v9 = v8;
if ( v5 )
CFArrayAppendArray(v8, v2, 0LL, v5);
if ( v4 )
CFArrayAppendValue(v9, v4);
if ( v6 )
CFArrayAppendArray(v9, v3, 0LL, v6);
}
else
{
v9 = 0LL;
}
if ( v4 )
CFRelease(v4);
if ( v2 )
CFRelease(v2);
if ( v3 )
CFRelease(v3);
return v9;
}
可以看到首先获取 SecTaskCopyArrayOfStringsForEntitlement(a1, CFSTR("keychain-access-groups"));
然后获取 SecTaskCopyArrayOfStringsForEntitlement(v1, CFSTR("com.apple.security.application-groups"));
最后SecTaskCopyApplicationIdentifier(v1);
但是请注意,加入数组中的顺序:
首先是keychain-access-group,然后bundleID,最后是app-group
这个顺序很重要,即最后数组中的信息应该是:
1、keychain-access-group
2、bundleID
3、app-group
接下来就是switch 语句,分别是增删改查
switch ( v11 )
{
case 0uLL:
v14 = SecXPCDictionaryCopyDictionary(v2, kSecXPCKeyQuery[0], v253 + 3);
v15 = v14;
if ( v14 )
{
v244 = 0LL;
if ( (unsigned int)_SecItemAdd(v14, v8, (__int64)&v244, (__int64)(v253 + 3)) )
v16 = v244 == 0;
else
v16 = 1;
if ( !v16 )
{
SecXPCDictionarySetPList(v6, kSecXPCKeyResult[0], v244, v253 + 3);
v17 = v244;
goto LABEL_129;
}
goto LABEL_231;
}
goto def_100002C00;
case 1uLL:
v18 = SecXPCDictionaryCopyDictionary(v2, kSecXPCKeyQuery[0], v253 + 3);
v15 = v18;
if ( v18 )
{
v243 = 0LL;
if ( (unsigned int)_SecItemCopyMatching(v18, v8, &v243) )
v19 = v243 == 0;
else
v19 = 1;
if ( !v19 )
{
SecXPCDictionarySetPList(v6, kSecXPCKeyResult[0], v243, v253 + 3);
v17 = v243;
goto LABEL_129;
}
goto LABEL_231;
}
goto def_100002C00;
case 2uLL:
v20 = SecXPCDictionaryCopyDictionary(v2, kSecXPCKeyQuery[0], v253 + 3);
if ( v20 )
{
v21 = SecXPCDictionaryCopyDictionary(v2, kSecXPCKeyAttributesToUpdate[0], v253 + 3);
if ( !v21 )
goto LABEL_226;
v22 = _SecItemUpdate(v20, v21, v8, v253 + 3);
xpc_dictionary_set_bool(v6, kSecXPCKeyResult[0], v22);
v23 = v21;
goto LABEL_77;
}
goto def_100002C00;
case 3uLL:
v24 = SecXPCDictionaryCopyDictionary(v2, kSecXPCKeyQuery[0], v253 + 3);
v15 = v24;
if ( v24 )
{
v25 = _SecItemDelete(v24, v8, v253 + 3);
goto LABEL_43;
}
- 插入
我们先来看看SecItemAdd
v14 = SecXPCDictionaryCopyDictionary(v2, (__int64)kSecXPCKeyQuery[0], (__int64)(v252 + 3));
在这个函数中点进去,可以看到:
__int64 __fastcall SecXPCDictionaryCopyPList(__int64 a1, __int64 a2, __int64 a3)
{
__int64 v3; // x19
__int64 v4; // x0
__int64 v5; // x0
__int64 result; // x0
__int64 v7; // [xsp+18h] [xbp-28h]
v3 = a3;
v7 = 0LL;
v4 = xpc_dictionary_get_data();
if ( v4 )
{
if ( der_decode_plist(kCFAllocatorDefault, 0LL, &v7, v3, v4, v4) != v4 )
{
SecError(4294967246LL, v3, CFSTR("trailing garbage after der decoded object for key %s"));
v5 = v7;
if ( v7 )
{
v7 = 0LL;
CFRelease(v5);
}
}
result = v7;
}
else
{
SecError(4294967246LL, v3, CFSTR("no object for key %s"));
result = 0LL;
}
return result;
}
可以看到v8 是数组,可以看到,v14
就是我们传入的参数,即消息体:
(lldb) po $x0
<CFBasicHash 0x136604690 [0x19f66db68]>{type = mutable dict, count = 7,
entries =>
0 : <CFString 0x136600230 [0x19f66db68]>{contents = "r_Data"} = <CFBoolean 0x19f66e0a0 [0x19f66db68]>{value = true}
3 : <CFString 0x136608420 [0x19f66db68]>{contents = "acct"} = <CFString 0x1366092a0 [0x19f66db68]>{contents = "lilei"}
4 : <CFString 0x136608220 [0x19f66db68]>{contents = "agrp"} = <CFString 0x136608240 [0x19f66db68]>{contents = "com.uusafe.keychainDemo"}
6 : <CFString 0x1366092c0 [0x19f66db68]>{contents = "class"} = <CFString 0x136604530 [0x19f66db68]>{contents = "genp"}
8 : <CFString 0x136604550 [0x19f66db68]>{contents = "r_Attributes"} = <CFBoolean 0x19f66e0a0 [0x19f66db68]>{value = true}
9 : <CFString 0x1366086b0 [0x19f66db68]>{contents = "svce"} = <CFString 0x136600e80 [0x19f66db68]>{contents = "com.uusafe"}
10 : <CFString 0x1366081e0 [0x19f66db68]>{contents = "m_LimitAll"} = <CFString 0x136608200 [0x19f66db68]>{contents = "m_Limit"}
}
消息体重包含keychain-access-group
此时,SecItemAdd
的第一个参数中包含keychain-access-group
,第二个参数是个SecTaskCopyAccessGroups
返回的数组,接着我们重点分析SecItemAdd
的实现:
__int64 __fastcall _SecItemAdd(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
__int64 v4; // x19
__int64 v5; // x21
__int64 v6; // x22
__int64 v7; // x23
__int64 v8; // x24
_QWORD *v9; // x20
__int64 v10; // x25
__int64 v11; // x23
int v12; // w0
__int64 v13; // x0
__int64 v14; // x2
signed __int64 v15; // x1
__int64 v17; // x0
void **v18; // [xsp+8h] [xbp-78h]
int v19; // [xsp+10h] [xbp-70h]
int v20; // [xsp+14h] [xbp-6Ch]
__int64 (__fastcall *v21)(); // [xsp+18h] [xbp-68h]
void *v22; // [xsp+20h] [xbp-60h]
__int64 v23; // [xsp+28h] [xbp-58h]
_QWORD *v24; // [xsp+30h] [xbp-50h]
__int64 v25; // [xsp+38h] [xbp-48h]
v4 = a4;
v5 = a3;
v6 = a2;
v7 = a1;
if ( a2 )
{
v8 = CFArrayGetCount(a2);
if ( v8 )
{
v9 = query_create_with_limit(v7, 0LL, v4);
if ( !v9 )
return 0LL;
v10 = kSecAttrAccessGroup;
v11 = CFDictionaryGetValue(v7, kSecAttrAccessGroup);
v12 = CFArrayContainsValue(v6, 0LL, v8, CFSTR("*"));
if ( v11 )
{
if ( v12 )
v13 = 0LL;
else
v13 = v6;
if ( !accessGroupsAllows(v13, v11) && !(unsigned int)SecError(4294942053LL, v4, CFSTR("NoAccessForItem")) )
{
v15 = 0LL;
return query_notify_and_destroy(v9, v15, v4);
}
}
else
{
v11 = CFArrayGetValueAtIndex(v6, 0LL);
query_add_attribute(v10, v11, v9);
}
query_ensure_access_control((__int64)v9, v11, v14);
if ( v9[9] )
{
v17 = SecError(4294967246LL, v4, CFSTR("q_row_id"));
}
else
{
if ( v9[5] )
{
v15 = 1LL;
return query_notify_and_destroy(v9, v15, v4);
}
v18 = _NSConcreteStackBlock;
v19 = 0x40000000;
v20 = 0;
v21 = ___SecItemAdd_block_invoke;
v22 = &__block_descriptor_tmp30;
v23 = v4;
v24 = v9;
v25 = v5;
v17 = kc_with_dbt(1LL, v4, &v18);
}
v15 = v17;
return query_notify_and_destroy(v9, v15, v4);
}
}
if ( SecTaskDiagnoseEntitlements )
SecTaskDiagnoseEntitlements(v6);
return SecError(
4294933278LL,
v4,
CFSTR("client has neither application-identifier nor keychain-access-groups entitlements"));
}
我们来简单分析一下上面的代码
首先判断a2 是否为空(一般情况下,a2 不会为空,因为APP 总会有bundleID),如果为空,直接返回-34018 错误。
如果不为空,则从v7 中取出kSecAttrAccessGroup
v11。 这个v11 即是我们最开始处
queryDict[(__bridge id)kSecAttrAccessGroup] = @"com.uusafe.keychainDemo";
的kSecAttrAccessGroup
- 如果v11 不为空,则判断v8 数组中是否包含
*
若果有元素是*
则v13 = 0,否则v13 = v6,这个v6 即是数组。接着通过accessGroupsAllows
判断keychain-access-group
是否可以访问。
bool __fastcall accessGroupsAllows(__int64 a1, __int64 a2)
{
__int64 v2; // x20
__int64 v3; // x19
__int64 v4; // x21
__int64 v5; // x21
_BOOL8 result; // x0
v2 = a2;
v3 = a1;
result = 1;
if ( a1 )
{
if ( !a2
|| (v4 = CFGetTypeID(a2), v4 != CFStringGetTypeID())
|| (v5 = CFArrayGetCount(v3)) == 0
|| !(unsigned int)CFArrayContainsValue(v3, 0LL, v5, v2)
&& !(unsigned int)CFArrayContainsValue(v3, 0LL, v5, CFSTR("*")) )
{
result = 0;
}
}
return result;
}
可以看到v13 = 0 时,永远返回的是 1.即如果是*
表示所有的都有访问权限。
- 如果v11 为空,即我们再存储的时候不传入
keychain-access-group
,那么就会去数组的第一个。
v11 = CFArrayGetValueAtIndex(v6, 0LL);
query_add_attribute(v10, v11, v9);
并且将取到的 默认keychain
添加到keychain
的属性中。
接着便是通过query_ensure_access_control
来决定访问权限列表,即在文章开头出提到的pdmn
字段
__int64 __fastcall query_ensure_access_control(__int64 result, __int64 a2, __int64 a3)
{
__int64 v3; // x20
__CFString ***v4; // x19
__int64 v5; // x2
__int64 *v6; // x8
v3 = a2;
v4 = (__CFString ***)result;
if ( !*(_QWORD *)(result + 112) )
{
if ( (unsigned int)CFEqual(a2, CFSTR("com.apple.apsd"), a3)
|| (unsigned int)CFEqual(v3, CFSTR("lockdown-identities"), v5) )
{
v6 = (__int64 *)&kSecAttrAccessibleAlwaysThisDeviceOnly;
}
else if ( *v4 == &cert_class )
{
v6 = (__int64 *)&kSecAttrAccessibleAlways;
}
else
{
v6 = (__int64 *)&kSecAttrAccessibleWhenUnlocked;
}
result = query_add_attribute(kSecAttrAccessible, *v6, v4);
}
return result;
}
这些上面有过解释,这里就不说了。也可以与上面提到的一一对应上。
当这些全都通过后,便通过kc_with_dbt
进行数据库操作。
__int64 __fastcall kc_with_dbt(int a1, __int64 *a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7, __int64 a8)
{
__int64 v8; // x19
__int64 *v9; // x20
int v10; // w21
__int64 v11; // x20
__int64 v12; // x19
__int64 v14; // [xsp+0h] [xbp-20h]
v8 = a3;
v9 = a2;
v10 = a1;
if ( a1 )
SecItemDataSourceFactoryGetDefault();
if ( _kc_dbhandle_once != -1 )
dispatch_once(&_kc_dbhandle_once, &__block_literal_global223);
if ( !_kc_dbhandle )
{
SecError(-25316, v9, (__int64)CFSTR("failed to get a db handle"), a4, a5, a6, a7, a8, v14);
return 0LL;
}
v11 = SecDbConnectionAquire(_kc_dbhandle, v10 ^ 1u, (__int64)v9);
if ( !v11 )
return 0LL;
v12 = (*(__int64 (__fastcall **)(__int64, __int64))(v8 + 16))(v8, v11);
SecDbConnectionRelease(v11);
return v12;
}
可以看到,这里基本上就是数据库操作了,至此,keychain
的add 操作权限鉴定阶段完成。
上面讲的是SecItemAdd
, 接下来讲讲SecItemCopyMatching
查询操作。
首先我们看上面的代码,v18
是我们传入的消息体,v8
是权限数组列表。
从这里可以解释我们重新签名后从keychain
中为什么取不出数据了。
if ( (unsigned int)_SecItemCopyMatching(v18, v8, (__int64)&v243, (__int64)(v253 + 3)) )
- 查询
与SecItemAdd
一样,先判断权限数组里是否有*
,如果有*
表示匹配所有,否则取出数组
(lldb) po $x1
<CFArray 0x14de295a0 [0x19f66db68]>{type = mutable-small, count = 1, values = (
0 : <CFString 0x14de295d0 [0x19f66db68]>{contents = "56MY3ZN5V7.com.uusafe.KeychainDemo"}
)}
再来看主要逻辑
v10 = query_create_with_limit(v7, 1LL, v4);
v11 = (__int64)v10;
if ( !v10 )
return 0LL;
v12 = *((_QWORD *)v10 + 1);
v13 = CFDictionaryGetValue();
if ( v13 && accessGroupsAllows(v9, v13) )
{
v29 = v13;
v6 = CFArrayCreate();
}
else if ( v9 )
{
CFRetain(v9);
}
else
{
v6 = 0LL;
}
query_set_caller_access_groups(v11, v6);
v10
表示是从我们传入的参数中取出kSecMatchLimit
字段,v13
是取出我们传入的keychain-access-group
字段,如果我们指定这个参数,则使用这个参数重新创建一个数组,否则,使用权限数组列表,
最终调用 query_set_caller_access_groups
指定查询条件,接下来就是数据库操作。至此查询操作也完成。
4、如何修改
在这里牵涉到我们业务操作,这里就不多说了。
三、总结
经过上面的分析,我们可以总结如下:
1、如果keychain-access-group
有*
,表示匹配所有,即不存在no-access-item
的情况
2、在书写代码时,如果我们不指定access-group
则默认把数组中第一个作为默认值。
作者:
@杨旭 @殷海兵