#-----------------------------------------------------------
# shellbags.pl
# RR plugin to parse (Vista, Win7/Win2008R2) shell bags
#
# History:
#   20130514 - added checking of ShellNoRoam\Bags subkeys for WinXP
#   20120814 - created
#
# References
#    http://c0nn3ct0r.blogspot.com/2011/11/windows-shellbag-forensics.html
#  Andrew's Python code for Registry Decoder
#    http://code.google.com/p/registrydecoder/source/browse/trunk/templates/template_files/ShellBag.py
#  Joachim Metz's shell item format specification
#    http://download.polytechnic.edu.na/pub4/download.sourceforge.net/pub/
#      sourceforge/l/project/li/liblnk/Documentation/Windows%20Shell%20Item%20format/
#      Windows%20Shell%20Item%20format.pdf
#  Converting DOS Date format
#    http://msdn.microsoft.com/en-us/library/windows/desktop/ms724274(v=VS.85).aspx
#
# Thanks to Willi Ballenthin and Joachim Metz for the documentation they 
# provided, Andrew Case for posting the Registry Decoder code, and Kevin 
# Moore for writing the shell bag parser for Registry Decoder, as well as 
# assistance with some parsing.
#
# License: GPL v3 
# copyright 2013 Quantum Analytics Research, LLC
# Author: H. Carvey, keydet89@yahoo.com
#-----------------------------------------------------------
package itempos;
use strict;
use Time::Local;

my %config = (hive          => "NTUSER\.DAT",
							hivemask      => 16,
							output        => "report",
							category      => "User Activity",
              osmask        => 16, #Win7/Win2008R2
              hasShortDescr => 1,
              hasDescr      => 0,
              hasRefs       => 0,
              version       => 20130514);

sub getConfig{return %config}

sub getShortDescr {
	return "Shell/Bags/1/Desktop ItemPos* value parsing; Win7 NTUSER\.DAT hives";	
}
sub getDescr{}
sub getRefs {}
sub getHive {return $config{hive};}
sub getVersion {return $config{version};}

my $VERSION = getVersion();

sub pluginmain {
	my $class = shift;
	my $hive = shift;
	::logMsg("Launching itempos v.".$VERSION);
	::rptMsg("itempos v.".$VERSION); # banner
	::rptMsg("(".$config{hive}.") ".getShortDescr()."\n"); # banner
	my %itempos = ();

	my $reg = Parse::Win32Registry->new($hive);
	my $root_key = $reg->get_root_key;

	my $key_path = "Software\\Microsoft\\Windows\\Shell\\Bags\\1\\Desktop";
	my $key;
	
	if ($key = $root_key->get_subkey($key_path)) {
		::rptMsg($key_path);
		my $lw = $key->get_timestamp();
		::rptMsg("LastWrite: ".gmtime($lw));
		::rptMsg("");
		
		my @vals = $key->get_list_of_values();
		foreach my $v (@vals) {
			my $name = $v->get_name();
			if ($name =~ m/^ItemPos/) {
				$itempos{$name} = $v->get_data();
			}
		} 
		
		if (scalar keys %itempos > 0) {
			foreach my $i (keys %itempos) {
				::rptMsg("Value: ".$i);
				::rptMsg(sprintf "%-10s|%-20s|%-20s|%-20s|Name","Size","Modified","Accessed","Created");
				::rptMsg(sprintf "%-10s|%-20s|%-20s|%-20s|"."-" x 10,"-" x 10,"-" x 20,"-" x 20,"-" x 20);
				parseBagEntry($itempos{$i});
				::rptMsg("");
			}
		}
		else {
			::rptMsg("No ItemPos* values found.");
		}
	}
	else {
		::rptMsg($key_path." not found.");
	}
#	::rptMsg("");
# The following was added on 20130514 to address Windows XP systems	
	my $key_path = "Software\\Microsoft\\Windows\\ShellNoRoam\\Bags";
	my $key;
	if ($key = $root_key->get_subkey($key_path)) {
		my @sk = $key->get_list_of_subkeys();
		if (scalar(@sk) > 0) {
			foreach my $s (@sk) {
				my %itempos = ();
				my @vals = $s->get_subkey("Shell")->get_list_of_values();
				
				if (scalar(@vals) > 0) {
					foreach my $v (@vals) {
						my $name = $v->get_name();
					  if ($name =~ m/^ItemPos/) {
					  	$itempos{$name} = $v->get_data();
					  }
					}
					
					if (scalar keys %itempos > 0) {
						::rptMsg($key_path."\\".$s->get_name()."\\Shell");
						foreach my $i (keys %itempos) {
							::rptMsg("Value: ".$i);
							::rptMsg(sprintf "%-10s|%-20s|%-20s|%-20s|Name","Size","Modified","Accessed","Created");
							::rptMsg(sprintf "%-10s|%-20s|%-20s|%-20s|"."-" x 10,"-" x 10,"-" x 20,"-" x 20,"-" x 20);
							parseBagEntry($itempos{$i});
							::rptMsg("");
						}
					}
							
				}
			}
		}
		else {
# No subkeys			
		}
	}
	else {
		::rptMsg($key_path." not found\.");
	}
}

