# Library that reads and parses CIFS audit event files. # For event file format, see: # http://msdn2.microsoft.com/en-us/library/aa363659%28VS.85%29.aspx # http://www.ultimatewindowssecurity.com/encyclopedia.aspx package Evt::Record; use strict; use warnings; use POSIX ("strftime"); my $OFFSET = 56; my %EVENTS = ( "528" => "Local Login", "529" => "Login Failure - Unknown user name or bad password", "530" => "Login Failure - Account time restriction violation", "531" => "Login Failure - Account currently disabled", "532" => "Login Failure - Account has expired", "533" => "Login Failure - Not allowed at this computer", "534" => "Login Failure - Requested login type denied", "535" => "Login Failure - Password has expired", "536" => "Login Failure - NetLogin component is not active", "537" => "Login Failure - Unspecified reason", "538" => "Logout", "539" => "Login Failure - Account locked out", "540" => "Login" ); # extract $n strings from $buf. Each string consists of 16 bit # little endian characters terminated by a 16 bit NULL. We convert # each 16 bit character to 8 bits by discarding the high order # byte (second byte little endian): s/(.)./$1/g # We extract $n + 1 strings with split(/\0/, $buf, $n + 1) # and discard the last string (trailing junk) with splice() sub extract_strings { my($buf, $n) = @_; $buf =~ s/(.)./$1/g; my @str = split(/\0/, $buf, $n + 1); splice(@str, 0, $n) } sub date_format { my($fmt, $t) = @_; strftime($fmt, localtime($t)); } sub new { my($class, $raw) = @_; my $self = bless([]); @$self = unpack("Va4V4v4V6", $raw); push(@$self, substr($raw, $self->userSidOffset, $self->userSidLength)); push(@$self, substr($raw, $self->dataOffset, $self->dataLength)); push(@$self, &extract_strings(substr($raw, $OFFSET), 2)); push(@$self, [ &extract_strings( substr($raw, $self->stringOffset), $self->numStrings) ]); $self; } sub length { shift->[0] } sub reserved { shift->[1] } sub recordNumber { shift->[2] } sub timeGenerated { shift->[3] } sub timeWritten { shift->[4] } sub eventID { shift->[5] } sub eventType { shift->[6] } sub numStrings { shift->[7] } sub eventCategory { shift->[8] } sub reservedFlags { shift->[9] } sub closingRecordNumber { shift->[10] } sub stringOffset { shift->[11] } sub userSidLength { shift->[12] } sub userSidOffset { shift->[13] } sub dataLength { shift->[14] } sub dataOffset { shift->[15] } sub userSid { my $self = shift; return "" if $self->userSidLength < 28; join("-", "S", unpack("CCx6V5", $self->[16])); } sub data { shift->[17] } sub sourceName { shift->[18] } sub computerName { shift->[19] } sub strings { shift->[20] } sub loginid { shift->strings->[0] } sub loginFromName { shift->strings->[6] } sub loginFromIP { shift->strings->[13] } sub eventMessage { my $id = shift->eventID; exists($EVENTS{$id}) ? $EVENTS{$id} : "Unknown"; } # return true if this is a valid login record sub is_login_record { my $self = shift; $self->eventID == 540 && $self->numStrings > 13 && $self->loginid ne ""; } # read the next event record sub read { my $fh = shift; my $raw; for (;;) { CORE::read($fh, $raw, 4) == 4 or return undef; my $len = unpack("V", $raw); CORE::read($fh, $raw, $len - 4, 4) == $len - 4 or return undef; last if $len > $OFFSET; } Evt::Record->new($raw); } # ugly colon separated dump of the event record sub raw_dump { my $self = shift; my $fmt = "%Y.%m.%d.%H.%M.%S"; join(":", $self->length, $self->recordNumber, &date_format($fmt, $self->timeGenerated), &date_format($fmt, $self->timeWritten), $self->eventID, $self->eventMessage, @{$self}[6..15, 18, 19], $self->userSid, @{$self->strings}); } 1;