[PowerShell Script] PowerDbg – Using PowerShell to Control WinDbg

[Note: According to Lee Holmes (one of the PowerShell creators) recommendation I changed the name convention. The images were not updated.]

Sometime ago a colleague of mine, Vandy Rodrigues, from the Messaging team, was enthusiastic to tell me about PowerShell and why I should learn it.

I must admit to my readers that my reaction was skeptical: Why do I need to learn yet another script language?

However, after the first demonstration, I fell in love with PowerShell.

After reading some PowerShell books, I decided to create my first PowerShell code. I thought about the subject and had some ideas. The idea I decided to implement is a PowerShell script that interacts with WinDbg as a replacement for the WinDbg programming language.

Thus, in this article, I introduce you to the PowerDbg library!

The initial idea was to interact with DBGENG.DLL that is part of WinDbg. I discarded this possibility for several reasons, among them limited documentation of DBGENG.DLL.

Then I considered MDBENG.DLL, which is a wrapper for DBGENG.DLL. This is the approach I wanted to use; unfortunately this dll is for internal use only.

In the future, when MDBENG.DLL becomes public, I’ll use it.

Therefore, I chose a third approach, one using the same tools that my readers and customers can use: WSH – Windows Script Host.

PowerDbg is composed of functions that basically do this:

- Send commands to WinDbg.

- Extract the output from commands sent to WinDbg.

OVERVIEW

The PowerDbg functions use 3 files:

POWERDBG.LOG              ß Created where WinDbg is running.

POWERDBG-OUTPUT.LOG  ß Created where the functions/scripts are called.

POWERDBG-PARSED.LOG  ß Created where the functions/scripts are called.

POWERDBG.LOG is the output from commands sent to WinDbg. The parser functions extract information from this file.

POWERDBG-OUTPUT.LOG is the summarized command output. It has just the relevant data. The parser family of functions uses this file.

POWERDBG-PARSED.LOG is created by the parser functions that read the content from POWERDBG-OUTPUT.LOG and create a CSV file, which is the POWERDBG-PARSED.LOG.

The CSV file can be mapped to a hash table, and there’s a specific PowerDbg function that converts this CSV file into a hash table.

The commands are sent to WinDbg using Send-PowerDbgCommand.

This function sends commands to the first WinDbg instance that has the title PowerDbg.

If the WinDbg is not running you can use Start-PowerDbgWinDbg. This function automatically changes the WinDbg title to PowerDbg.

Note: PowerDbg requires a WinDbg instance using the title PowerDbg. If you have an opened WinDbg instance you can change the title by using:

.wtitle PowerDbg

Parse-PowerDbgCOMMANDNAME – These functions extract data from a specific command. There are several parser functions, and I continue to create more.

Some of them are:

Parse-PowerDbgLMI

Parse-PowerDbgDUMPMODULE

Parse-PowerDbgDUMPMD

Parse-PowerDbgDT

To send commands to WinDbg:

Send-PowerDbgDML  - Sends commands using DML, in other words, hyperlinks. It uses the function below.

Send-PowerDbgCommand - Uses WMI to communicate with WinDbg. Each command and its output are saved into a log file that is constantly rewritten.

To convert a CSV file into a hash table:

Convert-PowerDbgCSVtoHashTable – Converts a CSV file into a hash table.

To start WinDbg, if there’s no instance running, you can use:

Start-PowerDbgWinDbg

With PowerDbg we can create PowerShell scripts that send commands to WinDbg and identify the output from these commands.

If you want to create your own parser functions, you just need to use mine as templates.

Example #1 - Starting Windbg from PowerShell:

 

Start-

PowerDbg

WinDbg "c:\debuggers32bit\windbg.exe" "c:\dumps\dumptest.dmp" "SRV*c:\PUBLICsymbols*https://msdl.microsoft.com/download/symbols"

Example #2 - Using current Windbg instance:

Changes Windbg title:

 

Send-

PowerDbg

DML "Display all Threads and TEB " "~* e !teb;kL 1000"

 

 

 

Clicking over the hyperlink above:

 

 

 

 

This is the source code for PowerDbg (save it into your $profile):

 

 

 

########################################################################################################

# Global variables.

########################################################################################################

$global:g_instance = $null

$global:g_fileParsedOutput = "POWERDBG-PARSED.LOG"

$global:g_fileCommandOutput = "POWERDBG-OUTPUT.LOG"

########################################################################################################

# Function: Start-PowerDbgWinDbg

#

# Parameters: [string] <$debuggerPathExe>

# Path and executable where is located your WinDbg.

#

# [string] <$nameOfDumpOrProcess>

# Name of dump file or process to debug. The process must be running.$#

#

# [string] <$symbolPath>