#-----------------------------------------------------------
# 
#-----------------------------------------------------------


#-----------------------------------------------------------
# parseBagEntry()
#-----------------------------------------------------------
sub parseBagEntry {
	my $data = shift;
	my $ofs = 24;
	my $len = length($data);
	while ($ofs < $len) {
		my %item = ();
		my $sz = unpack("v",substr($data,$ofs,2));
		
		my $data = substr($data,$ofs,$sz);
		
		my $type = unpack("C",substr($data,2,1));
		
		if ($type == 0x1f) {
			%item = parseSystemBagItem($data);
			::rptMsg(sprintf "%-10s|%-20s|%-20s|%-20s|".$item{name},"","","","");
		}
		elsif ($type == 0x31 || $type == 0x32 || $type == 0x3a) {
			%item = parseFolderItem($data);
			
			my ($m,$a,$c);
 			(exists $item{mtime_str} && $item{mtime_str} ne "0") ? ($m = $item{mtime_str}) : ($m = "");
 			(exists $item{atime_str} && $item{atime_str} ne "0") ? ($a = $item{atime_str}) : ($a = "");
 			(exists $item{ctime_str} && $item{ctime_str} ne "0") ? ($c = $item{ctime_str}) : ($c = "");
			my $str = sprintf "%-10s|%-20s|%-20s|%-20s|",$item{size},$m,$a,$c;
			::rptMsg($str.$item{name});
			
		}
		else {
			
		}
		$ofs += $sz + 8;
	}
}
#-----------------------------------------------------------
# parseSystemBagItem()
#-----------------------------------------------------------
sub parseSystemBagItem {
	my $data = shift;
	my %item = ();
	my %vals = (0x00 => "Explorer",
	            0x42 => "Libraries",
	            0x44 => "Users",
	            0x4c => "Public",
	            0x48 => "My Documents",
	            0x50 => "My Computer",
	            0x58 => "My Network Places",
	            0x60 => "Recycle Bin",
	            0x68 => "Explorer",
	            0x70 => "Control Panel",
	            0x78 => "Recycle Bin",
	            0x80 => "My Games");
	
	$item{type} = unpack("C",substr($data,2,1));
	$item{id}   = unpack("C",substr($data,3,1));
	if (exists $vals{$item{id}}) {
		$item{name} = $vals{$item{id}};
	}
	else {
		$item{name} = parseGUID(substr($data,4,16));
	}
	return %item;
}

#-----------------------------------------------------------
# parseFolderItem()
#-----------------------------------------------------------
sub parseFolderItem {
	my $data = shift;
	my %item = ();
	my $ofs_mdate = 0x08;
	$item{type} = unpack("C",substr($data,2,1));
	
	$item{size} = unpack("V",substr($data,4,4));
	
	my @m = unpack("vv",substr($data,$ofs_mdate,4));
	($item{mtime_str},$item{mtime}) = convertDOSDate($m[0],$m[1]);
		
	my $ofs_shortname = $ofs_mdate + 6;	
	my $tag = 1;
	my $cnt = 0;
	my $str = "";
	while($tag) {
		my $s = substr($data,$ofs_shortname + $cnt,1);
		if ($s =~ m/\00/ && ((($cnt + 1) % 2) == 0)) {
			$tag = 0;
		}
		else {
			$str .= $s;
			$cnt++;
		}
	}
#	$str =~ s/\00//g;
	my $shortname = $str;
	my $ofs = $ofs_shortname + $cnt + 1;
# Read progressively, 1 byte at a time, looking for 0xbeef	
	my $tag = 1;
	my $cnt = 0;
	while ($tag) {
		if (unpack("v",substr($data,$ofs + $cnt,2)) == 0xbeef) {
			$tag = 0;
		}
		else {
			$cnt++;
		}
	}
	$item{extver} = unpack("v",substr($data,$ofs + $cnt - 4,2));
	$ofs = $ofs + $cnt + 2;
	
	my @m = unpack("vv",substr($data,$ofs,4));
	($item{ctime_str},$item{ctime}) = convertDOSDate($m[0],$m[1]);
	$ofs += 4;
	my @m = unpack("vv",substr($data,$ofs,4));
	($item{atime_str},$item{atime}) = convertDOSDate($m[0],$m[1]);
	
	my $jmp;
	if ($item{extver} == 0x03) {
		$jmp = 8;
	}
	elsif ($item{extver} == 0x07) {
		$jmp = 26;
	}
	elsif ($item{extver} == 0x08) {
		$jmp = 30;
	}
	else {}
	
	$ofs += $jmp;
	
	my $str = substr($data,$ofs,length($data) - 30);
	my $longname = (split(/\00\00/,$str,2))[0];
	$longname =~ s/\00//g;
	
	if ($longname ne "") {
		$item{name} = $longname;
	}
	else {
		$item{name} = $shortname;
	}
	return %item;
	
	
}

#-----------------------------------------------------------
# convertDOSDate()
# subroutine to convert 4 bytes of binary data into a human-
# readable format.  Returns both a string and a Unix-epoch
# time.
#-----------------------------------------------------------
sub convertDOSDate {
	my $date = shift;
	my $time = shift;
	
	if ($date == 0x00 || $time == 0x00){
		return (0,0);
	}
	else {
		my $sec = ($time & 0x1f) * 2;
		$sec = "0".$sec if (length($sec) == 1);
		if ($sec == 60) {$sec = 59};
		my $min = ($time & 0x7e0) >> 5;
		$min = "0".$min if (length($min) == 1);
		my $hr  = ($time & 0xF800) >> 11;
		$hr = "0".$hr if (length($hr) == 1);
		my $day = ($date & 0x1f);
		$day = "0".$day if (length($day) == 1);
		my $mon = ($date & 0x1e0) >> 5;
		$mon = "0".$mon if (length($mon) == 1);
		my $yr  = (($date & 0xfe00) >> 9) + 1980;
		my $gmtime = timegm($sec,$min,$hr,$day,($mon - 1),$yr);
    return ("$yr-$mon-$day $hr:$min:$sec",$gmtime);
#		return gmtime(timegm($sec,$min,$hr,$day,($mon - 1),$yr));
	}
}

#-----------------------------------------------------------
# parseGUID()
# Takes 16 bytes of binary data, returns a string formatted
# as an MS GUID.
#-----------------------------------------------------------
sub parseGUID {
	my $data     = shift;
  my $d1 = unpack("V",substr($data,0,4));
  my $d2 = unpack("v",substr($data,4,2));
  my $d3 = unpack("v",substr($data,6,2));
	my $d4 = unpack("H*",substr($data,8,2));
  my $d5 = unpack("H*",substr($data,10,6));
  return sprintf "{%08x-%x-%x-$d4-$d5}",$d1,$d2,$d3;
}

#-----------------------------------------------------------
# printData()
# subroutine used primarily for debugging; takes an arbitrary
# length of binary data, prints it out in hex editor-style
# format for easy debugging
#-----------------------------------------------------------
sub printData {
	my $data = shift;
	my $len = length($data);
	my $tag = 1;
	my $cnt = 0;
	
	my $loop = $len/16;
	$loop++ if ($len%16);
	
	foreach my $cnt (0..($loop - 1)) {
#	while ($tag) {
		my $left = $len - ($cnt * 16);
		
		my $n;
		($left < 16) ? ($n = $left) : ($n = 16);

		my $seg = substr($data,$cnt * 16,$n);
		my @str1 = split(//,unpack("H*",$seg));

		my @s3;
		my $str = "";

		foreach my $i (0..($n - 1)) {
			$s3[$i] = $str1[$i * 2].$str1[($i * 2) + 1];
			
			if (hex($s3[$i]) > 0x1f && hex($s3[$i]) < 0x7f) {
				$str .= chr(hex($s3[$i]));
			}
			else {
				$str .= "\.";
			}
		}
		my $h = join(' ',@s3);
		::rptMsg(sprintf "0x%08x: %-47s  ".$str,($cnt * 16),$h);
	}
}
1;