2018-01-03 14:43:27 -05:00

384 lines
10 KiB
Perl

#-----------------------------------------------------------
# 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
$key_path = "Software\\Microsoft\\Windows\\ShellNoRoam\\Bags";
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);
return %item unless (defined $s);
if ($s =~ m/\x00/ && ((($cnt + 1) % 2) == 0)) {
$tag = 0;
}
else {
$str .= $s;
$cnt++;
}
}
# $str =~ s/\x00//g;
my $shortname = $str;
my $ofs = $ofs_shortname + $cnt + 1;
# Read progressively, 1 byte at a time, looking for 0xbeef
$tag = 1;
$cnt = 0;
while ($tag) {
my $s = substr($data,$ofs + $cnt,2);
return %item unless (defined $s);
if (unpack("v",$s) == 0xbeef) {
$tag = 0;
}
else {
$cnt++;
}
}
$item{extver} = unpack("v",substr($data,$ofs + $cnt - 4,2));
$ofs = $ofs + $cnt + 2;
@m = unpack("vv",substr($data,$ofs,4));
($item{ctime_str},$item{ctime}) = convertDOSDate($m[0],$m[1]);
$ofs += 4;
@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;
$str = substr($data,$ofs,length($data) - 30);
my $longname = (split(/\x00\x00/,$str,2))[0];
$longname =~ s/\x00//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;