Mark Eschbach

Software Developer && System Analyst

Dropping privileges in a SUID binary

I required an application to run as a SetUID application under FreeBSD, requiring the application to run under a different user and set of groups after completing a task. I choose the term 'set of groups' because in some systems (like FreeBSD :) ) may associate a process with a number of groups besides the standard POSIX group the process is running under. To drop the privileges my application had to accomplish the following steps:

  1. Locate the user and groups the process wished to become
  2. Change the groups
  3. Change the user
  4. Issue an exec

Locating the user and group are optional, as you may hardwire the numbers (not recommended), however steps two and three must be done in order. These tasks were easy enough to complete, however I decided to share as general knowledge because I didn't find many resources detailing process :).

Locating the user and groups we wish to become

Of course the target user and group are dependent on your goals, however you need at least one group identifier (GID) and one user identifier (UID). These may be obtained in a number of ways, including hardwiring your numbers that correlate to your /etc/passwd and /etc/group entries. I'll show two methods of retrieving both identifiers: from a file's user and group, and another through the system's user and group database module.

Obtaining a file's {GID,UID}

stat and friends are your friend in this case as the struct stat provides the information. The two fields you are looking for are st_uid and st_gid within the output of type struct stat. Be careful when using lstat as you will be refereeing to the symbolic link itself instead of the target of the link.

Example

The following is an example of locating the {GID,UID} from a file using stat

#include <errno.h>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>

using namespace std;

int main(int argc, char** argv){
        if(argc < 2){
                cout << "Requires a file as an argument" << endl;
        }else{
                char* fileName = argv[1];
                cout << "Owner and group of " << fileName << ":" << endl;
                struct stat info;

                if(stat(fileName,&info) != 0){
                        cout << "Unable to retrieve file information because "<<strerror(errno) << endl;
                }else{
                        cout << "Owner: " << info.st_uid << endl;
                        cout << "Group: " << info.st_gid << endl;
                }
        }
        return 0;
}

Locating a {GID,UID} pair from the system databases by name

To locate the {GID,UID} pairs requires accessing two different system databases to obtain the information. The databases are usually stored in /etc/passwd and /etc/group in simple deployments and maybe be stored in a remote LDAP database in large enterprises. The easiest method of accessing the data is to utilize the functions which are apart of the standard C library. I'll detail a simple example of both user name to UID and group name to GID.

User name to UID

To convert a user name to UID you utilize the getpwnam function, taking the user name as input and providing a structure as a result. The memory returned in the pointer is owned by the getpwnam module. This function is not thread-safe, although there are thread safe functsion providing the same functionality. The resulting struct is of type struct passwd and provides all of the fields in the password database. The pw_uid field will provide the user identifier. As an added bonus if you are only searching for the primary group you could also pickup the pw_gid.

User name to GID

Conversion from the user name to GID is just as simple, utilizing the getgrnam function. The returned struct group contains a gr_gid field for your group.

Example of {GID,UID}

The following example is taken from the library which you may download from below

/**
 * Retrieves the GID number for the given group name.
 *
 * @arg groupName is the name of the group to retrieve the GID for
 * @return the GID for the given group
 * @throws std::invalid_argument if the group doesn't exist or otherwise can't be located
 */
gid_t getGroup(const char* groupName) throw(PUGException){
	struct group* tuple;
	errno = 0;

	//Grab the group name from the group database
	tuple = getgrnam(groupName);
	if(tuple == NULL){
		if(errno == 0){
			std::stringstream buffer;
			buffer << "Unable to find a group by the name '"<< groupName << "'";
			throw PUGException(buffer);
		}else{
			std::stringstream buffer;
			buffer << "The following error occured while attempting to lookup an entry for group '"<< groupName << "': " << strerror(errno);
			throw PUGException(buffer);
		}
	}
	return tuple->gr_gid;
}

/**
 * Retrieves the UID number for the given user name.
 *
 * @arg userName is the user name to retreive the UID for
 * @return the UID for the given user
 * @throws std::invalid_argument if the user doesn't exist or otherwise can't be located
 */
