UtilityFunctions.ps1

# Copyright 2021 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Format-ObjectTable {
    Param(
        [parameter(Mandatory, Position = 0)]
        $InputObject,
        [switch]$HideTableHeaders,
        [switch]$NoTrailingLine
    )

    $output = $InputObject | Format-Table -HideTableHeaders:$HideTableHeaders | Out-String
    $output -Split "`r`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Write-Output
    if (!$NoTrailingLine) {
        Write-Output ""
    }
}

<#
.SYNOPSIS
Get API set entries
.DESCRIPTION
This cmdlet gets API set entries for the current system.
.PARAMETER Name
Specify an API set name to lookup.
.INPUTS
None
.OUTPUTS
NtApiDotNet.ApiSet.ApiSetEntry[]
.EXAMPLE
Get-NtApiSet
Get all API set entries.
.EXAMPLE
Get-NtApiSet -Name "api-ms-win-base-util-l1-1-0"
Get an API set by name.
#>

function Get-NtApiSet {
    [CmdletBinding(DefaultParameterSetName="All")]
    param (
        [parameter(Mandatory, Position = 0, ParameterSetName="FromName")]
        [string]$Name
    )

    if ($PSCmdlet.ParameterSetName -eq "FromName") {
        [NtApiDotNet.ApiSet.ApiSetNamespace]::Current.GetApiSet($Name)
    } else {
        [NtApiDotNet.ApiSet.ApiSetNamespace]::Current.Entries | Write-Output
    }
}

<#
.SYNOPSIS
Get the SDK name for an enumerated type or other type.
.DESCRIPTION
This cmdlet removes a package SID from the list of granted loopback exceptions.
.PARAMETER InputObject
The package SID to remove.
.INPUTS
object
.OUTPUTS
string
.EXAMPLE
Get-NtAccessMask 0x1 -AsSpecificAccess File | Get-NtSDKName
Get the SDK names for an access mask.
#>

function Get-NtSDKName { 
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        $InputObject
    )
    PROCESS {
        [NtApiDotNet.Utilities.Reflection.ReflectionUtils]::GetSDKName($InputObject)
    }
}

<#
.SYNOPSIS
Converts a text hexdump into bytes.
.DESCRIPTION
This cmdlet tries to convert a hexdump into the original bytes.
.PARAMETER Hex
The hex dump.
.INPUTS
string
.OUTPUTS
byte[]
.EXAMPLE
1, 2, 3, 4 | Format-HexDump | ConvertFrom-HexDump
Convert some bytes to a hex dump and back again.
#>

function ConvertFrom-HexDump { 
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [string]$Hex
    )

    PROCESS {
        [NtApiDotNet.Utilities.Text.HexDumpBuilder]::ParseHexDump($Hex)
    }
}

<#
.SYNOPSIS
Gets a certificate object.
.DESCRIPTION
This cmdlet gets a certificate object from a path.
.PARAMETER Path
Specify the path to the certificate or file. Can only be a cert:\ drive path.
.PARAMETER Pin
Specify the PIN for the certificate's private key if needed.
.PARAMETER Byte
Specify the certificate as bytes.
.INPUTS
None
.OUTPUTS
System.Security.Cryptography.X509Certificates.X509Certificate2
#>

function Get-X509Certificate {
    [CmdletBinding(DefaultParameterSetName="FromPath")]
    param(
        [Parameter(Position = 0, Mandatory, ParameterSetName="FromPath")]
        [string]$Path,
        [Parameter(Position = 0, Mandatory, ParameterSetName="FromByte")]
        [byte[]]$Byte,
        [NtObjectManager.Utils.PasswordHolder]$Pin
    )

    switch($PSCmdlet.ParameterSetName) {
        "FromPath" {
            $Path = Resolve-Path -Path $Path
            if ($null -ne $Path) {
                $cert = Get-Item $Path
                if ($cert -is [Security.Cryptography.X509Certificates.X509Certificate]) {
                    [Security.Cryptography.X509Certificates.X509Certificate2]::new($cert)
                } elseif ($Pin -eq $null) {
                    [Security.Cryptography.X509Certificates.X509Certificate2]::new($Path)
                } else {
                    [Security.Cryptography.X509Certificates.X509Certificate2]::new($Path, $Pin.Password)
                }
            }
        }
        "FromByte" {
            if ($Pin -eq $null) {
                [Security.Cryptography.X509Certificates.X509Certificate2]::new($Byte)
            } else {
                [Security.Cryptography.X509Certificates.X509Certificate2]::new($Byte, $Pin.Password)
            }
        }
    }
}

