Yet Another [à compléter]

A Better MonoMac Privilege Elevation

Back in my article on Macdoc, I described a way to use the Authorization APIs to execute privileged operations from an otherwise perfectly classic user-level application.

With Macdoc, we use an helper shipped with MonoTouch that merge our documentation with the Apple documentation on your system.

To do that, it needs to access the files stored on your system at /Library/Framework/Mono.framework/External/monodoc. That directory is not writable by a common user so the only way to run the script correctly is to run it with root privileges.

In our previous incarnation, we where using the AuthorizationExecuteWithPrivileges call that can launch an external process (in our case a shell) as root by asking the user to input its credentials.

Although this is a perfectly working solution, that call was recently deprecated in favor of an alternative approach that Apple is now pushing. In the new model, application should ship with a setuid binary which owner has been set to root (by, like, your installer).

That helper can then be executed normally by anyone without prior authorization and it will run with its euid (effective user id) set to root allowing it to carry action unavailable to a standard user.

The helper is also advised to include a self-repairing mode (usually a flag that is passed to the binary) to fix its setuid bit/ownership when called by the parent process. In that case since the helper is broken, we have to use again the Authorization API to launch the process as root until it fixes itself.

This seems all and well but in our case the major issue was that our helper was in fact not a binary but a traditional shell script.

If you are familiar with shell scripting, you know that most of them carry a shebang (of the form #!) which reference the interpreter to use to execute the file.

In practise it means that when you are executing that file, the system will pick that line and exec the referenced interpreter binary and pass the content of the file to it. The consequence is that setting the setuid bit on a shell script has absolutely no effect since it’s not what is executed in the end.

To circumvent this, we can use a little trick in a form of a C shim that wraps our shell script. The source code of the shim is given below:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#include "script.xxd"

int main (int argc, char* argv[])
{
	int i = 0;

	/* We check that the shim was launched as a root setuid binary */
	if (geteuid () != 0) {
		puts ("This tool needs to be run as root");
		return 1;
	}
	/* To propagate the permissions to the shell, we have to really become root */
	if (setuid (0) != 0) {
		perror ("Error morphing ourselves to root");
		return 1;
	}
	/* Transmit the extra arguments given to the script as env variables */
	for (i = 0; i < argc; ++i) {
		if (strcmp (argv[i], "--self-repair") == 0)
			setenv ("SELF_REPAIR", "yes", 1);
		if (strcmp (argv[i], "--force-download") == 0)
			setenv ("FORCE_DOWNLOAD", "yes", 1);
	}
	/* We execute the bundled script and return its exit code */
	return system ((const char*)script_sh);
}

Let’s breakdown a bit what we have here. The first thing the code does is to check a couple of condition to make sure the binary has been executed correctly either by root directly or with an euid set to root.

The interesting thing here is that we cannot limit ourself to having just our euid set to root. Since that shim will eventually also launch a shell process the euid will be lost in the process just like before.

Hence we use the opportunity given by our euid powers to properly upgrade the uid of the process to root meaning anything we now launch will also have root privileges.

The remaining thing is to gather the arguments that were passed to the shim and forward them to the script using environment variable before launching it using the good old system(3) call.

Now, as you can see we are passing the script_sh variable to system() but it’s not declared anywhere. That’s where a bit of build magic happen. Since we want our shim to be self-contained, we have to somehow include our shell script inside the shim.

The usual way to do this is to simply define an array of char that contains the raw content of the resource we embed.

The process is actually surprisingly easy thanks to the xdd utility which can convert any input into a C-style hex-based character array. It’s not entirely satisfactory though as the produced array still need to be manually NULL-terminated which we can thankfully be hacked with a bit of sed.

Following is the Makefile fragment where we do these operations so that the shim is automatically updated any time we change the script itself, notice how we specify both x86 and x86_64 as -arch arguments to have our binary executes natively on both flavor.

script: shim.c script.sh
	xxd -i $(word 2, $^) > $@.xxd
	sed -i '' -e 's/};/,0x0};/' $@.xxd
	cc -o $@ -arch i386 -arch x86_64 $<
	rm -f $@.xxd

Comments