Developer

Our alert script

Need a quick, simple, and cheap paging system? Vincent Danen shows you how, with the help of Perl, you can set up your Linux box to page you when syslog discovers nefarious activity, such as failed password attempts.


In this Daily Drill Down, I’ll take a look at some home-brew Perl goodness. Perl makes an awesome programming language because it's so easy to use and so flexible. It is far more than just a CGI language for Web sites. Indeed, as I’ll illustrate here, Perl can be used for many system administrative tasks, and any sysadmin would find learning some basic Perl to be a definite asset.

In this Daily Drill Down, I’ll show how to write a program in Perl that will alert you about failed password attempts, which are usually a sign of someone trying to break into your system. Because most crackers will try random user names (or known user names) with different passwords, becoming aware of these attempts can help you prevent system break-ins. Having a real-time monitor of password failures can ensure you pull the plug before someone gets into your system. Without a program like this, you might never know someone is probing your system, unless you scan log files on a regular basis. Have I captured your interest so far?

The syslog_mon script
The basic premise of the script is simple: It watches syslog messages and reports any that may signify a possible break-in attempt. These are typically failed login attempts, so the script will also pick up legitimate users mistyping their passwords. However, more often than not, these alerts will signify intruders trying to log in to your system, whether it be locally or remotely using Telnet/ssh.

Of course, syslog generates a number of messages at a crazy rate. We don't want the script to process syslog facilities that it doesn't need to, so we also have to tell syslog to do things a little differently.

Let me quickly explain how the script will work. Typically, syslog writes only to files. This is how your /var/log directory gets filled up. You can, however, tell syslog to write to a named pipe. A named pipe is what the name implies: a communications pipe with a name. One program writes data and sends it into the pipe while another program reads the information from the pipe. Commands such as
cat somefile|more

are actually using a pipe. The output of the cat command is used as the input for the more command. By using the same method of communication, we eliminate the need for a text file “middleman.” This also gives us instantaneous access to the messages syslog is generating instead of relying on a cron job to read a given log file every few minutes. This will also reduce the number of “false alarms” by preventing our script from parsing the same file over and over again and perhaps resending a message for the same alert.

Our script will read the information from the named pipe and will parse the text syslog sends. It will send us messages based on a string that implies a failed login attempt. Once it detects this string, it will send an e-mail message to any e-mail address we specify.