<#
.SYNOPSIS
Waits on an async task and gets the result.
.DESCRIPTION
This cmdlet waits on a .net asynchronous task and returns any result.
.PARAMETER Task
Specify the asynchronous task to wait on.
.PARAMETER TimeoutSec
Specify the timeout in seconds to wait for.
.INPUTS
None
.OUTPUTS
object
.EXAMPLE
Wait-AsyncTaskResult -Task $task
Wait on the task and result.
.EXAMPLE
Wait-AsyncTaskResult -Task $task -TimeoutSec 10
Wait on the task and result for up to 10 seconds.
#>

function Wait-AsyncTaskResult {
    Param(
        [parameter(Mandatory, Position = 0)]
        [System.Threading.Tasks.Task]$Task,
        [int]$TimeoutSec = [int]::MaxValue
    )

    while (-not $Task.Wait(1000)) {
        $TimeoutSec--
        if ($TimeoutSec -le 0) {
            return
        }
    }

    $Task.GetAwaiter().GetResult() | Write-Output
}

<#
.SYNOPSIS
Formats a hex dump for a byte array.
.DESCRIPTION
This cmdlet converts a byte array to a hex dump string. If invoked as Out-HexDump will write the to the console.
.PARAMETER Bytes
The bytes to convert.
.PARAMETER ShowHeader
Display a header for the hex dump.
.PARAMETER ShowAddress
Display the address for the hex dump.
.PARAMETER ShowAscii
Display the ASCII dump along with the hex.
.PARAMETER HideRepeating
Hide repeating 16 byte patterns.
.PARAMETER Buffer
Show the contents of a safe buffer.
.PARAMETER Offset
Specify start offset into the safe buffer or the file.
.PARAMETER Length
Specify length of safe buffer or the file.
.PARAMETER BaseAddress
Specify base address for the display when ShowAddress is enabled.
.INPUTS
byte[]
.OUTPUTS
String
#>

function Format-HexDump {
    [CmdletBinding(DefaultParameterSetName = "FromBytes")]
    Param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = "FromBytes")]
        [Alias("Bytes")]
        [AllowEmptyCollection()]
        [byte[]]$Byte,
        [Parameter(Mandatory, Position = 0, ParameterSetName = "FromFile")]
        [string]$Path,
        [Parameter(Mandatory, Position = 0, ParameterSetName = "FromBuffer")]
        [System.Runtime.InteropServices.SafeBuffer]$Buffer,
        [Parameter(ParameterSetName = "FromBuffer")]
        [Parameter(ParameterSetName = "FromFile")]
        [int64]$Offset = 0,
        [Parameter(ParameterSetName = "FromBuffer")]
        [Parameter(ParameterSetName = "FromFile")]
        [int64]$Length = 0,
        [Parameter(ParameterSetName = "FromBytes")]
        [int64]$BaseAddress = 0,
        [switch]$ShowHeader,
        [switch]$ShowAddress,
        [switch]$ShowAscii,
        [switch]$ShowAll,
        [switch]$HideRepeating
    )

    BEGIN {
        if ($ShowAll) {
            $ShowHeader = $true
            $ShowAscii = $true
            $ShowAddress = $true
        }

        $WriteToHost = $PSCmdlet.MyInvocation.InvocationName -eq "Out-HexDump"

        switch ($PSCmdlet.ParameterSetName) {
            "FromBytes" {
                $builder = [NtApiDotNet.Utilities.Text.HexDumpBuilder]::new($ShowHeader, $ShowAddress, $ShowAscii, $HideRepeating, $BaseAddress);
            }
            "FromBuffer" {
                $builder = [NtApiDotNet.Utilities.Text.HexDumpBuilder]::new($Buffer, $Offset, $Length, $ShowHeader, $ShowAddress, $ShowAscii, $HideRepeating);
            }
            "FromFile" {
                $builder = [NtApiDotNet.Utilities.Text.HexDumpBuilder]::new($ShowHeader, $ShowAddress, $ShowAscii, $HideRepeating, $Offset);
            }
        }
    }

    PROCESS {
        switch ($PSCmdlet.ParameterSetName) {
            "FromBytes" {
                $builder.Append($Byte)
            }
            "FromFile" {
                $Path = Resolve-Path $Path -ErrorAction Stop
                $builder.AppendFile($Path, $Offset, $Length)
            }
        }
    }

    END {
        $builder.Complete()
        $output = $builder.ToString()
        if ($WriteToHost) {
            $output | Write-Host
        } else {
            $output | Write-Output
        }
    }
}

