# # $Date: 2005/03/16 00:03:40 $ # # Keyword search mode # # Brian Carrier [carrier@sleuthkit.org] # Copyright (c) 2001-2005 by Brian Carrier. All rights reserved # # This file is part of the Autopsy Forensic Browser (Autopsy) # # Autopsy 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. # # Autopsy 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 Autopsy; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED # WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR ANY PARTICULAR PURPOSE. # IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, DATA, OR PROFITS OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package Kwsrch; require 'search.pl'; $Kwsrch::ENTER = 1; $Kwsrch::RESULTS_FR = 2; $Kwsrch::RUN = 3; $Kwsrch::LOAD = 4; $Kwsrch::BLANK = 5; my $IMG_DETAILS = 0x80; sub main { # By default, show the main frame $Args::args{'view'} = $Args::enc_args{'view'} = $Kwsrch::ENTER unless (exists $Args::args{'view'}); Args::check_view(); my $view = Args::get_view(); if ($view == $Kwsrch::BLANK) { blank(); return 0; } # Check Basic Args Args::check_vol('vol'); # These windows don't need the meta data address if ($view == $Kwsrch::ENTER) { return enter(); } elsif ($view == $Kwsrch::RESULTS_FR) { return results_fr(); } elsif ($view == $Kwsrch::RUN) { return run(); } elsif ($view == $Kwsrch::LOAD) { return load(); } else { Print::print_check_err("Invalid Keyword Search View"); } } my $CASE_INSENS = 1; my $CASE_SENS = 0; my $REG_EXP = 1; my $STRING = 0; # Form to enter search data sub enter { my $vol = Args::get_vol('vol'); Print::print_html_header("Search on $Caseman::vol2sname{$vol}"); my $ftype = $Caseman::vol2ftype{$vol}; if ($ftype eq 'dls') { print "

Keyword Search of Unallocated Space

\n"; } elsif ($ftype eq 'swap') { print "

Keyword Search of swap partition

\n"; } elsif ($ftype eq 'raw') { print "

Keyword Search of raw data

\n"; } elsif ($Caseman::vol2cat{$vol} eq "disk") { print "

Keyword Search of disk

\n"; } else { print "

Keyword Search of Allocated and Unallocated Space

\n"; } # @@@ Fix this - caused by writing all results to a file if ($::LIVE == 1) { Print::print_err( "
Keyword searching is temporarily not available during live analysis mode
" ); } print "
\n" . "Enter the keyword string or expression to search for:


\n" . Args::make_hidden() . "\n" . "\n" . "\n"; print "\n" . "" . "\n" . "\n" . "
" . "ASCII \n" . "Unicode
" . "Case Insensitive\n" . "grep Regular Expression
\n" . "\n
\n"; if ($::LIVE == 0) { print "\n"; # If we are a non-dls image and one exists - make a button to load it if (($ftype ne 'dls') && (exists $Caseman::vol2dls{$vol})) { print "\n"; } # If we are a dls and the original exists - make a button to load it elsif (($ftype eq 'dls') && (exists $Caseman::mod2vol{$vol})) { print "\n"; } # Strings Button if ( (!(exists $Caseman::vol2str{$vol})) || (!(exists $Caseman::vol2uni{$vol}))) { my $dest_vol = $vol; $dest_vol = $Caseman::mod2vol{$vol} if exists($Caseman::mod2vol{$vol}); print "\n"; } # Unallocated Space Button if ( ($Fs::is_fs{$ftype}) && (!(exists $Caseman::vol2dls{$vol}))) { print "\n"; } print "
" . "
\n" . "\n" . "\n" . "\n" . Args::make_hidden() . "\n
" . "
\n" . "\n" . "\n" . "\n" . Args::make_hidden() . "\n
" . "
\n" . "\n" . "\n" . "\n" . Args::make_hidden() . "\n
" . "
\n" . "\n" . "\n" . "\n" . Args::make_hidden() . "\n
\n"; } print "" . "Regular Expression Cheat Sheet\n

\n"; print "

NOTE: The keyword search runs " . "grep on the image.
\n" . "A list of what will and " . "what will not be found is available " . "here.
\n"; # Section for previous searches if ($::LIVE == 0) { my $srch_name = get_srch_fname(0); if (-e $srch_name) { print "


Previous Searches

