Valet Beats the OS X Keychain Access Control List Zero-Day Vulnerability
How we used test driven development to replicate an attack vector and harden our code against it
Written by Dan Federman.
The world is on fire
On June 17th, The Register reported that there was a zero-day vulnerability in the iOS and OS X Keychain that compromised secure data stored in the Keychain. The article claimed that data written to the keychain could be read by a malicious application. At Square, we write iOS code that moves money. Security is always our first priority, and we took these claims very seriously. If the article’s assertions were correct, the world was on fire.
When we became aware of the vulnerability a little after 8am PDT, we immediately opened up the paper describing the attack. We found that the attack worked as follows:
-
Malicious app searches the Keychain for keys written by benign apps. The malicious app can see that the keys exist, but can’t read the associated secret values.
-
Malicious app deletes the keys written by the benign apps.
-
Malicious app then adds the keys back to the keychain without the values, and adds itself and the benign app to the keys’ Access Control List (ACL), allowing the malicious app to read whatever secrets the benign app later writes to those keys.
At this point, we could breathe a sigh of relief: Access Control Lists only exist on Mac OS X — not iOS — so despite the headlines, our applications were not vulnerable to an attack. However we had just open sourced Valet, a cross-platform keychain wrapper, two weeks prior, and its OS X component was vulnerable. This wasn’t acceptable to us, so we hardened Valet against this attack.
A poisoned tool
Apple’s Keychain offers only three tools for updating the keychain: add, update, and delete. But we now know that on OS X, updating is inherently insecure. We cannot trust that an existing key in Keychain does not have a compromised Access Control List. So, we’re left with just adding and deleting.
The solution seems obvious. Instead of updating an existing keychain entry when changing the value of an item in the keychain, delete the existing item and then add a new one.
But Apple’s docs are very clear: Do not delete and then add — always update. Why? Because “[w]hen you delete a keychain item, you lose any access controls and trust settings added by the user or by other applications.” While that sounds ominous, it’s just the effect we want. Better yet, this warning does not apply to Valet, which shares keychain values on OS X and iOS using secure Shared Access Groups rather than ACLs.
Test the hypothesis
Our first step was to write a unit test to examine our hypothesis. We wanted the test to mimic the attack as closely as possible; so we started by inserting a key into the keychain that had multiple applications in the ACL. This compromised key is added using the same base query as our test VALValet, so that our valet is able to read and write to the compromised key.
VALValet *****valet **=** [[VALValet alloc] initWithIdentifier:@"MacOSVulnTest" accessibility:VALAccessibilityWhenUnlocked];
NSString ***const** vulnKey **=** @"AccessControlListVulnTestKey";
NSString ***const** vulnKeyValue **=** @"AccessControlListVulnTestValue";
// Add an entry to the keychain with an access control list.
NSMutableDictionary *keychainData = [valet.baseQuery mutableCopy];
keychainData[(__bridge id)kSecAttrAccount] = vulnKey;
keychainData[(__bridge id)kSecValueData] = [vulnKeyValue dataUsingEncoding:NSUTF8StringEncoding];
SecAccessRef accessList = NULL;
SecTrustedApplicationRef trustedAppSelf = NULL;
SecTrustedApplicationRef trustedAppSystemUIServer = NULL;
XCTAssertEqual(SecTrustedApplicationCreateFromPath(NULL, &trustedAppSelf), errSecSuccess);
XCTAssertEqual(SecTrustedApplicationCreateFromPath("/System/Library/CoreServices/SystemUIServer.app", &trustedAppSystemUIServer), errSecSuccess);
XCTAssertEqual(SecAccessCreate((__bridge CFStringRef)@"Access Control List",
(__bridge CFArrayRef)@[ (__bridge id)trustedAppSelf, (__bridge id)trustedAppSystemUIServer ],
&accessList),
errSecSuccess);
keychainData[(__bridge id)kSecAttrAccess] = (__bridge id)accessList;
XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)keychainData, NULL), errSecSuccess);
Once we ran this code, we saw the compromised value in the keychain.
Now that we had a compromised value, we changed Valet’s code on OS X to call SecItemDelete followed by SecItemAdd on setObject:forKey:. We then added the following lines to the test:
*// Update the vulnerable keychain value with Valet, and see that we have deleted the existing keychain item (rather than updating it) are therefore no longer vulnerable.*
NSString ***const** vulnKeyOtherValue **=** @"AccessControlListVulnOtherTestValue";
[valet setString:vulnKeyOtherValue forKey:vulnKey];
After running that code, we saw that the value was no longer compromised.
We then added a few lines to programmatically test the compromised keychain entry was indeed deleted on setString:forKey. We ran the test locally on our Mac OS X Yosemite machine — it passed, and we pushed!
Not yet in the clear
Then something surprising happened. Our test failed in CI. Specifically, the line that programmatically tested that the compromised keychain entry was deleted was failing. The biggest difference between our development environment and the CI environment was that Travis CI — our CI solution for our public GitHub projects — was running Mac OS X Mavericks (10.9) rather than Yosemite (10.10). So we found a machine in the office we hadn’t yet upgraded to Yosemite and ran the test locally, and voilà! Our test failed in the same spot.
After some experimentation and a little Googling, we found that prior to Yosemite, SecItemDelete did not in fact delete keychain items that has ACLs associated with them, despite a return code suggesting that it had succeeded. This means that our patch wouldn’t work on 10.9, since the vulnerability occurs when someone has maliciously added an ACL to your keychain item.
We briefly experimented with using SecKeychainItemDelete (which does manage to delete items with ACLs on them) on 10.9 machines, but then found that SecKeychainItem* and SecItem* don’t play nicely together. So instead of rewriting all of Valet for 10.9 using SecKeychainItem*, we bumped Valet’s minimum version to 10.10.
Patched!
Only 8 hours after being made aware of the vulnerability, we had successfully patchedValet. Developers utilizing Valet on Mac OS X are immune to the hack.
Think we missed something? Let us know by filing an issue or opening a PR against Valet. If you’re interested in creating a SecKeychainItem* solution for 10.9, we’d love to hear from you! Dan Federman *Follow the latest activity of Dan Federman on Medium. 57 people are following Dan Federman to see their stories and…*medium.com