Set-Alias -Name Out-HexDump -Value Format-HexDump

<#
.SYNOPSIS
Get a service principal name.
.DESCRIPTION
This cmdlet gets SPN for a string.
.PARAMETER Name
Specify the SPN.
.INPUTS
None
.OUTPUTS
NtApiDotNet.Win32.Security.Authentication.ServicePrincipalName
.EXAMPLE
Get-ServicePrincipalName -Name "HTTP/www.domain.com"
Get the SPN from a string.
#>

function Get-ServicePrincipalName {
    param (
        [parameter(Mandatory, Position = 0)]
        [string]$Name
    )
    [NtApiDotNet.Win32.Security.Authentication.ServicePrincipalName]::Parse($Name) | Write-Output
}

<#
.SYNOPSIS
Get a MD4 hash of a byte array or string.
.DESCRIPTION
This cmdlet calculates the MD4 hash of a byte array or string.
.PARAMETER Bytes
Specify a byte array.
.PARAMETER String
Specify string.
.PARAMETER Encoding
Specify string encoding. Default to Unicode.
.INPUTS
None
.OUTPUTS
byte[]
.EXAMPLE
Get-MD4Hash -String "ABC"
Get the MD4 hash of the string ABC in unicode.
.EXAMPLE
Get-MD4Hash -String "ABC" -Encoding "ASCII"
Get the MD4 hash of the string ABC in ASCII.
.EXAMPLE
Get-MD4Hash -Bytes @(0, 1, 2, 3)
Get the MD4 hash of a byte array.
#>

function Get-MD4Hash {
    [CmdletBinding(DefaultParameterSetName="FromString")]
    Param(
        [AllowEmptyString()]
        [Parameter(Mandatory, Position = 0, ParameterSetName="FromString")]
        [string]$String,
        [Parameter(Position = 1, ParameterSetName="FromString")]
        [string]$Encoding = "Unicode",
        [Parameter(Mandatory, Position = 0, ParameterSetName="FromBytes")]
        [byte[]]$Bytes
    )
    switch($PSCmdlet.ParameterSetName) {
        "FromString" {
            $enc = [System.Text.Encoding]::GetEncoding($Encoding)
            [NtApiDotNet.Utilities.Security.MD4]::CalculateHash($String, $enc)
        }
        "FromBytes" {
            [NtApiDotNet.Utilities.Security.MD4]::CalculateHash($Bytes)
        }
    }
}

<#
.SYNOPSIS
Formats ASN.1 DER data to a string.
.DESCRIPTION
This cmdlet formats ASN.1 DER data to a string either from a byte array or a file.
.PARAMETER Byte
Specify a byte array containing the DER data.
.PARAMETER Path
Specify file containing the DER data.
.PARAMETER Depth
Specify initialize indentation depth.
.INPUTS
None
.OUTPUTS
string
.EXAMPLE
Format-ASN1DER -Byte $ba
Format the byte array with ASN.1 DER data.
.EXAMPLE
Format-ASN1DER -Byte $ba -Depth 2
Format the byte array with ASN.1 DER data with indentation depth of 2.
.EXAMPLE
Format-ASN1DER -Path file.bin
Format the file containing ASN.1 DER data.
#>