\n" . "\n"; my $row_idx = 0; # Cycle through the files for (my $srch_idx = 0;; $srch_idx++) { $srch_name = get_srch_fname($srch_idx); last unless (-e $srch_name); # Open the file to get the string and count unless (open(SRCH, "$srch_name")) { print "Error opening search file: $srch_name\n"; return 1; } my $prev_str = ""; my $prev_cnt = 0; while () { unless (/^(\d+)\|(.*?)?\|(.*)$/) { print "Error pasing header of search file: $srch_name\n"; return 1; } $prev_cnt = $1; $prev_str = $3; if (length($prev_str) > 32) { $prev_str = substr($prev_str, 0, 32); $prev_str .= "..."; } last; } close(SRCH); print "\n" if ($row_idx == 0); print " \n"; $row_idx = 0; } else { $row_idx++; } } print "
\n" . "
\n" . "\n" . "\n" . "\n" . "\n" . Args::make_hidden(); print "" . "
\n"; if ($row_idx == 3) { print "
\n"; } } # Predefined expressions from search.pl print "

Predefined Searches

\n"; print "\n"; my $row_idx = 0; my $r; foreach $r (keys %Kwsrch::auto_srch) { $Kwsrch::auto_srch_reg{$r} = 0 unless (defined $Kwsrch::auto_srch_reg{$r}); $Kwsrch::auto_srch_csense{$r} = 1 unless (defined $Kwsrch::auto_srch_csense{$r}); print "\n" if ($row_idx == 0); # @@@ Make a unicode option in predefined print " \n"; if ($row_idx == 3) { print "\n"; $row_idx = 0; } else { $row_idx++; } } print "
\n" . "
\n" . "\n" . "\n" . "\n" . "\n" . "\n" . Args::make_hidden(); if ($Kwsrch::auto_srch_reg{$r} == 1) { print "\n"; } if ($Kwsrch::auto_srch_csense{$r} == 0) { print "\n"; } print "
\n" . "
\n"; Print::print_html_footer(); return 0; } # MAIN WITH RESULTS # Page that makes frame with the results on left and data units on right sub results_fr { my $vol = Args::get_vol('vol'); # A string was given for a new search if (exists $Args::args{'str'}) { Args::check_str(); Print::print_html_header_frameset( "Search on $Caseman::vol2sname{$vol} for $Args::args{'str'}"); print "\n"; my $srch_case = ""; $srch_case = "&srch_case=$Args::args{'srch_case'}" if (exists $Args::args{'srch_case'}); my $regexp = ""; $regexp = "®exp=$Args::args{'regexp'}" if (exists $Args::args{'regexp'}); my $ascii = ""; $ascii = "&ascii=$Args::args{'ascii'}" if (exists $Args::args{'ascii'}); my $unicode = ""; $unicode = "&unicode=$Args::args{'unicode'}" if (exists $Args::args{'unicode'}); # Block List print "\n"; } elsif (exists $Args::args{'srchidx'}) { Args::check_srchidx(); Print::print_html_header_frameset( "Search on $Caseman::vol2sname{$vol} for Index $Args::args{'srchidx'}" ); print "\n"; # Block List print "\n"; } # Block Contents print "\n" . "\n"; Print::print_html_footer_frameset(); return 0; } # Find an empty file to save the keyword searches to sub find_srch_file { my $vol = Args::get_vol('vol'); my $out_name = "$::host_dir" . "$::DATADIR/$Caseman::vol2sname{$vol}"; my $i; for ($i = 0; -e "${out_name}-${i}.srch"; $i++) { } return "${out_name}-${i}.srch"; } # Pass the index # return the full path of the file returned sub get_srch_fname { my $idx = shift; my $vol = Args::get_vol('vol'); return "$::host_dir" . "$::DATADIR" . "/$Caseman::vol2sname{$vol}-${idx}.srch"; } sub load { Args::check_srchidx(); Print::print_html_header(""); if ($::LIVE == 1) { print "Searches cannot be loaded during live analysis
\n"; return 1; } my $srch_name = get_srch_fname($Args::args{'srchidx'}); print "New Search\n

"; print_srch_results($srch_name); Print::print_html_footer(); return 0; } # performs actual search, saves hits to file, and calls method to print sub run { Args::check_str(); Print::print_html_header(""); my $vol = Args::get_vol('vol'); my $ftype = $Caseman::vol2ftype{$vol}; my $img = $Caseman::vol2path{$vol}; my $offset = $Caseman::vol2start{$vol}; my $imgtype = $Caseman::vol2itype{$vol}; my $orig_str = Args::get_str(); my $grep_str = $orig_str; # we will escape some values in the grep ver # Check for a search string if ($orig_str eq "") { print "You must enter a string value to search
\n"; print "New Search\n

