The story behind CVE-2019-13013

This blog post targets fellow software developers. It’s a story of how it could happen that we shipped a version of Little Snitch with a serious vulnerability and, more importantly, what we can learn from it.

It all began with a security improvement by Apple in macOS High Sierra (10.13). Apple had revoked access to the folder /Library/Logs/DiagnosticReports for non-admin users. The protection goes so far that even a root process spawned by AuthorizationExecuteWithPrivileges() cannot access the folder.

No problem for our developers: AuthorizationExecuteWithPrivileges() is deprecated anyway and there is a replacement API: SMJobBless(). Root processes spawned using SMJobBless() can access /Library/Logs/DiagnosticReports. However, SMJobBless() is not a drop-in replacement for AuthorizationExecuteWithPrivileges(). It has completely different semantics. While AuthorizationExecuteWithPrivileges() is made for one-shot operations (requesting authorization every time), SMJobBless() permanently installs a system-wide daemon.

Talking about “permanently”: Every installer application which needs root permissions is now urged to install a system-wide daemon for this purpose. This system-wide daemon is usually left behind, because Apple provides no API for removing it. If you look at /Library/PrivilegedHelperTools on your computer, you’ll probably find a couple of them. If you have not done a clean macOS install for a while, there may be some very old helper tools from long ago, maybe from an app you just tried once. All these helpers are still there, waiting for a job to be done — or somebody to exploit a vulnerability.

But back to the vulnerability in Little Snitch. There’s another issue with the SMJobBless() API: It is not well documented. One example: The docs say: “The helper tool must have an embedded launchd plist.” That’s all there is about embedding the plist. It does not explain how and where to embed the file. Neither is there an Xcode template target. You can’t get this right without a code example. In the first place, Apple provided one example for SMJobBless(): SMJobBless (named after the framework function). It’s a good starting point for getting the code signature and plist embedding stuff right. But it does nothing productive, it’s a kind of privileged “Hello World!” program. When people asked how the app could communicate with the helper, the answer was “XPC”. So people merged examples for XPC processes with this privileged helper example (e.g. SMJobBlessXPC by Nathan de Vries). A typical listener delegate was implemented like this:

- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)connection {
    connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HelperToolProtocol)];
    connection.exportedObject = [[HelperTool alloc] init];
    [connection resume];
    return YES;
}

After a while Apple published the “EvenBetterAuthorizationSample”. This example primarily documents how to obtain authorization from the user, serialize it as AuthorizationExternalForm and send it over to the privileged helper, but it was also based on SMJobBless() and XPC. However, sending an authorization with every request may not be practicable: Consider a backup program which wants to perform unattended backups at regular intervals. It cannot ask the user for authorization every time!

Consequently our developer began his new helper project based on one of the examples where SMJobBless() and XPC were merged. It took a while until he got all the linker options, code signatures and Info.plist settings right: There is an entry named SMPrivilegedExecutables in the parent app and another one named SMAuthorizedClients in the helper. He expected (short of reading all the docs closely) that the requirement strings in SMAuthorizedClients define which clients are allowed to connect to the helper. XPC is said to be inherently secure, so he thought that this was the security mechanism in place for privileged helpers to prevent unauthorized clients (malware in particular) from utilizing the helper’s privileged operations. And he’s not the only one! Many documents found on the Internet contain a wording like “SMAuthorizedClients lists applications registered to have access to the helper tool”.

But that’s a misunderstanding! These Info.plist entries only define which app can install or update a privileged helper tool. The original SMJobBless() example was made completely without XPC, after all!

We finally published a version of Little Snitch with a privileged helper that did not verify connecting clients in any way. Luckily for us, the privileged helper was only installed when a hidden feature, the creation of a diagnostics report, was used, and our uninstaller always removes the privileged helper.

The rest of the story is straight forward. In an internal code review, another developer looked over the code and verified all assumptions. He did not find a proof for the assumption that XPC connections are authorized by the system. Since there was little information available, he made a test project and could exploit our privileged helper tool!

