Files
2026-10-24 20:15:16 +00:00

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));
}