Some programs require a terminal in order to function (such as an interactive shell, the passwd program, etc.) We may want to "pipe" data to and from such programs, but we cannot use a simple pipe that replacing stdin/out/err for such programs. To control the input and output of such programs we much use a pseudo-terminal which acts very much like a bi-directional pipe but with a virtual TTY for the program(s) on the other end.
A following steps are required to open a pseudo terminal: 0) Get/save our preferred TTY line discipline and window size.
#define _XOPEN_SOURCE 600 #include <stdlib.h> #include <fcntl.h> int posix_openpt(int flags);
Opens an unused pseudo-terminal master device (/dev/ptmx), returning a valid file descriptor on success or -1 on failure.
ex: int pty = posix_openpt(O_RDWR | O_NOCTTY);
int pty = posix_openpt(O_RDWR | O_NOCTTY);
#define _XOPEN_SOURCE 500 #include <stdlib.h> int grantpt(int fd);
Changes the mode (0620) and owner of the slave device to that of the current processes uid.
Returns 0 on success, -1 on failure.
ex: grantpt(pty);
grantpt(pty);
#define _XOPEN_SOURCE 500 #include <stdlib.h> int unlockpt(int fd);
"Unlocks" the slave side of the pseudo-terminal.
I'm not sure why this function exists, seems to make it possible to open the slave device for writing to in the child process.
ex: unlock(pty);
unlock(pty);
#define _XOPEN_SOURCE 500 #include <stdlib.h> char *ptsname(int fd);
Returns the name (such as "/dev/pts/2") of the slave pty device that corresponds to the master pty. Use the returned name to open the pty in the child and attach it to stdin/out/err.
/dev/pts/2
Returns a string or NULL on error.
ex: char *slave_name = ptsname(pty);
char *slave_name = ptsname(pty);
#include <termios.h> #include <unistd.h> int tcgetattr(int fd, struct termios *termios_p); int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
optional_actions
Returns 0 on success, -1 on error. ex:
struct termios save; tcgetattr(STDIN_FILENO, &save); ... tcsetattr(slave_pty, TCSANOW, &save);
#include <sys/ioctl.h> int ioctl(int fd, unsigned long request, ...);
Requests: TIOCGWINSZ Get the window size (stored in a struct winsize pointer) TIOCSWINSZ Set the window size
struct winsize { unsigned short ws_row; unsigned short ws_col; unsigned short ws_xpixel; /* unused */ unsigned short ws_ypixel; /* unused */ };
A SIGWINCH signal will be sent to the process when the window size changes.
ex:
struct winsize ws; ioctl(STDIN_FILENO, TIOCGWINSZ, &ws); ... ioctl(slave_pty, TIOCSWINSZ, &ws);
#define _XOPEN_SOURCE 600 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <termios.h> #include <sys/ioctl.h> #include <errno.h> #define K 1024 // man pty int main(int argc, char *argv[]) { struct termios tio; struct winsize ws; pid_t pid; int pty, tty, r; char *pts, buf[K]; // Add a signal handler to handle things like SIGCHLD, maybe SIGWINCH // Get line attributes from our TTY: if (tcgetattr(0, &tio) < 0) { perror("tcgetattr"); exit(1); } // Get the window size from our TTY: if (ioctl(0, TIOCGWINSZ, &ws) < 0) { fprintf(stderr,"Getting winsize\n"); exit(1); } // Open master end of the PTY: if ((pty = posix_openpt(O_RDWR | O_NOCTTY)) < 0) { perror("openpt"); exit(1); } // The remaining can probably be done in the child process. // Allocate a slave terminal to us: if (grantpt(pty) < 0) { perror("grantpt"); exit(1); } // Unlock and set permissions on slave terminal side: if (unlockpt(pty) < 0) { perror("unlockpt"); exit(1); } // Find out what the device name of the slave side pseudo terminal is: if ((pts = ptsname(pty)) == NULL) { perror("ptsname"); exit(1); } pid = fork(); if (pid < 0) { perror("fork"); exit(1); } if (pid == 0) { // Open slave side PTY: if ((tty = open(pts, O_RDWR)) < 0) { perror("slave open"); exit(1); } dup2(tty, 0); dup2(tty, 1); dup2(tty, 2); close(tty); // Set line attributes (immediately): if (tcsetattr(0, TCSANOW, &tio) < 0) { perror("tcsetattr"); } // Set the window size: if (ioctl(0, TIOCSWINSZ, &ws) < 0) { fprintf(stderr,"winsize\n"); } execlp("arogue", "arogue", NULL); perror("execlp"); exit(1); } // In the parent process: // Add support for async I/O on keyboard and from the master pty: while((r = read(pty, buf, K)) > 0) { write(STDOUT_FILENO, buf, r); } exit(0); }
/** * Termios are documented in man 3 tcgetattr */ void noecho() { struct termios t; tcgetattr(STDIN_FILENO,&t); // Turn off: // ICANNON - canonical mode (line buffered/processed input) // ECHO - echoing characters as we type them // ISIG - Send signals when special keys are typed (Ctrl-C, Ctrl-Z, etc) // IEXTEN - Enable line editing t.c_lflag &= ~(ICANON|ECHO|ISIG|IEXTEN); // Set MIN minimum number of characters to block read on and TIME maximum // amount of time to wait for input. t.c_cc[VMIN] = 1; t.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO,TCSANOW,&t); } void echo() { struct termios t; tcgetattr(0,&t); t.c_lflag |= (ICANON|ECHO|ISIG|IEXTEN); tcsetattr(0,TCSANOW,&t); }
To generate reasonable hashes for passwords we can use the crypt(3) function:
#define _XOPEN_SOURCE #include <unistd.h> char *crypt(const char *key, const char *salt);
Link with -lcrypt. i.e. gcc -o prog prog.c -lcrypt
-lcrypt
gcc -o prog prog.c -lcrypt
In a makefile set the LD_FLAGS=-lcrypt
LD_FLAGS=-lcrypt
[a-zA-Z0-9./]
By default crypt attempts to hash the password using the DES algorithm which should be considered deprecated. Glibc supports an extended salt allowing one to select different hashing algorithms:
If the salt is in the form of $<id>$<salt>, the id selects one of the following different hashing algorithms:
$<id>$<salt>
The salt may then be up to 16 characters in length. An optional $rounds=<#> may be inserted between the id and salt to further perturb the hash by feeding the output back into the hash a number of times specified by rounds. This makes hashing a password more computationally expensive.
ex: crypt("password", "$5$rounds=4000$somesalt");
crypt("password", "$5$rounds=4000$somesalt");
The output will be:
$5$rounds=4000$somesalt$WkkHpTEp2ICiscJhlWgogGU0E9s2Yjwt.WvjTdRntu/ ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The salt string The encrypted password
The salt should be a random string of (16) characters that is different for each password. This prevents the use of "rainbow tables", tables of pre-generated hashed passwords that greatly speed up searching for a hash collision.
char *hash = crypt(...); char *lastds = strrchr(hash, '$'); lastds = '\0'; char *salt = $hash; char *hashed_pw = lastds+1;
char *salt_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNIOPQRSTUVWXYZ0123456789./"; srand(time(0)); char salt[K], salt_str[17]; for(int i = 0; i < 16; i++) salt_str[i] = salt_chars[rand()%strlen(salt_chars)]; salt_str[16] = '\0'; sprintf(salt, "$6$rounds=5000$%s", salt_str);
It should now be a simple matter to store a set of username/hashed-passwords in a file only accessible to us to use for username/password verification.
To authenticate, the username and password is retrieved from the user and the password is hashed in using the same salt and then the output of crypt() is compared with the stored hash, if they match, they are authenticated.