We fixed the issue immediately, assigned a CVE number and published an update as soon as possible. Maybe too fast: Thinking about the upgrade procedure revealed another issue: Version 4.4 fixed the vulnerability, but the fix was not applied unless the user created a new diagnostics report! Although our uninstaller always removes the privileged helper from /Library/PrivilegedHelperTools, the upgrade did not! Time for another security related update and a new CVE number…

Is other software also vulnerable?

The misunderstanding about client authorization in XPC seems to be wide-spread, at least judging from the documents we found on the Internet. The first thing we did was a search for “prior art”, others who have gone through the same insight as we just did. And indeed, we found at least three recent CVE numbers documenting the very same issue. And we found documents from Erik Berglund who describes related issues back in 2016!.

If so many people were misled and the problem is known at least since 2016, why did we still have to go through this same unpleasant experience as all the others?

We therefore decided to collect a list of software we knew to use a privileged helper and asked the developers to review their code and check for this particular vulnerability. We also contacted the authors of easy to find code examples and asked them to update their examples. And finally we decided to write this blog post and make some noise around the issue.

Why is this vulnerability serious?

Back to the original vulnerability. Why do we make so much noise around it? A local privilege escalation to root is always serious. What makes this issue even worse is that privileged helpers are usually left behind after the associated app is removed. They pile up over time, each with its individual vulnerabilities. If the associated app has been removed, there’s no automatic software update or any other mechanism which could upgrade or remove an existing, vulnerable helper. Erik Berglund has identified this problem back in 2016!

So our plea to fellow developers: Please make sure you remove all privileged helpers as soon as they are no longer needed! When your app has been removed, it is too late!

Is my code also affected?

If you publish software which includes a privileged helper, either using SMJobBless() or installed directly as a Launch Daemon, please check your code. You should be aware that the XPC communication endpoint is published in a global context and therefore available to any program on the computer, even to the least privileged one. You must authorize the connecting client in some way before you perform any operation on behalf of it. The following reasoning won’t protect you:

  • “The API functions of my helper cannot be used for an exploit.” Really? Then, why does it need to be privileged? Attackers even exploit differences in timing of CPU instructions these days! Just because you can’t imagine an exploit does not mean that an attacker can’t find one.
  • “My helper performs read access only, it cannot be used to modify files.” This may be true, but read access as root is bad enough. It can be used to obtain secrets, which in turn can be used to obtain privileges.
  • “I’m sending an an AuthorizationExternalForm with every request, as in the EvenBetterAuthorizationSample code example.” Good! This means that an attacker needs at least consent from the user (by means of an authorization dialog). But you can still add authentication of the peer in order to reduce the attack surface.
  • “I’m already checking the peer’s code signature.” Good! But please read our example code below. If you identify the peer by Unix PID, you’re not secure. If you allow peers without the hard and kill options to codesign, you may not be secure.
  • “I’m already doing it the way you suggest in your example code.” Great! But you might still not be secure. We are not aware of a vulnerability in our code, but that does not prove that none exists. So please review the code below and tell us if you find any vulnerabilities!

How should I check peers in XPC?

The helper (and the app using the helper) should check the identity of the peer before performing any operations. Even if an AuthorizationExternalForm is already used. The most secure way for such a check is the code signature.

Here is an example for an XPC listener delegate:

@interface NSXPCConnection(PrivateAuditToken)

// This property exists, but it's private. Make it available:
@property (nonatomic, readonly) audit_token_t auditToken;

@end

