#!/usr/bin/perl
#
my $revision = '$Id: MacroScanner.pm,v 1.2 2001/12/22 02:47:58 bre Exp $';
my $version = 'Anomy 0.0.0 : sanitizer.pl';
#
##  Copyright (c) 2001 Bjarni R. Einarsson. All rights reserved.
##  Based on code by John Hardin.
##
##  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.
#
##############################################################################
#
# NOTE:  Sanitizer development is for the most part sponsored by
#        FRISK Software International, http://www.frisk.is/.  Please
#        consider buying their anti-virus products to show your 
#        appreciation.
#
##############################################################################
#
# This is the built in macro scanner, based on John D. Hardin's code.
#
# It has been improved to use only a fixed, small amount of memory
# even when reading binary data.  The scanner checks a sliding 256-byte
# window - first for a sign that this is an MS document, then for stuff 
# that looks like macros.
#
# When macro stuff is seen, a score is incremented by an amount hopefully
# reflecting how "dangerous" it is.  If the total score exceeds a user 
# defined value, the attachment is considered poisoned and the scan 
# terminates immediately with a nonzero return value.
#
# Usage:
#
#  # Macro scanner configuration - this doesn't make sense within the 
#  # Sanitizer.pm module, since the Sanitizer.pm module knows nothing 
#  # about the macro scanner.
#  my @MACROSCAN = ('file_list_5_scanner = 0:1:2:builtin/macro 25',
#                  'file_list_5_policy  = unknown:mangle:mangle:defang',
#                  'file_list_5 = (?i)\.(do[tc]|xl[aswct]|p[po]t|pps|rtf|md[abw])$');
#
#  # WARNING: For brevity, error handling is omitted in this example, see
#  #          sanitizer.pl for a proper example.
#  #
#  $engine = new Anomy::Sanitizer;
#  $engine->register_scanner("macro", \&MacroScan);
#  $engine->configure(@MACROSCAN);
#
##[ Package definition ]######################################################

package Anomy::Sanitizer::MacroScanner;
use strict;
use IO::File;
use Anomy::Log;
 
BEGIN {
    use Exporter ();
    use vars         qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
    $VERSION         = do { my @r = (q$Revision: 1.2 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
    @ISA             = qw(Exporter);
    @EXPORT          = qw(MacroScanner);
    @EXPORT_OK       = qw( );
}

use vars @EXPORT_OK;

##[ Package implementation ]##################################################

sub MacroScanner
{
    my $self = shift;
    my $plog = shift;
    my $fh = shift;
    my $md5x2 = shift;
    my $poison_score = shift;

    my $score = 0;
    my $msapp = 0;  
    
    # Read relatively small chunks at a time, to minimize the extra
    # work done by the pattern maching.  We just trust the OS to make
    # this efficient...
    #
    my $chunksize = 128;
    local $/ = \$chunksize;
    
    # Initialize buffer.
    $fh->seek(0, SEEK_SET);
    my $buff = <$fh> . <$fh>;

    my $log = $plog->sublog("MacroScan", SLOG_TRACE, undef);

    while ($buff)
    {   
        unless ($msapp) 
        {
            if ($buff =~ /\000(Microsoft (Word Document|PowerPoint|Excel( (Spread|Work)sheet)?)|MSWordDoc|Word\.Document\.[0-9]+|Excel\.Sheet\.[0-9]+)\000/)
            {
                $msapp = 1;

                $log->entry("is-MS-document", SLOG_DEBUG, undef,
                    "This appears to be an MS document, restarting scan.");
                
                # Restart scan
                $fh->seek(0, SEEK_SET);
                $buff = <$fh> . <$fh>;
                next;
            }
        }
        else
        {
            # Lots of while loops here - we replace the leading \000 boundary
            # with 'x' characters to ensure this eventually completes.
            #
            $score += 99 while ($buff =~ s/\000(VirusProtection)/x$1/i);
            $score += 99 while ($buff =~ s/\000(select\s[^\000]*shell\s*\()/x$1/i);
            $score +=  9 while ($buff =~ s/\000(regedit|SaveNormalPrompt|Outlook.Application\000)/x$1/i);
            $score +=  4 while ($buff =~ s/\000(ID="{[-0-9A-F]+)$/x$1/i);
            $score +=  4 while ($buff =~ s/\000(CreateObject)/x$1/i);
            $score +=  4 while ($buff =~ s/(?:\000|\004)(([a-z0-9_]\.)*(Autoexec|Workbook_(Open|BeforeClose)|Document_(Open|New|Close)))/x$1/i);
            $score +=  4 while ($buff =~ s/(?:\000|\004)(Logon|AddressLists|AddressEntries|Recipients|Subject|Body|Attachments|Logoff)/x$1/i);
            $score +=  2 while ($buff =~ s/\000(Shell|Options|CodeModule)/x$1/i);
            $score +=  2 while ($buff =~ s/\000(([a-z]+\.)?Application\000)/x$1/i);
            $score +=  2 while ($buff =~ s/(?:\000|\004)(stdole|NormalTemplate)/x$1/i);
            $score +=  1 while ($buff =~ s/\000(ID="{[-0-9A-F]+}"|ThisWorkbook\000|PrivateProfileString)/x$1/i);
            $score +=  1 while ($buff =~ s/(?:\000|\004)(ActiveDocument|ThisDocument)/x$1/i);
            $score +=  1 while ($buff =~ s/\000(\[?HKEY_(CLASSES_ROOT|CURRENT_USER|LOCAL_MACHINE))/x$1/);

            # Save cycles!  Return early!
            if ($score > $poison_score) 
            {
                $log->entry("bad-score", SLOG_INFO, 
                    { score => $score, bytes => $chunksize * $. },
                    "Score (%score%) exceeded $poison_score after ".
                    "scanning %bytes% bytes!");
                return 1;
            }
        }

        # Read on...
        $buff = substr($buff, $chunksize, $chunksize) . <$fh>;
    }

    $log->entry("bad-score", SLOG_INFO, { score => $score }, 
                "Attachment passed macro scan with a score of %score%."); 
    return 0;
}

1;
#EOF#
