home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Book Home Perl for System AdministrationSearch this book

Chapter 10. Security and Network Monitoring

Any discussion of security is fraught with peril. There are at least three snares that can doom a discussion on security:

  1. Security means different things to different people. If you walked into a conference of Greco-Roman scholars and asked about Rome, the first scholar would rise dramatically to her feet and begin to lecture about aqueducts (infrastructure and delivery), the second Pax Romana (ideology and policies), a third would expound on the Roman legions (enforcement), a fourth on the Roman Senate (administration), and so on. The need to deal with every facet of security at once is security's first trap.

  2. People think that something can be secure, be it a program, a computer, a network, etc. This chapter will never claim to show you how to make anything secure; it will try to help you make something more secure, or at least recognize when something is less secure. Security is a continuum.

  3. Finally, one of the most deadly traps in this business is specificity. It is true that the deity of security is often in the details, but it is an ever-shifting set of details. Patching security holes A, B, and C on your system only guarantees (and perhaps not even) that those particular holes will not be a source of trouble. It does nothing to help when hole D is found. That's why this chapter will focus on principles and tools for improving security. It will not tell you how to fix any particular buffer overflow, vulnerable registry key, or world-writable system file.

One good way to get into a discussion of these principles is to examine how security manifests itself in the physical world. In both the real and virtual worlds, it all comes down to fear. Will something I care about be damaged, lost, or revealed? Is there something I can do to prevent this from happening? Is it happening right now?

If we look at how this fear is faced in the physical world, we can learn ways to deal with it in our domain as well. One way we deal with this fear is to invent stronger ways of partitioning space. With physical space we use constructs like bank vaults; with intellectual space we use data-hiding methods like "top secret clearance" or encryption. But this is a never-ending pursuit. For every hour spent designing a security system, there is at least an hour spent looking for a way to evade it. In our case, there are also hordes of bored teenagers with computers and disgruntled former employees looking for something to do with excess energy.

A slightly better approach to improving security that has persisted over the ages is the use of a designated person to allay these fears. Once upon a time, there was nothing so comforting as the sound of the night watchman's footsteps as he or she walked through the town, jiggling doors handles to make sure everything was locked and secure. We'll use this quaint image as the jump point for our exploration into security and network monitoring with Perl.

10.1. Noticing Unexpected or Unauthorized Changes

A good watchman notices change. He or she knows when things are in the wrong place in your environment. If your precious Maltese falcon gets replaced with a forgery, the watchman is the first person that should notice. Similarly, if someone modifies or replaces key files on your system, you want sirens to blare and klaxons to sound. More often than not, the change will be harmless. But the first time someone breaches your security and mucks with /bin/login, msgina.dll, or Finder, you'll be so glad you noticed that you will excuse any prior false alarms.

10.1.1. Local Filesystem Changes

Filesystems are an excellent place to begin our exploration into change-checking programs. We're going to explore ways to check if important files like operating system binaries and security-related files (e.g., /etc/passwd or msgina.dll ) have changed. Changes to these files made without the knowledge of the administrator are often signs of an intruder. There are some relatively sophisticated cracker toolkits available on the Net that do a very good job of installing Trojan versions of important files and covering up their tracks. That's the most malevolent kind of change we can detect. On the other end of the spectrum, sometimes it is just nice to know when important files have been changed (especially in environments where multiple people administer the same systems). The techniques we're about to explore will work equally well in both cases.

The easiest way to tell if a file has changed is to use the Perl functions stat( ) and lstat( ). These functions take a filename or a filehandle and return an array with information about that file. The only difference between the two functions manifests itself on operating systems like Unix that support symbolic links. In these cases lstat( ) is used to return information about the target of a symbolic link instead of the link itself. On all other operating systems the information returned by lstat( ) should be the same as that returned by stat( ).

Using stat( ) or lstat( ) is easy:

@information = stat("filename");

As demonstrated in Chapter 3, "User Accounts", we can also use Tom Christiansen's File::Stat module to provide this information using an object-oriented syntax.

