August 21, 2009

Looking for the right event

Not so long ago, during an incident investigation, I needed to reconstruct a series of events from several Windows systems. I needed to do so from the system that I was using to conduct the whole investigation which had Linux installed in it. That didn't make things easier because, as you will already know, Windows event logs are binary.

Two Google minutes later, I had downloaded a perl script written by Christophe Monniez that was able to do the work. The script turned out to be quite useful (Thanks Christophe!) but I need more. I had lots of events from several systems that were interrelated and needed to be interpreted to be able to understand the way the attack had been conducted, in order to add only the relevant stuff to the timeline. Going back and forward with such a big amount of events searching for the right one wasn't an option, so I decided to provide me with some search capabilities and add my own perl script to do so. The concept is trivial, I wanted to be able to search for some string with in the event, but I want the output to show the complete event instead of the line that matched the string only. You can do this easily with awk, but I rather use perl. Here is my little script in case it can also be helpful to you.

#!/usr/bin/perl
$/ = "\n\n\n";

die "Error: search string missing." if (@ARGV < 1);

while ($line = <stdin>) {

if ($ARGV[0] eq "-v") {
print $line if ($line !~ /$ARGV[1]/i);
} else {
print $line if ($line =~ /$ARGV[0]/i);
}
}


This incident investigation was fairly successful and we had access to one laptop involved in the attack. However the system had been reformated and reinstalled, but some information could be recovered using the usual forensic tools. The event file was partially corrupted and I needed to recover the events that were still available. I rewrote the Christophe's code, that was available under the GPL license, and ended up with the following script that does exactly that.


#!/usr/bin/perl -w

# Process Microsoft event file fragments.
#
# Copyright (c) Jorge D. Ortiz Fuentes, 2009
# Based on Monniez Christophe's code.
# - Added hability to process a fragmented event files.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#

use strict;
use Getopt::Std;

#
# Help information
#
sub usage {
print STDERR "\nUsage:\n\t$0 [-] \n";
print STDERR "Options:\n";
print STDERR "\t-d\tDebug information.\n";
print STDERR "\t-l\tUse localtime instead of GMT.\n";
print STDERR "\t-u\tPreserve unicode.\n";
print STDERR "\t-h\tPrint this help and exit.\n";
print STDERR "\nfile\tThe evt file to be analyzed.\n\n";
exit 1;
}

#
# Search for the first record inside the file.
# It doesn't require that the signature is DWORD aligned
#
sub next_signature {
(my $debug, my $file) = @_;

my $bytes_read;
my $signature;
my $sig_found = 0;

do {
$bytes_read = read($file, $signature, 1);
die("End of file reached.\n") if ($bytes_read <= 0);
if ($signature eq "L") {
$bytes_read = read($file, $signature, 3);
die("End of file reached. Signature not found.\n")
if ($bytes_read <= 0);
}
$sig_found = 1 if (($signature eq "fLe") && (tell($file) >=8));
} while ($sig_found == 0);
# Move the position in the file 8 bytes backwards, to 4 bytes before
# the signature that is where the length of the record is stored.
seek ($file, -8, 1);
if ($debug) {
print "Record starts in position ", tell($file), "\n";
}
}


#
# Extract record information.
#
sub process_record {
(my $debug, my $file, my $length, my $localtime, my $unicode) = @_;

# Local variables
my $record;
my $t_gen;
my $t_writ;
my $rest;
my $rest_reencoded;

# Process the fixed part of the record (at least 56 bytes and
# it is in position 4):
read($file, $record, 52);
$length -= 56;

# Extract the data from the structure
(my $reserved, my $record_nb, my $time_gen, my $time_writ,
my $event_id, my $event_type, my $nb_strings, my $evt_category,
my $reserved_flag, my $cl_record, my $string_offset,
my $SID_leng, my $SID_offset, my $data_len, my $data_offset) =
unpack "LLLLLSSSSLLLLLL" , $record;
# The reserved field must be 1699505740 otherwise skip this record
if ($reserved == 1699505740) {
# Convert dates into strings
if ($localtime) {
$t_gen = localtime($time_gen) . " localtime";
$t_writ = localtime($time_writ) . " localtime";
} else {
$t_gen = gmtime($time_gen) . " GMT";
$t_writ = gmtime($time_writ) . " GMT";
}
# Print data
print "Record number: $record_nb\n";
print "Time generated: $t_gen\n";
print "Time written: $t_writ\n";
print "Evt ID: $event_id Evt type: $event_type Evt category: $evt_category\n";
if ($debug) {
print "* Reserved: $reserved\n";
print "* $nb_strings strings\n";
print "* String offset: $string_offset\n";
print "* SID Len: $SID_leng SID offset: $SID_offset\n";
print "* Data len: $data_len Data offset: $data_offset\n";
}

# Process the rest of the record: Source program, computer name, SID
# and other strings
if (read($file, $rest, $length) < $length) {
die ("End of file reached while reading strings\n");
}

$rest_reencoded = pack "C*" , unpack "U0C*" , $rest;

# Split into several strings
my @strings = split(/\0\0/, $rest_reencoded);
my $str;
$str = $strings[0];
# hack to suppress unicode
$str =~ s/\0//g unless ($unicode);
print "Program: $str\n";
$str = $strings[1];
# hack to suppress unicode
$str =~ s/\0//g unless ($unicode);
print "Computer: $str\n";
my $i=0;
while ($i < $nb_strings) {
$str = $strings[$i+2];
$str =~ s/\0//g unless ($unicode);
print "String $i: $str\n";
$i++;
}
print "\n\n";
} else {
print "Reserved: $reserved\n" if ($debug);
print STDERR "RECORD REJECTED: reserved value fails to match!\n\n\n";
# Searching continues from where it is since this is a corrupted record
}
}


#
# Main program
#

# Variable declarations
my $evt_file = "";
my $record_sig;
my $record;
my $length;
my $dword;
# Option declarations
our ($opt_d, $opt_h, $opt_l, $opt_u);

# Process the command line parameters
getopts('dhlu');

# Debug option
print "\$opt_d:$opt_d\n" if (defined($opt_d));
# Help option
print "\$opt_h:$opt_h\n" if (defined($opt_d) && defined($opt_h));
# Localtime option
print "\$opt_l:$opt_l\n" if (defined($opt_d) && defined($opt_l));
# Unicode option
print "\$opt_u:$opt_u\n" if (defined($opt_d) && defined($opt_u));

# Obtain the file name
$evt_file = shift(@ARGV);
print "Event file: $evt_file\n" if (defined($opt_d) && defined($evt_file));

if ($opt_h) {
&usage();
}

# Open the selected file in binary mode.
open(FILE, $evt_file) or die "ERR: Couldn't open file $evt_file: $!";
binmode(FILE);

do {
&next_signature($opt_d, *FILE);

# The following condition should never be met, because:
# - A record has been found and the file has been rewinded 8 bytes
# - Or EOF was reached and next signature ended the program
die("End of file reached: Incomplete record.\n")
if (read(FILE, $dword, 4) <= 0);
# Obtain the length of this record
$length = unpack "L", $dword;
# A record should be at least 56 bytes long
if ($length > 51) {
# Read the record and process it
&process_record($opt_d, *FILE, $length, $opt_l, $opt_u);
} else {
# Probably corrupted record
print SDTERR "Record too short found and discarded! (Corrupted?)\n";
if ($opt_d) {
print "Record length was: $length\n";
}
# skip current signature to avoid infinite loop.
seek(FILE, 4, 1);
}
} while (!eof(FILE));
close FILE;

exit(0);


Enjoy!

Labels: , ,