"; return 1; } my $log = ""; # Log entry string my $ascii = 0; my $unicode = 0; if ((exists $Args::args{'ascii'}) && ($Args::args{'ascii'} == 1)) { $ascii = 1; $log .= "ASCII, "; } if ((exists $Args::args{'unicode'}) && ($Args::args{'unicode'} == 1)) { $unicode = 1; $log .= "Unicode, "; } if (($ascii == 0) && ($unicode == 0)) { print "You must choose either ASCII or Unicode to search
\n"; print "New Search\n

"; return 1; } my $grep_flag = ""; # Flags to pass to grep # Check if search is case insensitive my $case = 0; if ( (exists $Args::args{'srch_case'}) && ($Args::args{'srch_case'} == $CASE_INSENS)) { $grep_flag = "-i"; $case = 1; $log .= "Case Insensitive "; } # Check if search is a regular expression my $regexp = 0; if ((exists $Args::args{'regexp'}) && ($Args::args{'regexp'} == $REG_EXP)) { $grep_flag .= " -E"; $regexp = 1; $log .= "Regular Expression "; } # if not a reg-exp, we need to escape some special values that # 'grep' will misinterpret else { $grep_str =~ s/\\/\\\\/g; # \ $grep_str =~ s/\./\\\./g; # . $grep_str =~ s/\[/\\\[/g; # [ $grep_str =~ s/\^/\\\^/g; # ^ $grep_str =~ s/\$/\\\$/g; # $ $grep_str =~ s/\*/\\\*/g; # * # We need to add ' to end begin and end of the search as well if ($grep_str =~ /\'/) { $grep_str =~ s/\'/\\\'/g; # ' $grep_str = "'$grep_str'"; } $grep_str =~ s/^\-/\\\-/; # starting with - (mistakes for an arg) } Print::log_host_inv( "$Caseman::vol2sname{$vol}: ${log}search for $grep_str"); # Get the addressable unit of image my $bs = Args::get_unitsize(); # $norm_str is normalized to find the "hit" in the output my $norm_str = $orig_str; # make this lowercase if we are doing case insens $norm_str =~ tr/[A-Z]/[a-z]/ if ($case == 1); my $norm_str_len = length($norm_str); # array to pass to printing method my @results; my $name_uni = ""; my $name_asc = ""; # round 0 is for ASCII and 1 is for Unicode for (my $i = 0; $i < 2; $i++) { next if (($i == 0) && ($ascii == 0)); next if (($i == 1) && ($unicode == 0)); @results = (); local *OUT; my $hit_cnt = 0; $SIG{ALRM} = sub { if (($hit_cnt++ % 5) == 0) { print "+"; } else { print "-"; } alarm(5); }; alarm(5); if ($i == 0) { print "Searching for ASCII: "; } else { print "Searching for Unicode: "; } # if the string is less than 4 chars, then it will not be in the # strings file so it will be searched for the slow way if (length($orig_str) < 4) { my $ltmp = length($orig_str); if ($i == 0) { if ( (($ftype eq "raw") || ($ftype eq "swap")) && ($Caseman::vol2end{$vol} != 0)) { Exec::exec_pipe(*OUT, "'$::TSKDIR/dls' -e -f $ftype -i $imgtype $img " . $Caseman::vol2start{$vol} . "-" . $Caseman::vol2end{$vol} . " | '$::TSKDIR/srch_strings' -a -t d -$ltmp | '$::GREP_EXE' $grep_flag '$grep_str'" ); } else { Exec::exec_pipe(*OUT, "'$::TSKDIR/dls' -e -f $ftype -o $offset -i $imgtype $img | '$::TSKDIR/srch_strings' -a -t d -$ltmp | '$::GREP_EXE' $grep_flag '$grep_str'" ); } } else { if ( (($ftype eq "raw") || ($ftype eq "swap")) && ($Caseman::vol2end{$vol} != 0)) { Exec::exec_pipe(*OUT, "'$::TSKDIR/dls' -e -f $ftype -i $imgtype $img " . $Caseman::vol2start{$vol} . "-" . $Caseman::vol2end{$vol} . " | '$::TSKDIR/srch_strings' -a -t d -e l -$ltmp | '$::GREP_EXE' $grep_flag '$grep_str'" ); } else { Exec::exec_pipe(*OUT, "'$::TSKDIR/dls' -e -f $ftype -o $offset -i $imgtype $img | '$::TSKDIR/srch_strings' -a -t d -e l -$ltmp | '$::GREP_EXE' $grep_flag '$grep_str'" ); } } } # Use the strings file if it exists elsif (($i == 0) && (defined $Caseman::vol2str{$vol})) { my $str_vol = $Caseman::vol2path{$Caseman::vol2str{$vol}}; Exec::exec_pipe(*OUT, "'$::GREP_EXE' $grep_flag '$grep_str' $str_vol"); } elsif (($i == 1) && (defined $Caseman::vol2uni{$vol})) { my $str_vol = $Caseman::vol2path{$Caseman::vol2uni{$vol}}; Exec::exec_pipe(*OUT, "'$::GREP_EXE' $grep_flag '$grep_str' $str_vol"); } # Run strings on the image first and then grep that else { if ($i == 0) { if ( (($ftype eq "raw") || ($ftype eq "swap")) && ($Caseman::vol2end{$vol} != 0)) { Exec::exec_pipe(*OUT, "'$::TSKDIR/dls' -e -f $ftype -i $imgtype $img " . $Caseman::vol2start{$vol} . "-" . $Caseman::vol2end{$vol} . " | '$::TSKDIR/srch_strings' -a -t d | '$::GREP_EXE' $grep_flag '$grep_str'" ); } else { Exec::exec_pipe(*OUT, "'$::TSKDIR/dls' -e -f $ftype -o $offset -i $imgtype $img | '$::TSKDIR/srch_strings' -a -t d | '$::GREP_EXE' $grep_flag '$grep_str'" ); } } else { if ( (($ftype eq "raw") || ($ftype eq "swap")) && ($Caseman::vol2end{$vol} != 0)) { Exec::exec_pipe(*OUT, "'$::TSKDIR/dls' -e -f $ftype -i $imgtype $img " . $Caseman::vol2start{$vol} . "-" . $Caseman::vol2end{$vol} . " | '$::TSKDIR/srch_strings' -a -t d -e l | '$::GREP_EXE' $grep_flag '$grep_str'" ); } else { Exec::exec_pipe(*OUT, "'$::TSKDIR/dls' -e -f $ftype -o $offset -i $imgtype $img | '$::TSKDIR/srch_strings' -a -t d -e l | '$::GREP_EXE' $grep_flag '$grep_str'" ); } } } alarm(0); $SIG{ALRM} = 'DEFAULT'; # Cycle through the results and put them in an array while ($_ = Exec::read_pipe_line(*OUT)) { # Parse out the byte offset and hit string if (/^\s*(\d+)\s+(.+)$/) { my $off = $1; my $hit_str_orig = $2; my $idx = 0; # Make a copy that we can modify & play with my $hit_str = $hit_str_orig; $hit_str =~ tr/[A-Z]/[a-z]/ if ($case == 1); # How long was the string that we hit? my $hit_str_len = length($hit_str); # I'm not sure how to find a grep re in the hit yet, so # for now we do not get the exact offset if ($regexp) { my $b = int($off / $bs); my $o = int($off % $bs); # $hit =~ s/\n//g; push @results, "${b}|${o}|"; next; } # There could be more than one keyword in the string # so cycle through all of them my $psize = scalar(@results); while (($idx = index($hit_str, $norm_str, $idx)) > -1) { # The summary of the hit starts 5 chars before it my $sum_min = $idx - 5; $sum_min = 0 if ($sum_min < 0); # Goto 5 after, if there is still space my $sum_max = $idx + $norm_str_len + 5; $sum_max = $hit_str_len if ($sum_max > $hit_str_len); my $sum_hit = substr($hit_str_orig, $sum_min, $sum_max - $sum_min); # remove new lines $sum_hit =~ s/\n/ /g; # The actual offset for Unicode is 2 bytes per char my $tmpidx = $idx; $tmpidx *= 2 if ($i == 1); my $b = int(($off + $tmpidx) / $bs); my $o = int(($off + $tmpidx) % $bs); push @results, "${b}|${o}|$sum_hit"; # advance index to find next hit $idx++; } # If we did not find a term, then just print what # was found-this occurs bc index does not find it # sometimes. if ($psize == scalar(@results)) { my $b = int($off / $bs); my $o = int($off % $bs); # $hit =~ s/\n//g; push @results, "${b}|${o}|"; next; } } # A negative offset is common on FreeBSD with large images elsif (/^\s*(\-\d+):?\s*(.+)$/) { print "ERROR: Negative byte offset ($1) Your version of " . "strings likely does not support large files: $2
\n"; } else { print "Error parsing grep result: $_
\n"; } } close(OUT); print " Done
"; my $cnt = scalar(@results); my $srch_name = ""; if ($::LIVE == 0) { print "Saving: "; # Find a file to save the results to $srch_name = find_srch_file(); unless (open(IDX, ">$srch_name")) { print "Error opening $srch_name\n"; return (1); } # Print the header if ($i == 0) { print IDX "$cnt|${grep_flag}|${orig_str}|ascii\n"; $name_asc = $srch_name; } else { print IDX "$cnt|${grep_flag}|${orig_str}|unicode\n"; $name_uni = $srch_name; } for (my $a = 0; $a < $cnt; $a++) { print IDX "$results[$a]\n"; } close(IDX); print " Done
\n"; } if ($i == 0) { print "$cnt hits"; print "- link to results" if ($cnt > 0); print "
\n"; } else { print "$cnt hits"; print "- link to results" if ($cnt > 0); print "
\n"; } print "


