Virus Scanner

One of my websites (not this one) was recently hit with a virus, which prompted me to write the following php virus scanner. The script is also useful for getting a directory structure listing or to search for strings other than virus signatures. The code comments below explain usage…

//
// Author: Jay Mackley. For public use with attribution.
// PHP version 5.2
// This program scans every file in every directory for potential virus strings
//
// Only valid 'fix' value is 'Y' otherwise just don't use it. Don't set this
// unless you KNOW what you are doing as this could destroy your
// website if there's a bug in your virus removal section...
$fix_mode = $_GET["fix"];
//
// 'list' triggers a scan results report and support one or more of these flags:
//   'N' for including files Not found by the scan (no virus string detected)
//   'E' for including Empty, zero length files detected
//   'F' for including files Found by the scan (virus string detected)
//   'C' for including files Changed by the scan (only possible if fix=Y)
//   'A' to print All directories and files, including indenting by directory structure.
// Multiple list flags can be combined in the URL. Example: list=FE
$list = $_GET["list"];
//
// 'type' is one or more substrings separate by a '|', that filters which filenames
// are considered for scanning. Usually the extension would be targeted
// but it could be any file substring. Example: .css|.js|.htm
// In the example, css, js, an .htm files would be scanned. html files are
// also scanned since .htm is a substring of .html
$type = $_GET["type"];
//
// 'find' is the substring of the file representing the signature of the
// virus being scanned for. Example: eval(escape
// The example would likely find the Gumbar web virus and variants
$find = $_GET["find"];
//
// The scan won't happen unless a password is passed in. Not very tight
// security but it keeps this scanning program from being run carelessly.
$pswd = $_GET["pswd"];
//
// Set default value to zero for counter variables.
$total_good = 0;
$total_bad = 0;
$total_links = 0;
$total_scanned = 0;
$total_files = 0;
$total_dirs = 0;
$total_links = 0;
$total_all = 0;
//
// This function checks for the virus and returns a string value indicating
// the results for each file path passed in.
function CheckVirus($my_path, $fix_mode, $my_virus_signature) {
  $my_text = file_get_contents($my_path);
  global $total_good;
  global $total_bad;
  global $total_scanned;
  global $missing_files;
  $total_scanned++;
  if ($my_text != false) {
    $signature_pos = strpos($my_text,$my_virus_signature);
    if ($signature_pos !== false) {
      $total_bad++;
      // Show the found virus signature string, plus 30 chars more - just a quick peek.
      $snippet = substr($my_text,$signature_pos,30);
      // make tags appear as literals so we don't mess up the listing
      $snippet = str_replace("<","<",$snippet);
      $snippet = str_replace(">",">",$snippet);
      if ($fix_mode === "Y") {
        // Here's where you need to be careful. The next line needs to be
        // customized for each specific type of virus occurrence. Here, I am
        // assuming the virus is always at the end of the file where it can
        // easily be stripped off.
        $my_text = rtrim(substr($my_text, 0, $signature_pos-120));
        //
        if (file_put_contents($my_path, $my_text)) {
          $output = " - <b>Changed</b>: <span style='font:orange'>$snippet</span>";
        }
      } else {
        $output = " - <b>Found</b>: <span style='color:red'>$snippet</span>";
      }
    } else {
      $total_good++;
      $output = " - <b>Not Found</b>: <span style='color:blue'>$my_virus_signature</span>";
    }
  } else {
    $missing_files++;
    $output = " - <b>Empty</b>: <span style='color:green'>Missing or zero length file</span></b>";
  }
  return $output;
}

function GetDirFiles($path, $level, $types, $find_string, $my_fix_mode, $list) {
  // Let's not scan the following, including ourself
  $ignore = array('.', '..','cgi-bin', $me_name);
  $dh = @opendir($path);
  global $total_all;
  global $total_dirs;
  global $total_files;
  global $total_links;
  global $my_types;
  global $me_name;
  while (($file = readdir($dh)) == true) {
    if (!in_array($file, $ignore)) {
      $total_all++;
      // We can easily tell how much to indent by counting the forward slashes
      if (strpos($list,"A") !== false) $spaces = str_repeat('&nbsp;', substr_count("$path/$file",'/')*4);
      if (is_dir("$path/$file")) {
        // Report on each directory path encountered
        $total_dirs++;
        if (strpos($list,"A") !== false) echo "<strong>$spaces $path/$file</strong><br />";
        // This routine is recursive. That's how it checks all the directories...
        GetDirFiles("$path/$file", ($level+1), $types, $find_string, $my_fix_mode, $list);
      } elseif (is_link("$path/$file")) {
        // Report on links encountered.
        $total_links++;
        if (strpos($list,"A") !== false) echo "$spaces@$path/$file<br />"; // Report links
      } else {
        if (strpos($list,"A") !== false) echo "$spaces$path/$file ";
        $output = "";
        $total_files++;
        for ($i=0; $i<count($types); $i++) {
          // We shouldn't encounter an empty string but check just in case
          if ($types[$i] !== "") {
            // If the file qualifies by type sub-string then continue
            if (strpos($file,$types[$i]) !== false) {
              // If we're looking for an empty string then skip but that shouldn't happen either...
              if ($find_string !== "") {
                // Set the path a call the CheckVirus function to see if we got a problem
                $my_path = "$path/$file";
                $my_path = substr($my_path,2,9999);
                $output = CheckVirus($my_path, $my_fix_mode, $find_string, $list);
                // A is a full directory listing
                if (strpos($list,"A") !== false) {
                  echo $output;
                } else {
                  // Just print out the results of the virus check. One of these cases should apply.
                  if (strpos($list,"C") !== false && strpos($output,"Changed") !== false) {
                    echo "$my_path - $output<br />";
                  } else if (strpos($list,"F") !== false && strpos($output,">Found") !== false) {
                    echo "$my_path - $output<br />";
                  } else if (strpos($list,"N") !== false && strpos($output,"Not Found") !== false) {
                    echo "$my_path - $output<br />";
                  } else if (strpos($list,"E") !== false && strpos($output,"Empty") !== false) {
                    echo "$my_path - $output<br />";
                  }
                }
              }
            }
          }
        }
        if (strpos($list,"A") !== false) echo "<br />";
      }
    }
  }
  closedir($dh);
}
// Main Program
if ($pswd === "your password here") {
  echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"';
  // Get my own name for later use.
  $me_path = $_SERVER["SCRIPT_NAME"];
  $pieces = explode('/', $me_path);
  $me_name = $pieces[count($pieces) - 1];
  //
  echo '<html><head></head><body><br />';
  $types = explode('|', $type);
  echo "Looking for the string: <b>".$find."</b><br />";
  echo "For file names containing: <b>".implode(" ", $types)."</b><br /><br />";
  GetDirFiles(".",$listing,$types,$find,$fix_mode,$list);
  echo "<br />";
  echo "Total empty files = $missing_files<br />";
  echo "Total non matching files = $total_good<br />";
  echo "Total matching files = $total_bad<br />";
  echo "Total scanned files = $total_scanned<br />";
  echo "<br />";
  echo "Total directories = $total_dirs<br />";
  echo "Total files = $total_files<br />";
  echo "Total links = $total_links<br />";
  echo "Total links, files, directories = $total_all<br />";
  echo "</body></html>";
}

Leave a Reply

Your email address will not be published. Required fields are marked *