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:
Perl-based, so modifiable by admin. See also
Integrity Checkers
About: afick is another file integrity checker, designed to
be fast and fully portable between Unix and Windows platforms. It works
by first creating a database that represents a snapshot of the most
essential parts of your computer system. You can then run the script
to discover all modifications made since the snapshot was taken (i.e.
files added, changed, or removed). The configuration
syntax is very close to that of aide or tripwire, and a graphical interface
is provided.
Changes: The code now works with perl 5.10. On Windows, afick_planning
now sends a report instead of a summary and uses the "LINES" macro.
On Unix, a new MOUNT macro allows you to use a remote database in afick_cron.
Udev files were removed from scan. The tar.gz installer was recoded
to display better diagnostics.
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.