206 lines
9.0 KiB
PowerShell
206 lines
9.0 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)][string]$Executable,
|
|
[Parameter(Mandatory = $true)][string]$ArgumentsBase64,
|
|
[Parameter(Mandatory = $true)][string]$TraceOutput,
|
|
[Parameter(Mandatory = $true)][string]$ResultOutput,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^AASVerifier-[A-Za-z0-9-]+$')][string]$SessionName,
|
|
[Parameter(Mandatory = $true)][ValidateRange(1, 900000)][int]$CandidateTimeoutMilliseconds
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$jobSource = Join-Path $PSScriptRoot "windows-job.cs"
|
|
if (!(Test-Path -LiteralPath $jobSource -PathType Leaf)) { throw "Windows Job Object helper is unavailable" }
|
|
Add-Type -Path $jobSource
|
|
$etl = Join-Path ([IO.Path]::GetDirectoryName($TraceOutput)) "$SessionName.etl"
|
|
$csv = Join-Path ([IO.Path]::GetDirectoryName($TraceOutput)) "$SessionName.csv"
|
|
$arguments = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($ArgumentsBase64)) | ConvertFrom-Json
|
|
$providers = @(
|
|
"Microsoft-Windows-Kernel-Process",
|
|
"Microsoft-Windows-Kernel-File",
|
|
"Microsoft-Windows-Winsock-AFD",
|
|
"Microsoft-Windows-DNS-Client"
|
|
)
|
|
$jobProcess = $null
|
|
|
|
try {
|
|
& logman.exe create trace $SessionName -o $etl -ets | Out-Null
|
|
foreach ($provider in $providers) {
|
|
& logman.exe update trace $SessionName -p $provider 0xffffffffffffffff 0xff -ets | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "Unable to enable ETW provider $provider" }
|
|
}
|
|
$started = [DateTimeOffset]::UtcNow
|
|
$jobProcess = [AasVerifier.JobProcess]::Start(
|
|
$Executable,
|
|
[string[]]$arguments,
|
|
"$ResultOutput.stdout",
|
|
"$ResultOutput.stderr"
|
|
)
|
|
$waitResult = [AasVerifier.JobProcess]::Wait($jobProcess, $CandidateTimeoutMilliseconds)
|
|
$timedOut = $waitResult -eq [AasVerifier.JobProcess]::WaitTimeout
|
|
if ($timedOut) {
|
|
[AasVerifier.JobProcess]::Terminate($jobProcess, 124)
|
|
$waitResult = [AasVerifier.JobProcess]::Wait($jobProcess, 5000)
|
|
}
|
|
if ($waitResult -ne [AasVerifier.JobProcess]::WaitObject0) {
|
|
throw "Windows Job Object did not reach an empty process-tree state"
|
|
}
|
|
$rootExitCode = [AasVerifier.JobProcess]::ExitCode($jobProcess)
|
|
$jobTotalProcesses = [AasVerifier.JobProcess]::TotalProcesses($jobProcess)
|
|
$ended = [DateTimeOffset]::UtcNow
|
|
& logman.exe stop $SessionName -ets | Out-Null
|
|
& tracerpt.exe $etl -of CSV -o $csv -y | Out-Null
|
|
if ($LASTEXITCODE -ne 0 -or !(Test-Path -LiteralPath $csv -PathType Leaf)) { throw "ETW trace export failed" }
|
|
$rootPid = $jobProcess.ProcessId
|
|
$lines = New-Object System.Collections.Generic.List[string]
|
|
for ($processIndex = 1; $processIndex -lt $jobTotalProcesses; $processIndex++) {
|
|
$lines.Add("process|job-object|index=$processIndex")
|
|
}
|
|
$childPids = New-Object System.Collections.Generic.HashSet[int]
|
|
$childPids.Add($rootPid) | Out-Null
|
|
function Get-IntegerField($row, [string[]]$patterns) {
|
|
foreach ($property in $row.PSObject.Properties) {
|
|
foreach ($pattern in $patterns) {
|
|
if ($property.Name -match $pattern) {
|
|
$parsed = 0
|
|
$raw = ([string]$property.Value).Trim()
|
|
if ([int]::TryParse($raw, [ref]$parsed)) { return $parsed }
|
|
if ($raw -match '^0[xX][0-9a-fA-F]+$') {
|
|
try { return [Convert]::ToInt32($raw.Substring(2), 16) } catch { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
function Convert-ObservedInteger([string]$raw) {
|
|
$value = $raw.Trim().Trim('"').Trim("'")
|
|
$parsed = 0
|
|
if ([int]::TryParse($value, [ref]$parsed)) { return $parsed }
|
|
if ($value -match '^0[xX][0-9a-fA-F]+$') {
|
|
try { return [Convert]::ToInt32($value.Substring(2), 16) } catch { }
|
|
}
|
|
return 0
|
|
}
|
|
function Get-PayloadInteger($row, [string]$name) {
|
|
foreach ($property in $row.PSObject.Properties) {
|
|
if (([string]$property.Name).Trim() -ieq $name) {
|
|
$direct = Convert-ObservedInteger ([string]$property.Value)
|
|
if ($direct -gt 0) { return $direct }
|
|
}
|
|
}
|
|
$payloadPattern = '(?i)(?:^|[;,{\s])["'']?' + [regex]::Escape($name) + '["'']?\s*[:=]\s*["'']?(0[xX][0-9a-fA-F]+|[0-9]+)["'']?'
|
|
foreach ($property in $row.PSObject.Properties) {
|
|
$text = [string]$property.Value
|
|
$match = [regex]::Match($text, $payloadPattern)
|
|
if ($match.Success) { return Convert-ObservedInteger $match.Groups[1].Value }
|
|
}
|
|
return 0
|
|
}
|
|
$totalRows = 0
|
|
$rootRows = 0
|
|
$networkRows = 0
|
|
$writeRows = 0
|
|
$winsockCreateRows = 0
|
|
$winsockDecodedPids = New-Object System.Collections.Generic.List[int]
|
|
$processStartRows = 0
|
|
$rootStopRows = 0
|
|
$rootExitObserved = $false
|
|
$postRootDescendantWriteRows = 0
|
|
$rootEventSamples = New-Object System.Collections.Generic.List[string]
|
|
foreach ($row in (Import-Csv -LiteralPath $csv)) {
|
|
$totalRows++
|
|
$serialized = $row | ConvertTo-Json -Compress
|
|
$eventName = (($row.PSObject.Properties | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join " ")
|
|
$providerName = ([string]$row.'Event Name').Trim()
|
|
$eventType = ([string]$row.Type).Trim()
|
|
$eventId = Get-IntegerField $row @("(?i)^Event ID$")
|
|
$opcode = Get-IntegerField $row @("(?i)^Opcode$")
|
|
if ($providerName -eq 'Microsoft-Windows-Kernel-Process' -and $eventType -eq 'Start' -and $opcode -eq 1) {
|
|
$processStartRows++
|
|
# Process Start events are emitted in the creating process context, so the
|
|
# ETW header PID is the parent. The payload ProcessID is the new process.
|
|
$parentPid = Get-IntegerField $row @("(?i)^PID$")
|
|
if ($parentPid -eq 0) { $parentPid = Get-PayloadInteger $row 'ParentProcessID' }
|
|
$newPid = Get-PayloadInteger $row 'ProcessID'
|
|
if ($childPids.Contains($parentPid) -and $newPid -gt 0 -and !$childPids.Contains($newPid)) {
|
|
$childPids.Add($newPid) | Out-Null
|
|
$lines.Add("process|$newPid|parent=$parentPid")
|
|
}
|
|
continue
|
|
}
|
|
if ($providerName -eq 'Microsoft-Windows-Kernel-Process' -and $eventType -eq 'Stop' -and $opcode -eq 2) {
|
|
$stoppedPid = Get-PayloadInteger $row 'ProcessID'
|
|
if ($stoppedPid -eq 0) { $stoppedPid = Get-IntegerField $row @("(?i)^PID$") }
|
|
if ($stoppedPid -eq $rootPid) {
|
|
$rootStopRows++
|
|
$rootExitObserved = $true
|
|
}
|
|
continue
|
|
}
|
|
if ($providerName -like 'Microsoft-Windows-Winsock*' -and $eventId -eq 1000) {
|
|
$winsockCreateRows++
|
|
$userModePid = Get-PayloadInteger $row 'UserModePid'
|
|
if ($userModePid -gt 0 -and $winsockDecodedPids.Count -lt 8 -and !$winsockDecodedPids.Contains($userModePid)) {
|
|
$winsockDecodedPids.Add($userModePid)
|
|
}
|
|
$networkPid = Get-IntegerField $row @("(?i)^Process.*Id$", "(?i)^PID$")
|
|
if ($childPids.Contains($networkPid) -or $childPids.Contains($userModePid)) {
|
|
$networkRows++
|
|
$lines.Add("network|$networkPid|provider=$providerName;event=$eventId")
|
|
}
|
|
continue
|
|
}
|
|
$pidValue = Get-IntegerField $row @("(?i)^Process.*Id$", "(?i)^PID$")
|
|
if (!$childPids.Contains($pidValue)) { continue }
|
|
$rootRows++
|
|
if ($rootEventSamples.Count -lt 12) {
|
|
$safeIdentity = (($row.PSObject.Properties | Where-Object {
|
|
$_.Name -match '(?i)^(Event Name|Type|Event ID|Opcode|Task|Keyword|PID|Provider Name|Provider Guid)$'
|
|
} | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join ';')
|
|
if ($safeIdentity -and !$rootEventSamples.Contains($safeIdentity)) { $rootEventSamples.Add($safeIdentity) }
|
|
}
|
|
if ($providerName -eq 'Microsoft-Windows-DNS-Client') {
|
|
$networkRows++
|
|
$lines.Add("network|$pidValue|provider=$providerName;event=$eventId")
|
|
}
|
|
elseif ($providerName -eq 'Microsoft-Windows-Kernel-File' -and $eventId -in @(16,17,18,19)) {
|
|
$writeRows++
|
|
if ($rootExitObserved -and $pidValue -ne $rootPid) { $postRootDescendantWriteRows++ }
|
|
$lines.Add("write|$pidValue|provider=$providerName;event=$eventId")
|
|
}
|
|
}
|
|
[IO.File]::WriteAllLines($TraceOutput, $lines, (New-Object Text.UTF8Encoding($false)))
|
|
$receipt = [ordered]@{
|
|
code = $(if ($timedOut) { 124 } else { $rootExitCode })
|
|
signal = $null
|
|
stdout = [IO.File]::ReadAllText("$ResultOutput.stdout")
|
|
stderr = [IO.File]::ReadAllText("$ResultOutput.stderr")
|
|
timedOut = $timedOut
|
|
outputLimitExceeded = $false
|
|
startedAt = $started.ToString("o")
|
|
endedAt = $ended.ToString("o")
|
|
observerDiagnostics = [ordered]@{
|
|
totalRows = $totalRows
|
|
rootRows = $rootRows
|
|
networkRows = $networkRows
|
|
writeRows = $writeRows
|
|
winsockCreateRows = $winsockCreateRows
|
|
winsockDecodedPids = @($winsockDecodedPids)
|
|
processStartRows = $processStartRows
|
|
rootStopRows = $rootStopRows
|
|
postRootDescendantWriteRows = $postRootDescendantWriteRows
|
|
rootEventSamples = @($rootEventSamples)
|
|
sessionName = $SessionName
|
|
processTreeTimedOut = $timedOut
|
|
processTreeEmpty = $waitResult -eq [AasVerifier.JobProcess]::WaitObject0
|
|
jobTotalProcesses = $jobTotalProcesses
|
|
}
|
|
}
|
|
[IO.File]::WriteAllText($ResultOutput, ($receipt | ConvertTo-Json -Compress), (New-Object Text.UTF8Encoding($false)))
|
|
}
|
|
finally {
|
|
& logman.exe stop $SessionName -ets 2>$null | Out-Null
|
|
[AasVerifier.JobProcess]::Close($jobProcess)
|
|
Remove-Item -LiteralPath $etl,$csv,"$ResultOutput.stdout","$ResultOutput.stderr" -Force -ErrorAction SilentlyContinue
|
|
}
|