// In the NSXPCListenerDelegate:
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)connection {
   audit_token_t auditToken = connection.auditToken;
   NSData *tokenData = [NSData dataWithBytes:&auditToken length:sizeof(audit_token_t)];
   NSDictionary *attributes = @{(__bridge NSString *)kSecGuestAttributeAudit : tokenData};
   SecCodeRef code = NULL;
   if (SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef)attributes, kSecCSDefaultFlags, &code) != errSecSuccess) {
       return NO;
   }
   // Before checking the requirement make sure that code signing flags
   // CS_HARD and CS_KILL are set. Dynamic code signature checks can only
   // check the code pages already swapped into memory, so make sure that
   // no malicious code can be loaded at a later time. You may want to
   // disable this check in debug builds.
   CFDictionaryRef csInfo = NULL;
   if (SecCodeCopySigningInformation(code, kSecCSDynamicInformation, &csInfo) != errSecSuccess) {
       return NO;
   } else {
       uint32_t csFlags = [((__bridge NSDictionary *)csInfo)[(__bridge NSString *)kSecCodeInfoStatus] intValue];
       CFRelease(csInfo);
       const uint32_t cs_hard = 0x100;         // don't load invalid pages
       const uint32_t cs_kill = 0x200;         // kill process if page is invalid
       const uint32_t cs_restrict = 0x800;     // prevent debugging
       const uint32_t cs_require_lv = 0x2000;  // Library Validation
       const uint32_t cs_runtime = 0x10000;    // hardened runtime
       if ((csFlags & (cs_hard | cs_kill)) != (cs_hard | cs_kill)) {
           // add all flags to check which are in your code signature!
           // In particular, we recommend cs_require_lv and cs_restrict.
           return NO;    // Not accepted because it can be tampered with
       }
   }

   NSString *requirementString = @"anchor apple generic and certificate leaf[subject.OU] = \"MyTeamIdentifier\"";
   SecRequirementRef requirement = NULL;

   // Check at least the peer's TeamID, e.g.
   // "anchor apple generic and certificate leaf[subject.OU] = MyTeamIdentifier"
   if (SecRequirementCreateWithString((__bridge CFStringRef)requirementString, kSecCSDefaultFlags, &requirement) != errSecSuccess) {
       abort(); // error in requirement string
   }

   OSStatus status = SecCodeCheckValidityWithErrors(code, kSecCSDefaultFlags, requirement, NULL);
   CFRelease(code);
   CFRelease(requirement);
   if (status != errSecSuccess) {
       return NO;
   }

   // further initialization of connection goes here...

   return YES;
}

The same code can be used in the app to authenticate the privileged helper after sending the first XPC request (auditToken is not valid before the first request was sent).

Note that this example uses the private NSXPCConnection.auditToken property. If we want to avoid using a private property, we need to use the Unix process ID. But this is inherently insecure (see Don’t trust the PID! by Samuel Groß). We therefore decided to use auditToken anyway.

Also note that SecCodeCheckValidityWithErrors() may succeed without any error although the loaded binary has been modified. That’s because code signature verification is done lazily when memory pages are swapped in. As long as the modification is in a page which has not (yet) been loaded into memory, SecCodeCheckValidityWithErrors() will not detect an error. This can be used for an exploit, unless the code is signed with codesign options hard and kill. We therefore accept only clients which have these options set.

Dynamically loaded code is also an issue. We recommend the codesign option library, which enforces library validation. This basically means that only libraries signed by you or by Apple can be loaded. Note that this limits your ability to load third party plugins, though.

And, although the insight is not new: Always make sure that the privileged helper is removed when your app is removed so that it can’t escape from software updates. When your software is updated, make sure that the privileged helper is either removed or updated!

A final note to all Mac users

As you can see from the explanations above – the possibilities of an app developer to resolve a potential vulnerability of his privileged helper tool are somewhat limited.

If the user doesn’t have the app installed anymore, there’s no way for the developer to help him to get rid of the vulnerable component that’s left behind from a previous installation.

So we highly recommend all Mac users to review the contents of their /Library/PrivilegedHelperTools folder in Finder to watch for installed helper tools and to delete those that are obviously no longer needed (the helper’s name often gives a hint about the corresponding app and its purpose).

If you are not sure about a particular helper, it may even be advisable to remove one helper too much (assuming that the app will reinstall it anyway, if needed) rather than leave a possible vulnerability on your Mac.