uid_t getUser(const char* userName) throw (PUGException){
	struct passwd* tuple;
	errno = 0;
	tuple = getpwnam(userName);
	if(tuple == NULL){
		if(errno == 0){
			std::stringstream buffer;
			buffer << "Unable to find user '"<pw_uid;
}

Modifying the process's group

You have three fields related to the process group which must be modified. From my understanding, in many configuration root may assign arbitrary values to the fields, however non-superusers may only assign specific values. The effective user ID and real user ID must be set in that order. They are set using the functions setuid and seteuid respectively. If working with an operating system other than FreeBSD there may be additional fields you may consider setting like FSGID on Linux. Under FreeBSD a process running as root may also sets the access groups associated with the current process using the setgroups function. User FreeBSD this is an important step, as the list will allow the process access to the resources owned by the groups within the list.

Modifying the process's user

Convenient enough there is a seteuid and setuid functions to change the UID. This one is straight forward and easy without much fuss. As with the group, set the effective UID first with the seteuid function, then the actual UID with setuid.

Issuing an exec

To lock the effective and current identifiers you should issue an a call to one of the many exec functions. If you do not, then the operating system may allow the user to revert their user ID back using a feature known as the "saved-user-id". Tragic if you are attempting to isolate a process from the reset of the system which was running under root.

Example

Download PUG and an example driving application. This is an excerpt from the attached code base and is written in C++. Why C++ for an otherwise C application? Exceptions for error handling :).
/*
 * POSIX includes
 */
#include <errno .h>
#include <grp .h>
#include <pwd .h>
[..]
#include <stdexcept>
#include <string>
#include <sstream>
[...]
/**
 * This method attempts to determine if the current user of the process is a super user.
 * The current implementation of this method is rather naive as the implemenetation
 * just checks to see if the current user id is zero.  Should probably work under a
 * majority of cases.
 *
 * @returns true if the current user is a super user, otherwise false
 */
bool isSuperUser() throw();

/**
 * Changes teh associated groups for the current process, include the current,
 * effective, and access groups.  Under most opreating systems if the current
 * user is not a super user, then you may only set the process group to the
 * current group.
 *
 * @param group is the primary group to associate with this process
 * @param setGroups will also set the access groups to the given group
 * @throws PUGException if a problem occurs in an underlying functions
 */
void setGroup(const gid_t group, const bool setGroups = isSuperUser()) throw(PUGException);

/**
 * Drops teh privileges into the current user and group.  If the current user of the
 * process is not the super user, then the only valid parameters are teh current
 * user and group.  Otherwise if the process user is the super user, then any valid
 * value for the system will result in a change to the specified user.
 *
 * @param uid is the user to drop into
 * @param gid is the group to drop into
 * @throws PUGException if a POSIX function fails
 */
void dropPrivileges(const uid_t uid, const gid_t gid) throw (PUGException);
[...]
bool isSuperUser(){
	return getuid() == 0;
}

void setGroup(const gid_t group, const bool setGroups){
	if(setegid(group) != 0 ){
		std::stringstream msg;
		msg << "unable to set effective group becaues " << strerror(errno);
		throw PUGException(msg);
	}
	if(setgid(group) != 0){
		std::stringstream msg;
		msg << "unable to set real group because " << strerror(errno);
		throw PUGException(msg);
	}
	if(setGroups){
		if(setgroups(1,&group) < 0) {
			std::stringstream msg;
			msg <<  "Unable to the associated groups because " <<  strerror(errno);
			throw PUGException(msg);
		}
	}
}

void dropPrivileges(const uid_t uid, const gid_t gid){
	setGroup(gid);
	if(seteuid(uid) != 0 ){
		std::stringstream msg;
		msg <<  "Unable to change the effective user because " <<  strerror(errno);
		throw PUGException(msg);
	}
	if(setuid(uid) != 0 ){
		std::stringstream msg;
		msg <<  "Unable to change the real user because " <<  strerror(errno);
		throw PUGException(msg);
	}
}