\n"; } print "New Search\n

"; if ($::LIVE == 0) { if ($ascii == 1) { print_srch_results($name_asc); } if ($unicode == 1) { print_srch_results($name_uni); } } Print::print_html_footer(); return 0; } # Args are search string, grep flags, and array of hits sub print_srch_results { if (scalar(@_) != 1) { print "Missing Args for print_srch_results()\n"; return 1; } my $srch_name = shift; my $vol = Args::get_vol('vol'); my $ftype = $Caseman::vol2ftype{$vol}; my $addr_str = $Fs::addr_unit{$ftype}; unless (open(SRCH, "$srch_name")) { print "Error opening search file: $srch_name\n"; return 1; } my @results; my $grep_str = ""; my $grep_flag = ""; my $cnt = 0; my $type = 0; # ASCII iis 0 and Unicode is 1 my $prev = -1; while () { # The first line is a header if ($. == 1) { if (/^(\d+)\|(.*?)?\|(.*)$/) { $cnt = $1; $grep_flag = $2; $grep_str = $3; $type = 0; } else { print "Error pasing header of search file: $srch_name\n"; close(SRCH); return 1; } if ($grep_str =~ /^(.*?)\|unicode$/) { $grep_str = $1; $type = 1; } elsif ($grep_str =~ /^(.*?)\|ascii$/) { $grep_str = $1; } my $grep_str_html = Print::html_encode($grep_str); print "


