Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
419 lines (330 sloc) 13.3 KB
<?php
ob_start();
require_once('XmlValidator.inc.php');
require_once('FileInfo.inc.php');
require_once('Writer.inc.php');
require_once('Quarantiner.inc.php');
ob_end_clean();
class MalwareDetector
{
function __construct()
{
global $projectRootDir, $projectTmpDir;
if (!function_exists('escapedHexToHex') || !function_exists('escapedOctDec')) {
die('escapedHexToHex or escapedOctDec is missing');
}
$this->SIGNATURE_FILENAME = $projectRootDir . '/static/signatures/malware_db.xml';
$this->QUEUE_FILENAME = $projectTmpDir . '/scan_queue.manul.tmp.txt';
$this->QUEUE_OFFSET_FILENAME = $projectTmpDir . '/queue_offset.manul.tmp.txt';
$this->MALWARE_LOG_FILENAME = $projectTmpDir . '/malware_log.manul.tmp.txt';
$this->MALWARE_QUARANTINE_FILENAME = $projectTmpDir . '/malware_quarantine.manul.tmp.txt';
$this->MALWARE_QUARANTINE_FILEPATH_FILEPATH = $projectTmpDir . '/malware_quarantine_filepath.tmp.txt';
$this->XML_LOG_FILENAME = $projectTmpDir . '/scan_log.xml';
$this->SCRIPT_START = time();
$this->MAX_FILESIZE = 1 * 1024 * 1024; // 1MB
$this->MAX_PREVIEW_LENGTH = 80; // characters
$this->MAX_EXECUTION_DURATION = 20;
$validator = new XmlValidator();
if (!$validator->validate(implode('', file($this->SIGNATURE_FILENAME)), $projectRootDir . '/static/xsd/malware_db.xsd')) {
die(basename(__FILE__) . PS_ERR_MALWARE_DB_BROKEN);
}
$this->signatures = new DOMDocument();
$this->signatures->load($this->SIGNATURE_FILENAME);
}
function setRequestDelay($delay)
{
$this->MAX_EXECUTION_DURATION = $delay;
}
function throwTimeout($filePath)
{
echo $this->AJAX_HEADER_ERROR . "\n";
echo basename(__FILE__) . ': timeout while scanning ' . $filePath . "\nTry to increase an interval in settings.\n";
exit;
}
function normalizeContent($content)
{
$content = preg_replace_callback('/\\\\x([a-fA-F0-9]{1,2})/i', 'escapedHexToHex', $content); // strip hex ascii notation
$content = preg_replace_callback('/\\\\([0-9]{1,3})/i', 'escapedOctDec', $content); // strip dec ascii notation
$content = preg_replace('/[\'"]\s*?\.\s*?[\'"]/smi', '', $content); // concat fragmented strings
$content = preg_replace('|/\*.*?\*/|smi', '', $content); // remove comments to detect fragmented pieces of malware
return $content;
}
function getFragment($content, $pos)
{
$maxChars = $this->MAX_PREVIEW_LENGTH;
$maxLen = strlen($content);
$rightPos = min($pos + $maxChars, $maxLen);
$minPos = max(0, $pos - $maxChars);
$start = substr($content, 0, $pos);
$start = str_replace('\r', '', $start);
$lineNo = strlen($start) - strlen(str_replace("\n", '', $start)) + 1;
$res = 'L' . $lineNo . ': ' . substr($content, $minPos, $pos - $minPos) .
'@_MARKER_@' .
substr($content, $pos, $rightPos - $pos - 1);
return htmlspecialchars($res);
}
function queueQuarantine($filename)
{
if ($fh = fopen($this->MALWARE_QUARANTINE_FILENAME, 'a')) {
fputs($fh, $filename . "\n");
fclose($fh);
}
}
function checkForValidPhp($content)
{
$len = strlen($content);
$start = 0;
$valid = false;
while (($start = strpos($content, '<?', $start)) !== false) {
$valid = true;
$start++;
$end = strpos($content, '?>', $start + 1);
if ($end === false) {
$end = $len;
}
while (++$start < $end) {
$c = ord($content[$start]);
if ($c < 9 || ($c >= 14 && $c <= 31) || $c == 11 || $c == 12) {
return false;
}
}
}
return $valid;
}
function detectMalware($filePath, &$foundFragment, &$pos, $startTime, $timeout, $ext)
{
if (filesize($filePath) > $this->MAX_FILESIZE) {
return 'skipped';
}
if (!is_file($filePath)) {
return 'no_read';
}
$needToScan = false;
$extensions = array('ph' /* php, php3, phtml */, 'htm' /* htm, html */, 'txt', 'js', 'pl', 'cgi', 'py', 'bash', 'sh', 'xml', 'ssi', 'inc', 'pm', 'tpl');
// do scan for all kind of scripts
foreach ($extensions as $scanExt) {
if (strpos($ext, $scanExt) !== false) {
$needToScan = true;
}
}
$content = implode('', file($filePath));
$fileToBeScannedSignatureList = array(
'<?php',
'<?=',
'#!/usr/',
'#!/bin/',
'#!/local/',
'eval(',
'assert(',
'base64_decode('
);
if (!$needToScan) {
foreach ($fileToBeScannedSignatureList as $scanSig) {
if (strpos($content, $scanSig) !== false) {
$needToScan = true;
}
}
}
if (!$needToScan && $this->checkForValidPhp($content)) {
$needToScan = true;
}
if (!$needToScan) {
return 'no_need_to_check';
}
$normalized = $this->normalizeContent($content);
$db = $this->signatures->getElementsByTagName('signature');
$detected = false;
foreach ($db as $sig) {
if ($detected) break;
$currentTime = time();
if ($currentTime - $startTime > $timeout) {
return 'timeout';
}
$pos = -1;
$sigContent = $sig->nodeValue;
$attr = $sig->attributes;
$attrId = $attr->getNamedItem('id')->nodeValue;
$attrFormat = $attr->getNamedItem('format')->nodeValue;
$attrChildId = $attr->getNamedItem('child_id')->nodeValue;
$attrSeverity = $attr->getNamedItem('sever')->nodeValue;
switch ($attrFormat) {
case 're':
if ((preg_match('#(' . $sigContent . ')#smi', $content, $found, PREG_OFFSET_CAPTURE)) ||
(preg_match('#(' . $sigContent . ')#smi', $normalized, $found, PREG_OFFSET_CAPTURE))
) {
$detected = true;
$pos = $found[0][1];
continue;
}
break;
case 'const':
if ((($pos = strpos($content, $sigContent)) !== FALSE) ||
(($pos = strpos($normalized, $sigContent)) !== FALSE)
) {
$detected = true;
continue;
}
break;
}
}
if ($detected) {
$foundFragment = $this->getFragment($content, $pos);
return $attrSeverity;
}
}
function parseXml($xmlFilename)
{
$dom = null;
try {
$dom = new DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$dom->load($xmlFilename);
} catch (Exception $e) {
die('An exception has occured: ' . $e->getMessage() . "\n");
}
if (!$dom) {
die('An exception has occured: ');
}
return $dom;
}
function repackXMLLog()
{
global $projectRootDir;
$xmlLogFilename = $this->XML_LOG_FILENAME;
$xml = $this->parseXml($xmlLogFilename);
$xpath = new DOMXpath($xml);
if (!is_file($this->MALWARE_LOG_FILENAME)) {
die(basename(__FILE__) . ': cannot open ' . $this->MALWARE_LOG_FILENAME . ' during repacking');
}
$lines = file($this->MALWARE_LOG_FILENAME);
foreach ($lines as $lineNum => $line) {
#Example /home/www/badcode.tk/web_root/robots.txt.dist;detected=;pos=-1;snippet=
$data = explode(';', $line);
$filePath = $data[0];
$detected = substr($data[1], strlen('detected='));
if ($detected) {
$pos = substr($data[2], strlen('pos='));
$snippet = trim(substr($data[3], strlen('snippet=')));
#Getting current element in DOM
$relativePath = str_replace($_SERVER['DOCUMENT_ROOT'], '.', $filePath);
$filePathNode = $xpath->query('/website_info/files/file/path[text()="' . $relativePath . '"]')->item(0);
$fileinfoNode = $filePathNode->parentNode;
if (!$fileinfoNode) {
die("XML path error filePath={$filePath} relativePath={$relativePath} projectRootDir={$projectRootDir} docRoot={$_SERVER['DOCUMENT_ROOT']}");
}
#Adding detection info to DOM
if ($fileinfoNode) {
$fileinfoNode->setAttribute('detected', $detected);
$fileinfoNode->setAttribute('snippet', $snippet);
$fileinfoNode->setAttribute('pos', $pos);
}
}
}
$xml->save($xmlLogFilename);
return $xml->saveXML();
}
function buildQuarantineArchive()
{
if (file_exists($this->MALWARE_QUARANTINE_FILENAME)) {
$list = file($this->MALWARE_QUARANTINE_FILENAME);
if (count($list) > 0) {
$quarantiner = new Quarantiner();
foreach ($list as $filename) {
$quarantiner->add(trim($filename));
}
return $quarantiner->getArchive();
}
}
return null;
}
function finishMalwareScan()
{
global $php_errormsg;
$xml = $this->repackXMLLog();
if (file_exists($this->MALWARE_LOG_FILENAME)) {
unlink($this->MALWARE_LOG_FILENAME);
}
if (file_exists($this->QUEUE_OFFSET_FILENAME)) {
unlink($this->QUEUE_OFFSET_FILENAME);
}
if (file_exists($this->QUEUE_FILENAME)) {
@unlink($this->QUEUE_FILENAME);
}
$quarantineFilepath = $this->buildQuarantineArchive();
if ($quarantineFilepath) {
file_put_contents2($this->MALWARE_QUARANTINE_FILEPATH_FILEPATH, $quarantineFilepath);
}
if (file_exists($this->MALWARE_QUARANTINE_FILENAME)) {
@unlink($this->MALWARE_QUARANTINE_FILENAME);
}
$result = json_encode(array('type' => 'getSignatureScanResult', 'status' => 'finished', 'phpError' => $php_errormsg));
return $result;
}
function malwareScanRound()
{
global $php_errormsg;
$startTime = time();
if (!is_file($this->QUEUE_FILENAME)) {
die(basename(__FILE__) . ': cannot open ' . $this->QUEUE_FILENAME . ' on scan round');
}
$fh = fopen($this->QUEUE_FILENAME, 'r');
$offset = 0;
if (file_exists($this->QUEUE_OFFSET_FILENAME)) {
$offset = (int)file_get_contents($this->QUEUE_OFFSET_FILENAME);
fseek($fh, $offset);
}
if (filesize($this->QUEUE_FILENAME) - $offset <= 0) {
fclose($fh);
return $this->finishMalwareScan();
}
$queueText = fread($fh, filesize($this->QUEUE_FILENAME) - $offset);
$queueLines = explode("\n", $queueText);
$numFilesScanned = 0;
if (count($queueLines) < 1) {
fclose($fh);
return $this->finishMalwareScan();
}
foreach ($queueLines as $line) {
$executionTime = time() - $startTime;
if ($executionTime >= round($this->MAX_EXECUTION_DURATION * 0.8)) {
break;
} else if (empty($line)) {
continue;
}
$offset += strlen($line) + 1;
$fileinfo = explode(' ', $line);
$filePath = $fileinfo[0];
$fileHash = $fileinfo[1];
$snippet = '';
$fileExtension = pathinfo(basename($filePath), PATHINFO_EXTENSION);
$res = $this->detectMalware($filePath, $snippet, $pos, $this->SCRIPT_START, $this->MAX_EXECUTION_DURATION, $fileExtension);
switch ($res) {
case 'no_need_to_check':
break;
case 'no_read':
break;
case 'skipped':
break;
case 'timeout':
file_put_contents2($this->QUEUE_OFFSET_FILENAME, $offset);
$this->throwTimeout($filePath);
break;
default:
$numFilesScanned++;
$content = $filePath . ';detected=' . $res . ';pos=' . $pos . ';snippet=' . base64_encode($snippet) . PHP_EOL;
file_put_contents2($this->MALWARE_LOG_FILENAME, $content, 'a');
if ($res) {
$this->queueQuarantine($filePath);
}
}
}
file_put_contents2($this->QUEUE_OFFSET_FILENAME, $offset);
fclose($fh);
if (count($queueLines) <= 1) {
return $this->finishMalwareScan();
}
$data = array('filesScannedThisTime' => $numFilesScanned, 'filesLeft' => count($queueLines), 'lastFile' => $filePath);
$result = json_encode(array('type' => 'getSignatureScanResult', 'status' => 'inProcess', 'data' => $data, 'phpError' => $php_errormsg));
return $result;
}
} // End of class
You can’t perform that action at this time.