ETW Research Scripts

VersionN/A
Updated
AuthorJoshua FinleyLicenseDBE
Table of Contents
1The Scripts

Another brief post sharing some quick-hack PowerShell scripts I’ve created while researching ETW on Windows 11 and following along in Matt Hand’s Evading EDR.

1. The Scripts

The scripts are:

Both of these scripts are quick experiments but have proved useful in my testing. Improvements could be made to increase accuracy and performance. Additionally, for Get-PossibleEtwRefs.ps1, it would likely not be so difficult to leverage SDKs for IDA/Ghidra/Binja to compute cross-references to the identified GUIDs and see if any are near any relevant ETW functions, allowing us to easily associate the reference with Provider / Controller / Consumer code (similar to what is demonstrated manually in Evading EDR ch. 8), or do similar analysis.

Note: The versions here won’t see any updates. I will do that on the respective GitHub gists.

1.1. Get-PossibleEtwRefs

# Get-PossibleEtwRefs.ps1
#
# Search for GUIDs in a target file or directory and check
# if it might be an ETW provider GUID using logman.
#
# Inspired by chapter 8 of "Evading EDR" by Matt Hand.
#
# Not fast but easy to deploy and use.

param (
    [string]$Path
)

function Get-GUIDsFromFile($filePath) {
    $regex = '[{(]?[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}[)}]?'
    $foundGuids = @{}
    
    # Process file in chunks
    $stream = [System.IO.File]::OpenRead($filePath)
    $buffer = New-Object byte[] 4MB
    $totalBytesRead = 0
    
    try {
        $overlap = 36  # GUID can be up to 36 chars, need to keep this overlap between chunks
        $carryover = ""
        
        while ($true) {
            $bytesRead = $stream.Read($buffer, 0, $buffer.Length)
            if ($bytesRead -eq 0) { break }
            
            # Convert bytes to ASCII replacing non-printable chars with spaces
            $text = -join ($buffer[0..($bytesRead-1)] | ForEach-Object {
                if ($_ -ge 32 -and $_ -le 126) { [char]$_ } else { ' ' }
            })
            
            # Combine with carryover from previous chunk
            $textToSearch = $carryover + $text
            
            # Find GUIDs
            $matches = [regex]::Matches($textToSearch, $regex)
            foreach ($match in $matches) {
                $matchOffset = $totalBytesRead - $carryover.Length + $match.Index
                # Only add if we haven't seen this GUID before
                if (-not $foundGuids.ContainsKey($match.Value)) {
                    $foundGuids[$match.Value] = $matchOffset
                }
            }
            
            # Save overlap for next chunk
            $carryover = $text.Substring([Math]::Max(0, $text.Length - $overlap))
            $totalBytesRead += $bytesRead
            
            # Force garbage collection periodically
            [System.GC]::Collect()
        }
    }
    finally {
        $stream.Close()
        $stream.Dispose()
    }
    
    # Return results as objects
    $results = @()
    foreach ($guid in $foundGuids.Keys | Sort-Object) {
        $results += [PSCustomObject]@{
            GUID = $guid
            Offset = $foundGuids[$guid]
        }
    }
    
    return $results
}

function Is-ETWProviderGUID($guid) {
    # Cache ETW provider GUIDs to avoid repeated lookups
    if (-not (Test-Path variable:global:ETWProviderCache)) {
        $global:ETWProviderCache = @{}
        
        # Pre-populate cache with all ETW providers - do this only once
        Write-Host "Caching ETW providers..." -ForegroundColor Yellow
        $providers = logman query providers
        foreach ($provider in $providers) {
            if ($provider -match '(?:\{|\()([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(?:\}|\))') {
                $global:ETWProviderCache[$Matches[1].ToLower()] = $true
            }
        }
    }
    
    $cleanedGuid = $guid.Trim('{}()').ToLower()
    return $global:ETWProviderCache.ContainsKey($cleanedGuid)
}

# Main script
if (-not (Test-Path $Path)) {
    Write-Error "Path not found: $Path"
    exit 1
}

$files = if ((Get-Item $Path).PSIsContainer) {
    Get-ChildItem -Path $Path -Recurse -File
} else {
    Get-Item $Path
}

# Define colors for highlighting
$etwColor = "Green"
$unknownColor = "Gray"
$defaultColor = (Get-Host).UI.RawUI.ForegroundColor

