Logo  

CS473 - Computer Networks

The Pseudo-terminal interface:

  • 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.

    1. Open the pty (pseudo-terminal) master device using "posix_openpt(3)".
    2. Call grantpt(3) to get a "slave" device.
    3. Call unlockpt(3) to unlock the slave device.
    4. Use ptsname(3) to learn the slave device name (ex: "/dev/pts/2")
    5. Fork a new process:
      • In the child:
        a) open the slave TTY using open()
        b) Make the opened slave the stdin/out/err of the new process.
        c) Apply the saved TTY disciplines/window size from step 0.
        d) exec the new program.
      • In the parent:
        a) Read and write on the master device descriptor to communicate with the child process, a`la socketpair().

The posix_openpt(3) function call:

  #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.

    flags:
    O_RDWR Open device for reading and writing
    O_NOCTTY Don't make the new pty the controlling terminal for this process.

    ex: int pty = posix_openpt(O_RDWR | O_NOCTTY);

The grantpt(3) function call:

  #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);

The unlockpt(3) function call:

  #define _XOPEN_SOURCE 500
  #include <stdlib.h>

  int unlockpt(int fd);
  • "Unlocks" the slave side of the pseudo-terminal.

  • Returns 0 on success, -1 on failure.

  • 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);

The ptsname(3) function call:

  #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.

  • Returns a string or NULL on error.

    ex: char *slave_name = ptsname(pty);

The tcgetattr(3) and tcsetattr(3) function calls:

  #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);
  • Get or set terminal line disciplines.
  • optional_actions can be one of:
TCSANOW Change things immediately.
TCSADRAIN Change after everything has been written.
TCSAFLUSH Change after everything is written and anything not read is flushed.
  • Returns 0 on success, -1 on error. ex:

      struct termios save;
    
      tcgetattr(STDIN_FILENO, &save);
      ...
      tcsetattr(slave_pty, TCSANOW, &save);

Getting the "window" size of the terminal (width/height):

The ioctl(2) system call:

  #include <sys/ioctl.h>

  int ioctl(int fd, unsigned long request, ...);
  • Makes some request w/ respect to the given file descriptor. There are many ioctls. Returns 0 on success, or -1 on error.
  • Additional parameters after the request may be required.
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);

pty.c

#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);
}

Functions for setting terminal echo() and noecho()

/**
 * 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);
}

Security issues for a poor-mans ssh client:

  1. Authentication - We don't want just anyone logging into our account.
  2. Encrypting the data transfers - If someone has access to view the network between hosts it can be possible for them to view passwords or other sensitive info "in flight".

Authentication:

  • We require a database of usernames and passwords, the latter preferably hashed using a cryptographically secure "hash".
    • A hash maps an input of arbitrary size to a bit string of fixed size. It should only work in one direction (a one-way function), such that the input cannot be discovered by trying to reverse the algorithm on the output.
    • A good hash should be resistant to collisions, i.e. some messages m1 and m2 such that hash(m1) == hash(m2).

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

In a makefile set the LD_FLAGS=-lcrypt

Terms:
key The password string to be encrypted.
salt Previously two additional characters from the set [a-zA-Z0-9./] used to "perturb" the hash even more. Makes a regular password more "random".

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 Algo:
1 MD5
5 SHA-256
6 SHA-512

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");

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.

Separating the salt from the hashed password:

  char *hash = crypt(...);
  char *lastds = strrchr(hash, '$');
  lastds = '\0';

  char *salt = $hash;
  char *hashed_pw = lastds+1;

Randomly generating a salt:

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.