The information returned by stat( ) or lstat( ) is operating-system-dependent. stat( ) and lstat( ) began as Unix system calls, so the Perl documentation for these calls is skewed towards the return values for Unix systems. Table 10-1 shows how these values compare to those returned by stat( ) on Windows NT/2000 and MacOS. The first two columns show the Unix field number and description.

Table 10.1. stat() Return Value Comparison

#

Unix Field Description

Valid for NT/2000

Valid for MacOS

0

Device number of filesystem

Yes (drive #)

Yes (but is vRefNum)

1

Inode number

No (always 0)

Yes (but is fileID/dirID)

2

File mode (type and permissions)

Yes

Yes (but is 777 for dirs and applications, 666 for unlocked documents, 444 for locked documents)

3

Number of (hard) links to the file

Yes (for NTFS)

No (always 1)

4

Numeric user ID of file's owner

No (always 0)

No (always 0)

5

Numeric group ID of file's owner

No (always 0)

No (always 0)

6

The device identifier (special files only)

Yes (drive #)

No (always null)

7

Total size of file, in bytes

Yes (but does not include the size of any alternate data streams)

Yes (but returns size of data fork only)

8

Last access time since the epoch

Yes

Yes (but epoch is 66 years earlier than Unix, at 1/1/1904, and value is same as field #9)[1]

9

Last modify time since the epoch

Yes

Yes (but epoch is 1/1/1904 and value is same as field #8)

10

Inode change time since the epoch

Yes (but is file creation time)

Yes (but epoch is 1/1/1904 and is file creation time)

11

Preferred block size for filesystem I/O

No (always null)

Yes

12

Actual number of blocks allocated

No (always null)

Yes

[1]Also, MacOS epoch is counted from local time, not Universal Time Coordinated (UTC). So if the clocks in two MacOS computers are synchronized, but one has a time zone setting (TZ) of -0800 and the other has a TZ of -0500, the values for time( )on these computers will be three hours apart.

In addition to stat( ) and lstat( ), other non-Unix versions of Perl have special functions to return attributes of a file that are peculiar to that OS. See Chapter 2, "Filesystems", for discussions of functions like MacPerl::GetFileInfo( ) and Win32::FileSecurity::Get( ).

Once you have queried the stat( )ish values for a file, the next step is to compare the "interesting" values against a known set of values for that file. If the values changed, something about the file must have changed. Here's a program that both generates a string of lstat( ) values and checks files against a known set of those values. We intentionally exclude field #8 from the above table (last access time) because it changes every time a file is read.

This program takes either a -p filename argument to print lstat( ) values for a given file or a -c filename argument to check the lstat( ) values all of the files listed in filename.

use Getopt::Std;

# we use this for prettier output later in &printchanged(  )
@statnames = qw(dev ino mode nlink uid gid rdev 
                size mtime ctime blksize blocks);

getopt('p:c:');

die "Usage: $0 [-p <filename>|-c <filename>]\n"
  unless ($opt_p or $opt_c);

if ($opt_p){
    die "Unable to stat file $opt_p:$!\n"
      unless (-e $opt_p);
    print $opt_p,"|",join('|',(lstat($opt_p))[0..7,9..12]),"\n";
    exit;
}

if ($opt_c){
    open(CFILE,$opt_c) or
      die "Unable to open check file $opt_c:$!\n";
    while(<CFILE>){
        chomp;
        @savedstats = split('\|');
        die "Wrong number of fields in line beginning with
            $savedstats[0]\n"
          unless ($#savedstats == 12);
        @currentstats = (lstat($savedstats[0]))[0..7,9..12];
        
        # print the changed fields only if something has changed
        &printchanged(\@savedstats,\@currentstats)
          if ("@savedstats[1..13]" ne "@currentstats");
    }
    close(CFILE);
}

# iterates through attributes lists and prints any changes between
# the two
sub printchanged{
    my($saved,$current)= @_;
    
    # print the name of the file after popping it off of the array read
    # from the check file
    print shift @{$saved},":\n";

    for (my $i=0; $i < $#{$saved};$i++){
         if ($saved->[$i] ne $current->[$i]){
             print "\t".$statnames[$i]." is now ".$current->[$i];
             print " (should be ".$saved->[$i].")\n";
         }     
    }
}

To use this program, we might type checkfile -p /etc/passwd >> checksumfile. checksumfile should then contain a line that looks like this:

/etc/passwd|1792|11427|33060|1|0|0|24959|607|921016509|921016509|8192|2

We would then repeat this step for each file we want to monitor. Then, running the script with checkfile -c checksumfile will show any changes. For instance, if I remove a character from /etc/passwd, this script will complain like this:

/etc/passwd:
        size is now 606 (should be 607)
        mtime is now 921020731 (should be 921016509)
        ctime is now 921020731 (should be 921016509)

There's one quick Perl trick in this code to mention before we move on. The following line demonstrates a quick-and-dirty way of comparing two lists for equality (or lack thereof ):

if ("@savedstats[1..12]" ne "@currentstats");

The contents of the two lists are automatically "stringified" by Perl by concatenating the list elements with a space between them:

join(" ",@savedstats[1..12]))

and then the resulting strings are compared. For short lists where the order and number of the list elements is important, this technique works well. In most other cases, you'll need an iterative or hash solution like the ones documented in the Perl FAQs.

Now that you have file attributes under your belt, I've got bad news for you. Checking to see that a file's attributes have not changed is a good first step, but it doesn't go far enough. It is not difficult to alter a file while keeping attributes like the access and modification times the same. Perl even has a function, utime( ), for changing the access or modification times of a file. Time to pull out the power tools.

Detecting change in data is one of the fortes of a particular set of algorithms known as "message-digest algorithms." Here's how Ron Rivest describes a particular message-digest algorithm called the "RSA Data Security, Inc. MD5 Message-Digest Algorithm" in RFC1321:

The algorithm takes as input a message of arbitrary length and produces as output a 128-bit "fingerprint" or "message digest" of the input. It is conjectured that it is computationally infeasible to produce two messages having the same message digest, or to produce any message having a given prespecified target message digest.

For our purposes this means that if we run MD5 on a file, we'll get a unique fingerprint. If the data in this file were to change in any way, no matter how small, the fingerprint for that file will change. The easiest way to harness this magic from Perl is through the Digest module family and its Digest::MD5 module.

The Digest::MD5 module is easy to use. You create a Digest::MD5 object, add the data to it using the add( ) or addfile( ) methods, and then ask the module to create a digest (fingerprint) for you.

To compute the MD5 fingerprint for a password file on Unix, we could use something like this:

use Digest::MD5 qw(md5);

$md5 = new Digest::MD5;

open(PASSWD,"/etc/passwd") or die "Unable to open passwd:$!\n";
$md5->addfile(PASSWD);
close(PASSWD);

print $md5->hexdigest."\n";

The Digest::MD5 documentation demonstrates that we can string methods together to make the above program more compact:

use Digest::MD5 qw(md5);

open(PASSWD,"/etc/passwd") or die "Unable to open passwd:$!\n";
print Digest::MD5->new->addfile(PASSWD)->hexdigest,"\n";
close(PASSWD);

Both of these code snippets print out:

a6f905e6b45a65a7e03d0809448b501c

If we make even the slightest change to that file, the output changes. Here's the output after I transpose just two characters in the password file:

335679c4c97a3815230a4331a06df3e7

Any change in the data now becomes obvious. Let's extend our previous attribute-checking program to include MD5:

use Getopt::Std;
use Digest::MD5 qw(md5);

@statnames = 
 qw(dev ino mode nlink uid gid rdev size mtime ctime blksize blocks md5);

getopt('p:c:');

die "Usage: $0 [-p <filename>|-c <filename>]\n"
  unless ($opt_p or $opt_c);

if ($opt_p){
    die "Unable to stat file $opt_p:$!\n"
      unless (-e $opt_p);

    open(F,$opt_p) or die "Unable to open $opt_p:$!\n";
    $digest = Digest::MD5->new->addfile(F)->hexdigest;
    close(F);

    print $opt_p,"|",join('|',(lstat($opt_p))[0..7,9..12]),
          "|$digest","\n";
    exit;
}

if ($opt_c){
    open(CFILE,$opt_c) or 
      die "Unable to open check file $opt_c:$!\n";

    while (<CFILE>){
        chomp;
        @savedstats = split('\|');
        die "Wrong number of fields in \'$savedstats[0]\' line.\n"
          unless ($#savedstats == 13);

        @currentstats = (lstat($savedstats[0]))[0..7,9..12];

        open(F,$savedstats[0]) or die "Unable to open $opt_c:$!\n";
        push(@currentstats,Digest::MD5->new->addfile(F)->hexdigest);
        close(F);

        &printchanged(\@savedstats,\@currentstats)
          if ("@savedstats[1..13]" ne "@currentstats");
    }
    close(CFILE);
}

sub printchanged {
    my($saved,$current)= @_;
    
    print shift @{$saved},":\n";

    for (my $i=0; $i <= $#{$saved};$i++){
         if ($saved->[$i] ne $current->[$i]){
             print " ".$statnames[$i]." is now ".$current->[$i];
             print " (".$saved->[$i].")\n";
         }
     }
}

10.1.2. Network Service Changes

We've looked at ways to detect change on our local filesystem. How about noticing changes on other machines or in the services they provide? In Chapter 5, "TCP/IP Name Services", we saw ways to query NIS and DNS. It would be easy to check repeated queries to these services for changes. For instance, if our DNS servers are configured to allow this, we can pretend to be a secondary server and request a dump (i.e., a "zone transfer") of that server's data for a particular domain:

use Net::DNS;

# takes two command-line arguments: the first is the name server 
# to query, the # second is the domain to query from that name server
$server = new Net::DNS::Resolver;
$server->nameservers($ARGV[0]); 

print STDERR "Transfer in progress...";
@zone = $server->axfr($ARGV[1]);
die $server->errorstring unless (defined @zone);
print STDERR "done.\n";

for $record (@zone){
  $record->print;
}

Combine this idea with MD5. Instead of printing the zone information, let's take a digest of it:

use Net::DNS;
use FreezeThaw qw{freeze};
use Digest::MD5 qw(md5);

$server = new Net::DNS::Resolver;
$server->nameservers($ARGV[0]);

print STDERR "Transfer in progress...";
@zone = $server->axfr($ARGV[1]);
die $server->errorstring unless (defined @zone);
print STDERR "done.\n";

$zone = join('',sort map(freeze($_),@zone));

print "MD5 fingerprint for this zone transfer is: ";
print Digest::MD5->new->add($zone)->hexdigest,"\n";

MD5 works on a scalar chunk of data (a message), not a Perl list-of-hashes data structure like @zone. That's where this line of code comes into play:

$zone = join('',sort map(freeze($_),@zone));

We're using the FreezeThaw module we saw in Chapter 9, "Log Files", to flatten each @zone record data structure into a plain text string. Once flattened, the records are sorted before being concatenated into one large scalar value. The sort step allows us to ignore the order in which the records are returned in the zone transfer.

Dumping the contents of an entire server's zone file is a bit extreme, especially for large zones, so it may make more sense to monitor only an important subset of addresses. See Chapter 5, "TCP/IP Name Services" for an example of this. Also, it is a good idea to restrict the ability to do zone transfers to as few machines as possible for security reasons.

The material we've seen so far doesn't get you completely out of the woods. Here are a few questions you might want to ponder:

  • What if someone tampers with your database of MD5 digests and substitutes valid fingerprints for their Trojan file replacements or service changes?

  • What if someone tampers with your script so it only appears to check the digests against your database?

  • What if someone tampers with the MD5 module on your system?

  • For the ultimate in paranoia, what if someone manages to tamper with the Perl executable, one of its shared libraries, or the operating system core itself?

The usual answers to these questions (poor as they may be) involve keeping known good copies of everything related to the process (digest databases, modules, statically-linked Perl, etc.) on read-only medium.

This conundrum is another illustration of the continuum of security. It is always possible to find more to fear.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.