function Format-ASN1DER {
    [CmdletBinding(DefaultParameterSetName="FromBytes")]
    Param(
        [Parameter(Mandatory, Position = 0, ParameterSetName="FromPath")]
        [string]$Path,
        [Parameter(Mandatory, Position = 0, ParameterSetName="FromBytes")]
        [byte[]]$Byte,
        [int]$Depth = 0
    )
    switch($PSCmdlet.ParameterSetName) {
        "FromPath" {
            [NtApiDotNet.Utilities.ASN1.ASN1Utils]::FormatDER($Path, $Depth)
        }
        "FromBytes" {
            [NtApiDotNet.Utilities.ASN1.ASN1Utils]::FormatDER($Byte, $Depth)
        }
    }
}

<#
.SYNOPSIS
Parses ASN.1 DER data to objects.
.DESCRIPTION
This cmdlet parses ASN.1 DER data into an object model.
.PARAMETER Byte
Specify a byte array containing the DER data.
.PARAMETER Path
Specify file containing the DER data.
.INPUTS
None
.OUTPUTS
NtApiDotNet.Utilities.ASN1.Parser.ASN1Object
.EXAMPLE
Get-ASN1DER -Bytes $ba
Parse the byte array into ASN.1 DER data objects.
#>

function Get-ASN1DER {
    [CmdletBinding(DefaultParameterSetName="FromBytes")]
    Param(
        [Parameter(Mandatory, Position = 0, ParameterSetName="FromPath")]
        [string]$Path,
        [Parameter(Mandatory, Position = 0, ParameterSetName="FromBytes")]
        [byte[]]$Byte,
        [int]$Depth = 0
    )
    switch($PSCmdlet.ParameterSetName) {
        "FromPath" {
            [NtApiDotNet.Utilities.ASN1.ASN1Utils]::ParseDER($Path)
        }
        "FromBytes" {
            [NtApiDotNet.Utilities.ASN1.ASN1Utils]::ParseDER($Byte)
        }
    }
}

<#
.SYNOPSIS
Creates a new ASN.1 DER builder.
.DESCRIPTION
This cmdlet creates a new ASN.1 DER builder object which can be used to create DER encoded data.
.INPUTS
None
.OUTPUTS
NtApiDotNet.Utilities.ASN1.Builder.DERBuilder
.EXAMPLE
New-ASN1DER
Creates a new ASN.1 DER builder.
#>

function New-ASN1DER {
    [NtApiDotNet.Utilities.ASN1.Builder.DERBuilder]::new()
}

<#
.SYNOPSIS
Split a command line into its component parts.
.DESCRIPTION
This cmdlet take a process command line and split it into its component parts.
.PARAMETER CommandLine
The command line.
.INPUTS
None
.OUTPUTS
string[]
.EXAMPLE
Split-Win32CommandLine -CommandLine "notepad test.txt"
Split the command line "notepad test.txt"
#>

function Split-Win32CommandLine {
    Param(
        [parameter(Position = 0, Mandatory)]
        [string]$CommandLine
    )
    [NtApiDotNet.Win32.Win32Utils]::ParseCommandLine($CommandLine) | Write-Output
}

# We use this incase we're running on a downlevel PowerShell.
function Get-IsPSCore {
    return ($PSVersionTable.Keys -contains "PSEdition") -and ($PSVersionTable.PSEdition -ne 'Desktop')
}

<#
.SYNOPSIS
Protect a byte array using RC4.
.DESCRIPTION
This cmdlet used the RC4 encryption algorithm to protect a byte array. Note as encryption
and decryption are symmetrical this function process encrypts and decrypts. Note this
returns the encrypted data, it doesn't encrypt place.
.PARAMETER Data
The bytes to encrypt.
.PARAMETER Key
The key to use.
.PARAMETER Offset
The offset into the data to unprotect. Defaults to the start of the data.
.PARAMETER Length
The length of the data to unprotect. Defaults to all remaining data.
.INPUTS
None
.OUTPUTS
byte[]
.EXAMPLE
Protect-RC4 -Byte @(0, 1, 2, 3) -Key @(4, 7, 1, 254)
Protect the byte array with RC4.
#>

function Protect-RC4 {
    Param(
        [Parameter(Mandatory, Position = 0)]
        [byte[]]$Data,
        [Parameter(Mandatory, Position = 1)]
        [byte[]]$Key,
        [int]$Offset = 0,
        [int]$Length = -1
    )

    if ($Length -lt 0) {
        $Length = $Data.Length - $Offset
    }
    [NtApiDotNet.Utilities.Security.ARC4]::Transform($Data, $Offset, $Length, $Key)
}

Set-Alias -Name Unprotect-RC4 -Value Protect-RC4

<#
.SYNOPSIS
Selects strings out a binary value.
.DESCRIPTION
This cmdlet searches through a byte buffer for ASCII or Unicode strings.
.PARAMETER Bytes
Show the strings in a bytes.
.PARAMETER Buffer
Show the strings in a safe buffer.
.PARAMETER Path
Show the strings in a file.
.PARAMETER MinimumLength
Specify the minimum string length to return.
.PARAMETER Type
Specify the types of string to return. Defaults to ASCII and Unicode.
.INPUTS
byte[]
.OUTPUTS
NtApiDotNet.Utilities.Text.ExtractedString
#>

function Select-BinaryString {
    [CmdletBinding(DefaultParameterSetName = "FromBytes")]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = "FromBytes")]
        [Alias("Bytes")]
        [AllowEmptyCollection()]
        [byte[]]$Byte,
        [Parameter(Mandatory, Position = 0, ParameterSetName = "FromFile")]
        [string]$Path,
        [Parameter(Mandatory, Position = 0, ParameterSetName = "FromBuffer")]
        [System.Runtime.InteropServices.SafeBuffer]$Buffer,
        [NtApiDotNet.Utilities.Text.ExtractedStringType]$Type = "Ascii, Unicode",
        [int]$MinimumLength = 3
    )

    BEGIN {
        $stm = [System.IO.MemoryStream]::new()
        $in_pipeline = $PSCmdlet.MyInvocation.PipelinePosition -eq 1
    }

    PROCESS {
        switch ($PSCmdlet.ParameterSetName) {
            "FromBytes" {
                if ($in_pipeline) {
                    $stm.Write($Byte, 0, $Byte.Length)
                } else {
                    [NtApiDotNet.Utilities.Text.StringExtractor]::Extract($Byte, $MinimumLength, $Type) | Write-Output
                }
            }
            "FromBuffer" {
                [NtApiDotNet.Utilities.Text.StringExtractor]::Extract($Buffer, $MinimumLength, $Type) | Write-Output
            }
            "FromFile" {
                $Path = Resolve-Path $Path -ErrorAction Stop
                [NtApiDotNet.Utilities.Text.StringExtractor]::Extract($Path, $MinimumLength, $Type) | Write-Output
            }
        }
    }

    END {
        if ($stm.Length -gt 0) {
            $stm.Position = 0
            [NtApiDotNet.Utilities.Text.StringExtractor]::Extract($stm, $MinimumLength, $Type) | Write-Output
        }
    }
}

<#
.SYNOPSIS
Send a message to update the environment from the registry.
.DESCRIPTION
This cmdlet sends the WM_SETTINGCHANGE broadcast message to force explorer (and anyone else listenting)
to update their environment variables from the registry.
.INPUTS
None
.OUTPUTS
None
#>

function Update-Win32Environment {
    $str = [System.Runtime.InteropServices.Marshal]::StringToHGlobalUni("Environment")
    try {
        [NtApiDotNet.NtWindow]::Broadcast.SendMessage(0x1A, [System.IntPtr]::Zero, $str) | Out-Null
    } finally {
        [System.Runtime.InteropServices.Marshal]::FreeHGlobal($str)
    }
}

function Read-BinaryFile {
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]$Path
    )
    $ba = if (Get-IsPSCore) {
        Get-Content -Path $Path -AsByteStream
    } else {
        Get-Content -Path $Path -Encoding Byte
    }
    [byte[]]$ba
}

function Write-BinaryFile {
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]$Path,
        [Parameter(Mandatory, Position = 1)]
        [byte[]]$Byte
    )
    if (Get-IsPSCore) {
        $Byte | Set-Content -Path $Path -AsByteStream
    } else {
        $Byte | Set-Content -Path $Path -Encoding Byte
    }
}