One of the exercises of “The Linux Programming Interface” chapter 38 is about implementing program,
called douser
, that should have the same functionality as the sudo
progam. This means that if you run
$ douser whoami
, this should asks for the root
user password and, if the password matches, should run
the command whoami
, which would print root
. If the -u <user>
flag is used, douser
should ask for
the user
password and execute whoami
on behaf of that user, printing its name.
To be able to authenticate users on the system, our program must read the /etc/shadow
file,
which is only readable by the root
user. This means that it must run as root
.
But it would be pretty bad if we needed to know the root
password to run execute a command as an
unprivileged user, e.g, $ douser -u ubuntu ls
. That is where the set-user-id permission bit comes to
our rescue.
Set-User-ID programs
A Set-User-ID program sets the process effective user ID to the same as the user ID that owns the executable file. So it does not matter what user executes the program, the process will always run as the owner of the executable.
If we inspect our sudo binary we can see that the set-user-ID permission bit is set on the file:
$ ls -l /usr/bin/sudo
-rwsr-xr-x 1 root root 155008 Oct 14 2016 /usr/bin/sudo
We can see that it shows an s
instead of x
on the execution permission bit.
The implementation
My full implementation is available on github. In this section I will discuss the most relevant parts of it.
Authenticating
User authenticating is the most sensitive part of our program and is handled by the authenticate
function.
This function uses getpwnam(username)
to obtain a struct contained the fields available at the
/etc/passwd
file on linux for the user with that particular username.
The user password is actually stored in a different file (/etc/shadow
), read only by the root
user.
We use the function getspnam(username)
to obtain a struct representing the data on that file.
Then we use the function getpass
to prompt the user for the password and the function crypt
to
encrypt the password. If the input password and the real password matches, authenticate returns 0.
/*
authenticate prompts the user for the password
and validates it, returning 0 on success.
*/
int
authenticate(char *username)
{
struct spwd *spwd;
struct passwd *pwd;
char *encrypted;
Boolean authOk;
pwd = getpwnam(username);
if (pwd == NULL)
fatal("could not get password file");
spwd = getspnam(username);
if (spwd == NULL && errno == EACCES)
fatal("no permission to read shadow password file");
if (spwd != NULL)
pwd->pw_passwd = spwd->sp_pwdp;
encrypted = crypt(getpass("Password:"), pwd->pw_passwd);
return strcmp(encrypted, pwd->pw_passwd);
}
Executing the user provided command
After authenticating the user, we use the setuid
syscall to set the effective user ID of the running
process to the ID of the authenticated user.
Following that, we use fork
to create a child process that will have its text segment replaced by
using the execvpe
library function. We replace the environment variables setting $PATH
to known
safe locations. The parent process exits right after forking.
pwd = getpwnam(username);
if (pwd == NULL)
fatal("unable to retrieve user info");
if (setuid(pwd->pw_uid) != 0)
errExit("setuid");
pid = fork();
if (pid == -1) {
errExit("fork");
} else if (pid != 0) {
execvpe(argv[optind], &argv[optind], envp);
errExit("execvpe");
}
exit(EXIT_SUCCESS);
Usage
After compiling the code, one must login as root
and properly set the douser
binary using
chown root:root ./douser
and chmod u+s ./douser
. The latter turns on the set-user-ID permission bit
on the executable.
After the setup, run ./douser whoami
and it should print root
. Woot!
Disclaimer
The implementation is far from being a complete copy of sudo (that is probably obvious but wanted to make it clear). The real sudo implementation can be read here.
I decided to share my implementation and some of my reasoning as a way to both share the knowlged and also enforce it. Would love to get some feedback.