Assignment 1 : Payback

CS233/333 - Networks and Distributed Systems
Fall, 1999.

Program Due : Friday, October 8, 11:59 p.m.

Introduction

Still bitter about having his password changed as a security demo in the Operating Systems class last spring, and knowing that Dave (a.k.a, "that bum") is leaving town to go give a "talk" in Hawaii next week, Dustin (a.k.a., fearless operating systems TA) has decided it would be a good idea for everyone to break into Dave's desktop machine while he is away. Unfortunately, Dave, suffering from paranoid delusions (the first rule of computer security), has removed all user accounts from his machine.

Therefore, your first assignment is to compromise the security of Dave's desktop machine while he is away sitting on the beach, looking at volcanic rocks, and giving his so-called "talk." To do this, you can pursue one (or both) of the following attack strategies:

The Ground Rules


Plan A: Break the root password

Password Basics

If you are going to try and break the password, you will need to write a program that cycles through various combinations of passwords and tries to find a password match. On Unix, user passwords are encrypted using a modified 56-bit DES encryption algorithm. Whenever a user enters their password, it is encrypted using this algorithm and compared against the encrypted entry in the password database. This scheme generally works because the encryption algorithm is not easily invertible (i.e., there is no simple algorithm to turn an encrypted password back into its unencrypted form) and the plaintext password is not stored anywhere on the system.

Of course, this has not stopped people from cracking the password database. One common approach is to use dictionary-based attacks in which every word (and various combinations of words) in a dictionary are run through the encryption algorithm and compared to the entries in the password database. Such a scheme works fairly well if people have chosen common words as their password. Alternatively, you could perform an exhaustive search of all 2^56 (7.2e16) keys--a time consuming task to be sure, but entirely possible given enough computing power and patience.

Only the first 8 characters of a Unix password are significant. To create an encrypted password, the lower 7-bits of each character are combined to create a 56-bit key. In addition, a special two character sequence known as a "salt value" is used to initialize the state of the encryption algorithm (the purpose of which is to discourage the use of hardware DES implementations as a password-cracking tool). To encrypt a password, the char *crypt(char *key, char *salt) function is used by supplying the plain-text password key and a salt value. crypt() returns a pointer to a string containing a printable version of the encrypted key. For example:

#include <crypt.h>
#include <stdio.h>
/* Print out the encrypted version of a password */
int main(int argc, char *argv[]) {
    char *key, *salt, *encrypted;

    key = argv[1];        /* Get key value */
    salt = argv[2];       /* Get salt      */

    encrypted = crypt(key,salt);
    printf("encrypted : %s\n", encrypted);
}
To compile the above program, you will probably need to do something like this:
unix % cc encrypt.c -lcrypt -o encrypt
Finally, let's try it out:
unix % encrypt kevin xy
encrypted : xynJzgmAhsBOo 
unix %
The only thing to notice about the output is that the salt value always appears as the first two characters of the encrypted password. In your cracking program, you will need to strip off the first two characters of the encrypted password and use them as the salt value given to crypt().

Checking to see if a password is valid is pretty similar. Here is a program that checks to see if a plaintext password matches the encrypted version.

#include <crypt.h>
#include <stdio.h>
#include <string.h>

/* Check if a password is correct */
int main(int argc, char *argv[]) {
    char *password, *encrypted, salt[3];

    password = argv[1];     /* Get the plaintext password */
    encrypted = argv[2];    /* Get the encrypted version */
    salt[0] = encrypted[0]; /* Pull off the salt value   */
    salt[1] = encrypted[1];
    salt[2] = 0;

    if (strcmp(crypt(password,salt),encrypted) == 0) {
        printf("%s is the password!\n", password);
    } else {
        printf("%s is not the password. Get lost.\n", password);
    }
}
This works about the same way:
unix % cc pwcheck.c -lcrypt -o pwcheck
unix % pwcheck kevin xynJzgmAhsBOo 
kevin is the password!
unix % pwcheck dave xynJzgmAhsBOo 
dave is not the password. Get lost.
unix %

You should compile the above programs and mess around with them just to get a feel for how they work. You might try them out with your own password just to see if you can reproduce the encrypted version of your password for instance (although some newer systems are using MD5 to encode passwords).

The password

The encrypted version of the root password (7 characters) is: ab2PW2MKs3fNY

How to proceed

It is pretty unlikely that you will be able to break the password on a single machine. Therefore, your cracking application is going to consist of two distinct programs--a server and a client. The server is going to manage the entire cracking process and is responsible for subdividing the key search space, giving work to clients, and receiving results. The clients on the other hand do nothing more than request a subset of the password search space and try to break the password using crypt().

Here's a rough plan on how to tackle this problem:

Step 1: Write a cracking program

First, write a program that cracks simple Unix passwords on a single CPU using a brute force attack. Your program might work like this:
% crack encrypted_password size start end
Where encrypted_password is the encrypted password, size is the number of letters in the unencrypted password, start is the starting password guess, and end is the ending guess. For example:
% crack abO2KZUsQYIAA 2 "00" "zz"
The password is "g4"
%
Note: This is not the only way to do this and having the above interface is not a strict requirement. Also, remember that the "salt" value is the first two characters of the encrypted password.

Step 2: Crack some simple passwords

Test the program created in Step 1 against the following passwords: Your program should get all of the above passwords correctly. The last case (a 4 letter password), should be possible on a single CPU, but may take several hours of processing time to find. If you don't get the above passwords, there is little point in proceeding further.

Step 3: Learn about sockets

Read the "Unix Network Programming" book for information about elementary socket programming. Socket programming is pretty ugly and I don't expect you to understand all of it yet. Take a look at the two programs attached at the end of this handout for some simple examples. Your programs shouldn't be much more complicated than what is given in the examples.