foreach ($file in $files) {
    Write-Host "`nScanning: $($file.FullName)" -ForegroundColor Cyan
    
    # Process each file individually and clean up after
    $guidResults = Get-GUIDsFromFile $file.FullName
    
    foreach ($result in $guidResults) {
        $guid = $result.GUID
        $offset = $result.Offset
        $cleaned = $guid.Trim('{}()')
        
        $offsetHex = "0x{0:X8}" -f $offset
        
        if (Is-ETWProviderGUID $cleaned) {
            Write-Host "  [ETW] " -ForegroundColor $etwColor -NoNewline
            Write-Host "$guid " -ForegroundColor $etwColor -NoNewline
            Write-Host "at offset $offsetHex" -ForegroundColor $etwColor
        } else {
            Write-Host "  [UNK] " -ForegroundColor $unknownColor -NoNewline
            Write-Host "$guid " -ForegroundColor $defaultColor -NoNewline
            Write-Host "at offset $offsetHex" -ForegroundColor $defaultColor
        }
    }
    
    # Force cleanup after each file
    Remove-Variable guidResults -ErrorAction SilentlyContinue
    [System.GC]::Collect()
}

# Clean up the cache at the end
Remove-Variable -Name ETWProviderCache -Scope Global -ErrorAction SilentlyContinue

An example run through System32 shows some successful matches:

...
Scanning: C:\Windows\System32\windowsperformancerecordercontrol.dll
  [ETW] {36b6f488-aad7-48c2-afe3-d4ec2c8b46fa} at offset 0x00133A87
  [ETW] 0a002690-3839-4e3a-b3b6-96d8df868d99 at offset 0x0010B2FD
  [ETW] 0CC157B3-CF07-4FC2-91EE-31AC92E05FE1 at offset 0x000FFC97
  [ETW] 245f975d-909d-49ed-b8f9-9a75691d6b6b at offset 0x0010BC4D
  [ETW] 315a8872-923e-4ea2-9889-33cd4754bf64 at offset 0x00105CB4
  [ETW] 36b6f488-aad7-48c2-afe3-d4ec2c8b46fa at offset 0x000FE24B
  [ETW] 486A5C7C-11CC-46C5-9DE7-43DFE0BB57C1 at offset 0x0010C7D5
  [ETW] 48D445A8-2F64-4D49-B093-A5774D8DC531 at offset 0x00101902
  [ETW] 4bd2826e-54a1-4ba9-bf63-92b73ea1ac4a at offset 0x00108FE4
  [ETW] 531a35ab-63ce-4bcf-aa98-f88c7a89e455 at offset 0x0010BA6B
  [ETW] 5412704e-b2e1-4624-8ffd-55777b8f7373 at offset 0x0010551B
  [ETW] 57277741-3638-4A4B-BDBA-0AC6E45DA56C at offset 0x000FE80C
  [ETW] 59819d0a-adaf-46b2-8d7c-990bc39c7c15 at offset 0x00105727
  [ETW] 5c8bb950-959e-4309-8908-67961a1205d5 at offset 0x00108D24
  [ETW] 7426a56b-e2d5-4b30-bdef-b31815c1a74a at offset 0x0010593F
  [ETW] 751ef305-6c6e-4fed-b847-02ef79d26aef at offset 0x0010B38C
  [ETW] 7E7D3382-023C-43cb-95D2-6F0CA6D70381 at offset 0x0010B1A7
  ... And many more
...

Just based on this DLL name, it makes sense that we would see some ETW provider GUIDs inside it.

1.2. Get-EtwProviderAces

# Get-EtwProviderAces.ps1
#
# Helper script to get SDDL/ACES for ETW Providers
# Inspired by / draws from Chapter 8 of "Evading EDR" by Matt Hand
param (
    [Parameter(Mandatory = $false)]
    [string]$ProviderName,

    [Parameter(Mandatory = $false)]
    [Guid]$Guid
)

if (-not $ProviderName -and -not $Guid) {
    Write-Error "You must specify either -ProviderName or -Guid."
    exit 1
}

if ($ProviderName) {
    $providerInfo = wevtutil gp "$ProviderName" 2>$null
    if (-not $providerInfo) {
        Write-Error "Could not retrieve provider info."
        exit 1
    }

    $guidLine = $providerInfo | Where-Object { $_ -match '^GUID:' }
    if (-not $guidLine) {
        Write-Error "GUID not found in provider info."
        exit 1
    }

    $guid = $guidLine -replace '^GUID:\s*', ''
    $guid = $guid.Trim('{}').ToUpper()
} else {
    $guid = $Guid.ToString().ToUpper()
}

try {
    $sdTable = Get-ItemProperty -Path HKLM:\System\CurrentControlSet\Control\WMI\Security
    $binarySD = $sdTable.$guid
    if (-not $binarySD) {
        throw "No binary security descriptor found for GUID: {$guid}"
    }

    $sddl = ([wmiclass]"Win32_SecurityDescriptorHelper").BinarySDToSDDL($binarySD).SDDL
    $decoded = ConvertFrom-SddlString -Sddl $sddl

    if ($ProviderName) {
        Write-Output "Provider Name: $ProviderName"
    }
    Write-Output "GUID: {$guid}"
    Write-Output "SDDL: $sddl"
    Write-Output "Decoded:"
    $decoded
} catch {
    Write-Error $_
}