\n"; if ($type == 0) { print "\n"; } else { print "\n"; } if ($cnt == 0) { print "$grep_str_html was not found
\n"; } elsif ($cnt == 1) { print "1 occurrence of $grep_str_html was found
\n"; } else { print "$cnt occurrences of $grep_str_html were found
\n"; } print "Search Options:
\n"; if ($type == 0) { print "  ASCII
\n"; } else { print "  Unicode
\n"; } if ($grep_flag =~ /\-i/) { print "  Case Insensitive
\n"; } else { print "  Case Sensitive
\n"; } if ($grep_flag =~ /\-E/) { print "  Regular Expression
\n"; } print "
\n"; if ($cnt > 1000) { print "There were more than 1000 hits.
\n"; print "Please revise the search to a managable amount.\n"; print "

The $cnt hits can be found in: $srch_name
\n"; close(SRCH); return 0; } next; } unless (/^(\d+)\|(\d+)\|(.*)?$/) { print "Error parsing results: $_\n"; close(SRCH); return 1; } my $blk = $1; my $off = $2; my $str = $3; if ($blk != $prev) { my $url = "$::PROGNAME?mod=$::MOD_DATA&view=$Data::CONT_MENU_FR&$Args::baseargs&block=$blk"; print "
\n$addr_str $blk (
Hex - " . "" . "Ascii"; print " - Original" if ( ($ftype eq 'dls') && (exists $Caseman::mod2vol{$vol})); print ")
"; $prev = $blk; } my $occ = $. - 1; if ($str ne "") { $str = Print::html_encode($str); print "$occ: $off ($str)
\n"; } else { print "$occ: $off
\n"; } } close(SRCH); return 0; } # Blank Page sub blank { Print::print_html_header(""); print "\n"; Print::print_html_footer(); return 0; } 1;