Private/Invoke-Win32Api.ps1

# Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)


Function Invoke-Win32Api {
    <#
    .SYNOPSIS
    Call a native Win32 API or a function exported in a DLL.

    .DESCRIPTION
    This method allows you to call a native Win32 API or DLL function
    without compiling C# code using Add-Type. The advantages of this over
    using Add-Type is that this is all generated in memory and no temporary
    files are created.

    The code has been created with great help from various sources. The main
    sources I used were;
    # http://www.leeholmes.com/blog/2007/10/02/managing-ini-files-with-powershell/
    # https://blogs.technet.microsoft.com/heyscriptingguy/2013/06/27/use-powershell-to-interact-with-the-windows-api-part-3/
    
    .PARAMETER DllName
    [String] The DLL to import the method from.
    
    .PARAMETER MethodName
    [String] The name of the method.
    
    .PARAMETER ReturnType
    [Type] The type of the return object returned by the method.
    
    .PARAMETER ParameterTypes
    [Type[]] Array of types that define the parameter types required by the
    method. The type index should match the index of the value in the
    Parameters parameter.

    If the parameter is a reference or an out parameter, use [Ref] as the type
    for that parameter.
    
    .PARAMETER Parameters
    [Object[]] Array of objects to supply for the parameter values required by
    the method. The value index should match the index of the value in the
    ParameterTypes parameter.

    If the parameter is a reference or an out parameter, the object should be a
    [Ref] of the parameter.
    
    .PARAMETER SetLastError
    [Bool] Whether to apply the SetLastError Dll attribute on the method,
    default is $false
    
    .PARAMETER CharSet
    [Runtime.InteropServices.CharSet] The charset to apply to the CharSet Dll
    attribute on the method, default is [Runtime.InteropServices.CharSet]::Auto

    .OUTPUTS
    [Object] The return result from the method, the type of this value is based
    on the ReturnType parameter.
    
    .EXAMPLE
    # Use the Win32 APIs to open a file handle
    $handle = Invoke-Win32Api -DllName kernel32.dll `
        -MethodName CreateFileW `
        -ReturnType Microsoft.Win32.SafeHandles.SafeFileHandle `
        -ParameterTypes @([String], [System.Security.AccessControl.FileSystemRights], [System.IO.FileShare], [IntPtr], [System.IO.FileMode], [UInt32], [IntPtr]) `
        -Parameters @(
            "\\?\C:\temp\test.txt",
            [System.Security.AccessControl.FileSystemRights]::FullControl,
            [System.IO.FileShare]::ReadWrite,
            [IntPtr]::Zero,
            [System.IO.FileMode]::OpenOrCreate,
            0,
            [IntPtr]::Zero) `
        -SetLastError $true `
        -CharSet Unicode
    if ($handle.IsInvalid) {
        $last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
        throw [System.ComponentModel.Win32Exception]$last_err
    }
    $handle.Close()

    # Lookup the account name from a SID
    $sid_string = "S-1-5-18"
    $sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $sid_string
    $sid_bytes = New-Object -TypeName byte[] -ArgumentList $sid.BinaryLength
    $sid.GetBinaryForm($sid_bytes, 0)

    $name = New-Object -TypeName System.Text.StringBuilder
    $name_length = 0
    $domain_name = New-Object -TypeName System.Text.StringBuilder
    $domain_name_length = 0

    $invoke_args = @{
        DllName = "Advapi32.dll"
        MethodName = "LookupAccountSidW"
        ReturnType = [bool]
        ParameterTypes = @([String], [byte[]], [System.Text.StringBuilder], [Ref], [System.Text.StringBuilder], [Ref], [Ref])
        Parameters = @(
            $null,
            $sid_bytes,
            $name,
            [Ref]$name_length,
            $domain_name,
            [Ref]$domain_name_length,
            [Ref][IntPtr]::Zero
        )
        SetLastError = $true
        CharSet = "Unicode"
    }

    $res = Invoke-Win32Api @invoke_args
    $name.EnsureCapacity($name_length)
    $domain_name.EnsureCapacity($domain_name_length)
    $res = Invoke-Win32Api @invoke_args
    if (-not $res) {
        $last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
        throw [System.ComponentModel.Win32Exception]$last_err
    }
    Write-Output "SID: $sid_string, Domain: $($domain_name.ToString()), Name: $($name.ToString())"
    
    .NOTES
    The parameters to use for a method dynamically based on the method that is
    called. There is no cut and fast way to automatically convert the interface
    listed on the Microsoft docs. There are great resources to help you create
    the "P/Invoke" definition like pinvoke.net.
    #>

    [CmdletBinding()]
    [OutputType([Object])]
    param(
        [Parameter(Position=0, Mandatory=$true)] [String]$DllName,
        [Parameter(Position=1, Mandatory=$true)] [String]$MethodName,
        [Parameter(Position=2, Mandatory=$true)] [Type]$ReturnType,
        [Parameter(Position=3)] [Type[]]$ParameterTypes = [Type[]]@(),
        [Parameter(Position=4)] [Object[]]$Parameters = [Object[]]@(),
        [Parameter()] [Bool]$SetLastError = $false,
        [Parameter()] [Runtime.InteropServices.CharSet]$CharSet = [Runtime.InteropServices.CharSet]::Auto
    )
    if ($ParameterTypes.Length -ne $Parameters.Length) {
        throw [System.ArgumentException]"ParameterType Count $($ParameterTypes.Length) not equal to Parameter Count $($Parameters.Length)"
    }

    # First step is to define the dynamic assembly in the current AppDomain
    $assembly = New-Object -TypeName System.Reflection.AssemblyName -ArgumentList "Win32ApiAssembly"
    $dynamic_assembly = [AppDomain]::CurrentDomain.DefineDynamicAssembly($assembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)

    # Second step is to create the dynamic module and type/class that contains
    # the P/Invoke definition
    $dynamic_module = $dynamic_assembly.DefineDynamicModule("Win32Module", $false)
    $dynamic_type = $dynamic_module.DefineType("Win32Type", [Reflection.TypeAttributes]"Public, Class")

    # Need to manually get the reference type if the ParameterType is [Ref], we
    # define this based on the Parameter type at the same index
    $parameter_types = $ParameterTypes.Clone()
    for ($i = 0; $i -lt $ParameterTypes.Length; $i++) {
        if ($ParameterTypes[$i] -eq [Ref]) {
            $parameter_types[$i]  = $Parameters[$i].Value.GetType().MakeByRefType()
        }
    }

    # Next, the method is created where we specify the name, parameters and
    # return type that is expected
    $dynamic_method = $dynamic_type.DefineMethod(
        $MethodName,
        [Reflection.MethodAttributes]"Public, Static",
        $ReturnType,
        $parameter_types
    )

    # Build the attributes (DllImport) part of the method where the DLL
    # SetLastError and CharSet are applied
    $constructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([String])
    $method_fields = [Reflection.FieldInfo[]]@(
        [Runtime.InteropServices.DllImportAttribute].GetField("SetLastError"),
        [Runtime.InteropServices.DllImportAttribute].GetField("CharSet")
    )
    $method_fields_values = [Object[]]@($SetLastError, $CharSet)
    $custom_attributes = New-Object -TypeName Reflection.Emit.CustomAttributeBuilder -ArgumentList @(
        $constructor,
        $DllName,
        $method_fields,
        $method_fields_values
    )
    $dynamic_method.SetCustomAttribute($custom_attributes)

    # Create the custom type/class based on what was configured above
    $win32_type = $dynamic_type.CreateType()

    # Invoke the method with the parameters supplied and return the result
    $result = $win32_type::$MethodName.Invoke($Parameters)
    return $result
}