Skip to content

Instantly share code, notes, and snippets.

@stdStudent
Last active August 21, 2025 09:00
Show Gist options
  • Save stdStudent/03f1a7942a8a636d576a179ab1d60276 to your computer and use it in GitHub Desktop.
Save stdStudent/03f1a7942a8a636d576a179ab1d60276 to your computer and use it in GitHub Desktop.
A PowerShell script to determine a VST version (VST2, VST3).
<#
# License: MPL-2.0
# Text: https://www.mozilla.org/en-US/MPL/2.0/
#>
<#
.SYNOPSIS
Determines the VST plugin type by analyzing the DLL exports.
.DESCRIPTION
Analyzes a DLL file to determine if it is a VST2 or VST3 plugin by examining its exports.
.PARAMETER Path
The path to the VST DLL file to analyze.
.LINK
https://www.mozilla.org/en-US/MPL/2.0/
.EXAMPLE
.\Get-VstVersion.ps1 'C:\Program Files\VSTPlugins\MyPlugin.dll'
.EXAMPLE
PS> . .\Get-VstVersion.ps1 .
PS> Get-VstVersion -Path 'C:\Program Files\VSTPlugins\MyPlugin.dll'
#>
[CmdletBinding()]
param (
[Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)]
[string]$Path
)
function Get-PEHeaders {
param (
[Parameter(Mandatory=$true)]
[byte[]]$Bytes
)
function Get-Int32 ($offset) { [BitConverter]::ToInt32($Bytes, $offset) }
function Get-UInt32 ($offset) { [BitConverter]::ToUInt32($Bytes, $offset) }
function Get-UInt16 ($offset) { [BitConverter]::ToUInt16($Bytes, $offset) }
# Validate DOS header
if ($Bytes[0] -ne 77 -or $Bytes[1] -ne 90) {
throw "Not a valid PE file (missing MZ signature)."
}
$e_lfanew = Get-Int32 0x3C
# Validate PE signature
if ($Bytes[$e_lfanew] -ne 80 -or $Bytes[$e_lfanew + 1] -ne 69 -or
$Bytes[$e_lfanew + 2] -ne 0 -or $Bytes[$e_lfanew + 3] -ne 0) {
throw "Not a valid PE file (missing PE signature)."
}
$opt_offset = $e_lfanew + 24
$magic = Get-UInt16 $opt_offset
if ($magic -eq 0x10b) { # PE32
$data_dir_offset = $opt_offset + 96
} elseif ($magic -eq 0x20b) { # PE32+
$data_dir_offset = $opt_offset + 112
} else {
throw "Unknown PE optional header magic value."
}
$numberOfSections = Get-UInt16 ($e_lfanew + 6)
$sizeOfOptionalHeader = Get-UInt16 ($e_lfanew + 20)
$section_offset = $opt_offset + $sizeOfOptionalHeader
# Parse sections
$sections = @()
for ($i = 0; $i -lt $numberOfSections; $i++) {
$sec_start = $section_offset + $i * 40
$nameBytes = $Bytes[$sec_start..($sec_start + 7)]
$name = [System.Text.Encoding]::ASCII.GetString($nameBytes).TrimEnd("`0")
$virtualSize = Get-UInt32 ($sec_start + 8)
$virtualAddress = Get-UInt32 ($sec_start + 12)
$sizeOfRawData = Get-UInt32 ($sec_start + 16)
$pointerToRawData = Get-UInt32 ($sec_start + 20)
$sections += [PSCustomObject]@{
Name = $name
VirtualAddress = $virtualAddress
VirtualSize = $virtualSize
PointerToRawData = $pointerToRawData
SizeOfRawData = $sizeOfRawData
}
}
return @{
E_LFANEW = $e_lfanew
DataDirOffset = $data_dir_offset
Sections = $sections
}
}
function Get-AsciiString {
param (
[Parameter(Mandatory=$true)]
[byte[]]$Bytes,
[Parameter(Mandatory=$true)]
[int]$Offset
)
$strBytes = @()
$j = 0
while ($Bytes[$Offset + $j] -ne 0 -and ($Offset + $j) -lt $Bytes.Length) {
$strBytes += $Bytes[$Offset + $j]
$j++
}
return [System.Text.Encoding]::ASCII.GetString($strBytes)
}
function ConvertTo-FileOffset {
param (
[Parameter(Mandatory=$true)]
[int]$Rva,
[Parameter(Mandatory=$true)]
[array]$Sections
)
foreach ($sec in $Sections) {
if ($Rva -ge $sec.VirtualAddress -and $Rva -lt ($sec.VirtualAddress + $sec.VirtualSize)) {
return $sec.PointerToRawData + ($Rva - $sec.VirtualAddress)
}
}
throw "RVA $Rva not mapped to any section."
}
function Get-PEExports {
param (
[Parameter(Mandatory=$true)]
[string]$Path
)
$bytes = [IO.File]::ReadAllBytes($Path)
$headers = Get-PEHeaders -Bytes $bytes
function Get-UInt32 ($offset) { [BitConverter]::ToUInt32($bytes, $offset) }
# Get export directory
$export_rva = Get-UInt32 $headers.DataDirOffset
$export_size = Get-UInt32 ($headers.DataDirOffset + 4)
if ($export_rva -eq 0 -or $export_size -eq 0) { return @() }
$export_offset = ConvertTo-FileOffset -Rva $export_rva -Sections $headers.Sections
$num_names = Get-UInt32 ($export_offset + 24)
$address_names_rva = Get-UInt32 ($export_offset + 32)
$exports = @()
if ($num_names -gt 0) {
$names_offset = ConvertTo-FileOffset -Rva $address_names_rva -Sections $headers.Sections
for ($i = 0; $i -lt $num_names; $i++) {
$name_rva = Get-UInt32 ($names_offset + $i * 4)
$name_offset = ConvertTo-FileOffset -Rva $name_rva -Sections $headers.Sections
$name = Get-AsciiString -Bytes $bytes -Offset $name_offset
$exports += $name
}
}
return $exports
}
function Get-PEImports {
param (
[Parameter(Mandatory=$true)]
[string]$Path
)
$bytes = [IO.File]::ReadAllBytes($Path)
$headers = Get-PEHeaders -Bytes $bytes
function Get-UInt32 ($offset) { [BitConverter]::ToUInt32($bytes, $offset) }
# Get import directory (data dir index 1)
$import_rva = Get-UInt32 ($headers.DataDirOffset + 8) # after export (index 0), import is +8
if ($import_rva -eq 0) { return @() }
$import_offset = ConvertTo-FileOffset -Rva $import_rva -Sections $headers.Sections
$imports = @()
$descriptor_offset = $import_offset
while ($true) {
$name_rva = Get-UInt32 ($descriptor_offset + 12)
if ($name_rva -eq 0) { break } # end of descriptors
$name_offset = ConvertTo-FileOffset -Rva $name_rva -Sections $headers.Sections
$dllName = Get-AsciiString -Bytes $bytes -Offset $name_offset
$imports += $dllName
$descriptor_offset += 20 # size of IMAGE_IMPORT_DESCRIPTOR
}
return $imports | Sort-Object -Unique
}
function Get-VstVersion {
param (
[Parameter(Mandatory=$true)]
[string]$Path
)
try {
$exports = Get-PEExports -Path $Path
if ($exports -contains "GetPluginFactory") {
return "VST3"
} elseif ($exports -contains "VSTPluginMain") {
return "VST2"
} else {
return "Not a recognized VST plugin"
}
} catch {
return "Failed to parse PE file: $($_.Exception.Message)"
}
}
# Entry Point
if ($MyInvocation.InvocationName -ne '.') {
$result = Get-VstVersion -Path $Path
Write-Output $result
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment