Analysis/Get-MemoryAnalysis.ps1
|
function Get-MemoryAnalysis { <# .SYNOPSIS Analyzes process memory for suspicious indicators .DESCRIPTION Examines running processes for memory-based threats including code injection, hollowing, and suspicious memory patterns. The hollowing check is a name/location heuristic: without reading process memory it cannot compare in-memory code against the on-disk image, so it flags well-known Windows system process names (svchost, lsass, csrss, ...) whose main module image resides outside the expected %SystemRoot% location instead. .PARAMETER ProcessId Specific process ID to analyze (optional) .PARAMETER CheckAllProcesses Analyze all running processes .PARAMETER IncludeStrings Extract suspicious strings from process memory .EXAMPLE Get-MemoryAnalysis -CheckAllProcesses Get-MemoryAnalysis -ProcessId 1234 -IncludeStrings .OUTPUTS PSCustomObject with memory analysis results #> [CmdletBinding()] param( [Parameter()] [int]$ProcessId, [Parameter()] [switch]$CheckAllProcesses, [Parameter()] [switch]$IncludeStrings ) begin { Write-Host "Analyzing process memory..." -ForegroundColor Cyan # Check for admin privileges (the Windows principal API is unavailable # on non-Windows platforms - skip the check there instead of throwing) $isAdmin = $false $adminCheckAvailable = $true try { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { $adminCheckAvailable = $false Write-Verbose "Administrator privilege check not available on this platform: $_" } if ($adminCheckAvailable -and -not $isAdmin) { Write-Warning "Administrator privileges recommended for complete memory analysis" } $results = [PSCustomObject]@{ AnalysisDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ProcessAnalysis = @() InjectionIndicators = @() HollowingIndicators = @() SuspiciousModules = @() Statistics = @{ ProcessesAnalyzed = 0 InjectionIndicators = 0 HollowingIndicators = 0 SuspiciousModules = 0 } } # Suspicious module names $suspiciousModulePatterns = @( 'inject', 'hook', 'reflective', 'payload', 'shellcode', 'mimikatz', 'meterpreter', 'cobalt', 'beacon' ) # Expected on-disk image for well-known Windows system process names. # A process using one of these names whose main module lives elsewhere is a # masquerading / process-hollowing heuristic signal (no memory is read). # Empty when %SystemRoot% is unavailable (non-Windows platforms). $expectedSystemImages = @{} if (-not [string]::IsNullOrEmpty($env:SystemRoot)) { $expectedSystemImages = @{ 'svchost' = "$env:SystemRoot\System32\svchost.exe" 'lsass' = "$env:SystemRoot\System32\lsass.exe" 'csrss' = "$env:SystemRoot\System32\csrss.exe" 'smss' = "$env:SystemRoot\System32\smss.exe" 'wininit' = "$env:SystemRoot\System32\wininit.exe" 'winlogon' = "$env:SystemRoot\System32\winlogon.exe" 'services' = "$env:SystemRoot\System32\services.exe" 'spoolsv' = "$env:SystemRoot\System32\spoolsv.exe" 'lsm' = "$env:SystemRoot\System32\lsm.exe" 'dwm' = "$env:SystemRoot\System32\dwm.exe" 'sihost' = "$env:SystemRoot\System32\sihost.exe" 'taskhostw' = "$env:SystemRoot\System32\taskhostw.exe" 'runtimebroker' = "$env:SystemRoot\System32\RuntimeBroker.exe" 'explorer' = "$env:SystemRoot\explorer.exe" } } } process { try { $processesToAnalyze = @() if ($ProcessId) { $proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue if ($proc) { $processesToAnalyze += $proc } else { Write-Error "Process with ID $ProcessId not found" return } } elseif ($CheckAllProcesses) { $processesToAnalyze = Get-Process | Where-Object { $_.Id -ne 0 -and $_.Id -ne 4 } } else { Write-Warning "No process specified. Use -ProcessId or -CheckAllProcesses" return } foreach ($process in $processesToAnalyze) { try { $results.Statistics.ProcessesAnalyzed++ Write-Verbose "Analyzing process: $($process.ProcessName) (PID: $($process.Id))" $processAnalysis = [PSCustomObject]@{ ProcessName = $process.ProcessName ProcessId = $process.Id Path = $process.Path StartTime = $process.StartTime Threads = $process.Threads.Count Modules = @() MemorySize = [math]::Round($process.WorkingSet64 / 1MB, 2) VirtualMemorySize = [math]::Round($process.VirtualMemorySize64 / 1MB, 2) PrivateMemorySize = [math]::Round($process.PrivateMemorySize64 / 1MB, 2) Findings = @() } # Get loaded modules try { $modules = $process.Modules foreach ($module in $modules) { $moduleName = $module.ModuleName.ToLower() $modulePath = $module.FileName # Check for suspicious modules $isSuspicious = $false $reason = "" foreach ($pattern in $suspiciousModulePatterns) { if ($moduleName -match $pattern) { $isSuspicious = $true $reason = "Module name matches suspicious pattern: $pattern" break } } # Check for modules loaded from suspicious locations if ($modulePath -match '(temp|appdata\\local\\temp|users\\public)') { $isSuspicious = $true $reason = "Module loaded from suspicious location" } if ($isSuspicious) { $results.SuspiciousModules += [PSCustomObject]@{ ProcessName = $process.ProcessName ProcessId = $process.Id ModuleName = $moduleName ModulePath = $modulePath Reason = $reason } $results.Statistics.SuspiciousModules++ } $processAnalysis.Modules += [PSCustomObject]@{ Name = $moduleName Path = $modulePath Size = [math]::Round($module.ModuleMemorySize / 1KB, 2) Suspicious = $isSuspicious } } } catch { Write-Verbose "Could not enumerate modules for $($process.ProcessName): $_" } # Process hollowing / masquerading heuristic. # Process.Path IS MainModule.FileName in the .NET Process API, so # comparing the two (as this check previously did) is structurally # false and can never fire. Without reading process memory there is # no reliable hollowing check, so instead flag well-known Windows # system process names whose main module image does not reside at # the expected system location (e.g. a process named 'svchost' # whose image is not %SystemRoot%\System32\svchost.exe). $processNameLower = $process.ProcessName.ToLower() if ($expectedSystemImages.ContainsKey($processNameLower)) { $actualImagePath = $null try { $mainModule = $process.MainModule if ($null -ne $mainModule) { $actualImagePath = $mainModule.FileName } } catch { Write-Verbose "Could not read main module for $($process.ProcessName)" } if (-not [string]::IsNullOrWhiteSpace($actualImagePath)) { # Tolerate the SysWOW64 twin of System32 images (32-bit services) $expectedImagePath = $expectedSystemImages[$processNameLower] $expectedImagePaths = @($expectedImagePath) if ($expectedImagePath -match '\\System32\\') { $expectedImagePaths += ($expectedImagePath -replace '\\System32\\', '\SysWOW64\') } $imagePathMatches = $false foreach ($expectedPath in $expectedImagePaths) { if ($actualImagePath.Equals($expectedPath, [System.StringComparison]::OrdinalIgnoreCase)) { $imagePathMatches = $true break } } if (-not $imagePathMatches) { $results.HollowingIndicators += [PSCustomObject]@{ ProcessName = $process.ProcessName ProcessId = $process.Id ExpectedPath = $expectedImagePath ActualPath = $actualImagePath Reason = "Well-known system process name running from an unexpected image location - masquerading/hollowing heuristic (image paths compared, memory contents NOT compared)" } $results.Statistics.HollowingIndicators++ $processAnalysis.Findings += "System process name with unexpected image location: $actualImagePath (masquerade/hollowing heuristic)" } } } # Check for code injection indicators # Unusual memory characteristics $memoryRatio = if ($process.VirtualMemorySize64 -gt 0) { $process.WorkingSet64 / $process.VirtualMemorySize64 } else { 0 } if ($memoryRatio -gt 0.9) { $results.InjectionIndicators += [PSCustomObject]@{ ProcessName = $process.ProcessName ProcessId = $process.Id Indicator = "High memory ratio" Value = [math]::Round($memoryRatio, 2) Reason = "Unusually high working set to virtual memory ratio - possible injection" } $results.Statistics.InjectionIndicators++ $processAnalysis.Findings += "Unusual memory characteristics" } # Check for suspicious thread count $threadCount = $process.Threads.Count if ($threadCount -gt 100) { $processAnalysis.Findings += "Unusually high thread count: $threadCount" } # Check for remote threads try { foreach ($thread in $process.Threads) { # Remote threads often have start addresses outside main module if ($thread.StartAddress -and $process.MainModule) { $moduleBase = $process.MainModule.BaseAddress.ToInt64() $moduleEnd = $moduleBase + $process.MainModule.ModuleMemorySize $startAddr = $thread.StartAddress.ToInt64() if ($startAddr -lt $moduleBase -or $startAddr -gt $moduleEnd) { $results.InjectionIndicators += [PSCustomObject]@{ ProcessName = $process.ProcessName ProcessId = $process.Id Indicator = "Remote thread detected" ThreadId = $thread.Id Reason = "Thread start address outside main module - possible remote injection" } $results.Statistics.InjectionIndicators++ break # Only report once per process } } } } catch { Write-Verbose "Could not analyze threads for $($process.ProcessName)" } # Extract strings from memory if requested (basic implementation) if ($IncludeStrings) { try { # This is a simplified version - full string extraction requires more complex memory reading $processAnalysis.Findings += "String extraction requires external tools like Sysinternals Strings" } catch { Write-Verbose "Could not extract strings from $($process.ProcessName)" } } $results.ProcessAnalysis += $processAnalysis } catch { Write-Verbose "Error analyzing process $($process.ProcessName): $_" } } } catch { Write-Error "Error during memory analysis: $_" throw } } end { Write-Host "`nMemory Analysis Complete!" -ForegroundColor Green Write-Host "`n=== Statistics ===" -ForegroundColor Cyan Write-Host "Processes analyzed: $($results.Statistics.ProcessesAnalyzed)" -ForegroundColor Yellow Write-Host "Injection indicators: $($results.Statistics.InjectionIndicators)" -ForegroundColor $(if ($results.Statistics.InjectionIndicators -gt 0) { 'Red' } else { 'Green' }) Write-Host "Hollowing indicators: $($results.Statistics.HollowingIndicators)" -ForegroundColor $(if ($results.Statistics.HollowingIndicators -gt 0) { 'Red' } else { 'Green' }) Write-Host "Suspicious modules: $($results.Statistics.SuspiciousModules)" -ForegroundColor $(if ($results.Statistics.SuspiciousModules -gt 0) { 'Red' } else { 'Green' }) return $results } } |