126 lines
7.5 KiB
PHP
126 lines
7.5 KiB
PHP
<?php
|
|
// Eingebaute Dateianalyse des Virenscans: braucht nichts auf dem Server, führt nie etwas aus und liest nur die ersten Megabytes.
|
|
// Sie erkennt gefährliche Dateitypen und Tricks, keine neuen Viren (dafür gibt es ClamAV und die Hash-Abfrage).
|
|
|
|
/** Dateiendungen, die Programme oder Skripte starten können. */
|
|
const VIRENSCAN_BAD_EXT = ['exe', 'scr', 'com', 'pif', 'bat', 'cmd', 'vbs', 'vbe', 'js', 'jse', 'wsf', 'wsh', 'ps1', 'psm1', 'msi', 'msp', 'hta', 'jar', 'cpl', 'dll', 'sys', 'lnk', 'reg', 'inf', 'sh', 'apk', 'iso', 'img', 'vhd', 'vhdx', 'gadget', 'application', 'appx', 'msix', 'settingcontent-ms', 'chm'];
|
|
/** Office-Dateien mit eingebauten Makros. */
|
|
const VIRENSCAN_MACRO_EXT = ['docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'ppam', 'ppsm', 'sldm', 'xlam'];
|
|
/** Archive, die sich nicht öffnen lassen. */
|
|
const VIRENSCAN_OTHER_ARCHIVES = ['rar', '7z', 'ace', 'arj', 'cab', 'z', 'zipx'];
|
|
|
|
function virenscan_file_ext(string $name): string
|
|
{
|
|
return strtolower(pathinfo(rtrim($name, '. '), PATHINFO_EXTENSION));
|
|
}
|
|
|
|
/** Art des Programms nach den ersten Bytes, oder null. */
|
|
function virenscan_exec_magic(string $head): ?string
|
|
{
|
|
if (str_starts_with($head, 'MZ')) return 'Windows-Programm (EXE/DLL)';
|
|
if (str_starts_with($head, "\x7fELF")) return 'Linux-Programm (ELF)';
|
|
foreach (["\xCF\xFA\xED\xFE", "\xCE\xFA\xED\xFE", "\xFE\xED\xFA\xCE", "\xFE\xED\xFA\xCF"] as $m) if (str_starts_with($head, $m)) return 'macOS-Programm (Mach-O)';
|
|
if (str_starts_with($head, "\xCA\xFE\xBA\xBE")) return 'Java-Programm oder Universalprogramm';
|
|
return null;
|
|
}
|
|
|
|
/** Namenstricks: umgekehrte Schreibrichtung, doppelte Endung. @return list<array{0:string,1:string}> */
|
|
function virenscan_name_findings(string $name): array
|
|
{
|
|
$out = [];
|
|
if (preg_match('/[\x{202A}-\x{202E}\x{2066}-\x{2069}]/u', $name)) $out[] = ['exec', 'Verschleierter Dateiname (Zeichen zur Umkehr der Schreibrichtung)'];
|
|
$ext = virenscan_file_ext($name);
|
|
if (in_array($ext, VIRENSCAN_BAD_EXT, true)) $out[] = ['exec', 'Gefährlicher Dateityp (.' . $ext . ')'];
|
|
elseif (preg_match('/\.(' . implode('|', array_map('preg_quote', VIRENSCAN_BAD_EXT)) . ')\s*\.[a-z0-9]{2,5}$/i', $name, $m)) $out[] = ['exec', 'Doppelte Endung (.' . strtolower($m[1]) . ' versteckt)'];
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Untersucht eine Datei. @return list<array{0:string,1:string}> Funde als [Kategorie, Text]. Kategorien: exec (Programme, Skripte, Tricks),
|
|
* macro (Office-Makros), script (Skripte in HTML/SVG/RTF/PDF), archive (auffällige Archive), encrypted (passwortgeschützt), unscannable (nicht prüfbare Archive), eicar.
|
|
*/
|
|
function virenscan_analyze(string $path, string $name, int $depth = 0): array
|
|
{
|
|
$f = [];
|
|
$size = (int) @filesize($path);
|
|
$head = (string) @file_get_contents($path, false, null, 0, 4096);
|
|
if ($head === '' && $size === 0) return $f;
|
|
$ext = virenscan_file_ext($name);
|
|
|
|
if (str_contains((string) @file_get_contents($path, false, null, 0, 2 * 1024 * 1024), virenscan_eicar())) return [['eicar', 'Eicar-Test-Signature']];
|
|
foreach (virenscan_name_findings($name) as $x) $f[] = $x;
|
|
if (($kind = virenscan_exec_magic($head)) !== null) $f[] = ['exec', $kind . ($ext !== '' && !in_array($ext, VIRENSCAN_BAD_EXT, true) ? ', getarnt als .' . $ext : '')];
|
|
if (in_array($ext, VIRENSCAN_MACRO_EXT, true)) $f[] = ['macro', 'Office-Datei mit Makros (.' . $ext . ')'];
|
|
if (in_array($ext, VIRENSCAN_OTHER_ARCHIVES, true)) $f[] = ['unscannable', 'Archiv, das nicht geöffnet werden kann (.' . $ext . ')'];
|
|
|
|
// ZIP (auch Office-Dateien): Inhalt prüfen
|
|
if (str_starts_with($head, "PK\x03\x04") && class_exists('ZipArchive')) {
|
|
$za = new ZipArchive();
|
|
if ($za->open($path) === true) {
|
|
$n = $za->numFiles;
|
|
if ($n > 2000) $f[] = ['archive', 'Archiv mit sehr vielen Dateien (' . $n . ')'];
|
|
$total = 0; $encrypted = false; $macro = false; $bad = [];
|
|
for ($i = 0; $i < min($n, 2000); $i++) {
|
|
$st = $za->statIndex($i);
|
|
if (!$st) continue;
|
|
$total += (int) $st['size'];
|
|
$en = (string) $st['name'];
|
|
if (!empty($st['encryption_method'])) { $encrypted = true; continue; }
|
|
if (str_ends_with(strtolower($en), 'vbaproject.bin')) $macro = true;
|
|
foreach (virenscan_name_findings(basename($en)) as $x) { $bad[] = basename($en); break; }
|
|
if ($i < 300 && (int) $st['size'] > 0 && virenscan_exec_magic((string) $za->getFromIndex($i, 8)) !== null) $bad[] = basename($en);
|
|
}
|
|
if ($total > 1024 * 1024 * 1024 || ($size > 0 && $total / max(1, $size) > 2000 && $total > 100 * 1024 * 1024)) $f[] = ['archive', 'Archiv entpackt sich auf ein Vielfaches (Zip-Bombe)'];
|
|
if ($macro) $f[] = ['macro', 'Office-Datei mit Makros (VBA)'];
|
|
if ($bad) $f[] = ['exec', 'Archiv enthält ' . implode(', ', array_slice(array_values(array_unique($bad)), 0, 3))];
|
|
if ($encrypted) $f[] = ['encrypted', 'Passwortgeschütztes Archiv (Inhalt nicht prüfbar)'];
|
|
$za->close();
|
|
} elseif (!in_array($ext, ['docx', 'xlsx', 'pptx', 'odt', 'ods', 'jar', 'apk'], true)) {
|
|
$f[] = ['unscannable', 'Archiv lässt sich nicht öffnen'];
|
|
}
|
|
}
|
|
|
|
// Altes Office-Format (OLE): Makros erkennen
|
|
if (str_starts_with($head, "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1")) {
|
|
$body = (string) @file_get_contents($path, false, null, 0, 4 * 1024 * 1024);
|
|
if (str_contains($body, '_VBA_PROJECT') || str_contains($body, "_\0V\0B\0A\0_\0P\0R\0O\0J\0E\0C\0T") || str_contains($body, "M\0a\0c\0r\0o\0s\0")) $f[] = ['macro', 'Office-Datei mit Makros (VBA)'];
|
|
}
|
|
|
|
// PDF: startende Programme oder automatisches JavaScript
|
|
if (str_contains(substr($head, 0, 1024), '%PDF')) {
|
|
$body = (string) @file_get_contents($path, false, null, 0, 4 * 1024 * 1024);
|
|
if (preg_match('/\/Launch\b/', $body)) $f[] = ['script', 'PDF startet Programme (/Launch)'];
|
|
if (preg_match('/\/(JavaScript|JS)\b/', $body) && preg_match('/\/(OpenAction|AA)\b/', $body)) $f[] = ['script', 'PDF mit automatisch startendem JavaScript'];
|
|
if (preg_match('/\/EmbeddedFile\b/', $body) && preg_match('/\.(exe|scr|bat|cmd|vbs|js|jar|ps1|msi)\b/i', $body)) $f[] = ['exec', 'PDF mit eingebetteter ausführbarer Datei'];
|
|
}
|
|
|
|
// Webseiten und Bilder mit Skriptcode
|
|
if (in_array($ext, ['html', 'htm', 'xhtml', 'shtml', 'svg', 'mht', 'mhtml', 'xml'], true)) {
|
|
$body = (string) @file_get_contents($path, false, null, 0, 1024 * 1024);
|
|
if (preg_match('/<script\b|javascript:|\bon(load|error|click|mouseover)\s*=/i', $body)) $f[] = ['script', 'Enthält ausführbaren Skriptcode'];
|
|
}
|
|
// RTF mit eingebetteten Objekten
|
|
if (str_starts_with($head, '{\\rtf')) {
|
|
$body = (string) @file_get_contents($path, false, null, 0, 4 * 1024 * 1024);
|
|
if (preg_match('/\\\\(objdata|objupdate|objautlink)\b/', $body)) $f[] = ['exec', 'RTF mit eingebettetem Objekt'];
|
|
}
|
|
return $f;
|
|
}
|
|
|
|
/** Entscheidet nach den Einstellungen, welche Funde zum Sperren führen. @return list<string> Gründe, leer = sauber */
|
|
function virenscan_block_reasons(array $findings, array $cfg): array
|
|
{
|
|
$reasons = [];
|
|
foreach ($findings as [$cat, $text]) {
|
|
$on = match ($cat) {
|
|
'exec', 'archive', 'script', 'eicar' => true,
|
|
'macro' => $cfg['block_macros'],
|
|
'encrypted' => $cfg['block_encrypted'],
|
|
'unscannable' => $cfg['block_unscannable'],
|
|
default => true,
|
|
};
|
|
if ($on) $reasons[] = $text;
|
|
}
|
|
return array_values(array_unique($reasons));
|
|
}
|