# Specifies the symbol file search path. Separate multiple paths with a semicolon (;).

#

# Return: Global variable $debuggerInstance that has the debugger instance.

#

# Purpose: Start an WinDbg $g_instance and open a dump file or attach to a running process.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Start-PowerDbgWinDbg(

[string] $debuggerPathExe = $(throw "Error! You must provide the Windbg path."),

[string] $nameOfDumpOrProcess = $(throw "Error! You must provide dump file or process name."),

[string] $symbolPath = $(throw "Error! You must provide the symbol path.")

)

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

$debugger = $debuggerPathExe

$isExe = $false

# Check if the argument is a dump file or process name and use the corresponding WinDbg command.

if($nameOfDumpOrProcess -ilike "*.dmp")

{

$debugger += " -z " + $nameOfDumpOrProcess

}

elseif($nameOfDumpOrProcess -ilike "*.exe")

{

$isExe = $true

}

else

{

throw "Error! You must provide a valid dump file or executing process."

}

if($isExe)

{

$debugger += " -Q -QSY -QY -v -y " + "`"$symbolPath`"" + " -c `".symfix;.reload`"" + " -T PowerDbg" + " " + $nameOfDumpOrProcess

}

else

{

$debugger += " -Q -QSY -QY -v -y " + "`"$symbolPath`"" + " -c `".symfix;.reload`"" + " -T PowerDbg"

}

# Now we can start a new debugger instance.

$global:g_instance = new-object -comobject WScript.Shell

$output = $global:g_instance.Run($debugger, 3)

# Maximize window. It's not necessary to use the full name.

$output = $global:g_instance.AppActivate("PowerDbg")

}

########################################################################################################

# Function: Send-PowerDbgCommand

#

# Parameters: [string] <$command>

# Command from Windbg. Avoid mixing more than one command at the same line to be easier to parse the output.

#

# Return: Nothing.

#

# Purpose: Sends a command to the Windbg instance that has the PowerDbg title and saves the command and its output

# into a log file named POWERDBG.LOG.

# If there's no instance that has the PowerDbg title you need to use the .wtitle command from WinDbg

# and change the WinDbg window in order to start with the PowerDbg string.

# The command output will be into the POWERDBG-OUTPUT.LOG.

# Your parser functions should use POWERDBG-OUTPUT.LOG.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Send-PowerDbgCommand([string] $command = $(throw "Error! You must provide a Windbg command."))

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# First let's locate if Start-PowerDbgWinDbg had created an WinDbg instance.

# If not, let's try to use one running instance.

if($global:g_instance -eq $null)

{

$global:g_instance = new-object -comobject WScript.Shell

}

# Set focus to Windbg instance.

$return = $global:g_instance.AppActivate("PowerDbg")

start-sleep 1

# Set focus to Command window.

$return = $global:g_instance.AppActivate("Command")

start-sleep 1

# Get the directory where the log will be created.

$aux = get-Process windbg

# We may have several instances. That's why I use element 0.

if($aux.Count -gt 0)

{

$aux = $aux[0].MainModule.Filename

}

else

{

$aux = $aux.MainModule.Filename

}

$aux = [System.IO.Path]::GetDirectoryName($aux)

# Create log.

$output = $global:g_instance.SendKeys(".logopen $aux\POWERDBG.LOG")

$output = $global:g_instance.SendKeys("{ENTER}")

# Adjust specific commands.

$command = $command.Replace("~", "{~}")

$command = $command.Replace("%", "{%}")

$command = $command.Replace("+", "{+}")

# Send command.

$output = $global:g_instance.SendKeys($command)

$output = $global:g_instance.SendKeys("{ENTER}")

# Close log, saving last command and its output.

$output = $global:g_instance.SendKeys(".logclose")

$output = $global:g_instance.SendKeys("{ENTER}")

# A delay is required here.

start-sleep 3

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

get-content "$aux\POWERDBG.LOG" | foreach-object {if(($_ -notmatch "^.:...>") -and ($_ -notmatch "^Opened log file") -and ($_ -notmatch "^Closing open log file")){$builder = $builder.AppendLine([string] $_)}}

# Save the output into a file. The location is the same you are executing PowerDbg.

out-file -filepath $global:g_fileCommandOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgDT

#

# Parameters: [switch] [$useFieldNames]

# Switch flag. If $useFieldNames is present then the function saves the field

# names from struct/classes and their values. Otherwise, it creates saves the offsets

# and their values.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "dt" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgDT([switch] $useFieldNames)

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

if($useFieldNames)

{

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "0x\S+\s+(?<key>\w+).+\:\s+(?<value>.+)")

{

$builder = $builder.AppendLine($matches["key"] + "," + $matches["value"])

}

}

}

else

