自己动手从 iOS Keychain 中恢复保存的Wifi密码

最近在学用Theos编写插件和小工具,本来打算自己动手写个类似Wifi Passwords的工具,用于查看保存在iOS设备中的Wifi密码的。不过搜了下居然没找到具体的实现方法。

本来以为iOS和Android一样保存在某个plist里(Android的Wifi密码明文保存在/data/misc/wifi/wpa_supplicant.conf),不过Google了才发现iOS虽然在 /private/var/preferences/SystemConfiguration/com.apple.wifi.plist 里保存了Wifi信息,但密码却是存储在Keychain中的,而且网上没有给出具体的读取办法。

对Keychain Services不熟,只好用 Hopper Disassembler 反汇编WiFiPasswords自己看了。代码不多,大部分都是和UITableView相关的内容,排除掉这些很快发现 -[RootViewController refresh] 就是要找的函数:

StoryBoard

这里读取了 /private/var/preferences/SystemConfiguration/com.apple.wifi.plist 的List of known networks中的信息(意义不明,实际上直接用SecItemCopyMatching也是能获取到Wifi名的,而且它后面也这样做了)。到机器上用plutil查看该plist文件,会看到类似下面的内容:

    "List of known networks" =     (
                {
            "80211D_IE" =             {
                "IE_KEY_80211D_COUNTRY_CODE" = JP;
            };
            "80211W_ENABLED" = 0;
            AGE = 41;
            "AP_MODE" = 2;
            "ASSOC_FLAGS" = 1;
            "BEACON_INT" = 20;
            BSSID = "00:2e:1f:54:1d:82";
            CAPABILITIES = 1041;
            CHANNEL = 12;
		...

这个plist里存储的是Wifi的属性等信息,不过BSSID和信道之类的这里用不上。继续往后看就能发现关键数据:

StoryBoard

先构造一个NSMutableArray,内容为 [kSecClass, kSecAttrService, kSecReturnAttributes, kSecMatchLimit]

StoryBoard

接着构造另一个NSMutableArray,内容为 [kSecClassGenericPassword, @”AirPort”, kCFBooleanTrue, kSecMatchLimitAll]。根据接着调用的 SecItemCopyMatching 可以得知,这个数据是 参数1 CFDictionaryRef query 的内容,其作用是查询 Keychain 中 kSecClass = kSecClassGenericPassword,且 kSecAttrService 为 AirPort 的属性。

Apple的 Keychain Item Class Keys and Values 里有详细介绍这些属性的含义。kSecClass 还可以是这些值:

CFTypeRef kSecClassGenericPassword ;
CFTypeRef kSecClassInternetPassword ;
CFTypeRef kSecClassCertificate ;
CFTypeRef kSecClassKey ;
CFTypeRef kSecClassIdentity;

比如网络密码,证书等等。这里枚举Wifi密码只用到了 kSecClassGenericPassword。

参考StackOverflow中SecItemCopyMatching的用法例子,很容易还原出上面的代码。用Theos的nic.pl创建一个tools项目,输入验证用代码:

#import <Security/Security.h>

int main(int argc, char **argv, char **envp)
{
	NSMutableDictionary *query = [NSMutableDictionary dictionary];

	[query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
	[query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
	[query setObject:(__bridge id)@"AirPort" forKey:(__bridge id)kSecAttrService];
	[query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];

	CFTypeRef result = NULL;
	OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
	if (status != errSecSuccess) {
		printf("[ERROR] SecItemCopyMatching() failed! error = %d\n", (int)status);
		return;
	}

	NSArray *wifi_list = (NSArray *)result;
	for (int i = 0; i < wifi_list.count; i++) {
		NSDictionary *wifi = (NSDictionary*)wifi_list[i];

		NSString *output = [NSString stringWithFormat:@"%@", wifi];
		printf("%s\n", [output cStringUsingEncoding:NSUTF8StringEncoding]);
	}

	if (result != NULL) {
		CFRelease(result);
	}

	return 0;
}

make后传到iOS里运行,然后顺利的失败了。提示-34018:

[ERROR] SecItemCopyMatching() failed! error = -34018

再次请出Google大神,得知SecItemCopyMatching返回-34018(errSecMissingEntitlement)是权限问题。 用 ldid -e WiFiPasswords 查看entitlement,发现它比常规程序多了 keychain-access-groups 权限。于是编辑一个 ent.xml 如下:

<?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>application-identifier</key>
	<string>com.zzz.my-idevice-tools</string>
	<key>get-task-allow</key>
	<true/>
	<key>keychain-access-groups</key>
	<array>
		<string>apple</string>
	</array>
</dict>
</plist>

用 ldid -Sent.xml 签上带keychain-access-groups的签名后运行,成功打印出Wifi信息:

{
    accc = "<SecAccessControlRef: 0x1566a4c0>";
    acct = Magdalene;
    agrp = apple;
    cdat = "2014-11-02 04:56:39 +0000";
    mdat = "2014-11-02 04:56:39 +0000";
    pdmn = ck;
    svce = AirPort;
    sync = 0;
    tomb = 0;
}

acct就是kSecAttrAccount,这里也就是Wifi名 (而accc是指向 strcut SecAccessControl 的指针,只不过网上搜了很久也没找到这个结构体的定义;不过这里用不到,pass)

注意,ldid签名如果失败,一般是codesign_allocate没有用对导致的。在MacOS上/usr/bin/codesign_allocate并非iOS用的版本,需要手工export一下(注意替换为你的Xcode安装目录):

export CODESIGN_ALLOCATE=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/codesign_allocate
ldid -Sent.xml obj/wifi_passwords

已经在获取到的 NSDictionary 中找到Wifi的名称(acct),就可以开始查询对应的密码了。获取密码的参数为:

StoryBoard

和之前获取Wifi名类似,将kSecReturnAttributes换成了kSecReturnData,另外有kSecAttrAccount(acct)也不需要搜索了;kSecReturnData的返回为NSData,里面就是对应Wifi的密码。

完成后的恢复Wifi密码的函数如下:

void keychain_wifi_passwords()
{
	NSMutableArray *acct_name = [NSMutableArray array];

	// form KeyChain get AirPort.acct (Wifi Name)
	{
		NSMutableDictionary *query = [NSMutableDictionary dictionary];

		[query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
		[query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
		[query setObject:(__bridge id)@KEYCHAIN_SVCE_AIRPORT forKey:(__bridge id)kSecAttrService];
		[query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];

		CFTypeRef result = NULL;
		OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
		if (status != errSecSuccess) {
			printf("[ERROR] SecItemCopyMatching() failed! error = %d\n", (int)status);
			return;
		}

		NSArray *wifi_list = (NSArray *)result;
		for (int i = 0; i < wifi_list.count; i++) {
			NSDictionary *wifi = (NSDictionary*)wifi_list[i];
			// get wifi name
			[acct_name addObject:wifi[@KEYCHAIN_ACCT_NAME]];
		}

		if (result != NULL) {
			CFRelease(result);
		}
	}

	// get password for each AirPort.acct
	{
		for (NSString *acct in acct_name) {
			NSMutableDictionary *query = [NSMutableDictionary dictionary];

			[query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
			[query setObject:(__bridge id)@KEYCHAIN_SVCE_AIRPORT forKey:(__bridge id)kSecAttrService];
			[query setObject:acct forKey:(__bridge id)kSecAttrAccount];
			[query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];

			CFTypeRef result = NULL;
			OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
			if (status != errSecSuccess) {
				printf("[ERROR] SecItemCopyMatching() failed! error = %d\n", (int)status);
				return;
			}

			NSData *password = (NSData *)result;
			NSString *output = [[NSString alloc] initWithData:password encoding:NSASCIIStringEncoding];
			printf("%s: %s\n", [acct cStringUsingEncoding:NSUTF8StringEncoding], [output cStringUsingEncoding:NSUTF8StringEncoding]);

			if (result != NULL) {
				CFRelease(result);
			}
		}
	}
}

make编译并make ldid签上ent.xml后,传入iOS会输出Keychain中保存的Wifi名和密码:

root# ./wifi_passwords
Magdalene: Retrieve Wifi password.
iPhone: 123456

剩下就是给控制台程序加个界面了。完整Theos工程代码见:https://github.com/upbit/My-iDevice-Tools/blob/master/wifi_passwords.mm

初次接触Keychain,如果文中有错误之处,欢迎拍砖:)