Monday, January 14, 2013

OSX AuthorizationExecuteWithPrivileges example

Copyright © 2013, Steven E. Houchin. All rights reserved.

I've read various posts online about developers' travails with OSX's AuthorizationExecuteWithPrivileges API, and misunderstanding about how it works.  So, here is some of what I've learned about it.

Apple's documentation states that it "Runs an executable tool with root privileges."  But, users soon discover that the new process does not, in fact, run as root.  In reality, the new process executes at an elevated privilege level that allows it to become the root user by calling 'setuid(0)'.  For example:

// Fork a child process
pid_t child_pid = fork();
if(child_pid == 0)
{
  // This is done by the child process
           
  // Change to the root user
  uid_t userid = getuid();
  setuid(0);  // set root permissions 

  // Do things as root ... 

  // Restore normal permissions
  setuid(userid);
  exit(0);
}

Note that there is danger in using AuthorizationExecuteWithPrivileges.  For example, your app might invoke a helper tool named 'grok' that will execute with the elevated privileges.   If a hacker figures out that 'grok' is started this way, he can replace 'grok' with his own trojan binary, which can then make itself root and thus do nasty things to your system. It isn't really hard for a hacker to determine that 'grok' is invoked this way, because the MAC's Console utility logs that AuthorizationExecuteWithPrivileges was called to execute 'grok.'   So, if you must execute 'grok' this way, your app should first validate the 'grok' binary in some way to make sure it has not been tampered with.  What I did was to open 'grok' as a binary data file and then I scan it for a known string used in the code, such as an innocuous printf format string.

The first parameter to AuthorizationExecuteWithPrivileges is an AuthorizationRef object.  This is obtained via a call to AuthorizationCreate:

AuthorizationRef authorizationRef;
OSStatus status = AuthorizationCreate(NULL,
                       kAuthorizationEmptyEnvironment,
                       kAuthorizationFlagDefaults,
                       &authorizationRef);
if (status != errAuthorizationSuccess)
{
    // Notify user of the error ...
}

The actual elevated rights you request are specified via a call to AuthorizationCopyRights, at which time the user is prompted to enter his password.  Along with requesting certain rights, you can specify a custom icon and text for the password popup.  Note however that your icon will not appear if it resides in a directory beneath any ancestor directory that lacks Everyone access.  Here's what the icon specification looks like, which is passed as the AuthorizationEnvironment parameter to AuthorizationCopyRights:

AuthorizationItem kAuthEnv[1];
const char *iconPath = "/Applications/MyApp.app/Resources/myicon.icns";
kAuthEnv[0].name = kAuthorizationEnvironmentIcon;
kAuthEnv[0].valueLength = strlen(iconPath);
kAuthEnv[0].value = (void *)iconPath; // fully qualified path
kAuthEnv[0].flags = 0;
AuthorizationEnvironment authorizationEnvironment;
authorizationEnvironment.items = kAuthEnv;
authorizationEnvironment.count = 1;

Next, in order to use AuthorizationExecuteWithPrivileges, you must request the "system.privilege.admin" right.  You set this up as follows:

const char *grokPath = "/Utilities/grokUtil/grok";
AuthorizationItem executeRight = {
                        kAuthorizationRightExecute,
                        strlen(grokPath)
                        (void *)grokPath,
                        0};
AuthorizationRights rightsSet = {1, &executeRight};

Note that kAuthorizationRightExecute is defined as "system.privilege.admin" in the Security framework's AuthorizationTags.h.  This is then used in AuthorizationCopyRights to actually acquire the rights for the AuthorizationRef object:

AuthorizationFlags flags =
                  kAuthorizationFlagDefaults |
                  kAuthorizationFlagInteractionAllowed |
                  kAuthorizationFlagPreAuthorize |
                  kAuthorizationFlagExtendRights;

// Call AuthorizationCopyRights to determine
// or extend the allowable rights
OSStatus status = AuthorizationCopyRights(
                                authorizationRef,
                                &rightsSet,
                                &authorizationEnvironment,
                                flags,
                                NULL);
if (errAuthorizationCanceled == status)
{
    // User canceled authentication  ...
}
else if (status != errAuthorizationSuccess)
{
    // Notify the user of the error ...
}

All that's left at this point is to execute the privileged helper tool:

FILE *fpStdout = NULL;
status = AuthorizationExecuteWithPrivileges(
                        authorizationRef,
                        (const char *)grokPath,
                        kAuthorizationFlagDefaults,
                        argv,  // normal argv array of program args
                        &fpStdout);
bool success = (status == errAuthorizationSuccess);
pid_t newProcId;
if (success)
{
   // Get the new process id
   newProcId = fcntl(fileno(fpStdout), F_GETOWN, 0);
   fclose(fpStdout);
}
else
{
   // Notify user of the error ...
}

AuthorizationExecuteWithPrivileges returns as soon as the new process has started.  It does not execute it as a child of the current process, so a waitpid on the resulting process id gives an error.