2015年2月27日金曜日

Use PowerShell to Interact with the Windows API

In Windows PowerShell, there are three ways to interact with Windows API functions:

Use the Add-Type cmdlet to compile C# code. This is the officially documented method.
Get a reference to a private type in the .NET Framework that calls the method.
Use reflection to dynamically define a method that calls the Windows API function.
Background to using Add-Type

In the examples that follow, I use the CopyFile function in kernel32.dll as the function that Windows PowerShell will interact with. Now, you may have just asked the question, "Why would I want to call CopyFile when Windows PowerShell already has the Copy-Item cmdlet?"

That's a very good question, indeed. As it turns out though, there are certain file paths that the Windows PowerShell file provider doesn't know how to handle. In particular, it doesn't know how to interpret special device object paths, such as: \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy2\Windows\System32\calc.exe. These are the kinds of paths that you need to deal with when you interact with files that are backed up by the Volume Shadow Copy Service. For example, running the following command lists the device object paths of each volume shadow copy:

# Run this from an administrative prompt

Get-WmiObject Win32_ShadowCopy | Select-Object DeviceObject

Before diving into using Windows PowerShell to call CopyFile, it is helpful to have some background on C/C++ types vs. .NET types. According to MSDN documentation (see CopyFile function), CopyFile has the following function definition:

Image of code

So CopyFile has a return type of BOOL and three parameters?two 'T' strings and a bool. The key to interacting with Win API functions is knowing how to convert these C/C++ types to the equivalent .NET type. Fortunately, there is a site dedicated to this cause: PINVOKE.NET , which provides a treasure trove of the type of information you need to perform this task. It is worth a bookmark in your favorites.

If you search for CopyFile on PINVOKE.NET, you will find the following C# definition:

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists);

You can now see the translation of C/C++ types to .NET types:

BOOL -> bool

LPCTSTR -> string

Now, you should have the background needed to start using Windows PowerShell to interact with the CopyFile function. Let's jump into our first method of interaction, Add-Type.

Using Add-Type to call the CopyItem function

The Add-Type cmdlet is used to define .NET types that will be made available to your Windows PowerShell session. What's really cool about it is that it can compile C# code on the fly. If you view the Help for Add-Type, you'll find some good examples of how to call Windows API functions.

As an example, the following code allows me to call the CopyItem function within Windows PowerShell:

$MethodDefinition = @'

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]

public static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists);

'@

$Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -Namespace 'Win32' -PassThru

# You may now call the CopyFile function

# Copy calc.exe to the user's desktop

$Kernel32::CopyFile("$($Env:SystemRoot)\System32\calc.exe", "$($Env:USERPROFILE)\Desktop\calc.exe", $False)

The $MethodDefinition variable simply contains the C# definition that I took from PINVOKE.NET with one minor modification: I defined the CopyFile method to be public. Methods that are added with Add-Type must be public to easily interact with them in Windows PowerShell.

I then call Add-Type and provide the C# source code, a type name, and a namespace. By specifying the type name and namespace, after calling Add-Type you can reference the new type in WindowsPowerShell with `[Win32.Kernel32]`.

Lastly, by default, Add-Type doesn't output the type definition that it creates. The -PassThru parameter tells it to output the type definition.

After calling Add-Type, you can finally call CopyFile directly within Windows PowerShell. The previous example simply copies calc.exe to the user's desktop. It's worth noting the two colons that follow the $Kernel32 variable. These indicate that you are calling a static .NET method. All static methods are called this way in Windows PowerShell.

To wrap things up, I wrote the Copy-RawItem function that wraps the CopyFile function nicely. The full script is also available in the Script Center Repository: Copy-RawItem (Add-Type Version).

Copy-RawItem Function:

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

)

$MethodDefinition = @'

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]

public static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists);

'@



$Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -Namespace 'Win32' -PassThru



# 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)

}

}

The following image illustrates using the Copy-RawItem function.

Image of command output

The only code in the Copy-RawItem that warrants explanation is the last few lines where error checking occurs. The MSDN documentation states that if CopyFile returns FALSE, an error occurred. In C/C++, you would determine the cause for the error by calling GetLastError. In Windows PowerShell, you can channel the error by throwing a ComponentModel.Win32Exception object. This is what allows you to pry out the error from a Win API function without calling GetLastError.

0 件のコメント:

コメントを投稿