need advice on executing commands as root

I am working on a C program for FreeBSD and I need some advice on how to execute commands as root.

This program will need to call some other ports, portmaster in particular, and for that I need root privileges. Also some other commands like cp are going to be called from within this program via popen but I need them to be executed as "normal" user.

So far I have found 3 solutions:

1)

Check the $USER environment variable and if it's not root inform the user that he needs to run this program as root. Once the user has logged in as root call portmaster normally via popen. Then use this to execute commands that need to be executed as normal user:
Code:
su -m normal_user -c 'cp /path/source/t.txt /path/dest/t.txt'

2)

Same as first option but use pamfor authentication. I have tried some example codes involving pamand had zero success so far.

3)

net-mgmt/wifimgr has to run some commands as root also so I took a look at source code to see how they did it. What they do is that they have a separate executable (wifimgrsu) which is, upon compilation, copied to /usr/local/libexec/. Make file puts the owner root, group wheel and sets 4511 permissions.

Main program first ask the user to input the root password and then it opens a pipe with wifimgrsu. Passes the obtained password so that wifimgrsu can perform a check:

Code:
su_pass = strdup(getpwuid(0)->pw_passwd);
	if (strcmp(crypt(pass, su_pass), su_pass) != 0)
		printf("FAIL %s\n", gettext("invalid password"));
	else {
		auth = 1;
		printf("OK\n");
	}

Once it's "authenticated", wifimgrsu parses the input stream and executes commands via:

Code:
system(command);


This approach is very appealing to me and makes sense BUT is this the "right" way to do it? I'm a bit skeptical since it's more like a "roll my own authentication method" approach.


Thanks for any advice!
 
taz said:
Check the $USER environment variable and if it's not root inform the user that he needs to run this program as root. Once the user has logged in as root call portmaster normally via popen. Then use this to execute commands that need to be executed as normal user:
Code:
su -m normal_user -c 'cp /path/source/t.txt /path/dest/t.txt'

No need to access shell environment.. there is getuid(), return value of 0 would mean root otherwise not root.

To execute commands as other user you will need to fork another process and call setuid, seteuid, setgid, setegid (setuid(2)) syscalls that will change equivalent values for the process owner. But then you might also need to execute user's shell .rc scripts to setup his normal shell environment. Easy solution would be to just call login(1) to do all of this for you.

taz said:
Same as first option but use pamfor authentication. I have tried some example codes involving pamand had zero success so far.

To use PAM you need root privileges to start with, its mostly used with suid bit set on the binary.

taz said:
net-mgmt/wifimgr has to run some commands as root also so I took a look at source code to see how they did it. What they do is that they have a separate executable (wifimgrsu) which is, upon compilation, copied to /usr/local/libexec/. Make file puts the owner root, group wheel and sets 4511 permissions.

Main program first ask the user to input the root password and then it opens a pipe with wifimgrsu. Passes the obtained password so that wifimgrsu can perform a check:

Code:
su_pass = strdup(getpwuid(0)->pw_passwd);
	if (strcmp(crypt(pass, su_pass), su_pass) != 0)
		printf("FAIL %s\n", gettext("invalid password"));
	else {
		auth = 1;
		printf("OK\n");
	}

Once it's "authenticated", wifimgrsu parses the input stream and executes commands via:

Code:
system(command);


This approach is very appealing to me and makes sense BUT is this the "right" way to do it? I'm a bit skeptical since it's more like a "roll my own authentication method" approach.

Its good idea to separate the suid part from rest of your code, specially the GUI code that is rarely implemented in secure manner. But I'd recommend using PAM library to handle password checking as it does all the hard work for you and you dont need to re-implement it from scratch and then maintain it (don't need to worry about custom shadow files and custom hashing algorithms).
 
Thank you for your help. This is very informative.

Its good idea to separate the suid part from rest of your code, specially the GUI code that is rarely implemented in secure manner. But I'd recommend using PAM library to handle password checking as it does all the hard work for you and you dont need to re-implement it from scratch and then maintain it (don't need to worry about custom shadow files and custom hashing algorithms).

