2021-03-26 23:55:24 +03:00
|
|
|
<#
|
|
|
|
|
|
|
|
.SYNOPSIS
|
|
|
|
This script invokes an ETW trace collection
|
|
|
|
|
2021-03-30 22:03:31 +03:00
|
|
|
.PARAMETER SessionName
|
|
|
|
The name of an ETW session.
|
2021-03-27 00:34:28 +03:00
|
|
|
|
2021-03-30 22:03:31 +03:00
|
|
|
.PARAMETER SessionName
|
|
|
|
The name of a netsh tracing scenario to start.
|
|
|
|
|
|
|
|
.PARAMETER EtlPath
|
|
|
|
The name of an ETL file.
|
2021-03-27 00:34:28 +03:00
|
|
|
|
|
|
|
.PARAMETER TmfPath
|
2021-03-30 22:03:31 +03:00
|
|
|
The path of the TMF files (used to convert ETL).
|
|
|
|
|
|
|
|
.PARAMETER Start
|
|
|
|
If set, starts a new netsh.exe trace session for the given NetshScenario.
|
|
|
|
|
|
|
|
.PARAMETER Stop
|
|
|
|
If set, stops a running netsh.exe trace session.
|
|
|
|
|
|
|
|
.PARAMETER Flush
|
|
|
|
If set, flushes the specified SessionName. Also can populate EtlPath
|
|
|
|
automatically from the ETW session configuration.
|
|
|
|
|
|
|
|
.PARAMETER Convert
|
|
|
|
If set, convert the (either manually specified or automatically populated
|
|
|
|
by -Flush) EtlPath to text.
|
2021-03-27 00:34:28 +03:00
|
|
|
|
2021-03-26 23:55:24 +03:00
|
|
|
.PARAMETER Sanitize
|
2021-03-27 00:34:28 +03:00
|
|
|
If set, sanitizes IP addresses in the converted ETL file.
|
2021-06-18 00:13:14 +03:00
|
|
|
|
|
|
|
.PARAMETER UseSaltedHash
|
|
|
|
If set, IP address sanitization uses a salted hash instead of REDACTED.
|
2021-03-26 23:55:24 +03:00
|
|
|
#>
|
|
|
|
|
|
|
|
param (
|
|
|
|
[Parameter(Mandatory = $false)]
|
2021-03-30 22:03:31 +03:00
|
|
|
[string]$SessionName = "",
|
2021-03-27 00:34:28 +03:00
|
|
|
|
|
|
|
[Parameter(Mandatory = $false)]
|
2021-03-30 22:03:31 +03:00
|
|
|
[string]$NetshScenario = "",
|
|
|
|
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
|
|
[string]$EtlPath = "",
|
2021-03-27 00:34:28 +03:00
|
|
|
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
|
|
[string]$TmfPath = "",
|
2021-03-26 23:55:24 +03:00
|
|
|
|
2021-03-30 22:03:31 +03:00
|
|
|
[Parameter(Mandatory = $false)]
|
|
|
|
[switch]$Start,
|
|
|
|
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
|
|
[switch]$Stop,
|
|
|
|
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
|
|
[switch]$Flush,
|
|
|
|
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
|
|
[switch]$Convert,
|
|
|
|
|
2021-03-27 00:34:28 +03:00
|
|
|
[Parameter(Mandatory = $false)]
|
2021-06-18 00:13:14 +03:00
|
|
|
[switch]$Sanitize,
|
|
|
|
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
|
|
[switch]$UseSaltedHash
|
2021-03-26 23:55:24 +03:00
|
|
|
)
|
|
|
|
|
2021-03-30 22:03:31 +03:00
|
|
|
Set-StrictMode -Version 'Latest'
|
|
|
|
$PSDefaultParameterValues['*:ErrorAction'] = 'Stop'
|
|
|
|
|
2021-04-01 20:15:34 +03:00
|
|
|
$IPv4Pattern = '(((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))'
|
|
|
|
$IPv6Pattern = ":(?::[a-f\d]{1,4}){0,5}(?:(?::[a-f\d]{1,4}){1,2}|:(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})))|[a-f\d]{1,4}:(?:[a-f\d]{1,4}:(?:[a-f\d]{1,4}:(?:[a-f\d]{1,4}:(?:[a-f\d]{1,4}:(?:[a-f\d]{1,4}:(?:[a-f\d]{1,4}:(?:[a-f\d]{1,4}|:)|(?::(?:[a-f\d]{1,4})?|(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))))|:(?:(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|[a-f\d]{1,4}(?::[a-f\d]{1,4})?|))|(?::(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|:[a-f\d]{1,4}(?::(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|(?::[a-f\d]{1,4}){0,2})|:))|(?:(?::[a-f\d]{1,4}){0,2}(?::(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|(?::[a-f\d]{1,4}){1,2})|:))|(?:(?::[a-f\d]{1,4}){0,3}(?::(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|(?::[a-f\d]{1,4}){1,2})|:))|(?:(?::[a-f\d]{1,4}){0,4}(?::(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|(?::[a-f\d]{1,4}){1,2})|:))"
|
2021-03-26 23:55:24 +03:00
|
|
|
|
|
|
|
# Compile the regex for performance reasons, also ignore case
|
|
|
|
$RegexOptions = [System.Text.RegularExpressions.RegexOptions]::Compiled
|
|
|
|
$RegexOptions += [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
|
2021-04-01 20:15:34 +03:00
|
|
|
|
2021-06-18 00:13:14 +03:00
|
|
|
$IPv6Regex = [Regex]::new($IPv6Pattern, $RegexOptions)
|
2021-04-01 20:15:34 +03:00
|
|
|
$IPv4Regex = [Regex]::new($IPv4Pattern, $RegexOptions)
|
2021-03-26 23:55:24 +03:00
|
|
|
|
2021-06-18 00:13:14 +03:00
|
|
|
# Create HMAC for salted hashing.
|
|
|
|
$Secret = ""
|
|
|
|
for ($i = 0; $i -lt 4; $i++) {
|
|
|
|
$Secret+= $(Get-Random).ToString('x8')
|
|
|
|
}
|
|
|
|
$HmacSha = New-Object System.Security.Cryptography.HMACSHA256
|
|
|
|
$HmacSha.key = [Text.Encoding]::ASCII.GetBytes($Secret)
|
|
|
|
|
|
|
|
function Perform-SaltedHash($Input) {
|
|
|
|
return [Convert]::ToBase64String($HmacSha.ComputeHash([Text.Encoding]::ASCII.GetBytes($Input))).SubString(0,12)
|
|
|
|
}
|
|
|
|
|
2021-03-26 23:55:24 +03:00
|
|
|
function Format-IPAddresses {
|
|
|
|
[CmdletBinding()]
|
|
|
|
param(
|
|
|
|
[Parameter(ValueFromPipeline=$True)]
|
|
|
|
$InputData,
|
|
|
|
|
|
|
|
[boolean]$Sanitize
|
|
|
|
)
|
|
|
|
|
|
|
|
process {
|
|
|
|
if (!$Sanitize) {
|
|
|
|
return $_
|
|
|
|
}
|
|
|
|
$ShrunkLine = $InputData
|
|
|
|
# Skip to first space to bypass date, which is seen as a valid IPv6 address
|
|
|
|
$Idx = ([string]$InputData).IndexOf(' ');
|
|
|
|
if ($Idx -ge 0) {
|
|
|
|
$ShrunkLine = $InputData.Substring($Idx);
|
|
|
|
}
|
|
|
|
$Line = $InputData
|
|
|
|
|
2021-04-01 20:15:34 +03:00
|
|
|
# Explicitly check IPv4, as the way IPv4 is printed by ETW
|
|
|
|
# partially matches the IPv6 cases, which doesn't fully match
|
|
|
|
# and then exits, leaving the address exposed.
|
2021-06-18 00:13:14 +03:00
|
|
|
if ($UseSaltedHash) {
|
|
|
|
$IPv4Matches = $IPv4Regex.Matches($ShrunkLine)
|
|
|
|
foreach ($Match in $IPv4Matches) {
|
|
|
|
$HashedMatch = Perform-SaltedHash $Match
|
|
|
|
$Line = $Line.Replace($Match, $HashedMatch);
|
|
|
|
}
|
|
|
|
|
|
|
|
$IPv6Matches = $IPv6Regex.Matches($ShrunkLine)
|
|
|
|
foreach ($Match in $IPv6Matches) {
|
|
|
|
$HashedMatch = Perform-SaltedHash $Match
|
|
|
|
$Line = $Line.Replace($Match, $HashedMatch);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$IPv4Matches = $IPv4Regex.Matches($ShrunkLine)
|
|
|
|
foreach ($Match in $IPv4Matches) {
|
|
|
|
$Line = $Line.Replace($Match, "REDACTED");
|
|
|
|
}
|
|
|
|
|
|
|
|
$IPv6Matches = $IPv6Regex.Matches($ShrunkLine)
|
|
|
|
foreach ($Match in $IPv6Matches) {
|
|
|
|
$Line = $Line.Replace($Match, "REDACTED");
|
|
|
|
}
|
2021-03-26 23:55:24 +03:00
|
|
|
}
|
|
|
|
return $Line
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-30 22:03:31 +03:00
|
|
|
# Starts a netsh tracing ETW session for the given scenario.
|
|
|
|
if ($Start) {
|
|
|
|
if ($NetshScenario -eq "") {
|
|
|
|
Write-Error "No NetshScenario argument present"
|
|
|
|
}
|
|
|
|
if ($SessionName -eq "") {
|
|
|
|
Write-Error "No SessionName argument present"
|
|
|
|
}
|
|
|
|
if ($EtlPath -eq "") {
|
|
|
|
$EtlPath = Join-Path $env:temp "$($SessionName).etl"
|
|
|
|
}
|
|
|
|
|
|
|
|
# Start the ETW session running.
|
|
|
|
$Command = "netsh.exe trace start scenario=$($NetshScenario) sessionname=$($SessionName) tracefile=$($EtlPath)"
|
|
|
|
Write-Debug $Command
|
|
|
|
$Result = Invoke-Expression $Command
|
|
|
|
}
|
|
|
|
|
|
|
|
# Stops a netsh tracing session
|
|
|
|
if ($Stop) {
|
|
|
|
if ($SessionName -eq "") {
|
|
|
|
Write-Error "No SessionName argument present"
|
|
|
|
}
|
|
|
|
if ($EtlPath -eq "") {
|
|
|
|
$EtlPath = Join-Path $env:temp "$($SessionName).etl"
|
|
|
|
}
|
|
|
|
|
|
|
|
# Stop the ETW session.
|
|
|
|
$Command = "netsh.exe trace stopsessionname=$($SessionName)"
|
|
|
|
Write-Debug $Command
|
|
|
|
$Result = Invoke-Expression $Command
|
|
|
|
}
|
|
|
|
|
|
|
|
# Flush an ETW session.
|
|
|
|
if ($Flush) {
|
|
|
|
if ($SessionName -eq "") {
|
|
|
|
Write-Error "No SessionName argument present"
|
|
|
|
}
|
|
|
|
|
|
|
|
# Query the ETW session status (and config).
|
|
|
|
$Command = "logman.exe query $($SessionName) -ets"
|
|
|
|
Write-Debug $Command
|
|
|
|
$QueryResult = Invoke-Expression $Command
|
|
|
|
if (!$QueryResult.Contains("The command completed successfully.")) {
|
|
|
|
Write-Error "Unable to find the logging session`n$QueryResult"
|
|
|
|
}
|
|
|
|
|
2021-03-27 00:34:28 +03:00
|
|
|
# Flush the ETW memory buffers to disk.
|
2021-03-30 22:03:31 +03:00
|
|
|
$Command = "logman.exe update $($SessionName) -ets -fd"
|
2021-03-27 00:34:28 +03:00
|
|
|
Write-Debug $Command
|
|
|
|
Invoke-Expression $Command 2>&1 | Out-Null
|
2021-03-30 22:03:31 +03:00
|
|
|
|
|
|
|
# Grab the ETL output path if not already specified.
|
|
|
|
if ($EtlPath -eq "") {
|
|
|
|
$outputLocationLine = $QueryResult.Split("`r`n") | Select-String "Output Location:"
|
|
|
|
if ($outputLocationLine -eq "") {
|
|
|
|
Write-Error "Cannot extract EtlPath output location`n$($QueryResult)"
|
|
|
|
}
|
|
|
|
$EtlPath = $outputLocationLine.Line.ToString().Split(':', 2)[1].Trim()
|
|
|
|
}
|
2021-03-27 00:34:28 +03:00
|
|
|
}
|
|
|
|
|
2021-03-30 22:03:31 +03:00
|
|
|
# Convert an ETL to text (sanitizing as necessary).
|
|
|
|
if ($Convert) {
|
|
|
|
if ($EtlPath -eq "") {
|
|
|
|
Write-Error "No EtlPath argument present"
|
|
|
|
}
|
|
|
|
if (!(Test-Path $EtlPath)) {
|
|
|
|
Write-Error "$EtlPath file not found"
|
|
|
|
}
|
|
|
|
|
2021-03-27 00:34:28 +03:00
|
|
|
# Convert the ETL to text.
|
|
|
|
$OutputPath = Join-Path $env:temp "temp.log"
|
2021-03-30 22:03:31 +03:00
|
|
|
$Command = "netsh trace convert $($EtlPath) output=$($OutputPath) overwrite=yes report=no"
|
2021-03-27 00:34:28 +03:00
|
|
|
if ($TmfPath -ne "") {
|
|
|
|
$Command += " tmfpath=$($TmfPath)"
|
|
|
|
}
|
|
|
|
Write-Debug $Command
|
|
|
|
Invoke-Expression $Command 2>&1 | Out-Null
|
|
|
|
|
|
|
|
# Get the text content, sanitizing as necessary.
|
|
|
|
Get-Content -Path $OutputPath `
|
|
|
|
| Where-Object { !$_.Contains("(No Format Information found)")} `
|
|
|
|
| Format-IPAddresses -Sanitize $Sanitize
|
|
|
|
}
|