This script is useful for all Linux computers that are connected to the Internet. You can have it e-mail your account directly, making you aware of the attempt moments after it occurs. Or you can have it e-mail a special address that sends a text message to your cellular phone or pager (for example, 8001234567@pcs.cellphone.com) if your cellular or pager provider supports Short Message Service (SMS) messaging and you have obtained the service. If you want to be very creative, you can even have the script pop up a window on your desktop to make you aware of the attempt (but we won't be covering that in this Daily Drill Down).

Without further ado, let's take a look at the script we’ll call syslog_mon.
#!/usr/bin/perl
#
# usage ./syslog_mon < /var/run/syslog_auth
#
# monitors syslog to offer real-time alerts to login failures, which may
# signify break-in attempts

use Mail::Sendmail;

$email = "''joe\@work.com joe\@home.com, 7801234567@mypager.com";
$robot = "alert\@mydomain.com";

LOOP:
$monitor = <STDIN>;
$monitor =~ s/^... .. ..\:..\:.. .*\:/SYSLOG ALERT:/;
$mon = $monitor;
$syslog = $mon =~ /[Ff]ail/;
if ($syslog) {
  $Mail::Sendmail::mailcfg{mime} = 0;
  %mail = (
           smtp  => 'localhost',
           To    => $email,
           From  => $robot
          );
  $mail{"Subject: "}  = "Syslog Alert!";
  $mail{"Message: "} .= "System Activity alert, this might be an attempted unauthorized breakin:\n\n";
  $mail{"Message: "} .= "$monitor\n\n";
  sendmail(%mail) or die $Mail::Sendmail::error;
}

goto LOOP;

Configuring the system
Before we go through the script to examine what it does, you’ll need to install the Mail::Sendmail module for Perl. You can obtain the module from the CPAN Web site or from your Linux distribution if it packages the module. The script uses the sendmail() function from the module, so the script will not run without having Mail::Sendmail installed.

Next, you’ll need to create the named pipe itself for syslog_mon to monitor. We will create the named pipe in the /var/run directory like this:
mknod /var/run/syslog_auth p
chmod 600 /var/run/syslog_auth


Be sure to run this command as root. This creates the named pipe /var/run/syslog_auth and changes the mode on the file to 600, meaning it is read and write only by root. If you did a long list on the file, it will look like this:
prw———- 1 root root 0 Jan 11 18:29 /var/run/syslog_auth|

Notice the pipe (|) symbol at the end of the filename. This indicates it is a named pipe. Now, we should also edit the /etc/syslog.conf file and insert the following line at the top of the file:
auth.notice                       |/var/run/syslog_auth

This tells syslog to send all auth.notice facility messages to the named pipe /var/run/syslog_auth, which we just created. Finally, once you have done this, restart syslog using:
/etc/rc.d/init.d/syslog restart

Some distributions may have different commands for starting or stopping syslog like this. The other alternative is to find the PID (Process Identification) for syslog and send the process the SIGHUP (Signal Hang Up) signal by using:
ps ax|grep syslog

using the PID file found (let's say 1023), and then issuing:
kill -1 1023

Now syslog has reread its configuration file and will send all auth.notice messages to our named pipe. Don't worry—the notices will also be sent to the log files they used to be sent to, so you won't be sacrificing any verbosity in your log files by using this setup.

Incidentally, this script was written on a Linux-Mandrake 7.2 system. Other systems may use a different system logger, although most still use syslog. Some older versions of syslog may also print out messages in a slightly different format or with different wording. If the script does not work “out of the box,” you may need to investigate the messages that syslog outputs and adjust your search accordingly. You can do this by looking at the log files that the auth.notice facility logs to. (The name of the log file will be in the /etc/syslog.conf file.) But let's get on with evaluating the script itself.

Making sense of it all
The first line, of course, points to the Perl interpreter, which is often /usr/bin/perl. Next, we have a few comments on the usage of the script and what it does. The first real Perl command tells Perl that the script requires the Mail::Sendmail module by telling it to use the module.

The next thing we do is define a few variables. The first is $email, which will be used to tell the script where to mail alert messages to. As you can see, you can send to multiple e-mail addresses, separating them with commas. You also need to write the @ character as \@ so that Perl can parse it properly. (The @ character is a special character to Perl.) Because of this, we need to “escape” it by using the backslash. In our example, we’ll send the alert message to joe@work.com, joe@home.com, and 7801234567@mypager.com (the e-mail address you use to send text messages to Joe's pager).

The next variable we define is $robot. This variable, which represents to users where the e-mail is originating, may or may not be a valid e-mail address, but it should originate from your domain. In this case, we use alert@mydomain.com, which isn't a real e-mail address, although mydomain.com may be the real domain name. Since this is an informative e-mail that should never be replied to, it doesn't matter whether it is a real address. Some e-mail clients also allow you to prioritize messages based on the sender, so having a unique e-mail address may be useful to allow you to assign a “top-priority” status to the alert. Another idea is to include the machine name in the e-mail address if you’re going to use the same script to monitor multiple machines. For example, if you have a machine named smtp.mydomain.com, you might want one e-mail address to come from alert-smtp@mydomain.com, while messages generated on the machine named web.mydomain.com might have messages coming from the address alert-web@mydomain.com.

The next thing we do is define a statement header that the goto command uses. Basically, this header defines the start of a block of code that we’ll call LOOP. This is a very simple way to ensure that our script never exits but continues to process the input from the named pipe over and over again.

The first command in our loop is to read input from <STDIN> (standard input). Because you call the script like this:
syslog_mon </var/run/syslog_auth

the standard input is the data coming through the named pipe. In this case, we read a line of input from the named pipe and store it in the variable $monitor.

Matching patterns
The next line is the real meat of the script. This line performs the pattern matching to determine whether the messages syslog is feeding it are important enough to send a page to you. Let's take a closer look at this line:
$monitor =~ s/^... .. ..\:..\:.. .*\:/SYSLOG ALERT:/;

The variable $monitor holds the line of input from syslog. What we’re doing here is performing some pattern matching. By using the =~ assignment operator, we’re also changing the value of $monitor. The actual pattern matching statement is:
s/^... .. ..\:..\:.. .*\:/SYSLOG ALERT:/

The syntax for the pattern matching is:
s/[string_to_match]/[string_to_replace]/

So in this instance we want to replace the regular expression ^... .. ..\:..\:.. .*\: with the string SYSLOG ALERT:. The regular expression is the tricky part. Books can be written on regular expressions themselves (in fact, O'Reilly has a book out on the subject—Mastering Regular Expressions, by Jeffrey Friedl), so we’ll just look at the regular expression we’re using.

In Perl regular expressions, the ^ character stands for the beginning of the line. The . (period) character stands for any single character. The * character means zero or more occurrences of the previous character, so when you use a combination like .*, you are really matching zero or more occurrences of any character.

The pattern is specified in this way to match a specific format that syslog uses. For instance, a line that syslog will output might look like this:
Jan 11 11:25:19 logan PAM_unix[24107]: authentication failure;
LOGIN(uid=0) -> root for system-auth service


In an effort to trim this message, we want to replace the date and time, along with the machine name (in this case, logan) and the program reporting the error (in this case, PAM_unix) with a shorter string. Thus, the pattern to match is Jan 11 11:25:19 logan PAM_unix[24107]:. However, the date and time can change at any point, so these characters cannot be considered constant to match. In this string, the only constant characters are the colons. Compare our pattern match:
^... .. ..\:..\:.. .*\:

to our string:
Jan 11 11:35:19 logan PAM_unix[24107]:

The date and time remain constant and will always be at the beginning of the line. Therefore, we can see that specifying any three characters, a space, any two characters, a space, any two characters, a colon, any two characters, another colon, any two characters, a space, and then zero or more occurrences of any character followed by a colon matches our date, time, machine name, and calling program string perfectly. Because of this, the line:
Jan 11 11:25:19 logan PAM_unix[24107]: authentication failure;
LOGIN(uid=0) -> root for system-auth service


now becomes:
SYSLOG ALERT: authentication failure; LOGIN(uid=0) ->
root for system-auth service


This new string is assigned to our variable $monitor. Then, on the next line, we create a new variable called $mon, which holds the same content as $monitor. We then use the $mon variable in the next line for another pattern match. This time, we want to search for the word Fail or fail. (Some processes may put the first letter of the word in uppercase, while others in lowercase.) To do this, we use [Ff]ail as the expression to match. By placing the letters F and f within square brackets, we are telling Perl to match either occurrence of the letter, so the expression matches both occurrences of the word fail. If $mon matches this expression, the $syslog variable is set to true. If $mon does not match this expression, then $syslog is set to false. This is important to know because we want to send messages only for syslog messages with an occurrence of the word fail because these indicate a failed login attempt.

The next line starts an if() statement that is dependent on whether the $syslog variable is true. If it is true, this means we have matched the word fail and a login has been attempted with a failed password. At this point, we want to perform our action, which in this case is sending an e-mail message to you. If $syslog is false, the script does nothing and starts the loop over again.

The lines of code within the if() statement block deal with the e-mail message itself. The first thing we do is turn MIME off, and then we define the mail options. We define the SMTP server to connect to as localhost, we define who to send the e-mail to by providing the contents of the $email variable, and we also define who to send the mail from by using the $robot variable. After that, we define the subject and message body. Finally, we use the sendmail() function of the Mail::Sendmail module to send the message itself. If for some reason the mail cannot be sent, the script will exit with an error message.

And after that has completed, the goto statement starts the loop all over again.

Using syslog_mon
Now, to run the script, I recommend adding the following to one of your startup scripts (/etc/rc.d/rc.local is a good place for it). Simply add the following:
/usr/sbin/syslog_mon </var/run/syslog_auth&

and install the syslog_mon program into the /usr/sbin directory. You’ll also want to make the script read, write, and execute by root alone. This will prevent other users from messing with it. You can accomplish this with:
chmod 700 /usr/sbin/syslog_mon

Conclusion
And that's it! The script can probably use some refinement to shorten the strings further and perhaps send mail on other syslog messages as well. You can also change it to pop up a window on your desktop with an alert message if you’ll be running the script on your desktop or workstation computer. At any rate, the script will run quite nicely and keep you even more aware of what’s happening on your system in real time.
The authors and editors have taken care in preparation of the content contained herein but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for any damages. Always have a verified backup before making any changes.

About Vincent Danen

Vincent Danen works on the Red Hat Security Response Team and lives in Canada. He has been writing about and developing on Linux for over 10 years and is a veteran Mac user.

Editor's Picks

Free Newsletters, In your Inbox