Step 4: Write a cracking server

Modify the cracking program written in Step 1 to function as a cracking server. That is, it should subdivide the password search space into small chunks of work that are distributed to clients. It should also be able to receive responses from the clients indicating a successful or unsuccessful search. If a client finds the password, it should send it back to the server. The server itself should do no cracking work of its own--instead, it should only parcel out work to clients (which could number in the tens to thousands depending on how many people you get to help you out).

Step 5: Write a cracking client

Modify the program you wrote in step 1 to be a client program. This should be relatively easy. All you need to do is modify it to connect to the server and read its parameters from the server instead. command line from the from server instead.

Step 6: Run.

Test your distributed program on some simple passwords as shown above. When you are convinced it's working, move on to the real password (see below).

What to hand in

Hints


Plan B : Compromise the Operating System

If you choose this option, there are very few instructions: If you actually succeed in getting in as root, use the 'passwd' command to change the root password, close the security hole by which you entered, log out, and prepare your writeup. Note: since other groups may be trying to attack, you almost certainly want to block them out if you succeed in breaking in first. Think of this as "king of the hill" if you will.

What to hand in

If you are going to attack the machine, you must hand in the following:

Socket example code

The following programs illustrate the use of sockets. This will be needed by groups pursuing Plan A. It may also be used to Plan B people as well. These programs should work on Linux and Solaris machines. Some modifications might be necessary for other platforms.

A server

/* -----------------------------------------------------------------------------
 * server.c
 *
 * An echo server.   Reads a line of input from a client and echos it back to them.
 * ----------------------------------------------------------------------------- */

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
#include <fcntl.h>
#include <string.h>

int readline(int sd, char *buffer, int maxlen)
{
  char *c = buffer;
  int  nread = 0;

  while (nread < maxlen) {
    if (read(sd,c,1) < 0) return 0;
    nread++;
    if (*(c++) == '\n') break;
  }
  *c = 0;
  return nread;
}

void echo(int sockfd) {
  int n;
  char  line[256];
  while(1) {
    /* Read a line of data */
    n = readline(sockfd,line,256);
    if (n == 0) break;

    /* Write it back to the client */
    write(sockfd,line,n);
  }
}

/* Open up the socket and listen for connections */

int
main(int argc, char **argv) 
{
  int sd;                            /* Socket descriptor */
  int port;                          /* TCP port number */
  struct     sockaddr_in servaddr;   /* Server address  */

  if (argc != 2) {
    printf("Usage : server port\n");
    exit(1);
  }
  port = atoi(argv[1]);

  /* Open up a socket */
  sd = socket(AF_INET, SOCK_STREAM, 0);
  if (sd < 0) {
    printf("Unable to open socket!\n");
    exit(1);
  }

  /* Bind the socket to the specified port number */
  memset(&servaddr, 0, sizeof(servaddr));   /* Clear the server address */
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(port);          /* Set the port number */
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  
  if (bind(sd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
    printf("unable to bind to port %d\n", port);
    exit(1);
  }

  /* Allow no more than 5 pending connections */
  listen(sd,5);

  /* Start listening for connections */
  printf("Server listening on port %d\n", port);
  for (;;) {
    struct  sockaddr_in clientaddr;    /* Client address  */
    int len = sizeof(clientaddr);
    int clientfd;                         /* Client descriptor */
    char  message[256];                  /* Message buffer    */

    /* Accept a new connection */
    clientfd = accept(sd, (struct sockaddr *) &clientaddr, &len);
    if (clientfd < 0) {
      printf("Accept error!\n");
      exit(1);
    }

    /* Print a message saying where the connection came from */

    printf("Received a connection from %s, port %d\n",
		 inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

    /* echo */
    echo(clientfd);
    
    /* Close the connection */
    close(clientfd);
    printf("Client closed connection\n");
  }
  /* Never reached */
}

A client

/* -----------------------------------------------------------------------------
 * client.c
 *
 * Echo client.  Reads a line of input from the user, sends it to the
 * server.  The server sends it back to us.
 * ----------------------------------------------------------------------------- */

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
#include <fcntl.h>
#include <string.h>

int readline(int sd, char *buffer, int maxlen)
{
  char *c = buffer;
  int  nread = 0;

  while (nread < maxlen) {
    if (read(sd,c,1) < 0) return 0;
    nread++;
    if (*(c++) == '\n') break;
  }
  *c = 0;
  return nread;
}

int main(int argc, char **argv) {
  int      sd;
  struct   sockaddr_in servaddr;
  struct   hostent *host;
  char     line[256];

  if (argc != 3) {
    printf("usage : client hostname port\n");
    exit(1);
  }

  /* Create a socket */
  sd = socket(AF_INET, SOCK_STREAM, 0);

  /* Set the address */
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(atoi(argv[2]));
  if ((host = gethostbyname(argv[1])) == (NULL)) {
    printf("Unknown host : %s\n", argv[1]);
    exit(1);
  }
  memcpy(&servaddr.sin_addr,host->h_addr,host->h_length);

  /* Try to connect to the server */
  if (connect(sd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
    printf("Unable to connect with the server.\n");
    exit(1);
  }

  printf("Connected to %s\n", argv[1]);

  while (1) {
    int n = readline(0,line,256);     /* Read a line of input from user */
    if (n == 0) break;
    write(sd,line,n);                 /* Send the line to the server */
    n = readline(sd,line,256);        /* Read it back from the server */
    if (n == 0) break;
    printf("The server said: %s\n", line);
  }
  /* Close the socket */
  close(sd);
  exit(0);
}