Revisiting NetSession Function using PSReflect

In an article that I wrote a little while ago, I talked about how you can view the net sessions on systems by making use of some pinvoke magic to call some Win32 apis. If you have ever worked with this type of approach, you know that it can be sometimes painful in writing out the necessary code needed to build out the required methods, enums, structs and anything else that is required just to pull a particular piece of information.

So you might be asking why don’t we just use Add-Type and use the very handy pinvoke signatures that are available? Well, this is the easiest approach as it is all taken care of for you, but if you are more on the security side, you want to ensure that nothing is written to disk (which is what Add-Type does) whereas using reflection can give you the option to write to memory instead, making it leave less artifacts laying around.

Fortunately for all of us in the community, fellow MVP Matt Graeber (Blog | Twitter) has made an awesome module called PSReflect which seeks to make this process much simpler by handling all of the internal calls and building of the methods from the Win32 apis while leaving us with the necessary code to use the methods.

What I am going to do in this blog post is to highlight just how easy it is to build working code that uses pinvoke signatures by taking what I did in my post on viewing net sessions and applying the same process this time using the PSReflect module. The Github repository that I referenced earlier is one way to get a hold of the module, but if you are running PowerShell V5, you can install is simply by calling Install-Module.

Install-Module –Name PSReflect

image

And now we have our module that we can use in building out the necessary code. I won’t be using all of the commands here, but in case you are curious as to  what is available, here they are:

image

I want to show the code that is handling the reflection to dynamically build out the methods that will be used to grab the net sessions and won’t be showing everything else that happens afterwards to pull the net sessions. So with that, here is the code that I used in my Get-NetSession function to build up the types and methods:

#region Reflection
Try {
    [void][Net.Session]
}
Catch {
    Write-Verbose "Building pinvoke via reflection"
    #region Module Builder
    $Domain = [AppDomain]::CurrentDomain
    $DynAssembly = New-Object System.Reflection.AssemblyName('NetSession')
    $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) # Only run in memory
    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('NetSessionModule', $False)
    #endregion Module Builder

    #region Custom Attribute Builder
    $ctor = [System.Runtime.InteropServices.MarshalAsAttribute].GetConstructor(@([System.Runtime.InteropServices.UnmanagedType]))
    $CustomAttribute = [System.Runtime.InteropServices.UnmanagedType]::LPWStr
    $CustomAttributeBuilder = New-Object System.Reflection.Emit.CustomAttributeBuilder -ArgumentList $ctor, $CustomAttribute
    #endregion Custom Attribute Builder

    #region Struct
    #region SESSION_INFO_10
    $Attributes = 'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit'
    $STRUCT_TypeBuilder = $ModuleBuilder.DefineType('SESSION_INFO_10', $Attributes, [System.ValueType], 8, 0x0)
    $Field = $STRUCT_TypeBuilder.DefineField('OriginatingHost', [string], 'Public')
    $Field.SetCustomAttribute($CustomAttributeBuilder)
    $Field = $STRUCT_TypeBuilder.DefineField('DomainUser', [string], 'Public')
    $Field.SetCustomAttribute($CustomAttributeBuilder)
    [void]$STRUCT_TypeBuilder.DefineField('SessionTime', [uint32], 'Public')
    [void]$STRUCT_TypeBuilder.DefineField('IdleTime', [uint32], 'Public')
    [void]$STRUCT_TypeBuilder.CreateType()
    #endregion SESSION_INFO_10
    #endregion Struct

    $TypeBuilder = $ModuleBuilder.DefineType('Net.Session', 'Public, Class')

    #region Methods
    #region NetSessionEnum Method
    $PInvokeMethod = $TypeBuilder.DefineMethod(
        'NetSessionEnum', #Method Name
        [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
        [int32], #Method Return Type
        [Type[]] @(
            [string],
            [string],
            [string],
            [int32],
            [intptr].MakeByRefType(),
            [int],
            [int32].MakeByRefType(),
            [int32].MakeByRefType(),
            [int32].MakeByRefType()
        ) #Method Parameters
    )

    #Define first three parameters with custom attributes
    1..3 | ForEach {
        $Parameter = $PInvokeMethod.DefineParameter(
            $_,
            [System.Reflection.ParameterAttributes]::In,
            $Null
        )
        $Parameter.SetCustomAttribute(
            $CustomAttributeBuilder
        )
    }

    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
    $FieldArray = [Reflection.FieldInfo[]] @(
        [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
        [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
        [Runtime.InteropServices.DllImportAttribute].GetField('ExactSpelling')
        [Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig')
    )

    $FieldValueArray = [Object[]] @(
        'NetSessionEnum', #CASE SENSITIVE!!
        $True,
        $True,
        $True
    )

    $CustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
        $DllImportConstructor,
        @('Netapi32.dll'),
        $FieldArray,
        $FieldValueArray
    )

    $PInvokeMethod.SetCustomAttribute($CustomAttribute)
    #endregion NetSessionEnum Method
    #region NetApiBufferFree Method
    $PInvokeMethod = $TypeBuilder.DefineMethod(
        'NetApiBufferFree', #Method Name
        [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
        [int], #Method Return Type
        [Type[]] @(
            [intptr]
        ) #Method Parameters
    )

    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
    $FieldArray = [Reflection.FieldInfo[]] @(
        [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
        [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
        [Runtime.InteropServices.DllImportAttribute].GetField('ExactSpelling')
        [Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig')
    )

    $FieldValueArray = [Object[]] @(
        'NetApiBufferFree', #CASE SENSITIVE!!
        $True,
        $True,
        $True
    )

    $CustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
        $DllImportConstructor,
        @('Netapi32.dll'),
        $FieldArray,
        $FieldValueArray
    )

    $PInvokeMethod.SetCustomAttribute($CustomAttribute)
    #endregion NetApiBufferFree Method
    #endregion Methods

    [void]$TypeBuilder.CreateType()
}
#endregion Reflection

Yes, that is 129 lines of code that handles the building of 1 Struct and 2 Methods. Depending on the amount of data in a Struct, it could be a greater number of lines to account for more properties. Methods are usually going to be the same number of lines (~50) depending on your style of building it out. In my case this is what I would expect even for a simple method that might only take a single parameter and doesn’t return anything when called.

I want to make sure that the PSReflect module is available before running the function so I am going to add a REQUIRES statement and then import the module.

#REQUIRES -Module PSReflect
Import-Module -Name PSReflect

The first part is to create the module builder that we will then be referencing throughout the rest of the creations of the various components related to our structs and methods.

No PSReflect

#region Module Builder
$Domain = [AppDomain]::CurrentDomain
$DynAssembly = New-Object System.Reflection.AssemblyName('NetSession')
$AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) # Only run in memory
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('NetSessionModule', $False)
#endregion Module Builder

#region Custom Attribute Builder
$ctor = [System.Runtime.InteropServices.MarshalAsAttribute].GetConstructor(@([System.Runtime.InteropServices.UnmanagedType]))
$CustomAttribute = [System.Runtime.InteropServices.UnmanagedType]::LPWStr
$CustomAttributeBuilder = New-Object System.Reflection.Emit.CustomAttributeBuilder -ArgumentList $ctor, $CustomAttribute
#endregion Custom Attribute Builder

 

With PSReflect

$Module = New-InMemoryModule -ModuleName NetSessions

I am including the Custom Attribute Builder as it is required for my Struct. Next up is to build out the Struct that the method will require for handling the net sessions object.

No PSReflect

$Attributes = 'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit'
$STRUCT_TypeBuilder = $ModuleBuilder.DefineType('SESSION_INFO_10', $Attributes, [System.ValueType], 8, 0x0)
$Field = $STRUCT_TypeBuilder.DefineField('OriginatingHost', [string], 'Public')
$Field.SetCustomAttribute($CustomAttributeBuilder)
$Field = $STRUCT_TypeBuilder.DefineField('DomainUser', [string], 'Public')
$Field.SetCustomAttribute($CustomAttributeBuilder)
[void]$STRUCT_TypeBuilder.DefineField('SessionTime', [uint32], 'Public')
[void]$STRUCT_TypeBuilder.DefineField('IdleTime', [uint32], 'Public')
[void]$STRUCT_TypeBuilder.CreateType()

With PSReflect

$SESSION_INFO_10 = struct -Module $Module -FullName SESSION_INFO_10 -StructFields @{
    OriginatingHost = field -Position 0 -Type ([string]) -MarshalAs @('LPWStr')
    DomainUser = field -Position 1 -Type ([string]) -MarshalAs @('LPWStr')
    SessionTime = field -Position 2 -Type ([int32])
    IdleTime = field -Position 3 -Type ([int32])
}

You can already start to see the fewer lines of code that is being used and I can assure you that this trend will only continue. In fact, I don’t need to worry about a custom attribute builder with PSReflect as it is handled via the –MarshalAs parameter.

Up next are the method creations that we will be creating to round out all of the components.

No PSReflect

$TypeBuilder = $ModuleBuilder.DefineType('Net.Session', 'Public, Class')

#region Methods
#region NetSessionEnum Method
$PInvokeMethod = $TypeBuilder.DefineMethod(
    'NetSessionEnum', #Method Name
    [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
    [int32], #Method Return Type
    [Type[]] @(
        [string],
        [string],
        [string],
        [int32],
        [intptr].MakeByRefType(),
        [int],
        [int32].MakeByRefType(),
        [int32].MakeByRefType(),
        [int32].MakeByRefType()
    ) #Method Parameters
)

#Define first three parameters with custom attributes
1..3 | ForEach {
    $Parameter = $PInvokeMethod.DefineParameter(
        $_,
        [System.Reflection.ParameterAttributes]::In,
        $Null
    )
    $Parameter.SetCustomAttribute(
        $CustomAttributeBuilder
    )
}

$DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$FieldArray = [Reflection.FieldInfo[]] @(
    [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
    [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
    [Runtime.InteropServices.DllImportAttribute].GetField('ExactSpelling')
    [Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig')
)

$FieldValueArray = [Object[]] @(
    'NetSessionEnum', #CASE SENSITIVE!!
    $True,
    $True,
    $True
)

$CustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
    $DllImportConstructor,
    @('Netapi32.dll'),
    $FieldArray,
    $FieldValueArray
)

$PInvokeMethod.SetCustomAttribute($CustomAttribute)
#endregion NetSessionEnum Method
#region NetApiBufferFree Method
$PInvokeMethod = $TypeBuilder.DefineMethod(
    'NetApiBufferFree', #Method Name
    [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
    [int], #Method Return Type
    [Type[]] @(
        [intptr]
    ) #Method Parameters
)

$DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$FieldArray = [Reflection.FieldInfo[]] @(
    [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
    [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
    [Runtime.InteropServices.DllImportAttribute].GetField('ExactSpelling')
    [Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig')
)

$FieldValueArray = [Object[]] @(
    'NetApiBufferFree', #CASE SENSITIVE!!
    $True,
    $True,
    $True
)

$CustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
    $DllImportConstructor,
    @('Netapi32.dll'),
    $FieldArray,
    $FieldValueArray
)

$PInvokeMethod.SetCustomAttribute($CustomAttribute)
#endregion NetApiBufferFree Method
#endregion Methods

[void]$TypeBuilder.CreateType()

With PSReflect

$Functions = @(
    func -DllName Netapi32 -FunctionName NetSessionEnum -ReturnType ([int32]) -ParameterTypes @([string],[string],[string],[int32],
        [intptr].MakeByRefType(),[int],[int32].MakeByRefType(),[int32].MakeByRefType(),[int32].MakeByRefType())
    func -DllName Netapi32 -FunctionName NetApiBufferFree -ReturnType ([int]) -ParameterTypes @([intptr])
)

$Functions | Add-Win32Type -Module $Module -Namespace 'Net.Session'

This is, in my opinion, where you see the biggest gains with PSReflect in the amount of lines of code that you save using this module to build out the methods. The total lines of code that it takes to build out my code using PSReflect is 23 lines of code. Compare that to the 129 lines not using PSReflect and you can see that I saved over 100 lines! This was a pretty small amount of pinvoke happening here as some of my larger functions (Get-ChildItem2 and Get-Service2) require a lot more code to build out everything that I need.

If you also didn’t notice, the quality of the code is also much cleaner to read and understand as you are working with PowerShell functions, not just a lot of .Net code to build everything out. Defining a method via PowerShell code is so much easier to write and read than dynamically building everything out.

After this, the rest of the code is practically the same as I still need to perform the same actions to build the object, query the remote system and marshal the data back to the object.

I hope that if you work with pinvoke,  that you give this a try and see how much easier it will make your life writing code without having to worry about writing a bunch of code to create the necessary components to make everything work together.

References

http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/

About Boe Prox

Microsoft Cloud and Datacenter MVP working as a SQL DBA.
This entry was posted in powershell and tagged , , , , , , . Bookmark the permalink.

2 Responses to Revisiting NetSession Function using PSReflect

  1. I would argue that number of lines (even though impressive) is not the biggest “win” here. I would say that code that uses PSReflect is first of all easier to understand, read and maintain. And even though you still need to understand what and why, you don’t have to copy&paste&translate to PowerShell everything on pinvoke.net. 😉

    • Boe Prox says:

      All valid points. This article assumes that you already had working knowledge of pinvoke as it was a follow on from my last article (mentioned at the beginning).
      True, the readability of the code is definitely something to take away from this (and I glossed that over when writing this) and will update the article to properly reflect that 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s