Integrity checkers that support MD5 seems to be preferable as
MD5 checksums are often available from vendors. Perl implementations are
generally more flexible that written in C.
afick is current favorite and it got some positive press:
08/01/2000
by David N. Blank-Edelman, author of
Perl for System
Administration
It is not uncommon for system administrators to have to drop whatever
they are working on to deal with the security problem du jour. Some of these
problems involve serious breaches of security. In these cases, the first
question asked is often, "What has the intruder done?" In my recently
released O'Reilly book,
Perl for System Administration, I begin the chapter on security and
network monitoring with a discussion of some of the available Perl tools
that can help answer this question. Here's an excerpt from that chapter,
which deals with finding changes made to a local filesystem:
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 tool-kits 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() orlstat()
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. See Table 10-1 in
Perl for System
Administration for a comparison between the values returned by
stat() under UNIX and those returned by
stat() on Windows NT/2000 and MacOS.
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 last access time from
the comparison 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, as if we typed:
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 those
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 pre-specified 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";
}
}
}
With new threats showing up every day,
administrators find it increasingly hard to establish continued trust with
their filesystems. Luckily, it's easier than you might think to maintain
omniscient control of your filesystem. Through effective use of a filesystem
integrity checker, you can keep a watchful eye on every aspect of an
important machine's filesystem.
There are several filesystem integrity
checker applications, both commercial and open source. I chose to deploy
afick, because it is written in
Perl, which makes it lightweight and easily portable between different
operating systems. Though by nature designed for the command line, afick
also has an optional Webmin module and
a graphical interface written in perl-Tk.
For this article we will focus on the
command-line implementation on a SUSE Enterprise 8.0 server, but what you
see here should be applicable to just about any *nix distribution.
Installation
You can either download and build afick from
the source code, or, if you're using a package-based distribution, you can
install from an RPM or Debian binary packages. Let's walk through building
it from source. To begin, download and unpack the latest version, navigate
to the afick directory, and run the following command:
# perl Makefile.pl
Since you aren't installing the GUI, you can
safely ignore any errors about missing perl-Tk modules.
Next, type
make install to install
the console tool. When your machine finishes copying all the necessary
files, afick will be installed on your system. Installation is only half the
battle, though. The real fun lies in the configuration and testing phase.
Configuration
Afick begins by creating a database of values
representing the current state of your filesystem. When you run it later in
update mode, afick compares the current filesystem to the original and notes
changes.
The exact attributes of your filesystem that
are checked are controlled by a configuration file. Afick provides a default
configuration file for both Linux and Windows in the directory where it was
originally unpacked. In our case, we are interested in the file linux.conf.
You can modify a wealth of options in this file, but we will focus only on
the essentials.
Afick provides multiple ways to check every
file and directory on your system, so no one configuration file is going to
work for everyone. For instance, I am running PHPNuke on a Web server that
includes forums, which are going to change constantly as users post items
and change their preferences. I don't want those changes dumped into my
mailbox every day, possibly burying something important. Someone else with a
static Web site, however, might want to see that that content is never
changed unexpectedly, and would therefore closely monitor that directory. It
takes a bit of trial and error to fine-tune afick (or any other integrity
checker for that matter) for your specific needs.
Let's begin by initializing the database with
the command:
# afick -c
/[path_to_linux.conf]/linux.conf -i
The -c tells afick where to find the configuration file it should use,
while the -i instructs it to make an initial copy of the filesystem database.
This process will take a few minutes. Once it has finished, let your server
run for a while, then compare the databases with this slightly different
command:
# afick -c
/[path_to_linux.conf]/linux.conf -k
The -k argument tells afick to compare the current filesystem against
the database specified in the first line of the linux.conf file, which is
the initial database. Any changes will be noted on stdout.
Repeat this process a few times until you're
getting a feel for what is changing and how so. For instance, if you've got
busy log files somewhere outside of /var, they might produce a bundle of
changes every time you run afick, which will create white noise around
potentially useful data. After you've got a list of repeat offenders, you
can tune the linux.conf file.
The linux.conf file actually has a decent
description of all the file attributes you can monitor, including device,
inode, permissions, owner, last time modified, and several others. You can
even create your own rule sets for certain types of files and directories.
For instance, you don't want afick reporting warnings about the individual
files in /var/log being modified, as these files are going to be modified
almost constantly on some systems. To create a rule set that would check the
user and group ownership, the device they reside on, and the permissions,
you would first add:
#alias
specialrule = u+g+d+p
Then, to apply this custom rule to the
/var/log directory, you would add the following line to the =/Dir section of
the conf file:
/var/log specialrule
If you want to define a rule set that ignores
the /tmp directory, checks only the files ending in .backup in /root, and
ignores all files in /home/user that end in .old, you would add the
following lines to the alias section of the config file.
!/tmp
/root/*.backup special_rule
exclude_re := /home/user/.*\.old
Afick recognizes the standard Unix wild cards
and regular expressions in rule sets. With a little bit of tweaking, you can
tune afick to completely monitor your filesystem in all the necessary
places, while ignoring the spots that would generate useless noise.
After you've spent some time tweaking your
configuration file you need to ensure that afick itself cannot be modified.
The most secure way to accomplish this is to put the database, found by
default at /var/lib/afick/afick.pag, somewhere that is write-only.
Unfortunately a diskette isn't an option because of the size of the afick
database; my database is roughly 15MB for a 4GB server. I recommend using an
Iomega Zip disk for a couple reasons. Primarily, you can switch a Zip disk
from read-only to read-write with the flip of a switch. This is convenient
because every time you make a change to your filesystem you'll need to
update the database to clear the warnings that afick will produce every time
it runs thereafter, which could lead to wasting a lot of CDs if you tried to
deploy the database on a CD-R.
No matter where you store your database, you
still need to tell the configuration file where to find it. Mount your Zip
drive (or your CD) and copy /var/lib/afick/afick.pag to your mount point.
Then change the entry for the database location in the first section of the
config file to represent the path to your removable media.
database=/mnt/zip/afick
In addition to storing the database on
read-only media, I also choose to err on the side of paranoia, and I keep my
config file on read-only media as well. In this case, it's a diskette that
I've write-protected. This doesn't prevent afick from being run with a
separate config file created locally on the server, but it does allow me to
be sure that no small detail within the file can be changed without someone
physically touching my servers.
Automating it
The final step in configuring afick is
automating it. (Note that this is not strictly necessary if you wish to run
afick only after certain tasks.) The easiest way to automate afick is with
the afick.cron script included in the original directory where you unpacked
the source code. If you installed via an RPM, then afick.cron was
implemented upon your installation, and should be emailing root as changes
occur. If you followed the instructions here and installed from source, you
have to add it manually. At your command prompt, just type:
crontab -e
0*/2 * * * root /[path_to_afick.cron]/afick.cron
Then save and exit the editor. This tells
your operating system to run afick once every two hours and mail the
results, if any, to root.
Updating it
Every time you make a change to a filesystem,
you'll want to update the afick database. Unfortunately, this is a point
where security can begin to become an inconvenience. You must physically be
at the server to change the Zip disk to read-write mode so the database can
be updated.
To initiate update mode, simply replace
the -k in
the afick command line with -u. This will update the database to include any changes that have
happened since the last time afick was run. Since afick will continue using
the new database to ensure file integrity, you should always run afick once
in compare mode before updating it, to be sure that the database you are
about to create won't report recently compromised system commands or files
as legitimate. As a further level of protection, afick has built-in
integrity checks it performs on its own executables to ensure that afick
itself hasn't been modified.
On a short side note, you can also use the
update feature of afick to monitor exactly what changes a program's
installation procedure makes to a filesystem by using the update feature
immediately before and immediately after the installation. This is extremely
useful in situations where you cannot verify the authenticity of an
application before you install it. Just make sure when you test it with
afick that you don't do so on a mission-critical machine. You can also use
afick for retroactive testing to ensure uninstalling the software actually
returns your filesystem to its previous state.
The future of afick
Afick is a work in progress. In recent
conversations, developer Eric Gerbier said he intends to include in future
releases a daemon-enabled version that doesn't rely on cron to run afick,
thus delivering real-time filesystem monitoring. An option to export afick's
results in HTML/XML is in the works for version 3.0, due out sometime in the
next few months.
No system will ever be completely safe from
malicious users and unauthorized access. If a machine under your control
becomes compromised, you must have the proper precautions in place to
quickly mitigate the damage and restore services. With a file integrity
checker such as afick in place, when that dreaded day comes, you will be
prepared to determine exactly what has happened (or is happening) and react
accordingly.
In order for afick, or any other file
integrity checker, to work as needed, you'll need to take special care in
observing the general actions and changes in your filesystem in order to
correctly and efficiently craft your configuration rules. Once you've done
that, it's just a matter of staying on top of the ever-changing filesystem.
Update regularly and update often, so as to catch problems as they begin,
and not when it's too late. If you make these practices a regular part of
your daily administration routines, you'll be prepared to react efficiently
to a breach in security should the need arise.
Brian Warshawsky has built, supported,
and administered mission-critical IT infastructure for the United States
Naval Research Laboratory, Virginia Commonwealth University, RichmondAir
Wireless, and is currently employed by Sungard Collegis at Virginia State
University.
fcheck is GPL licensed perl code written by Michael A. Gumienny.
Its a medium size (80K source) PERL script. It's source can be downloaded
from various sites (debian).
The current version is 2.7.59
Description: IDS filesystem baseline integrity checker. The fcheck utility is an
IDS (Intrusion Detection System) which can be used to monitor changes to any
given filesystem.
Essentially, fcheck has the ability to monitor directories, files or complete
filesystems for any additions, deletions, and modifications. It is
configurable to exclude active log files, and can be ran as often as
needed from the command line or cron making it extremely difficult to
circumvent.
slipwire.pl is a simple filesystem integrity checker that uses Berkeley DBM.
It compares the SHA-1 hashes of files to an initial state and alerts the user of
any changes.Changes: A fix for a bug in the iteration count when comparing files
to hashes, a quick reader script for dumping the contents of the DBM file, an
example file list, and a tidied-up README.