Agreed on using PAM for password checking and I think I will go along this option. Could I ask you for a code snippet that checks a password with PAM? Or maybe you could point me to some source code I can examine.

Also one more question. Why did they install wifimgrsu to /usr/local/libexec/.
 
taz said:
Agreed on using PAM for password checking and I think I will go along this option. Could I ask you for a code snippet that checks a password with PAM? Or maybe you could point me to some source code I can examine.

Its included in official FreeBSD docs: http://www.freebsd.org/doc/en/articles/pam/pam-sample-appl.html

taz said:
Also one more question. Why did they install wifimgrsu to /usr/local/libexec/.

From http://www.gnu.org/prep/standards/html_node/Directory-Variables.html :

libexecdir
The directory for installing executable programs to be run by other programs rather than by users. This directory should normally be /usr/local/libexec, but write it as $(exec_prefix)/libexec. (If you are using Autoconf, write it as ‘@libexecdir@’.)

The definition of ‘libexecdir’ is the same for all packages, so you should install your data in a subdirectory thereof. Most packages install their data under $(libexecdir)/package-name/, possibly within additional subdirectories thereof, such as $(libexecdir)/package-name/machine/version.
 
taz said:
I have looked at that example last night but did not quite figure it out just by look at the code.

Anyway thank you for your help, information you provided was basically all I was looking for.
I'll play with PAM and if I'm not going to be able to "hack" it I'll post here for further guidance.

It shows full implementation of 'su' command using PAM. PAM also covers user environment setup not just authorization so the example is quite complex, PAM also supports all kind of authorization methods (over network, etc) so it needs quite a bit of information to setup a session.

I know that PAM API is confusing so here is minimal code for password checking:

Code:
/*minimal authorization message handler*/
int pamconv(
        int num_msg, 
        const struct pam_message **msg, 
        struct pam_response **resp, 
        void *appdata_ptr
        ){

        struct pam_response *aresp;
        int i;
        
        if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG)
                return PAM_CONV_ERR;
        if ((aresp = calloc(num_msg, sizeof *aresp)) == NULL)
                return PAM_BUF_ERR;
        
        for(i = 0; i < num_msg; ++i){
                aresp[i].resp_retcode = 0;
                aresp[i].resp = NULL;
                
                /*handle PAM messages*/
                switch(msg[i] -> msg_style){
                        case PAM_PROMPT_ECHO_OFF:
                                /*HERE YOU ASSIGN MEMORY WITH STRING FOR PASSWORD*/
                                aresp[i].resp = strdup((char *)appdata_ptr);
                        break;
                        case PAM_PROMPT_ECHO_ON: /*ignore*/
                        break;
                        case PAM_TEXT_INFO:      /*ignore*/
                        break;
                        case PAM_ERROR_MSG:      /*ignore*/
                        break;

                        default:
                                /*clean up response object*/
                                for(i = 0; i < num_msg; ++i){
                                        if(aresp[i].resp != NULL){
                                                memset(aresp[i].resp, 0, strlen(aresp[i].resp));
                                                free(aresp[i].resp);
                                        }
                                }
                                
                                memset(aresp, 0, num_msg * sizeof *aresp);
                                free(aresp);
                                *resp = NULL;
                                
                                return PAM_CONV_ERR;
                        break;
                }
        }
        *resp = aresp;
        
        return PAM_SUCCESS;
}


/*actual authorization here*/
pam_handle_t *pamh = NULL;
static struct pam_conv conv = {
        &pamconv,
        "APASSWORD"
};

pam_start("myservice", "root", &conv, &pamh);
pam_set_item(pamh, PAM_RHOST, hostname);
pam_set_item(pamh, PAM_RUSER, "root");
pam_set_item(pamh, PAM_TTY, ttyname(STDERR_FILENO));

rval = pam_authenticate(pamh, 0);
if(rval != PAM_SUCCESS){
  /*login failed*/
}

pam_end(pamh, rval);

I glued it together from my head so it might not compile, but should be a good reference.
 
  • Thanks
Reactions: taz
Thank you very much! Your code is way more understandable and a better starting point.

