Are you from the Mac App Store?
05/01/2016
Let's discuss how to use receipt verification to determine if an application is from Apple's Mac App Store. While this has been covered elsewhere, such resources are somewhat outdated, require external dependencies (openssl), are iOS specific, or are copyrighted. To follow along in code, grab the complete source code from GitHub.
$ ./fromAppStore /Volumes/Transmission/Transmission.app
checking if Transmission.app is from the Mac App Store
app is signed with an Apple Dev. ID
ERROR: could not find app store receipt url, or file (_MASReceipt/receipt)
ERROR: failed to init/decode/parse app's receipt
Transmission.app is *not* from the Mac App Store
Background
Recently I released version 1.0 of RansomWhere? - a tool that attempts to generically thwart OS X ransomware. One of the improvements that will be present in version 1.1 is the reduction of false positives. As mentioned in "Towards Generic Ransomware Detection", RansomWhere? flags untrusted processes that are generating encrypted files. A key component of this design, is correctly classifying processes as being 'untrusted' or not.
To reduce false-positives, version 1.1 will trust process instances that are backed by applications from Apple's Mac App Store. In other words, such processes will be trusted (i.e. not flagged even if they rapidly create encrypted files). The current version of RansomWhere? does not contain logic to determine if an application is from the App Store, and as such, currently does not classify such applications as trusted.
While classifying such applications as trusted will reduce false positives, this begs the question, will this reduction come at the cost of higher false negatives? That is to say, what if ransomware makes it into the App store, it will (no longer) be detected by RansomWhere?
I'm fairly confident that this is unlikely to happen, or technically is somewhat unfeasible. Why? First, Apple attempts to verify that all applications submitted and hosted in their store are not malicious. Thus, ransomware showing up in the App Store is unlikely. And yes, I know, I know; malicious code could sneak in (it's happened somewhat regularly in the iOS App Store). Which brings up the second point; applications from the App Store are heavily sandboxed. What does this have to do with ransomware? Well, due to the constrictive constraints of the Mac App sandbox, applications from the App Store cannot readily access arbitrary user files. In other words, even if ransomware made it into the App Store, on an end user's machine it would not unable to arbitrary encrypt user files (such as photos, documents etc) - unless it contained some 0day sandbox escape. At least this is my understanding of App Sandbox...if I'm wrong, I'd love to be corrected :)
Ok, so it's unlikely that ransomware would make it into the App Store, and even if it did, it would be sandboxed and thus unable to encrypt user files. As such RansomWhere? (version 1.1) will ignore such processes.
Existing Resources
So how does one tell if an application is legitimately from Apple's Mac App Store? Well, it turns out it's somewhat involved, but in short involves locating and cryptographically validating an embedded application receipt.
Before diving in, it's important to note that there are both blogs and source code samples that can somewhat effectively answer this question. In other words, what I'm describing here is not particularly novel. However these resources were either outdated, required external dependencies (openssl), are iOS specific, overly complex, or copyrighted. Also most (all?) were designed for an application to verify itself (i.e. to detect pirating/tampering), as opposed to verifying arbitrary applications. In other words, nothing quite fit the bill for RansomWhere? More importantly though, I like to learn new things and coding up something one's self, is an amazing way to gain a deeper understanding of a topic.
However, for completeness, some of the existing resource are listed here:
-
"Receipt Validation" (blog)
This blog provides a great overview of receipts and receipt validations. It's a highly recommended read before diving into other source code. However, code snippets in the blog require openssl, and a full code listing is not available.
-
"RMStore" (code)
A complete code sample that performs application receipt validation. Only 'complaint' here is that the code requires openssl. As openssl is not longer natively part of OS X, it has to be downloaded, compiled, and statically linked in. Not the end of the world - but it turns out there are ways to do application receipt validation, without this dependency.
-
"RVNReceiptValidation" (code)
Another complete code sample that uses updated Apple APIs, alleviating the need for external dependencies (i.e. openssl). While effective, the code is somewhat more involved than it needs to be, and most importantly is copyrighted (so I assumed I could not use it in RansomWhere?). Still, this resource was incredibly helpful and useful - mahalo Satoshi!
To keep this blog's length somewhat reasonable, I'm not going to dive into the details behind application receipts and validation. If you're interested check out that aforementioned blog or Apple's docs "About Receipt Validation".
Here, will simply focus on code :)
Programmatically Verifying App Receipts
Apple states; "Validating [an application receipt] locally requires code to read and validate a PKCS #7 signature, and code to parse and validate the signed payload." ...let's dive in, and look how this is accomplished in code!
When determining if an application is from the App Store, and in an untampered state, the first thing to do is simply verify its digital signature. Specifically, check that it's signed by a legitimate Apple Developer ID. In code; one can do this via the SecStaticCodeCheckValidity() function with the requirement reference "anchor apple generic".
//is app signed with apple dev id?
// note: error checking (other than on signature) is omitted
//create static code
SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:appPath]), kSecCSDefaultFlags, &staticCode);
//create req string w/ 'anchor apple generic'
SecRequirementCreateWithString(CFSTR("anchor apple generic"), kSecCSDefaultFlags, &requirementRef);
//check if file is signed w/ apple dev id by checking if it conforms to req string
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirementRef);
if(noErr != status)
{
//err msg
NSLog(@"ERROR: SecStaticCodeCheckValidity() failed with %d", status);
//bail
goto bail;
}
//hooray, app signed with apple dev id :)
However, just because an application is signed with an Apple Developer ID, doesn't mean it's from the App Store. In fact, the OS X ransomware KeRanger was signed with a legitimate developer ID! So, now one has to verify the application's receipt. If the application contains a valid receipt; it is from the App Store.
The first step in application receipt validation is locating the receipt. On OS X 10.7+ one can simply call the NSBundle's appStoreReceiptURL method to locate a url to the application's receipt. Also make sure to check that the receipt file actually exists on disk.
//does app have a receipt?
if( (nil == bundle.appStoreReceiptURL)||
(YES != [[NSFileManager defaultManager] fileExistsAtPath:bundle.appStoreReceiptURL.path]) )
{
//err msg
NSLog(@"ERROR: couldn't find app store receipt url/file (%@)", bundle.appStoreReceiptURL);
//bail
goto bail;
}
//app has a receipt
Once the application's receipt is located and loaded into memory, it must be decoded. Decoding begins with various functions provided by Apple's implementation of the "Cryptographic Message Syntax" (see
CMSDecoder.h/RFC 3852).
Basically, create a decoder, a X509 policy, add the encoded message to the decoder, then begin decoding! This is most clearly illustrated in code, shown below. Note that the code performs various initial validations on the receipt, such as ensuring the encoded data was signed, etc.
//begin receipt decoding
// note: error checking (other than on signer validations) is omitted
//create decoder
CMSDecoderCreate(&decoder);
//add encoded data to message
CMSDecoderUpdateMessage(decoder, self.encodedData.bytes, self.encodedData.length);
//finalize
CMSDecoderFinalizeMessage(decoder);
//create policy
policy = SecPolicyCreateBasicX509();
//CHECK 1:
// ->make sure there is a signer
status = CMSDecoderGetNumSigners(decoder, &signers);
if( (noErr != status) ||
(0 == signers) )
{
//bail
goto bail;
}
//CHECK 2:
// ->make sure signer status is ok
status = CMSDecoderCopySignerStatus(decoder, 0, policy, TRUE, &signerStatus, &trust, &certVerifyResult);
if( (noErr != status) ||
(kCMSSignerValid != signerStatus) )
{
//bail
goto bail;
}
//grab message content
CMSDecoderCopyContent(decoder, &data);
//convert to NSData
decoded = [NSData dataWithData:(__bridge NSData *)data];
Now, we have decoded 'message' data. Time to finalize decoding and parse out various components that will be used in receipt validation.
To parse the decoded 'message' receipt data, as shown in
RVNReceiptValidation.m, one can utilize Apple's SecAsn1Coder APIs (see SecAsn1Coder.h). Once a coder is created, invoke SecAsn1Decode() to finalize decoding:
//finalize decoding
// note: error checking is omitted
//create decoder
SecAsn1CoderCreate(&decoder);
//decode
SecAsn1Decode(decoder, self.decodedData.bytes, self.decodedData.length, kSetOfReceiptAttributeTemplate, &payload);
Then, simply iterate over attributes in the decoded receipt data to extract attributes of interest. For receipt validation, we're interested in the following attributes (and values) from the receipt:
- bundle id
- bundle id data
- app version
- 'opaque value'
- receipt hash
//extract attributes of interest
for(int i = 0; (attribute = payload.attrs[i]); i++)
{
//process each type
switch(getIntValueFromASN1Data(&attribute->type))
{
//bundle id
// ->save bundle id and data
case RECEIPT_ATTR_BUNDLE_ID:
{
//save bundle id
items[KEY_BUNDLE_ID] = decodeUTF8StringFromASN1Data(decoder, attribute->value);
//save bundle id data
items[KEY_BUNDLE_DATA] = [NSData dataWithBytes:attribute->value.data
length:attribute->value.length];
break;
}
...
With the receipt located, decoded and parsed, finally it can be validated.
» check 0x1
The first check is to make sure the application's bundle id matches the application bundle id that was embedded in the receipt:
//CHECK 0x1:
// ->app's bundle ID should match receipt's bundle ID
if(YES != [receipt.bundleIdentifier isEqualToString:appBundle.bundleIdentifier])
{
//err msg
NSLog(@"ERROR: receipt's bundle ID (%@) != app's bundle ID (%@)",
receipt.bundleIdentifier, appBundle.bundleIdentifier);
//bail
goto bail;
}
//continue checks...
» check 0x2
The second check is to make sure the application's version matches the application version that was embedded in the receipt:
//CHECK 0x2:
// ->app's version should match receipt's version
if(YES != [receipt.appVersion isEqualToString:appBundle.infoDictionary[@"CFBundleShortVersionString"]])
{
//err msg
NSLog(@"ERROR: receipt's app version (%@) != app's version (%@)",
receipt.appVersion, appBundle.infoDictionary[@"CFBundleShortVersionString"]);
//bail
goto bail;
}
//continue checks...
» check 0x3
The final check is to verify that a computed hash, matches the receipt's embedded hash.
This step is a little more involved, specifically in terms of computing the hash. In order to compute the hash (to check it against the one that's embedded in the receipt), first get the computer's MAC address.
Apple provides code to retrieve this (snippet):
//get computer's MAC address (snippet)
//iterate over services, looking for 'IOMACAddress'
while((service = IOIteratorNext(iterator)) != 0)
{
//parent
io_object_t parentService = 0;
//get parent
kernResult = IORegistryEntryGetParentEntry(service, kIOServicePlane, &parentService);
if(KERN_SUCCESS == kernResult)
{
//release prev
if(NULL != registryProperty)
{
//release
CFRelease(registryProperty);
}
//get registry property for 'IOMACAddress'
registryProperty = (CFDataRef) IORegistryEntryCreateCFProperty(parentService,
CFSTR("IOMACAddress"), kCFAllocatorDefault, 0);
//release parent
IOObjectRelease(parentService);
}
//release service
IOObjectRelease(service);
}
Once the MAC address is retrieved, create a buffer that contains this MAC address, then the 'opaque value' attributed that was extracted from the receipt, and finally the data associated with the bundle id attribute. Hash this via SHA1:
//add guid (MAC) to data obj
[digestData appendData:guid];
//add receipt's 'opaque value' to data obj
[digestData appendData:receipt.opaqueValue];
//add receipt's bundle id data to data obj
[digestData appendData:receipt.bundleIdentifierData];
//init SHA 1 hash
CC_SHA1(digestData.bytes, (CC_LONG)digestData.length, digestBuffer);
Finally, to complete the third check, compare this computed hash against the hash that was embedded in the receipt:
//CHECK 0x3:
// ->ensure computed and embedded hash are equal
if(0 != memcmp(digestBuffer, receipt.receiptHash.bytes, CC_SHA1_DIGEST_LENGTH))
{
//err msg
NSLog(@"ERROR: receipt's hash does not match computed one");
//hash check failed
goto bail;
}
//hooray, receipt is valid!
Assuming all three checks pass, according to Apple, one can be 'sure' that the application is from the App Store and is in its original (untampered) state. Mission (finally) accomplished.
Now we can quickly check such thing as whether the (signed) OS X ransomware KeRanger is from the Mac App Store (it's not):
$ ./fromAppStore /Volumes/Transmission/Transmission.app
checking if Transmission.app is from the Mac App Store
app is signed with an Apple Dev. ID
ERROR: could not find app store receipt url, or file (_MASReceipt/receipt)
ERROR: failed to init/decode/parse app's receipt
Transmission.app is *not* from the Mac App Store
Of course legitimate applications from the App Store are classified as such:
$ ./fromAppStore /Applications/1Password.app
checking if 1Password.app is from the OS X App Store
app is signed with an Apple Dev. ID
found receipt at: /Applications/1Password.app/Contents/_MASReceipt/receipt
check 1 passed: bundle ID's match
check 2 passed: app versions match
check 3 passed: hashes match
1Password.app is from the Mac App Store
Conclusions
In order to reduce false positives, I plan on having RansomWhere? version 1.1 trust (i.e. ignore) untampered applications from the official Mac App Store. Turns out determining if an arbitrary application originated from the App Store, was a little more involved than I would have thought. But I learned a lot, and hopefully via this blog, so did you! Remember; check out the complete code - and stay tuned for RansomWhere? version 1.1.
And as always, please feel free to email me at patrick@objective-see.com if you have any feedback or comments!