Escalating privileges on Mac OS X securely, and without using deprecated methods

· by Steve · Read in about 6 min · (1077 Words)

This week I implemented a much-requested feature in SourceTree for the upcoming 1.3 release (beta 1 went out on Monday, this will make it into beta 2) - a command-line tool so you can quickly pull up SourceTree for the repository you’re in from a terminal. Writing the command-line tool was trivial, but when I came to implement the menu item which would install it in /usr/local/bin, which inherently needs privilege escalation, it turned out to be a lot more complicated than I expected.

How so? Surely lots of people have done this sort of thing before? Well, that’s true, they have - but the problem is that just about all of the existing examples of this use the Authorization Services API call AuthorizationExecuteWithPrivileges - and this method is deprecated in OS X 10.7 (Lion). Now, of course that doesn’t stop you using it (yet), provided you’re willing to turn off the warning that building against the 10.7 SDK gives you, but any programmer worth his salt should take deprecation as a hint that they should be looking for another way.

There are basically 3 ways to escalate privileges on OS X, and only one of them is now recommended:

  1. Use a helper tool which has its setuid bit set so that it runs as root. Risky if that tool gets compromised, and the setuid bit can be lost, needing reinstatement by another privileged task.
  2. Execute a command as root via AuthorizationExecuteWithPrivileges. As mentioned above, this is now deprecated, and again if a hacker compromises either the app or the tool being launched, bad things can happen.
  3. Ask Launch Services to install a privileged helper tool via SMJobBless. This helper is subsequently run by launchd as root when invoked via a Unix socket, and can perform privileged tasks. Importantly, code signing is verified at both ends by Launch Services at install time to prevent tampering with either binary.

Clearly option 3 was the way to go - the only ‘downside’ about it is that is does require that you have the ability to sign your application and helper tool. I already have valid code signing certificates because I deploy on the App Store, so this isn’t an issue (even though this functionality won’t actually be in the App Store version of SourceTree because Apple disallow installer behaviour there, I still sign with the same certs). In fact, the fact that I know my app and helper tool can’t be interfered with without the code signatures becoming invalid is very reassuring. Given that it only costs $99 per year to be on the Mac Developer Programme which allows you to get certificates (even if you don’t deploy on the App Store), it’s something serious developers should consider strongly.

SMJobBless is ideally suited to installing daemons, but it’s perfectly acceptable to install tools which run simple one-off tasks too. When setting up the plist for the helper, you specify that it is ‘OnDemand’, with no ‘KeepAlive’ which means it’s not started by Launch Services at startup, only when a Unix socket is opened, and shuts down very quickly if there’s no activity. Unfortunately the SMJobBless example doesn’t do anything except to show you how to install a tool, it doesn’t tell you how to implement that tool to do anything useful, or how to call it from your main application.

To see how to do that, you need to refer to BetterAuthorizationSample , which includes a re-usable library for this. Ironically though, this example uses AuthorizationExecuteWithPrivileges to install its helper (this example pre-dated its deprecation in favour of SMJobBless). So you have to remove all the code associated with installation; you won’t need it anyway since SMJobBless does that function better. You keep the rest which gives you a framework for implementing and calling the helper. So here’s what I did:

  1. Implement the installation of a privileged helper, based directly on the SMJobBless example. I needed to change the bundle IDs and the certificate CN’s to match my setup of course.
  2. Extend the plist files from SMJobBless to register the helper with a socket. This was basically a case of copying the settings from the BetterAuthorizationSample plists, which already does this.
  3. Bring in BetterAuthorizationSampleLib.c/.h to assist with implementation of the helper, and the code for calling it in the app, but remove everything in the ‘Installation’ section. This eliminates all the references to AuthorizationExecuteWithPrivileges - we’re doing the install with SMJobBless so don’t need that.
  4. Follow the BetterAuthorizationSample for the implementation of the helper, and the bit in the application where you call the helper to perform privileged operations.

So in my case, the following happens when you click ‘Install Command Line Tool’ in SourceTree:

  1. A privileged helper is installed in Launch Services using SMJobBless. OS X checks the code signatures on both ends to ensure that the helper and the application asking to install it are valid (must be signed with my cert, and that cert must be issued by Apple).
  2. A connection is opened to the privileged helper over a socket which causes launchd to start it up
  3. I ask the helper via the BetterAuthorizationSampleLib to install the command-line tool in /usr/local/bin. As an additional check, the helper validates via ‘codesign -v ¬†-R=“conditions”’ that this tool is code signed with my cert (again, must be issued by Apple) - this is to prevent anyone else sniffing out this socket and trying to use it to install other things. If that passes, it installs the command.

This is quite a long-winded process compared to just calling a ‘cp’ command via AuthorizationExecuteWithPrivileges, but it’s also a lot more secure, since a malicious person can’t alter any of the moving parts without invalidating the code signatures. You’re also insulated from future changes when inevitably¬†AuthorizationExecuteWithPrivileges is removed entirely.

I apologise for the lack of a pre-packaged example here - I haven’t had time to extract one from my own implementation yet. However, as described above if you start with the SMJobBless sample and add-in the BetterAuthorizationSample, removing from the latter everything associated with installation, you’re basically there. If I get chance later I’ll post a shrink-wrapped example.

I hope that helps someone - I found there to be little information on this subject that was up-to-date, and lots of older information that was misleading so maybe this will save someone some time. Ideally, I hope Apple will combine the SMJobBless and BetterAuthorizationSample some time to produce a 10.7-compliant official example.