Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/antivirusliteAntivirusLite.ps1PowerShell
# AntivirusLite.ps1
# Author: Gorstak
$Config = @{
BaseDirectory = "C:\ProgramData\Antivirus"
QuarantineDirectory = "C:\ProgramData\Antivirus\Quarantine"
BackupDirectory = "C:\ProgramData\Antivirus\Backup"
LogFile = "C:\ProgramData\Antivirus\av.log"
BlockedLogFile = "C:\ProgramData\Antivirus\blocked.log"
DatabaseFile = "C:\ProgramData\Antivirus\known_files.db"
OwnPidFile = "C:\ProgramData\Antivirus\av_pids.txt"
MaxDatabaseEntries = 50000
MaxLogSizeMB = 10
MemoryScanIntervalSec = 300 # was 8s. Memory scans walk .Modules across
# every process, which bloats working set.
# Run periodically (every 5 min) instead.
MemoryScanMaxSizeMB = 150
ProcessTimeoutSeconds = 30
DatabaseCleanupDays = 30
EnableMemoryScanning = $true # Set $false to disable in-memory scanning.
# String/module heuristic scan only.
# Runs every MemoryScanIntervalSec
# (periodic), not in a tight loop.
EnableRealtimeMonitor = $true
EnableAutoStart = $true # On launch (as admin) register a scheduled
# task so this script runs at every startup.
AutoStartTaskName = "AntivirusLite"
EnableThreatIntel = $true
EnableAlerts = $true
AutoQuarantine = $true
MalwareBazaarApiKey = ""
CirclHashLookupUrl = "https://hashlookup.circl.lu/lookup/sha256"
CymruApiUrl = "https://api.malwarehash.cymru.com/v1/hash"
MalwareBazaarApiUrl = "https://mb-api.abuse.ch/api/v1/"
CymruDetectionThreshold = 60
SuspiciousFileSizeKB = 3072
}
$BehaviorConfig = @{
EnableBehaviorKill = $true
EnableAutoBlockC2 = $true
DeepScanIntervalHours = 6
ThreatIntelUpdateDays = 7
}
# ============================================================================
# C2 / Reverse-Shell killer configuration
# Adapted from Antivirus.ps1 (Gorstak) - scored C2 detection, beacon tracking,
# reverse-shell command-line heuristics, and firewall auto-block.
# ============================================================================
$C2Config = @{
EnableC2Detection = $true # Scored outbound-connection analysis + kill
EnableReverseShellKill = $true # Reverse-shell command-line + handle heuristics
EnableBeaconDetection = $true # Regular-interval callback (beacon) detection
EnableFirewallAutoBlock = $true # Drop a blocking firewall rule on the remote IP
KillScoreThreshold = 50 # Score at/above which the owning process is killed
FirewallBlockThreshold = 40 # Score at/above which the remote IP is firewall-blocked
BeaconMinSamples = 4 # Minimum callbacks before a beacon verdict
BeaconMaxJitterRatio = 0.25 # stdDev/avg below this = regular = beacon
BeaconMinIntervalSec = 10 # Ignore very chatty intervals (normal traffic)
BeaconMaxIntervalSec = 86400 # Ignore intervals longer than a day
IpRotationThreshold = 12 # One process talking to this many unique IPs = C2 rotation
}
# Ports strongly associated with Metasploit/backdoors/reverse shells.
# Value = base score contribution when a remote connection uses this port.
$C2SuspiciousPorts = @{
4444 = 30; 4445 = 30; 5555 = 30; 6666 = 20; 7777 = 20; 8888 = 15
9001 = 20; 9002 = 20; 9999 = 15; 1337 = 25; 31337 = 25
12345 = 20; 54321 = 20; 50050 = 35 # 50050 = Cobalt Strike team-server default
}
# Known-bad / bulletproof-hoster IP prefixes (coarse heuristic, scored not blind-blocked)
$C2MaliciousRangePrefixes = @('5.8.', '45.', '91.', '185.', '194.', '195.')
# TLDs commonly abused for free/anonymous C2 domains
$C2SuspiciousTLDs = @('.onion', '.bit', '.i2p', '.tk', '.ml', '.ga', '.cf', '.gq', '.top', '.xyz', '.duckdns.org', '.ddns.net', '.no-ip.org', '.hopto.org')
# Interpreters/LOLBins that should rarely hold a raw outbound socket to the internet
$C2SuspiciousProcesses = @('cmd', 'powershell', 'pwsh', 'wscript', 'cscript', 'mshta', 'rundll32', 'regsvr32', 'certutil', 'bitsadmin', 'installutil', 'msbuild')
# Processes that legitimately open localhost listeners + outbound 443 (skip for proxy pattern)
$C2ProxyExcludeProcesses = @('svchost', 'msedge', 'chrome', 'firefox', 'opera', 'brave', 'iexplore', 'ApplicationFrameHost', 'System', 'Idle')
# Reverse-shell command-line signatures (regex, case-insensitive)
$ReverseShellPatterns = @(
# PowerShell reverse shells
'powershell.*-nop.*-w.*hidden.*-(e|enc|encodedcommand)',
'powershell.*-(e|enc|encodedcommand)\s+[A-Za-z0-9+/=]{40,}',
'new-object\s+system\.net\.sockets\.tcpclient',
'new-object\s+net\.sockets\.tcpclient',
'\$client\s*=\s*new-object.*tcpclient',
'getstream\(\).*\.read\(|\.write\(.*getstream',
'iex\s*\(.*\.getstream',
# nix-style / bash reverse shells (via WSL, git-bash, etc.)
'/dev/tcp/\d{1,3}(\.\d{1,3}){3}/\d+',
'bash\s+-i\s+>&\s*/dev/tcp',
'sh\s+-i\s+2>&1',
# netcat / socat family
'\bnc(\.exe)?\b.*(-e|-c)\s+(cmd|/bin/sh|bash|powershell)',
'\bncat(\.exe)?\b.*(-e|--exec)',
'\bsocat\b.*exec',
# cmd piping its I/O to a socket helper
'cmd(\.exe)?\s+/c\s+.*(nc|ncat|socat|telnet)\b',
# python/perl/php one-liner reverse shells
'python.*socket.*connect.*subprocess.*(call|popen)',
'perl.*socket.*(exec|system)\(',
'php\s+-r.*fsockopen'
)
$RulesDirectory = Join-Path $Config.BaseDirectory "rules"
$KnownFilesCache = @{}
# Beacon tracking: per "PID:RemoteIP" key, keep the timestamps we've seen an
# established connection. A callback that recurs at a near-constant interval is
# a beacon. We sample on each scan cycle rather than hooking the network stack.
$script:BeaconHistory = @{}
# Remote IPs we have already firewall-blocked, so we don't re-add rules each cycle.
$script:BlockedC2Ips = [System.Collections.Generic.HashSet[string]]::new()
$IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
$AllowedSIDs = @('S-1-2-0', 'S-1-5-20')
$RiskyPaths = @('\temp\', '\downloads\', '\appdata\local\temp\', '\public\', '\windows\temp\', '\appdata\roaming\', '\desktop\')
$MonitoredExtensions = @(
# Standard executable and script extensions
'.exe','.dll','.sys','.ocx','.scr','.com','.cpl','.msi','.drv','.winmd',
'.ps1','.bat','.cmd','.vbs','.js','.hta','.jse','.wsf','.wsh','.psc1','.exif',
# Extended list (unchanged from original)
'.zoo','.zlo','.zfsendtotarget','.z','.xz','.xsl','.xps','.xpi','.xnk','.xml',
'.xlw','.xltx','.xltm','.xlt','.xlsx','.xlsm','.xlsb','.xls','.xlm','.xll',
'.xld','.xlc','.xlb','.xlam','.xla','.xip','.xbap','.xar','.wwl','.wsc',
'.ws','.wll','.wiz','.website','.webpnp','.webloc','.wbk','.was','.vxd',
'.vsw','.vst','.vss','.vsmacros','.vhdx','.vhd','.vbp','.vb','.url','.tz',
'.txz','.tsp','.tpz','.tool','.tmp','.tlb','.theme','.tgz','.terminal',
'.term','.tbz','.taz','.tar','.swf','.stm','.spl','.slk','.sldx',
'.sldm','.sit','.shs','.shb','.settingcontent-ms','.search-ms','.searchconnector-ms',
'.sea','.sct','.scf','.rtf','.rqy','.rpy','.rev','.reg','.rb',
'.rar','.r09','.r08','.r07','.r06','.r05','.r04','.r03','.r02','.r01',
'.r00','.pyzw','.pyz','.pyx','.pywz','.pyw','.pyt','.pyp','.pyo','.pyi',
'.pyde','.pyd','.pyc','.py3','.py','.pxd','.pstreg','.pst','.psdm1','.psd1',
'.prn','.printerexport','.prg','.prf','.pptx','.pptm','.ppt','.ppsx','.ppsm',
'.pps','.ppam','.ppa','.potx','.potm','.pot','.plg','.pl','.pkg','.pif',
'.pi','.perl','.pcd','.pa','.osd','.oqy','.ops','.one','.ods',
'.ntfs','.nsh','.nls','.mydocs','.mui','.msu','.mst','.msp','.mshxml',
'.msh2xml','.msh2','.msh1xml','.msh1','.msh','.mof','.mmc','.mhtml','.mht',
'.mdz','.mdw','.mdt','.mdn','.mdf','.mde','.mdb','.mda','.mcl','.mcf',
'.may','.maw','.mav','.mau','.mat','.mas','.mar','.maq','.mapimail',
'.manifest','.mam','.mag','.maf','.mad','.lzh','.local','.library-ms',
'.lha','.ldb','.laccdb','.ksh','.job','.jnlp','.jar','.its','.isp','.iso',
'.iqy','.ins','.ini','.inf','.img','.ime','.ie','.hwp','.htt','.htm',
'.htc','.hpj','.hlp','.hex','.gz','.grp','.glk','.gadget',
'.fxp','.fon','.fat','.elf','.ecf','.dqy','.dotx','.dotm',
'.dot','.docm','.docb','.doc','.dmg','.dir','.dif','.diagcab',
'.desktop','.desklink','.der','.dcr','.db','.csv','.csh','.crx','.crt',
'.crazy','.cpx','.command','.cnt','.cnv','.clb',
'.class','.cla','.chm','.chi','.cfg','.cer','.cdb','.cab','.bzip2','.bzip',
'.bz2','.bz','.bas','.ax','.asx','.aspx','.asp','.asa','.arj',
'.arc','.appref-ms','.application','.app','.air','.adp','.adn','.ade',
'.ad','.acm','.accdu','.accdt','.accdr','.accde','.accda','.c','.h'
)
$ProtectedProcesses = @(
'System', 'Idle', 'Registry', 'smss', 'csrss', 'wininit', 'services', 'lsass',
'svchost', 'winlogon', 'explorer', 'dwm', 'SearchUI', 'SearchIndexer', 'fontdrvhost',
'RuntimeBroker', 'sihost', 'taskhostw'
)
$EvilStrings = @(
'mimikatz', 'sekurlsa::', 'kerberos::', 'lsadump::', 'wdigest', 'tspkg',
'http-beacon', 'https-beacon', 'cobaltstrike', 'sleepmask', 'reflective',
'amsi.dll', 'AmsiScanBuffer', 'EtwEventWrite', 'MiniDumpWriteDump',
'VirtualAllocEx', 'WriteProcessMemory', 'CreateRemoteThread',
'ReflectiveLoader', 'sharpchrome', 'rubeus', 'safetykatz', 'sharphound',
'invoke-mimikatz', 'invoke-bloodhound', 'powersploit', 'empire'
)
New-Item -ItemType Directory -Path $Config.BaseDirectory, $Config.QuarantineDirectory, $Config.BackupDirectory, $RulesDirectory -Force -ErrorAction SilentlyContinue | Out-Null
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$logLine = "$timestamp | $Message"
try {
$logLine | Out-File -FilePath $Config.LogFile -Append -Encoding UTF8
} catch {
Write-Host "[LOG ERROR] $_"
}
Write-Host $logLine
if (Test-Path $Config.LogFile) {
$logSize = (Get-Item $Config.LogFile -ErrorAction SilentlyContinue).Length
if ($logSize -ge ($Config.MaxLogSizeMB * 1MB)) {
$archiveName = "$($Config.BaseDirectory)\av_log_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
try {
Rename-Item -Path $Config.LogFile -NewName $archiveName -ErrorAction SilentlyContinue
} catch {}
}
}
}
# --- Working-set trimming -----------------------------------------------
# GC.Collect() frees managed memory inside the process but does NOT hand it
# back to the OS, so Task Manager's working set stays high. EmptyWorkingSet
# (psapi.dll) asks Windows to release this process's physical pages back to
# the system. For a background scanner that idles between cycles, the trimmed
# pages are genuinely unneeded, so this is a safe way to keep the footprint low.
# Ref: https://learn.microsoft.com/windows/desktop/api/psapi/nf-psapi-emptyworkingset
if (-not ([System.Management.Automation.PSTypeName]'AV.MemTrim').Type) {
Add-Type -Namespace AV -Name MemTrim -MemberDefinition @'
[System.Runtime.InteropServices.DllImport("psapi.dll")]
public static extern bool EmptyWorkingSet(System.IntPtr hProcess);
'@ -ErrorAction SilentlyContinue
}
# --- Self-PID registry --------------------------------------------------
# Every AV-owned PowerShell process (the main script and each background job)
# records its own PID in a shared file. The memory scanner reads this file so
# it never scans or kills the antivirus's own processes - while still scanning
# every PowerShell process the AV did NOT spawn.
function Register-OwnPid {
param([int]$ProcessId = $PID)
try {
$mutex = New-Object System.Threading.Mutex($false, 'Global\AV_PidFile')
[void]$mutex.WaitOne(2000)
try {
Add-Content -Path $Config.OwnPidFile -Value $ProcessId -ErrorAction SilentlyContinue
} finally {
$mutex.ReleaseMutex(); $mutex.Dispose()
}
} catch {}
}
function Remove-DeadOwnPids {
# Rewrite the shared PID file keeping only PIDs whose process is still
# alive, so the registry does not accumulate stale entries over long uptime.
try {
$mutex = New-Object System.Threading.Mutex($false, 'Global\AV_PidFile')
[void]$mutex.WaitOne(2000)
try {
if (-not (Test-Path $Config.OwnPidFile)) { return }
$alive = [System.Collections.Generic.HashSet[int]]::new()
foreach ($line in (Get-Content -Path $Config.OwnPidFile -ErrorAction SilentlyContinue)) {
$n = 0
if ([int]::TryParse(($line.Trim()), [ref]$n)) {
if (Get-Process -Id $n -ErrorAction SilentlyContinue) { [void]$alive.Add($n) }
}
}
# Our own PID is always alive here; make sure it stays listed.
[void]$alive.Add($PID)
Set-Content -Path $Config.OwnPidFile -Value ($alive | Sort-Object) -ErrorAction SilentlyContinue
} finally {
$mutex.ReleaseMutex(); $mutex.Dispose()
}
} catch {}
}
# Fresh start: clear any stale PIDs from a previous run, then register self.
try { Remove-Item -Path $Config.OwnPidFile -Force -ErrorAction SilentlyContinue } catch {}
Register-OwnPid -ProcessId $PID
function Clear-ProcessMemory {
# Compact the Large Object Heap, do a blocking collect, then trim the
# working set back to the OS. Call this after a scan pass completes.
try {
[System.Runtime.GCSettings]::LargeObjectHeapCompactionMode = `
[System.Runtime.GCLargeObjectHeapCompactionMode]::CompactOnce
} catch {}
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
[System.GC]::Collect()
try {
[void][AV.MemTrim]::EmptyWorkingSet([System.Diagnostics.Process]::GetCurrentProcess().Handle)
} catch {}
}
Write-Log "=== Clean Antivirus Starting ==="
Write-Log "C2 killer: $($C2Config.EnableC2Detection) | ReverseShell killer: $($C2Config.EnableReverseShellKill) | Beacon detect: $($C2Config.EnableBeaconDetection) | FW auto-block: $($C2Config.EnableFirewallAutoBlock)"
Write-Log "Admin: $IsAdmin | User: $env:USERNAME | SID: $([Security.Principal.WindowsIdentity]::GetCurrent().User.Value)"
function Load-Database {
if (Test-Path $Config.DatabaseFile) {
try {
$lines = Get-Content $Config.DatabaseFile -ErrorAction Stop
$count = 0
foreach ($line in $lines) {
if ($line -match '^([0-9a-f]{64}),(true|false),(.+)$') {
$hash = $matches[1]
$safe = [bool]::Parse($matches[2])
$timestamp = $matches[3]
try {
$entryDate = [datetime]::Parse($timestamp)
if ($entryDate -lt (Get-Date).AddDays(-$Config.DatabaseCleanupDays)) {
continue
}
} catch {}
$KnownFilesCache[$hash] = $safe
$count++
if ($count -ge $Config.MaxDatabaseEntries) {
break
}
}
}
Write-Log "Loaded $count entries from database"
} catch {
Write-Log "Failed to load database: $_"
$KnownFilesCache.Clear()
}
} else {
New-Item -Path $Config.DatabaseFile -ItemType File -Force -ErrorAction SilentlyContinue | Out-Null
Write-Log "Created new database file"
}
}
function Save-ToDatabase {
param(
[string]$Hash,
[bool]$IsSafe
)
if (-not $Hash) { return }
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$entry = "$Hash,$IsSafe,$timestamp"
try {
$entry | Out-File -FilePath $Config.DatabaseFile -Append -Encoding UTF8
$KnownFilesCache[$Hash] = $IsSafe
} catch {
Write-Log "Failed to save to database: $_"
}
# Hard cap the in-memory cache. A dictionary that only ever grows is a leak;
# GC cannot reclaim live entries. Evict oldest ~10% when over the cap.
if ($KnownFilesCache.Count -gt $Config.MaxDatabaseEntries) {
$evictCount = [int]($Config.MaxDatabaseEntries * 0.1)
$toEvict = @($KnownFilesCache.Keys | Select-Object -First $evictCount)
foreach ($k in $toEvict) { [void]$KnownFilesCache.Remove($k) }
Write-Log "KnownFilesCache trimmed by $($toEvict.Count) entries (cap: $($Config.MaxDatabaseEntries))"
}
}
Load-Database
function Register-AutoStart {
# Registers a scheduled task that runs THIS script at system startup as
# SYSTEM with highest privileges (silent elevation, no UAC prompt).
# Idempotent: if the task already points at this script path, it is left
# as-is. Requires admin; silently skips otherwise.
if (-not $Config.EnableAutoStart) { return }
if (-not $IsAdmin) {
Write-Log "AutoStart skipped - not running as administrator (task registration needs admin)"
return
}
$taskName = $Config.AutoStartTaskName
$scriptPath = $PSCommandPath
if (-not $scriptPath) { $scriptPath = $MyInvocation.MyCommand.Definition }
if (-not $scriptPath -or -not (Test-Path $scriptPath)) {
Write-Log "AutoStart skipped - could not resolve this script's own path"
return
}
try {
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($existing) {
# Already registered - verify it still targets this script.
$argMatch = $existing.Actions | Where-Object { $_.Arguments -like "*$scriptPath*" }
if ($argMatch) {
Write-Log "AutoStart task '$taskName' already present and current"
return
}
Write-Log "AutoStart task '$taskName' exists but targets a different path - re-registering"
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
}
$psExe = (Get-Process -Id $PID).Path
if (-not $psExe) { $psExe = "powershell.exe" }
$action = New-ScheduledTaskAction -Execute $psExe `
-Argument ('-ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}"' -f $scriptPath) `
-WorkingDirectory (Split-Path -Parent $scriptPath)
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::Zero)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings `
-Description 'Launches AntivirusLite at system startup' -ErrorAction Stop | Out-Null
Write-Log "AutoStart task '$taskName' registered (runs at startup as SYSTEM)"
} catch {
Write-Log "AutoStart registration failed: $_"
}
}
Register-AutoStart
function Test-ShouldExclude {
param([string]$FilePath)
$lower = $FilePath.ToLower()
if ($lower -like '*\assembly\*') { return $true }
if ($lower -like '*\winsxs\*') { return $true }
if ($lower -like '*\microsoft.net\*') { return $true }
if ($lower -like '*\windows\system32\config\*') { return $true }
if ($lower -like '*ctfmon*' -or $lower -like '*msctf.dll' -or $lower -like '*msutb.dll') {
return $true
}
return $false
}
function Get-FileHashSafe {
param([string]$FilePath)
if (-not (Test-Path $FilePath)) { return $null }
try {
return (Get-FileHash -Path $FilePath -Algorithm SHA256 -ErrorAction Stop).Hash.ToLower()
} catch {
return $null
}
}
function Get-FileSignatureInfo {
param([string]$FilePath)
if (-not (Test-Path $FilePath)) { return $null }
try {
$signature = Get-AuthenticodeSignature -FilePath $FilePath -ErrorAction Stop
$hash = Get-FileHashSafe -FilePath $FilePath
return [PSCustomObject]@{
Hash = $hash
Status = $signature.Status
StatusMessage = $signature.StatusMessage
SignerName = $signature.SignerCertificate.Subject
}
} catch {
return $null
}
}
function Test-CirclHashLookup {
param([string]$SHA256)
if (-not $SHA256) { return $false }
try {
$url = "$($Config.CirclHashLookupUrl)/$SHA256"
$response = Invoke-RestMethod -Uri $url -TimeoutSec 8 -ErrorAction Stop
if ($response) {
Write-Log "CIRCL known-good match: $SHA256"
return $true
}
} catch {}
return $false
}
function Test-CymruMalwareHash {
param([string]$SHA256)
if (-not $SHA256) { return $false }
try {
$url = "$($Config.CymruApiUrl)/$SHA256"
$response = Invoke-RestMethod -Uri $url -TimeoutSec 8 -ErrorAction Stop
if ($response.detections -ge $Config.CymruDetectionThreshold) {
Write-Log "CYMRU malware match: $SHA256 (detections: $($response.detections))"
return $true
}
} catch {}
return $false
}
function Test-MalwareBazaarHash {
param([string]$SHA256)
if (-not $SHA256) { return $false }
try {
$body = @{
query = 'get_info'
hash = $SHA256
}
if ($Config.MalwareBazaarApiKey) {
$body.api_key = $Config.MalwareBazaarApiKey
}
$response = Invoke-RestMethod -Uri $Config.MalwareBazaarApiUrl -Method Post -Body $body -TimeoutSec 10 -ErrorAction Stop
if ($response.query_status -eq 'ok' -or ($response.data -and $response.data.Count -gt 0)) {
Write-Log "MalwareBazaar match: $SHA256"
return $true
}
} catch {}
return $false
}
function Test-SuspiciousUnsignedDll {
param([string]$FilePath)
$extension = [IO.Path]::GetExtension($FilePath).ToLower()
if ($extension -notin @('.dll', '.winmd')) { return $false }
try {
$signature = Get-AuthenticodeSignature -FilePath $FilePath -ErrorAction Stop
if ($signature.Status -eq 'Valid') { return $false }
} catch {}
$fileInfo = Get-Item $FilePath -ErrorAction SilentlyContinue
if (-not $fileInfo) { return $false }
$fileSizeKB = $fileInfo.Length / 1KB
$pathLower = $FilePath.ToLower()
foreach ($riskyPath in $RiskyPaths) {
if ($pathLower -like "*$riskyPath*" -and $fileSizeKB -lt $Config.SuspiciousFileSizeKB) {
return $true
}
}
if ($pathLower -like '*\appdata\roaming\*' -and $fileSizeKB -lt 800 -and $fileInfo.Name -match '^[a-z0-9]{4,12}\.(dll|winmd)$') {
return $true
}
return $false
}
function Test-FileLocked {
param([string]$FilePath)
try {
$stream = [IO.File]::Open($FilePath, 'Open', 'ReadWrite', 'None')
$stream.Close()
return $false
} catch {
return $true
}
}
function Stop-ProcessesUsingFile {
param([string]$FilePath)
$fileName = [IO.Path]::GetFileName($FilePath)
try {
Get-Process | Where-Object {
try {
$_.Modules.FileName -contains $FilePath
} catch {
$false
}
} | ForEach-Object {
if ($ProtectedProcesses -notcontains $_.Name) {
Write-Log "Stopping process $($_.Name) (PID: $($_.Id)) using file: $FilePath"
try {
$_.CloseMainWindow() | Out-Null
Start-Sleep -Milliseconds 500
if (-not $_.HasExited) {
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
}
} catch {}
}
}
} catch {
try {
taskkill /F /FI "MODULES eq $fileName" 2>&1 | Out-Null
} catch {}
}
}
function Set-FileOwnership {
param([string]$FilePath)
try {
takeown /F $FilePath /A 2>&1 | Out-Null
icacls $FilePath /reset 2>&1 | Out-Null
icacls $FilePath /grant "Administrators:F" /inheritance:d 2>&1 | Out-Null
Write-Log "Set ownership and permissions for: $FilePath"
return $true
} catch {
Write-Log "Failed to set ownership: $_"
return $false
}
}
function Move-ToQuarantine {
param(
[string]$FilePath,
[string]$Reason
)
if (-not (Test-Path $FilePath)) {
Write-Log "Cannot quarantine - file not found: $FilePath"
return
}
if (Test-FileLocked -FilePath $FilePath) {
Stop-ProcessesUsingFile -FilePath $FilePath
Start-Sleep -Milliseconds 500
}
$fileName = [IO.Path]::GetFileName($FilePath)
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$backupPath = Join-Path $Config.BackupDirectory "${fileName}_${timestamp}.bak"
$quarantinePath = Join-Path $Config.QuarantineDirectory "${fileName}_${timestamp}"
try {
Copy-Item -Path $FilePath -Destination $backupPath -Force -ErrorAction Stop
Move-Item -Path $FilePath -Destination $quarantinePath -Force -ErrorAction Stop
Write-Log "QUARANTINED [$Reason]: $FilePath -> $quarantinePath"
Send-ThreatAlert -Severity "HIGH" -Message "File quarantined: $Reason" -Details $FilePath
} catch {
Write-Log "Quarantine failed for $FilePath : $_"
if (Set-FileOwnership -FilePath $FilePath) {
try {
Copy-Item -Path $FilePath -Destination $backupPath -Force -ErrorAction Stop
Move-Item -Path $FilePath -Destination $quarantinePath -Force -ErrorAction Stop
Write-Log "QUARANTINED (after ownership fix) [$Reason]: $FilePath"
} catch {
Write-Log "Quarantine still failed after ownership fix: $_"
}
}
}
}
function Block-FileExecution {
param(
[string]$FilePath,
[int]$ProcessId,
[string]$Type
)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$blockEntry = "$timestamp | BLOCKED $Type | $FilePath | PID $ProcessId"
try {
$blockEntry | Out-File -FilePath $Config.BlockedLogFile -Append -Encoding UTF8
} catch {}
Write-Log "BLOCKED $Type | $FilePath | PID $ProcessId"
try {
$process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
if ($process -and ($ProtectedProcesses -notcontains $process.ProcessName)) {
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
Write-Log "Killed process PID $ProcessId"
}
} catch {}
if (Test-Path $FilePath) {
Move-ToQuarantine -FilePath $FilePath -Reason "Real-time $Type block"
}
}
$global:EmailConfig = $null
$global:WebhookUrl = $null
function Send-ThreatAlert {
param(
[string]$Severity,
[string]$Message,
[string]$Details
)
if (-not $Config.EnableAlerts) { return }
$severityUpper = $Severity.ToUpper()
$entryType = switch ($severityUpper) {
"CRITICAL" { [System.Diagnostics.EventLogEntryType]::Error }
"HIGH" { [System.Diagnostics.EventLogEntryType]::Warning }
"MEDIUM" { [System.Diagnostics.EventLogEntryType]::Warning }
default { [System.Diagnostics.EventLogEntryType]::Information }
}
try {
if (-not [System.Diagnostics.EventLog]::SourceExists("Antivirus")) {
New-EventLog -LogName Application -Source "Antivirus" -ErrorAction SilentlyContinue
}
Write-EventLog -LogName Application -Source "Antivirus" `
-EventId 1001 -Message "$Message - $Details" `
-EntryType $entryType -ErrorAction SilentlyContinue
} catch {}
if ($global:EmailConfig) {
try {
Send-MailMessage @global:EmailConfig `
-Subject "[$severityUpper] Antivirus Alert" `
-Body "$Message`n`nDetails: $Details" `
-ErrorAction SilentlyContinue
} catch {
Write-Log "Email alert failed: $_"
}
}
if ($global:WebhookUrl) {
try {
$payload = @{
severity = $severityUpper
message = $Message
details = $Details
timestamp = Get-Date -Format 'o'
hostname = $env:COMPUTERNAME
} | ConvertTo-Json
Invoke-WebRequest -Uri $global:WebhookUrl -Method Post -Body $payload -ContentType 'application/json' -ErrorAction SilentlyContinue | Out-Null
} catch {
Write-Log "Webhook alert failed: $_"
}
}
}
function Update-ThreatIntelligence {
if (-not $Config.EnableThreatIntel) { return }
Write-Log "Updating threat intelligence feeds..."
$hashLists = @(
"https://raw.githubusercontent.com/davidonzo/Threat-Intel/master/lists/latest-hashes.txt"
)
foreach ($url in $hashLists) {
$fileName = Split-Path $url -Leaf
$outputPath = Join-Path $Config.BaseDirectory $fileName
try {
Invoke-WebRequest -Uri $url -OutFile $outputPath -UseBasicParsing -TimeoutSec 15 -ErrorAction Stop
Write-Log "Downloaded hash list: $fileName"
} catch {
Write-Log "Failed to download hash list $fileName : $_"
}
}
}
function Invoke-ThreatAnalysis {
param([string]$FilePath)
if (-not (Test-Path $FilePath -PathType Leaf)) { return }
if (Test-ShouldExclude -FilePath $FilePath) { return }
$extension = [IO.Path]::GetExtension($FilePath).ToLower()
if ($extension -notin $MonitoredExtensions) { return }
$fileHash = Get-FileHashSafe -FilePath $FilePath
if (-not $fileHash) { return }
if ($KnownFilesCache.ContainsKey($fileHash)) {
if (-not $KnownFilesCache[$fileHash]) {
if (Test-Path $FilePath) {
Write-Log "Known bad file re-detected - HARD DELETING: $FilePath (hash: $fileHash)"
try {
Remove-Item -Path $FilePath -Force -ErrorAction Stop
Write-Log "Successfully deleted known-bad file: $FilePath"
Send-ThreatAlert -Severity "HIGH" -Message "Known-bad file auto-deleted" -Details $FilePath
} catch {
Write-Log "Delete failed - falling back to quarantine: $_"
Move-ToQuarantine -FilePath $FilePath -Reason "Known threat (delete failed)"
}
}
}
return
}
if (Test-CirclHashLookup -SHA256 $fileHash) {
Save-ToDatabase -Hash $fileHash -IsSafe $true
Write-Log "ALLOWED (CIRCL known-good): $FilePath"
return
}
if (Test-CymruMalwareHash -SHA256 $fileHash) {
Save-ToDatabase -Hash $fileHash -IsSafe $false
Move-ToQuarantine -FilePath $FilePath -Reason "Cymru MHR malware match (>= $($Config.CymruDetectionThreshold)% detection)"
return
}
if (Test-MalwareBazaarHash -SHA256 $fileHash) {
Save-ToDatabase -Hash $fileHash -IsSafe $false
Move-ToQuarantine -FilePath $FilePath -Reason "MalwareBazaar malware match"
return
}
if (Test-SuspiciousUnsignedDll -FilePath $FilePath) {
Save-ToDatabase -Hash $fileHash -IsSafe $false
Move-ToQuarantine -FilePath $FilePath -Reason "Suspicious unsigned DLL/WINMD in risky location"
return
}
$signatureInfo = Get-FileSignatureInfo -FilePath $FilePath
if ($signatureInfo) {
$isSafe = ($signatureInfo.Status -eq 'Valid')
Save-ToDatabase -Hash $fileHash -IsSafe $isSafe
if ($isSafe) {
Write-Log "ALLOWED (digitally signed): $FilePath"
} else {
Write-Log "ALLOWED (unsigned but no threat indicators): $FilePath"
}
}
}
function Start-MemoryScanner {
if (-not $Config.EnableMemoryScanning) {
Write-Log "Memory scanning is disabled in configuration"
return
}
Write-Log "Starting optimized memory scanner (interval: $($Config.MemoryScanIntervalSec)s, max size: $($Config.MemoryScanMaxSizeMB)MB)"
$mainPid = $PID
Start-Job -ScriptBlock {
$config = $using:Config
$protected = $using:ProtectedProcesses
$evilStrings = $using:EvilStrings
$mainPid = $using:mainPid # PID of the main AntivirusLite script
$selfPid = $PID # PID of this background job host
# Register this job's PID in the shared AV PID file so the scan loop
# below never flags the antivirus's own job host.
try {
$m = New-Object System.Threading.Mutex($false, 'Global\AV_PidFile')
[void]$m.WaitOne(2000)
try { Add-Content -Path $config.OwnPidFile -Value $selfPid -ErrorAction SilentlyContinue }
finally { $m.ReleaseMutex(); $m.Dispose() }
} catch {}
$logFile = Join-Path $config.BaseDirectory "memory_hits.log"
while ($true) {
Start-Sleep -Seconds $config.MemoryScanIntervalSec
$maxBytes = $config.MemoryScanMaxSizeMB * 1MB
# Load the set of AV-owned PIDs (main script + all AV jobs). These
# are never scanned/killed. Unrelated PowerShell is still scanned.
$ownPids = @{}
try {
foreach ($line in (Get-Content -Path $config.OwnPidFile -ErrorAction SilentlyContinue)) {
$n = 0
if ([int]::TryParse(($line.Trim()), [ref]$n)) { $ownPids[$n] = $true }
}
} catch {}
# Always include our own two known PIDs as a fallback.
$ownPids[$mainPid] = $true
$ownPids[$selfPid] = $true
Get-Process | Where-Object {
$_.WorkingSet64 -lt $maxBytes -and
$protected -notcontains $_.Name -and
-not $ownPids.ContainsKey($_.Id) # not an AV-owned process
} | ForEach-Object {
$process = $_
$suspicious = $false
$reasons = @()
try {
if (-not $process.Path -or $process.Path -eq '') {
$suspicious = $true
$reasons += "NoPath"
}
} catch {}
try {
$emptyModules = $process.Modules | Where-Object {
$_.FileName -eq '' -or $_.ModuleName -eq ''
}
if ($emptyModules) {
$suspicious = $true
$reasons += "EmptyModule"
}
} catch {}
try {
foreach ($module in $process.Modules) {
foreach ($evilString in $evilStrings) {
if ($module.ModuleName -match $evilString -or ($module.FileName -and $module.FileName -match $evilString)) {
$suspicious = $true
$reasons += "EvilString($evilString)"
break
}
}
if ($suspicious) { break }
}
} catch {}
if ($suspicious) {
$reasonString = $reasons -join '; '
$logEntry = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | MEMORY HIT [$reasonString] -> $($process.Name) (PID: $($process.Id)) Path: '$($process.Path)' WS: $([math]::Round($process.WorkingSet64/1MB, 2))MB"
try {
$logEntry | Out-File -FilePath $logFile -Append -Encoding UTF8
} catch {}
try {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
} catch {}
}
}
# Deterministic cleanup after each full pass. Enumerating .Modules
# across all processes retains native handles/metadata; collect,
# then trim this job's working set back to the OS.
try {
[System.Runtime.GCSettings]::LargeObjectHeapCompactionMode = `
[System.Runtime.GCLargeObjectHeapCompactionMode]::CompactOnce
} catch {}
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
try {
if (-not ([System.Management.Automation.PSTypeName]'AVJob.Mem').Type) {
Add-Type -Namespace AVJob -Name Mem -MemberDefinition @'
[System.Runtime.InteropServices.DllImport("psapi.dll")]
public static extern bool EmptyWorkingSet(System.IntPtr hProcess);
'@ -ErrorAction SilentlyContinue
}
[void][AVJob.Mem]::EmptyWorkingSet([System.Diagnostics.Process]::GetCurrentProcess().Handle)
} catch {}
}
} | Out-Null
}
function Find-FilelessIndicators {
$detections = @()
try {
$suspiciousPowerShell = Get-Process -Name powershell, pwsh -ErrorAction SilentlyContinue | Where-Object {
$_.MainWindowTitle -match 'encodedcommand|enc|iex|invoke-expression'
}
if ($suspiciousPowerShell) {
$detections += [PSCustomObject]@{
Type = "FilelessPowerShell"
Details = $suspiciousPowerShell | Select-Object Name, Id, MainWindowTitle
}
Write-Log "Fileless indicator detected: PowerShell without file"
}
} catch {}
try {
$wmiEvents = Get-WmiObject -Namespace root\Subscription -Class __EventFilter -ErrorAction SilentlyContinue |
Where-Object { $_.Query -match 'powershell|vbscript|javascript' }
if ($wmiEvents) {
$detections += [PSCustomObject]@{
Type = "WMIEventSubscription"
Details = $wmiEvents | Select-Object Name, Query
}
Write-Log "Fileless indicator detected: WMI event subscriptions"
}
} catch {}
try {
$registryKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
)
foreach ($key in $registryKeys) {
if (Test-Path $key) {
$props = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
if ($props) {
$props.PSObject.Properties | Where-Object {
$_.MemberType -eq 'NoteProperty' -and $_.Value -match 'powershell.*-enc|mshta|regsvr32.*scrobj'
} | ForEach-Object {
$detections += [PSCustomObject]@{
Type = "RegistryScript"
Details = "$key -> $($_.Name): $($_.Value)"
}
Write-Log "Fileless indicator detected: Registry script in $key"
}
}
}
}
} catch {}
return $detections
}
function Find-PersistenceMechanisms {
$suspiciousItems = @()
$locations = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run',
'C:\Windows\System32\Tasks',
'C:\Windows\Tasks',
"$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup",
'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup'
)
foreach ($location in $locations) {
try {
if ($location -match '^HK') {
if (Test-Path $location) {
$props = Get-ItemProperty -Path $location -ErrorAction SilentlyContinue
if ($props) {
$props.PSObject.Properties | Where-Object {
$_.MemberType -eq 'NoteProperty' -and $_.Value -match '\.(exe|dll|ps1|vbs|js|bat|cmd)$'
} | ForEach-Object {
$suspiciousItems += [PSCustomObject]@{
Location = $location
Name = $_.Name
Value = $_.Value
Type = 'Registry'
}
}
}
}
} else {
if (Test-Path $location) {
Get-ChildItem -Path $location -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -match '\.(exe|dll|lnk|ps1|vbs|js|bat|cmd)$' } |
ForEach-Object {
$suspiciousItems += [PSCustomObject]@{
Location = $location
Name = $_.Name
Value = $_.FullName
Type = 'FileSystem'
}
}
}
}
} catch {
Write-Log "Error checking persistence location $location : $_"
}
}
return $suspiciousItems
}
function Test-ProcessHollowing {
param($Process)
try {
$processPath = $Process.Path
if (-not $processPath) { return $false }
$modules = $Process.Modules
if ($modules -and $modules.Count -gt 0) {
return ($modules[0].FileName -ne $processPath)
}
} catch {}
return $false
}
function Test-CredentialAccessBehavior {
param($Process)
try {
$commandLine = (Get-CimInstance Win32_Process -Filter "ProcessId=$($Process.Id)" -ErrorAction SilentlyContinue).CommandLine
if ($commandLine -match 'mimikatz|procdump|sekurlsa|lsadump|credential|password') {
return $true
}
} catch {}
if ($Process.ProcessName -match 'vaultcmd|cred') {
return $true
}
return $false
}
function Test-LateralMovementBehavior {
param($Process)
try {
$connections = Get-NetTCPConnection -OwningProcess $Process.Id -ErrorAction SilentlyContinue
$externalConnections = $connections | Where-Object {
$_.RemoteAddress -notmatch '^(127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)|^(::1|fe80:)' -and
$_.RemoteAddress -ne '0.0.0.0' -and $_.RemoteAddress -ne '::'
}
return (($externalConnections | Measure-Object).Count -gt 10)
} catch {}
return $false
}
function Test-C2Communication {
param($Connection)
$suspiciousPorts = @(4444, 5555, 6666, 7777, 8080, 8443, 9001, 1337, 31337)
if ($Connection.RemotePort -in $suspiciousPorts) {
try {
$hostname = [System.Net.Dns]::GetHostEntry($Connection.RemoteAddress).HostName
$c2Domains = @('pastebin', 'ddns.net', 'no-ip.org', 'duckdns.org', 'bit.ly', 'tinyurl')
foreach ($domain in $c2Domains) {
if ($hostname -like "*$domain*") {
return $true
}
}
} catch {}
}
return $false
}
# ============================================================================
# C2 / Reverse-Shell killer
# ============================================================================
function Get-OwnPidSet {
# Return a hashtable of AV-owned PIDs (main script + all jobs) so the C2
# engine never blocks/kills the antivirus itself.
$own = @{ $PID = $true }
try {
foreach ($line in (Get-Content -Path $Config.OwnPidFile -ErrorAction SilentlyContinue)) {
$n = 0
if ([int]::TryParse(($line.Trim()), [ref]$n)) { $own[$n] = $true }
}
} catch {}
return $own
}
function Test-PrivateOrLocalAddress {
param([string]$Address)
if ([string]::IsNullOrWhiteSpace($Address)) { return $true }
if ($Address -eq '0.0.0.0' -or $Address -eq '::' -or $Address -eq '::1') { return $true }
$privatePrefixes = @('127.', '10.', '192.168.', '169.254.',
'172.16.', '172.17.', '172.18.', '172.19.', '172.20.', '172.21.',
'172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.',
'172.28.', '172.29.', '172.30.', '172.31.', 'fe80:', 'fc', 'fd')
foreach ($p in $privatePrefixes) {
if ($Address.ToLower().StartsWith($p)) { return $true }
}
return $false
}
function Get-ProcessCommandLine {
param([int]$ProcessId)
try {
return (Get-CimInstance Win32_Process -Filter "ProcessId=$ProcessId" -ErrorAction SilentlyContinue).CommandLine
} catch {
return $null
}
}
function Test-ReverseShellCommandLine {
# Returns $true when a command line matches a known reverse-shell signature.
param([string]$CommandLine)
if ([string]::IsNullOrWhiteSpace($CommandLine)) { return $false }
foreach ($pattern in $ReverseShellPatterns) {
if ($CommandLine -imatch $pattern) { return $true }
}
return $false
}
function Block-RemoteC2Ip {
# Drop an outbound-blocking firewall rule for a confirmed C2 IP (idempotent).
param([string]$RemoteAddress, [string]$Reason = "C2")
if (-not $C2Config.EnableFirewallAutoBlock) { return }
if (Test-PrivateOrLocalAddress -Address $RemoteAddress) { return }
if ($script:BlockedC2Ips.Contains($RemoteAddress)) { return }
if (-not $IsAdmin) {
Write-Log "C2 block skipped (needs admin): $RemoteAddress"
return
}
try {
$ruleName = "AVLite_Block_C2_$($RemoteAddress -replace '[:.]','_')"
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if (-not $existing) {
New-NetFirewallRule -DisplayName $ruleName -Direction Outbound -Action Block `
-RemoteAddress $RemoteAddress -Protocol Any -Profile Any -ErrorAction SilentlyContinue | Out-Null
Write-Log "FIREWALL: Blocked C2 IP $RemoteAddress [$Reason]"
}
[void]$script:BlockedC2Ips.Add($RemoteAddress)
} catch {
Write-Log "Firewall block failed for $RemoteAddress : $_"
}
}
function Stop-C2Process {
# Kill a process backing a C2/reverse-shell connection, honoring the
# protected-process list and the AV's own PID set.
param([int]$ProcessId, [string]$ProcessName, [string]$Reason, [hashtable]$OwnPids)
if ($OwnPids -and $OwnPids.ContainsKey($ProcessId)) {
Write-Log "C2 kill skipped - own AV process (PID: $ProcessId)"
return
}
if ($ProtectedProcesses -contains $ProcessName) {
Write-Log "C2 kill skipped - protected process $ProcessName (PID: $ProcessId) [$Reason]"
return
}
try {
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
Write-Log "KILLED C2/reverse-shell process $ProcessName (PID: $ProcessId) [$Reason]"
Send-ThreatAlert -Severity "CRITICAL" -Message "C2/reverse-shell process terminated" -Details "$ProcessName PID:$ProcessId - $Reason"
} catch {
Write-Log "Failed to kill C2 process $ProcessName (PID: $ProcessId): $_"
}
}
function Update-BeaconHistory {
# Record a timestamp for a (PID,RemoteIP) pair and return a verdict object
# when the callback cadence looks like a beacon.
param([int]$ProcessId, [string]$RemoteAddress)
$key = "$ProcessId|$RemoteAddress"
if (-not $script:BeaconHistory.ContainsKey($key)) {
$script:BeaconHistory[$key] = New-Object System.Collections.Generic.List[datetime]
}
$list = $script:BeaconHistory[$key]
$list.Add((Get-Date))
# Cap samples per key to bound memory.
while ($list.Count -gt 50) { $list.RemoveAt(0) }
if ($list.Count -lt $C2Config.BeaconMinSamples) { return $null }
$intervals = @()
for ($i = 1; $i -lt $list.Count; $i++) {
$intervals += ($list[$i] - $list[$i - 1]).TotalSeconds
}
if ($intervals.Count -lt 2) { return $null }
$avg = ($intervals | Measure-Object -Average).Average
if ($avg -lt $C2Config.BeaconMinIntervalSec -or $avg -gt $C2Config.BeaconMaxIntervalSec) { return $null }
$variance = ($intervals | ForEach-Object { [Math]::Pow($_ - $avg, 2) } | Measure-Object -Average).Average
$stdDev = [Math]::Sqrt($variance)
if ($stdDev -lt ($avg * $C2Config.BeaconMaxJitterRatio)) {
return [PSCustomObject]@{
AverageInterval = [Math]::Round($avg, 1)
Jitter = [Math]::Round($stdDev, 1)
Samples = $list.Count
}
}
return $null
}
function Get-C2ConnectionScore {
# Score a single established outbound connection. Higher = more likely C2.
param($Connection, $Process)
$score = 0
$reasons = @()
$remotePort = $Connection.RemotePort
$remoteAddr = $Connection.RemoteAddress.ToString()
# Suspicious / backdoor ports
if ($C2SuspiciousPorts.ContainsKey([int]$remotePort)) {
$score += $C2SuspiciousPorts[[int]$remotePort]
$reasons += "Backdoor/C2 port $remotePort"
}
# Known-bad IP prefixes
foreach ($prefix in $C2MaliciousRangePrefixes) {
if ($remoteAddr.StartsWith($prefix)) {
$score += 25
$reasons += "Known-bad IP range $prefix"
break
}
}
if ($Process) {
$pname = $Process.ProcessName.ToLower()
# Interpreter / LOLBin holding an outbound socket
if ($C2SuspiciousProcesses -contains $pname) {
$score += 25
$reasons += "Interpreter/LOLBin with outbound socket ($pname)"
}
# Process running from a non-standard location
if ($Process.Path -and $Process.Path -notmatch '(?i)^C:\\(Windows|Program Files|Program Files \(x86\))') {
$score += 15
$reasons += "Non-standard process path"
}
# Core system process making an outbound connection => likely injection
if ($pname -match '^(svchost|lsass|winlogon|services|csrss|smss|wininit)$') {
# svchost legitimately connects, but to standard ports only
if ($remotePort -notin @(80, 443, 123, 53, 88, 135, 389, 445, 636)) {
$score += 30
$reasons += "System process ($pname) outbound to unusual port $remotePort"
}
}
# Reverse-shell command line on the owning process
$cmd = Get-ProcessCommandLine -ProcessId $Process.Id
if (Test-ReverseShellCommandLine -CommandLine $cmd) {
$score += 45
$reasons += "Reverse-shell command line"
}
}
# Suspicious resolved hostname TLD
if ($remoteAddr -match '^\d{1,3}(\.\d{1,3}){3}$') {
try {
$hostName = [System.Net.Dns]::GetHostEntry($remoteAddr).HostName
if ($hostName) {
foreach ($tld in $C2SuspiciousTLDs) {
if ($hostName.ToLower().EndsWith($tld) -or $hostName.ToLower().Contains($tld)) {
$score += 20
$reasons += "Suspicious hostname TLD ($hostName)"
break
}
}
}
} catch {}
}
return [PSCustomObject]@{
Score = $score
Reasons = $reasons
}
}
function Invoke-C2Detection {
# Main scored C2 engine: walk established outbound connections, score each,
# fold in beacon cadence + IP-rotation, then firewall-block and/or kill.
if (-not $C2Config.EnableC2Detection) { return }
$ownPids = Get-OwnPidSet
$seenKeys = @{}
try {
$connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
Where-Object { -not (Test-PrivateOrLocalAddress -Address $_.RemoteAddress) }
} catch {
return
}
# Per-process unique remote-IP count for rotation detection.
$procIpMap = @{}
foreach ($conn in $connections) {
$procId = [int]$conn.OwningProcess
if ($procId -le 0 -or $ownPids.ContainsKey($procId)) { continue }
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
$pname = if ($proc) { $proc.ProcessName } else { "Unknown" }
if (-not $procIpMap.ContainsKey($procId)) {
$procIpMap[$procId] = New-Object System.Collections.Generic.HashSet[string]
}
[void]$procIpMap[$procId].Add($conn.RemoteAddress.ToString())
$result = Get-C2ConnectionScore -Connection $conn -Process $proc
$score = $result.Score
$reasons = $result.Reasons
# Beacon cadence (only meaningful once per key per cycle)
$bkey = "$procId|$($conn.RemoteAddress)"
if ($C2Config.EnableBeaconDetection -and -not $seenKeys.ContainsKey($bkey)) {
$seenKeys[$bkey] = $true
$beacon = Update-BeaconHistory -ProcessId $procId -RemoteAddress $conn.RemoteAddress.ToString()
if ($beacon) {
$score += 30
$reasons += "Beacon cadence ~$($beacon.AverageInterval)s (jitter $($beacon.Jitter)s, n=$($beacon.Samples))"
}
}
if ($score -le 0) { continue }
$target = "$($conn.RemoteAddress):$($conn.RemotePort)"
$reasonStr = $reasons -join '; '
if ($score -ge $C2Config.FirewallBlockThreshold) {
Write-Log "C2 SUSPECT (score $score): $pname (PID $procId) -> $target | $reasonStr"
Send-ThreatAlert -Severity "HIGH" -Message "Suspicious C2 connection (score $score)" -Details "$pname -> $target | $reasonStr"
Block-RemoteC2Ip -RemoteAddress $conn.RemoteAddress.ToString() -Reason "score $score"
}
if ($score -ge $C2Config.KillScoreThreshold) {
Stop-C2Process -ProcessId $procId -ProcessName $pname -Reason "C2 score $score -> $target" -OwnPids $ownPids
}
}
# IP-rotation: one process fanning out to many unique remote IPs = C2 rotation.
foreach ($entry in $procIpMap.GetEnumerator()) {
if ($entry.Value.Count -ge $C2Config.IpRotationThreshold) {
$procId = $entry.Key
if ($ownPids.ContainsKey($procId)) { continue }
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
$pname = if ($proc) { $proc.ProcessName } else { "Unknown" }
Write-Log "C2 IP-ROTATION: $pname (PID $procId) -> $($entry.Value.Count) unique remote IPs"
Send-ThreatAlert -Severity "HIGH" -Message "C2 IP rotation detected" -Details "$pname PID:$procId -> $($entry.Value.Count) IPs"
Stop-C2Process -ProcessId $procId -ProcessName $pname -Reason "IP rotation ($($entry.Value.Count) IPs)" -OwnPids $ownPids
}
}
# Prune beacon-history keys whose process no longer exists.
$deadKeys = @()
foreach ($k in $script:BeaconHistory.Keys) {
$kpid = 0
if ([int]::TryParse(($k -split '\|')[0], [ref]$kpid)) {
if (-not (Get-Process -Id $kpid -ErrorAction SilentlyContinue)) { $deadKeys += $k }
}
}
foreach ($k in $deadKeys) { [void]$script:BeaconHistory.Remove($k) }
}
function Invoke-ReverseShellKiller {
# Hunt reverse shells independent of an already-open socket: any interpreter
# process whose command line matches a reverse-shell signature is killed,
# and if it currently holds an outbound socket that remote IP is blocked.
if (-not $C2Config.EnableReverseShellKill) { return }
$ownPids = Get-OwnPidSet
try {
$procInfos = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine }
} catch {
return
}
foreach ($pi in $procInfos) {
$procId = [int]$pi.ProcessId
if ($procId -le 0 -or $ownPids.ContainsKey($procId)) { continue }
if (-not (Test-ReverseShellCommandLine -CommandLine $pi.CommandLine)) { continue }
$pname = ($pi.Name -replace '\.exe$', '')
if ($ProtectedProcesses -contains $pname) {
Write-Log "Reverse-shell signature on protected process $pname (PID $procId) - logging only"
Write-Log " CMD: $($pi.CommandLine)"
continue
}
Write-Log "REVERSE SHELL DETECTED: $pname (PID $procId)"
Write-Log " CMD: $($pi.CommandLine)"
Send-ThreatAlert -Severity "CRITICAL" -Message "Reverse shell command line detected" -Details "$pname PID:$procId"
# Block any remote endpoints this process is currently talking to.
try {
Get-NetTCPConnection -OwningProcess $procId -State Established -ErrorAction SilentlyContinue |
Where-Object { -not (Test-PrivateOrLocalAddress -Address $_.RemoteAddress) } |
ForEach-Object { Block-RemoteC2Ip -RemoteAddress $_.RemoteAddress.ToString() -Reason "reverse shell" }
} catch {}
Stop-C2Process -ProcessId $procId -ProcessName $pname -Reason "reverse-shell command line" -OwnPids $ownPids
# Quarantine the on-disk payload if the image path is resolvable.
if ($pi.ExecutablePath -and (Test-Path $pi.ExecutablePath)) {
$imgLower = $pi.ExecutablePath.ToLower()
# Don't quarantine the trusted system interpreters themselves.
if ($imgLower -notmatch '\\windows\\system32\\' -and $imgLower -notmatch '\\windows\\syswow64\\') {
Move-ToQuarantine -FilePath $pi.ExecutablePath -Reason "Reverse-shell host image"
}
}
}
}
function Invoke-ProcessAndNetworkScan {
Get-Process | ForEach-Object {
$process = $_
try {
$exePath = $process.MainModule.FileName
if ($exePath -and (Test-Path $exePath)) {
Invoke-ThreatAnalysis -FilePath $exePath
}
} catch {}
if ($BehaviorConfig.EnableBehaviorKill -and ($ProtectedProcesses -notcontains $process.Name)) {
try {
if (Test-ProcessHollowing -Process $process) {
Write-Log "BEHAVIOR: Process hollowing detected - $($process.Name) (PID: $($process.Id))"
Send-ThreatAlert -Severity "HIGH" -Message "Process hollowing detected" -Details "$($process.Name) PID: $($process.Id)"
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
elseif (Test-CredentialAccessBehavior -Process $process) {
Write-Log "BEHAVIOR: Credential access detected - $($process.Name) (PID: $($process.Id))"
Send-ThreatAlert -Severity "HIGH" -Message "Credential access behavior" -Details "$($process.Name) PID: $($process.Id)"
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
elseif (Test-LateralMovementBehavior -Process $process) {
Write-Log "BEHAVIOR: Lateral movement detected - $($process.Name) (PID: $($process.Id))"
Send-ThreatAlert -Severity "MEDIUM" -Message "Lateral movement behavior" -Details "$($process.Name) PID: $($process.Id)"
}
} catch {}
}
}
# Upgraded C2 / reverse-shell killer engines (scored, beacon-aware).
try { Invoke-ReverseShellKiller } catch { Write-Log "ReverseShellKiller error: $_" }
try { Invoke-C2Detection } catch { Write-Log "C2Detection error: $_" }
if ($BehaviorConfig.EnableAutoBlockC2) {
Get-NetTCPConnection -ErrorAction SilentlyContinue | Where-Object {
$_.State -in @('Established', 'Listen')
} | ForEach-Object {
$connection = $_
try {
$process = Get-Process -Id $connection.OwningProcess -ErrorAction SilentlyContinue
if ($process -and (Test-C2Communication -Connection $connection)) {
Write-Log "NETWORK: Suspicious C2 connection - $($process.Name) (PID: $($process.Id)) -> $($connection.RemoteAddress):$($connection.RemotePort)"
Send-ThreatAlert -Severity "HIGH" -Message "C2 communication detected" -Details "$($process.Name) -> $($connection.RemoteAddress):$($connection.RemotePort)"
if ($ProtectedProcesses -notcontains $process.Name) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
try {
$ruleName = "Block_C2_$($connection.RemoteAddress)"
New-NetFirewallRule -DisplayName $ruleName `
-Direction Outbound `
-Protocol TCP `
-RemoteAddress $connection.RemoteAddress `
-Action Block `
-Enabled True `
-ErrorAction SilentlyContinue | Out-Null
Write-Log "Created firewall rule to block: $($connection.RemoteAddress)"
} catch {}
}
} catch {}
}
}
}
Write-Log "Performing initial scan of high-risk folders"
$highRiskFolders = @(
"$env:USERPROFILE\Downloads",
"$env:USERPROFILE\Desktop",
"$env:TEMP",
"$env:APPDATA",
"$env:LOCALAPPDATA\Temp"
)
foreach ($folder in $highRiskFolders) {
if (Test-Path $folder) {
Write-Log "Scanning: $folder"
Get-ChildItem -Path $folder -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
Invoke-ThreatAnalysis -FilePath $_.FullName
}
}
}
if ($Config.EnableRealtimeMonitor) {
Write-Log "Setting up real-time file monitoring"
foreach ($folder in $highRiskFolders) {
if (-not (Test-Path $folder)) { continue }
try {
$watcher = New-Object IO.FileSystemWatcher $folder, '*.*' -Property @{
IncludeSubdirectories = $true
NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'
}
Register-ObjectEvent $watcher Created -Action {
$filePath = $Event.SourceEventArgs.FullPath
$extension = [IO.Path]::GetExtension($filePath).ToLower()
if ($MonitoredExtensions -contains $extension) {
Start-Sleep -Milliseconds 800
Invoke-ThreatAnalysis -FilePath $filePath
}
} | Out-Null
$watcher.EnableRaisingEvents = $true
Write-Log "File watcher active for: $folder"
} catch {
Write-Log "Failed to create file watcher for $folder : $_"
}
}
}
Write-Log "Registering WMI real-time execution monitors"
try {
Register-WmiEvent -Query "SELECT * FROM Win32_ProcessStartTrace" -SourceIdentifier "AV_ProcStart" -Action {
try {
$newEvent = $Event.SourceEventArgs.NewEvent
$filePath = $newEvent.ProcessName
$processId = $newEvent.ProcessId
$cim = Get-CimInstance Win32_Process -Filter "ProcessId=$processId" -ErrorAction SilentlyContinue
$ownerSid = "Unknown"
if ($cim) {
try { $ownerSid = ($cim | Invoke-CimMethod -MethodName GetOwnerSid).Sid } catch {}
$cim.Dispose()
}
if ($AllowedSIDs -contains $ownerSid) {
try {
$signature = Get-AuthenticodeSignature -FilePath $filePath -ErrorAction Stop
if ($signature.Status -eq 'Valid') { return }
} catch {}
}
Block-FileExecution -FilePath $filePath -ProcessId $processId -Type "EXE"
}
finally {
# Drain this event from the engine's event log so PSEventArgs
# objects do not accumulate in memory.
Remove-Event -EventIdentifier $Event.EventIdentifier -ErrorAction SilentlyContinue
}
} | Out-Null
Write-Log "WMI process start monitor registered"
} catch {
Write-Log "Failed to register WMI process start monitor: $_"
}
# NOTE: Win32_ModuleLoadTrace monitoring was removed intentionally.
# It fires on EVERY DLL load system-wide (thousands/sec). The PowerShell
# -Action event queue cannot drain that fast, so PSEventArgs objects pile up
# unbounded and consume gigabytes of RAM within minutes.
# DLL threats on disk are already covered by the FileSystemWatcher and by
# Invoke-ThreatAnalysis (which hashes and signature-checks .dll/.winmd files).
Write-Log "Module-load monitoring disabled by design (high-frequency event leak prevention)"
if ($Config.EnableMemoryScanning) {
Start-MemoryScanner
}
if ($IsAdmin -and $Config.EnableThreatIntel) {
$lastUpdateFile = Join-Path $Config.BaseDirectory "last_update.txt"
$currentTime = Get-Date
$shouldUpdate = $false
if (-not (Test-Path $lastUpdateFile)) {
$shouldUpdate = $true
} else {
try {
$lastUpdate = (Get-Item $lastUpdateFile).LastWriteTime
if ($lastUpdate -lt $currentTime.AddDays(-$BehaviorConfig.ThreatIntelUpdateDays)) {
$shouldUpdate = $true
}
} catch {
$shouldUpdate = $true
}
}
if ($shouldUpdate) {
Update-ThreatIntelligence
$currentTime | Out-File -FilePath $lastUpdateFile
}
Start-Job -ScriptBlock {
$config = $using:Config
$behaviorConfig = $using:BehaviorConfig
# Register this deep-scan job's PID so the memory scanner never flags it.
try {
$m = New-Object System.Threading.Mutex($false, 'Global\AV_PidFile')
[void]$m.WaitOne(2000)
try { Add-Content -Path $config.OwnPidFile -Value $PID -ErrorAction SilentlyContinue }
finally { $m.ReleaseMutex(); $m.Dispose() }
} catch {}
while ($true) {
Start-Sleep -Seconds (60 * 60 * $behaviorConfig.DeepScanIntervalHours)
try {
$persistence = Find-PersistenceMechanisms
if ($persistence -and $persistence.Count -gt 0) {
$outputFile = Join-Path $config.BaseDirectory "persistence_scan.csv"
$persistence | Export-Csv -Path $outputFile -NoTypeInformation
Write-Log "Persistence scan found $($persistence.Count) items -> $outputFile"
Send-ThreatAlert -Severity "MEDIUM" -Message "Persistence mechanisms detected" -Details "Found $($persistence.Count) items"
}
} catch {
Write-Log "Persistence scan error: $_"
}
try {
$fileless = Find-FilelessIndicators
if ($fileless -and $fileless.Count -gt 0) {
$outputFile = Join-Path $config.BaseDirectory "fileless_detections.xml"
$fileless | Export-Clixml -Path $outputFile
Write-Log "Fileless indicators found: $($fileless.Count) -> $outputFile"
Send-ThreatAlert -Severity "HIGH" -Message "Fileless malware indicators" -Details "Found $($fileless.Count) indicators"
}
} catch {
Write-Log "Fileless scan error: $_"
}
}
} | Out-Null
Write-Log "Deep scanner job started"
}
Write-Log "All monitoring systems active"
Write-Host "`nClean Antivirus is now running" -ForegroundColor Green
Write-Host "Press [Ctrl+C] to stop" -ForegroundColor Yellow
try {
while ($true) {
try {
Invoke-ProcessAndNetworkScan
Write-Log "Periodic scan cycle completed"
} catch {
Write-Log "Scan cycle error: $_"
}
# Deterministic cleanup each cycle. The scan creates large transient
# object graphs (process lists, modules, CIM/TCP objects); collect,
# compact the LOH, and trim the working set back to the OS.
Clear-ProcessMemory
Remove-DeadOwnPids
Start-Sleep -Seconds 30
}
} catch {
Write-Log "Main loop terminated: $($_.Exception.Message)"
Write-Host "`nAntivirus stopped. Check log file: $($Config.LogFile)" -ForegroundColor Red
}69,611 chars · codepastes.com