Recipe 9.6 Checking for Suspicious Account Use, Multiple Systems

9.6.1 Problem

You want to scan multiple computers for unusual or dangerous usage of accounts.

9.6.2 Solution

Merge the lastlog databases from several systems, using Perl:

use DB_File;
use Sys::Lastlog;
use Sys::Hostname;
my %omnilastlog;
tie(%omnilastlog, "DB_File", "/share/omnilastlog");
my $ll = Sys::Lastlog->new( );
while (my ($user, $uid) = (getpwent( ))[0, 2]) {
        if (my $llent = $ll->getlluid($uid)) {
                $omnilastlog{$user} = pack("Na*", $llent->ll_time( ),
                                           join("\0", $llent->ll_line( ),
                                                        $llent->ll_host( ),
                        if $llent->ll_time( ) >
                                (exists($omnilastlog{$user}) ?
                                        unpack("N", $omnilastlog{$user}) : -1);

To read the merged lastlog database, omnilastlog, use another Perl script:

use DB_File;
my %omnilastlog;
tie(%omnilastlog, "DB_File", "/share/omnilastlog");
while (my ($user, $record) = each(%omnilastlog)) {
        my ($time, $rest) = unpack("Na*", $record);
        my ($line, $host_from, $host_to) = split("\0", $rest, -1);
        printf("%-8.8s %-16.16s -> %-16.16s %-8.8s %s\n",
                $user, $host_from, $host_to, $line,
                $time ? scalar(localtime($time)) : "**Never logged in**");

9.6.3 Discussion

Perusing the output from the lastlog , last, and lastb commands [Recipe 9.5] might be sufficient to monitor activity on a single system with a small number of users, but the technique doesn't scale well in the following cases:

  • If accounts are shared among many systems, you probably want to know a user's most recent login on any of your systems.

  • Some system accounts intended for special purposes, such as bin or daemon, should never be used for routine logins.

  • Disabled accounts should be monitored to make sure they have no login activity.

Legitimate usage patterns vary, and your goal should be to notice deviations from the norm. We need more flexibility than the preceding tools provide.

We can solve this dilemma through automation. The Perl modules Sys::Lastlog and Sys::Utmp, which are available from CPAN, can parse and display a system's last-login data. Despite its name, Sys::Utmp can process the wtmp and btmp files; they have the same format as /var/log/utmp, the database containing a snapshot of currently logged-in users.

Our recipe merges lastlog databases from several systems into a single database, which we call omnilastlog, using Perl. The script steps through each entry in the password database on each system, looks up the corresponding entry in the lastlog database using the Sys::Lastlog module, and updates the entry in the merged omnilastlog database if the last login time is more recent than any other we have previously seen.

The merged omnilastlog database is tied to a hash for easy access. We use the Berkeley DB format because it is byte-order-independent and therefore portable: this would be important if your Linux systems run on different architectures. If all of your Linux systems are of the same type (e.g., Intel x86 systems), then any other Perl database module could be used in place of DB_File.

Our hash is indexed by usernames rather than numeric user IDs, in case the user IDs are not standardized among the systems (a bad practice that, alas, does happen). The record for each user contains the time, terminal (ll_line), and remote and local hostnames. The time is packed as an integer in network byte order (another nod to portability: for homogeneous systems, using the native "L" packing template instead of "N" would work as well). The last three values are glued together with null characters, which is safe because the strings never contain nulls.

Run the merge script on all of your systems, as often as desired, to update the merged omnilastlog database. Our recipe assumes a shared filesystem location, /share/omnilastlog; if this is not convenient, copy the file to each system, update it, and then copy it back to a central repository. The merged database is compact, often smaller than the individual lastlog databases.

An even simpler Perl script reads and analyzes the merged omnilastlog database. Our recipe steps through and unpacks each record in the database, and then prints all of the information, like the lastlog command.

This script can serve as a template for checking account usage patterns, according to your own conventions. For example, you might notice dormant accounts by insisting that users with valid shells (as listed in the file /etc/shells, with the exception of /sbin/nologin) must have logged in somewhere during the last month. Conversely, you might require that system accounts (recognized by their low numeric user IDs) with invalid shells must never login, anywhere. Finally, you could maintain a database of the dates when accounts are disabled (e.g., as part of a standard procedure when people leave your organization), and demand that no logins occur for such accounts after the termination date for each.

Run a script frequently to verify your assumptions about legitimate account usage patterns. This way, you will be reminded promptly after Joe's retirement party that his account should be disabled, hopefully before crackers start guessing his password.

9.6.4 See Also

The Sys::Lastlog and Sys::Utmp Perl modules are found at

Perl for System Administration (section 9.2) from O'Reilly shows how to unpack the utmp records used for wtmp and btmp files. O'Reilly's Perl Cookbook also has sample programs for reading records from lastlog and wtmp files: see the laston and tailwtmp scripts in Chapter 8 of that book.

    Chapter 9. Testing and Monitoring