最近在学用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] 就是要找的函数:
这里读取了 /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和信道之类的这里用不上。继续往后看就能发现关键数据:
先构造一个NSMutableArray,内容为 [kSecClass, kSecAttrService, kSecReturnAttributes, kSecMatchLimit]
接着构造另一个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 <app> 签上带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),就可以开始查询对应的密码了。获取密码的参数为:
和之前获取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
附件中为编译好的测试程序(armv7, arm64)。初次接触Keychain,如果文中有错误之处,欢迎用力拍砖
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开
发者可享99元/年,续费同价!
上传的附件: