Use PowerShell to Interact with the Windows API: Part 3

Doctor Scripto

Summary: Guest blogger, Matt Graeber, completes his three-part series about interacting with the Windows API.

Microsoft Scripting Guy, Ed Wilson, is here. Matt Graeber is back with us today to finish up his three-part series. Read the previous parts to catch up for today’s post:

In the last two posts, I described two methods of interacting with the Windows API. In this post, I’ll introduce the last and final method: reflection.

Reflection gives a programmer the ability to perform type introspection on code. The most common form of type introspection you would perform in Windows PowerShell would be using the Get-Member cmdlet, which enables you to discover the methods and properties of an object. In the last post, we also used reflection to find a non-public method that implemented the kernel32.dll CopyFile function. In this post, we will be using reflection to generate code dynamically. This concept is known as metaprogramming.

Steps to define a dynamic method

To define a dynamic method that will call the CopyFile method in kernel32.dll, the following steps are needed:

  1. Define a dynamic assembly. Recall that the assembly is the container for modules, types, and members.
  2. Define the dynamic assembly in your current AppDomain. Think of the AppDomain as your Windows PowerShell session. It’s the environment in which our method is going to execute.
  3. Define a dynamic module. A module is a container for types and their members.
  4. Define a dynamic type. A type (that is, class) is a container for members (methods, properties, nested types, fields, events, and constructors).
  5. Define our dynamic method. Here we specify attributes (Public and Static) and the method’s parameters and return type.
  6. Manually build a DllImport attribute. The result will be the equivalent of the following C# attribute:
    [DllImport(“kernel32.dll”, SetLastError = true, PreserveSig = true,                                    CallingConvention = CallingConvention.WinApi, CharSet = CharSet.Unicode)]
  7. Apply the custom DllImport attribute to the dynamic method.
  8. Call the CreateType method to bake everything together and make our method available to our Windows PowerShell session.

As you can see, this is not a trivial process. This process involves performing tasks that Add-Type and the C# compiler would typically take care of on your behalf.

So you may be asking now, “Why the heck would I want to go through all this trouble just to call a function?” The reason I typically do this is twofold:

  1. I need to maintain a minimal forensic footprint while executing my script. I don’t want to invoke the C# compiler and write temp files to disk.
  2. The Windows API function I want is not present in the portion of the .NET Framework loaded by my current Windows PowerShell session.

To see all of this in action, I wrote another implementation of the Copy-RawItem function that uses reflection. The complete text of this function is available in the Script Center Repository: Copy-RawItem (Reflection Version).

Copy-RawItem – Reflection Version:

function Copy-RawItem

{

<#

.SYNOPSIS

    Copies a file from one location to another including files contained within DeviceObject paths.

.PARAMETER Path

    Specifies the path to the file to copy.

.PARAMETER Destination

    Specifies the path to the location where the item is to be copied.

.PARAMETER FailIfExists

    Do not copy the file if it already exists in the specified destination.

.OUTPUTS

    None or an object representing the copied item.

.EXAMPLE

    Copy-RawItem ‘\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy2\Windows\System32\config\SAM’ ‘C:\temp\SAM’

#>

    [CmdletBinding()]

    [OutputType([System.IO.FileSystemInfo])]

    Param (

        [Parameter(Mandatory = $True, Position = 0)]

        [ValidateNotNullOrEmpty()]

        [String]

        $Path,

        [Parameter(Mandatory = $True, Position = 1)]

        [ValidateNotNullOrEmpty()]

        [String]

        $Destination,

        [Switch]

        $FailIfExists

    )

    # Create a new dynamic assembly. An assembly (typically a dll file) is the container for modules

    $DynAssembly = New-Object System.Reflection.AssemblyName(‘Win32Lib’)

    # Define the assembly and tell is to remain in memory only (via [Reflection.Emit.AssemblyBuilderAccess]::Run)

    $AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)

    # Define a new dynamic module. A module is the container for types (a.k.a. classes)

    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule(‘Win32Lib’, $False)

    # Define a new type (class). This class will contain our method – CopyFile

    # I’m naming it ‘Kernel32’ so that you will be able to call CopyFile like this:

    # [Kernel32]::CopyFile(src, dst, FailIfExists)

    $TypeBuilder = $ModuleBuilder.DefineType(‘Kernel32’, ‘Public, Class’)

    # Define the CopyFile method. This method is a special type of method called a P/Invoke method.

    # A P/Invoke method is an unmanaged exported function from a module – like kernel32.dll

    $PInvokeMethod = $TypeBuilder.DefineMethod(

‘CopyFile’,

[Reflection.MethodAttributes] ‘Public, Static’,

[Bool],

[Type[]] @([String], [String], [Bool]))

    #region DllImportAttribute

    # Set the equivalent of: [DllImport(

    #   “kernel32.dll”,

    #   SetLastError = true,

    #   PreserveSig = true,

    #   CallingConvention = CallingConvention.WinApi,

    #   CharSet = CharSet.Unicode)]

    # Note: DefinePInvokeMethod cannot be used if SetLastError needs to be set

    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))

    $FieldArray = [Reflection.FieldInfo[]] @(

        [Runtime.InteropServices.DllImportAttribute].GetField(‘EntryPoint’),

        [Runtime.InteropServices.DllImportAttribute].GetField(‘PreserveSig’),

        [Runtime.InteropServices.DllImportAttribute].GetField(‘SetLastError’),

        [Runtime.InteropServices.DllImportAttribute].GetField(‘CallingConvention’),

        [Runtime.InteropServices.DllImportAttribute].GetField(‘CharSet’)

    )

    $FieldValueArray = [Object[]] @(

        ‘CopyFile’,

        $True,

        $True,

        [Runtime.InteropServices.CallingConvention]::Winapi,

        [Runtime.InteropServices.CharSet]::Unicode

    )

    $SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(

$DllImportConstructor,

@(‘kernel32.dll’),

$FieldArray,

$FieldValueArray)

    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    #endregion

    # Make our method accesible to PowerShell

    $Kernel32 = $TypeBuilder.CreateType()

    # Perform the copy

    $CopyResult = $Kernel32::CopyFile($Path, $Destination, ([Bool] $PSBoundParameters[‘FailIfExists’]))

    if ($CopyResult -eq $False)

    {

        # An error occured. Display the Win32 error set by CopyFile

        throw ( New-Object ComponentModel.Win32Exception )

    }

    else

    {

        Write-Output (Get-ChildItem $Destination)

    }

}

Here is Copy-RawItem in action with descriptive errors that are displayed:

Image of command output

This wraps up my series about using Windows PowerShell to interact with the Windows API. As you can see, this process can be as simple or as complicated as you want it to be. Which method you choose will ultimately be determined by your unique execution requirements. However in most cases, using Add-Type to compile C# code will suffice.

For more resources about reflection, search the MSDN documentation for the System.Reflection and System.Reflection.Emit namespaces. Also, check out some of the awesome work by Windows PowerShell MVPs Oisin Grehan and Adam Driscoll. These guys are experts on metaprogramming and reflection, among other things.

The full script is available in the Script Center Repository: Copy-RawItem (Reflection Version).

~Matt

Thank you, Matt, for sharing your time and knowledge.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

0 comments

Discussion is closed.

Feedback usabilla icon