Doesn't matter if it dose not compile, just by looking at I got a better picture of what I need to do.
 
I got it to work so I'm posting a usable code in case someone needs it in the future:

compile:
$ clang -lpam test.c -o test

set owner and premissions:

# chown root test && chmod 4511 test

code:

Code:
#include <sys/param.h>

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

#include <security/pam_appl.h>

/*
 * 
 * pam conversation function
 *  
 */
int 
pamconv(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr)
{

        struct pam_response *aresp;
        int i;
        
      
        if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
                return PAM_CONV_ERR;
        }
        if ((aresp = calloc(num_msg, sizeof *aresp)) == NULL) {
                 return PAM_BUF_ERR;
        }
        
        for(i = 0; i < num_msg; ++i) {
                aresp[i].resp_retcode = 0;
                aresp[i].resp = NULL;
                
                /*handle PAM messages*/
                switch(msg[i] -> msg_style) {
                        case PAM_PROMPT_ECHO_OFF:                                              
                                aresp[i].resp = strdup((char *)appdata_ptr);                                
                        break;
                        case PAM_PROMPT_ECHO_ON: /*ignore*/
                        break;
                        case PAM_TEXT_INFO:      /*ignore*/
                        break;
                        case PAM_ERROR_MSG:      /*ignore*/
                        break;

                        default:
                                /*clean up response object*/
                                for(i = 0; i < num_msg; ++i) {
                                        if(aresp[i].resp != NULL) {
                                                memset(aresp[i].resp, 0, strlen(aresp[i].resp));
                                                free(aresp[i].resp);
                                        }
                                }
                                
                                memset(aresp, 0, num_msg * sizeof *aresp);
                                free(aresp);
                                *resp = NULL;
                                
                                return PAM_CONV_ERR;
                        break;
                }
        }
        
        *resp = aresp; 
               
        return (PAM_SUCCESS);
}

/*
 * 
 * main
 * 
 */
int
main (int argc, char *argv[])
{        
        
        char hostname[MAXHOSTNAMELEN];
        pam_handle_t *pamh = NULL;
        static struct pam_conv pamc;
        int rval;

        
        pamc.conv = &pamconv;
        pamc.appdata_ptr = strdup("passforroot"); /* pass the password to PAM conversation function */
        gethostname(hostname, sizeof(hostname));
        
        pam_start("myservice", "root", &pamc, &pamh);
        pam_set_item(pamh, PAM_RHOST, hostname);
        pam_set_item(pamh, PAM_RUSER, "root");
        pam_set_item(pamh, PAM_TTY, ttyname(STDERR_FILENO));
        
        rval = pam_authenticate(pamh, 0);
        
        if(rval != PAM_SUCCESS) {
                printf("Password NOT OK for root user...\n");
        }
        else {
                printf("Password OK for root user...\n");
        }

        pam_end(pamh, rval);

        return 0;
}

test it:

$ ./test
 
It seems ther is also a way to pass the password to PAM without the conversation function which simplifies the code.

This can be used to put the password into the pam stack

Code:
pam_set_item(pamh, PAM_AUTHTOK, "passforroot");


Code:
#include <sys/param.h>

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

#include <security/pam_appl.h>

/*
 * main
 */
int
main (int argc, char *argv[])
{        
        
        char hostname[MAXHOSTNAMELEN];
        pam_handle_t *pamh = NULL;
        int rval;

        gethostname(hostname, sizeof(hostname));
        
        pam_start("test", "root", NULL, &pamh);
        pam_set_item(pamh, PAM_RHOST, hostname);
        pam_set_item(pamh, PAM_RUSER, "root");
        pam_set_item(pamh, PAM_TTY, ttyname(STDERR_FILENO));
        pam_set_item(pamh, PAM_AUTHTOK, "passforroot");
        
        rval = pam_authenticate(pamh, 0);
        
        if(rval == PAM_SUCCESS) {
                printf("Password OK for root user...\n");
        }
        else {
                printf("Password NOT OK for root user...\n");
        }

        pam_end(pamh, rval);

        return 0;
}
 
Back
Top