{

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>0x\S+).+\:\s+(?<value>.+)")

{

$builder = $builder.AppendLine($matches["key"] + "," + $matches["value"])

}

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Convert-PowerDbgCSVtoHashTable

#

# Parameters: None.

#

# Return: Hash table.

#

# Purpose: Sometimes the Parse-PowerDbg#() functions return a CSV file. This function

# loads the data using a hash table.

# However, it works just when the CSV file has two fields: key and value.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Convert-PowerDbgCSVtoHashTable()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

$hashTable = @{}

import-csv -path $global:g_fileParsedOutput | foreach {$hashTable[$_.key] = $_.value}

return $hashTable

}

########################################################################################################

# Function: Send-PowerDbgDML

#

# Parameters: [string] <$hyperlinkDML>

# Hyperlink for the DML command.

#

# [string] <$commandDML>

# Command to execute when the hyperlink is clicked.

#

# Return: Nothing.

#

# Purpose: Creates a DML command and send it to Windbg.

# DML stands for Debug Markup Language. Using DML you can create hyperlinks that

# run a command when the user click on them.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Send-PowerDbgDML(

[string] $hyperlinkDML = $(throw "Error! You must provide the hyperlink for DML."),

[string] $commandDML = $(throw "Error! You must provide the command for DML.")

)

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

Send-PowerDbgCommand ".printf /D `"<link cmd=\`"$commandDML\`"><b>$hyperlinkDML</b></link>\n\`"`""

}

########################################################################################################

# Function: Parse-PowerDbgNAME2EE

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!name2ee" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgNAME2EE()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

# Attention! The Name: doesn't map to the right value, however, it should be the same method name provide as argument.

if($line -match "(?<key>\w+\:)\s+(?<value>\S+)")

{

$builder = $builder.AppendLine($matches["key"] + "," + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgDUMPMD

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!dumpmd" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgDUMPMD()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>((^Method Name :)|(^MethodTable)|(^Module:)|(^mdToken:)|(^Flags :)|(^Method VA :)))\s+(?<value>\S+)")

{

$builder = $builder.AppendLine($matches["key"] + "," + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgDUMPMODULE

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!dumpmodule" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgDUMPMODULE()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

[int] $countFields = 0

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

# Fields for .NET Framework 2.0

if($line -match "(?<key>((^dwFlags)|(^Assembly:)|(^LoaderHeap:)|(^TypeDefToMethodTableMap:)|(^TypeRefToMethodTableMap:)|(^MethodDefToDescMap:)|(^FieldDefToDescMap:)|(^MemberRefToDescMap:)|(^FileReferencesMap:)|(^AssemblyReferencesMap:)|(^MetaData start address:)))\s+(?<value>\S+)")

{

$builder = $builder.AppendLine($matches["key"] + "," + $matches["value"])

$countFields++

}

}

# If nothing was found, let's try to use the .NET Framework 1.1 fields.

if($countFields -lt 3)

{

foreach($line in $(get-content $global:g_fileCommandOutput))

{

# Fields for .NET Framework 2.0

if($line -match "(?<key>((^dwFlags)|(^Assembly\*)|(^LoaderHeap\*)|(^TypeDefToMethodTableMap\*)|(^TypeRefToMethodTableMap\*)|(^MethodDefToDescMap\*)|(^FieldDefToDescMap\*)|(^MemberRefToDescMap\*)|(^FileReferencesMap\*)|(^AssemblyReferencesMap\*)|(^MetaData starts at)))\s+(?<value>\S+)")

{

$builder = $builder.AppendLine($matches["key"] + "," + $matches["value"])

$hasFound = $true

}

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgLMI

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!lmi" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgLMI()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>((^.+\:)))\s+(?<value>\S+)")

{

$strNoLeftSpaces = $matches["key"]

$strNoLeftSpaces = $strNoLeftSpaces.TrimStart()

$builder = $builder.AppendLine($strNoLeftSpaces + "," + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Has-PowerDbgCommandSucceeded

#

# Parameters: None.

#

# Return: Return $true if the last command succeeded or $false if not.

#

# Purpose: Return $true if the last command succeeded or $false if not.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Has-PowerDbgCommandSucceeded

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -imatch "(Fail) | (Failed) | (Error) | (Invalid)")

{

return $false

}

}

return $true

}

########################################################################################################

# Function: Send-PowerDbgComment

#

# Parameters: [string] $comment

# Comment to be sent to the debugger.

#

# Return: Nothing.

#

# Purpose: Sends a bold comment to the debugger. Uses DML.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Send-PowerDbgComment(

[string] $comment = $(throw "Error! You must provide a comment.")

)

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

Send-PowerDbgCommand ".printf /D `"\n<b>$comment</b>\n\n\`"`""

}