Windows AMSI Analysis for Bypass
Recently I started looking into how Windows AMSI works. Found some materials like this PDF file. It contains a summary of the current AMSI bypass techniques and also 2 new ideas. With this reference, I looked into Ghidra disassmebly code on amsi.dll and recovered psudo C++ type code with chatgpt’s help.
Exported functions
AmsiInitialize AmsiOpenSession AmsiScanBuffer AmsiScanString AmsiUacInitialize AmsiUacScan AmsiCloseSession AmsiUacUninitialize AmsiUninitialize
Psudo Code
AmsiInitialize
HRESULT AmsiInitialize(LPCWSTR appName, HAMSICONTEXT *out)
{
if (!appName || !out) return E_INVALIDARG;
if (wcslen(appName) > 0x7ffd) return E_INVALIDARG;
auto ctx = (_HAMSICONTEXT*)CoTaskMemAlloc(32);
if (!ctx)
return E_OUTOFMEMORY;
ctx->Signature = 'AMSI'; // Win10 only
ctx->AppName = CopyString(appName);
if (!ctx->AppName) { Free(ctx); return E_OUTOFMEMORY; }
IClassFactory *cf;
RETURN_IF_FAILED(DllGetClassObject(CLSID_Antimalware, IID_IClassFactory, &cf));
RETURN_IF_FAILED(cf->CreateInstance(nullptr, IID_IAntimalware, &ctx->Antimalware));
ctx->SessionCount = rand();
*out = (HAMSICONTEXT)ctx;
cf->Release();
return S_OK;
}
AmsiOpenSession
HRESULT AmsiOpenSession(HAMSICONTEXT amsiContext, HAMSISESSION* amsiSession)
{
// 1. Validate arguments
if (amsiSession == nullptr ||
amsiContext == nullptr ||
amsiContext->Signature != 'AMSI' || // ASCII for “AMSI”
amsiContext->AppName == nullptr || // caller must have provided an app name
amsiContext->Antimalware == nullptr) // must have a CAmsiAntimalware instance
{
return E_INVALIDARG; // 0x80070057
}
// 2. Atomically bump the session counter
// We use InterlockedIncrement so no explicit lock/unlock is needed.
//
// This ensures that each call gets a unique, nonzero session ID.
ULONG newId = InterlockedIncrement(&amsiContext->SessionCount);
// 3. Handle the unlikely wrap‑around case (ID==0)
if (newId == 0)
{
// If SessionCount wrapped back to zero, bump again
newId = InterlockedIncrement(&amsiContext->SessionCount);
}
// 4. Return the session handle (just the ID)
*amsiSession = newId;
return S_OK; // 0
}
AmsiScanBuffer
HRESULT AmsiScanBuffer(HAMSICONTEXT* amsiContext,
void* buffer,
ULONG length,
LPCWSTR contentName,
HAMSISESSION session,
AMSI_RESULT* result)
{
// 1. Optional WPP trace of the call (if logging level >= 4)
// if (WPP_GLOBAL_Control->Flags & 0x4)
// WPP_SF_qqDqq(…, buffer, length, amsiContext, …);
// 2. Validate arguments
if (buffer == nullptr ||
length == 0 ||
result == nullptr ||
amsiContext == nullptr ||
amsiContext->Signature != 'AMSI' || // literal ASCII “AMSI”
amsiContext->AppName == nullptr ||
amsiContext->Antimalware == nullptr)
{
return E_INVALIDARG; // 0x80070057
}
// 3. Build the CAmsiBufferStream wrapper
CAmsiBufferStream stream;
stream.vtable = &CAmsiBufferStream::`vftable`;
stream.amsiSession = session;
stream.buffer = buffer;
stream.length = length;
stream.name = amsiContext->AppName;
stream.contentName = contentName;
// 4. Delegate to the provider chain
// HRESULT Scan(
// IAmsiStream* stream,
// AMSI_RESULT* result,
// IAntimalwareProvider* *provider // optional out
// );
HRESULT hr = amsiContext->Antimalware->Scan(
&stream,
result,
/*provider=*/ nullptr);
// 5. Return the provider’s verdict HRESULT
return hr;
}
AmsiScanString
HRESULT AmsiScanString(HAMSICONTEXT amsiContext,
LPCWSTR string,
LPCWSTR contentName,
HAMSISESSION session,
AMSI_RESULT* result)
{
// 1. Validate basic pointers
if (string == nullptr || result == nullptr)
{
return E_INVALIDARG; // 0x80070057
}
// 2. Compute the length of the string in characters
size_t charCount = 0;
while (string[charCount] != L'\0')
{
++charCount;
}
// 3. Convert to byte length and check for overflow
// (sizeof(wchar_t) == 2 on Windows)
uint64_t byteCount = charCount * sizeof(wchar_t);
if (byteCount > 0xFFFFFFFFu)
{
return E_INVALIDARG; // length too large to pass as a ULONG
}
// 4. Delegate to AmsiScanBuffer for the actual scan
return AmsiScanBuffer(
amsiContext,
const_cast<LPWSTR>(string), // buffer pointer
static_cast<ULONG>(byteCount), // buffer size in bytes
contentName,
session,
result);
}
AmsiUacInitialize
HRESULT AmsiUacInitialize(HAMSICONTEXT *outCtx)
{
if (WPP_INFO_ON) TraceEnter(outCtx);
if (!outCtx)
return E_INVALIDARG;
auto ctx = (_HAMSICONTEXT*)CoTaskMemAlloc(32);
if (!ctx)
return E_OUTOFMEMORY;
ZeroMemory(ctx, 32);
ctx->Signature = 'OMSI'; // UAC tag
auto engine = new (std::nothrow) CAmsiUacAntimalware;
if (!engine) {
AmsiUninitializeImpl(ctx);
return E_OUTOFMEMORY;
}
HRESULT hr = engine->Initialize(); // load UAC providers
if (FAILED(hr)) {
AmsiUninitializeImpl(ctx);
return hr;
}
ctx->AppName = engine; // note the field reuse!
ctx->Antimalware = nullptr; // not used in UAC flavour
ctx->SessionCount = 0; // unused for UAC
*outCtx = ctx;
if (WPP_INFO_ON) TraceSuccess(ctx);
return S_OK;
}
AmsiUacScan
HRESULT AmsiUacScan(HAMSICONTEXT hCtx,
const AMSI_UAC_REQUEST_CONTEXT *req,
AMSI_RESULT *result,
ULONGLONG *providerData)
{
TRACE_ENTRY(hCtx, req);
if (!hCtx || hCtx->Signature != 'OMSI' ||
!hCtx->AppName || !req || !result || !providerData)
return E_INVALIDARG;
IAntimalwareUacProvider *prov = nullptr;
HRESULT hr = static_cast<CAmsiUacAntimalware*>(hCtx->uacAntiMalware)
->UacScan(req, result, &prov);
if (FAILED(hr))
{
TRACE_FAILURE(hr);
if (prov) prov->Release();
return hr;
}
*providerData = 0;
if (prov)
{
prov->GetProviderData(providerData); // provider‑specific
prov->Release();
}
return S_OK;
}
AmsiCloseSession
void AmsiCloseSession(HAMSICONTEXT amsiContext,
HAMSISESSION session)
{
// Directly invoke the COM provider’s CloseSession implementation.
// This informs the anti‑malware provider that the given session is ending.
amsiContext->Antimalware->CloseSession(session);
}
AmsiUacUninitialize
void AmsiUacUninitialize(HAMSICONTEXT* amsiContext)
{
// 1. Optional WPP trace if the verbose‑trace flag is set
if (WPP_GLOBAL_Control != nullptr &&
(WPP_GLOBAL_Control->Flags & 0x4) != 0)
{
// opcode 0x1F ⇒ “UAC uninitialize” event
WPP_SF_q(
WPP_GLOBAL_Control->u,
0x1F,
&WPP_89359e7f70ff34f84acef37beb0b5af8_Traceguids,
amsiContext
);
}
// 2. Only uninitialize if we were ever initialized
if (amsiContext != nullptr)
{
AmsiUninitializeImpl(amsiContext);
}
}
AmsiUninitialize
void AmsiUninitialize(HAMSICONTEXT amsiContext)
{
// 1. Optional WPP trace if the verbose‑trace flag is set
if (WPP_GLOBAL_Control != nullptr &&
(WPP_GLOBAL_Control->Flags & 0x4) != 0)
{
// opcode 0x17 ⇒ “AMSI uninitialize” event
WPP_SF_q(
WPP_GLOBAL_Control->u,
0x17,
&WPP_89359e7f70ff34f84acef37beb0b5af8_Traceguids,
amsiContext
);
}
// 2. Only uninitialize if we were ever initialized
if (amsiContext != nullptr)
{
AmsiUninitializeImpl(amsiContext);
}
}
Data Structures and Classes
HAMSICONTEXT
typedef struct _AMSICONTEXT {
DWORD Signature; // 'AMSI' – absent on Win11+
#if defined(_WIN64)
DWORD _Padding0;
#endif
Union {
PCWSTR AppName; // points to heap‑copied string
CAmsiUacAntimalware * puacAntimalware;
};
IAntimalware * Antimalware;
ULONGLONG SessionCount; // incremented by AmsiOpenSession
} AMSICONTEXT, *PAMSICONTEXT;
typedef HAMSICONTEXT AMSICONTEXT *;
IAntimalware
[
object,
pointer_default(unique),
uuid(82d29c2e‑f062‑44e6‑b5c9‑3d9a2f24a2df) // IID_IAntimalware
]
interface IAntimalware : IUnknown
{
HRESULT Scan(
[in] IAmsiStream *stream,
[out] AMSI_RESULT *result,
[out] IAntimalwareProvider **provider // optional – who detected it?
);
void CloseSession(
[in] ULONGLONG session // HAMSISESSION from AmsiOpenSession
);
};
CAmsiAntimalware
//----------------------------------------------------------------------
// Pseudo‑class definition (fields only shown as comments)
//----------------------------------------------------------------------
class CAmsiAntimalware
: public CComObjectRootEx<CComMultiThreadModel>,
public IAntimalware
{
public:
// COM map
BEGIN_COM_MAP(CAmsiAntimalware)
COM_INTERFACE_ENTRY(IAntimalware)
END_COM_MAP()
// Constructor / Destructor
CAmsiAntimalware();
~CAmsiAntimalware();
// ATL lifecycle hooks
HRESULT FinalConstruct();
void FinalRelease();
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) override;
STDMETHOD_(ULONG, AddRef)() override;
STDMETHOD_(ULONG, Release)() override;
// IAntimalware
STDMETHOD(Scan)(
IAmsiStream* stream,
AMSI_RESULT* result,
IAntimalwareProvider** provider
) override;
STDMETHOD(CloseSession)(
ULONGLONG session
) override;
private:
// ETW configuration callback
static void WINAPI EtwConfigurationCallback(
const GUID* sourceId,
ULONG isEnabled,
UCHAR level,
ULONGLONG matchAnyKeyword,
ULONGLONG matchAllKeyword,
const EVENT_FILTER_DESCRIPTOR* filterData,
PVOID context
);
// Helper to emit the AMSI_SCANBUFFER event
HRESULT GenerateEtwEvent(
IAmsiStream* stream,
ScanStatus scanStatus,
AMSI_RESULT result
);
private:
// COM reference count
LONG m_refCount;
// Critical section + init flag
CRITICAL_SECTION m_cs;
bool m_csInitialized;
// Dynamically loaded antimalware providers
CComPtr<IAntimalwareProvider> m_providers[16];
ULONG m_providerCount;
// ETW registration
REGHANDLE m_regHandle;
bool m_isEventProvEnabled;
// ETW content‐size thresholds (min & max bytes to include in events)
ULONG m_minEtwContentSize;
ULONG m_maxEtwContentSize;
// CLSIDs read from the registry for provider creation
GUID m_clsids[16];
};
//----------------------------------------------------------------------
// Pseudo‑implementation of the constructor
//----------------------------------------------------------------------
CAmsiAntimalware::CAmsiAntimalware()
: m_refCount(0) // *&this->super_CComObjectRootEx = 0
, m_providerCount(0) // this->providerCount = 0
, m_regHandle(nullptr) // this->regHandle = 0
, m_isEventProvEnabled(0) // this->m_isEventProvEnabled = 0
, m_minEtwContentSize(6) // this->m_minEtwContentSize = 6
, m_maxEtwContentSize(0x8000) // this->m_maxEtwContentSize = 0x8000
{
// 1) Initialize the critical section
InitializeCriticalSection(&m_cs);
// (equivalent of zeroing OwningThread and SpinCount)
// 2) Construct the provider array slots
// (calls default CComPtr ctor 16 times)
for (int i = 0; i < 16; ++i) {
new (&m_providers[i]) CComPtr<IAntimalwareUacProvider>();
}
// 3) Zero out the CLSID array
for (int i = 0; i < 16; ++i) {
m_clsids[i] = GUID{ 0, 0, 0, {0,0,0,0,0,0,0,0} };
}
// note: other internal fields (e.g. session lists) would be initialized here
}
CAmsiAntimalware::~CAmsiAntimalware()
{
// 1) If we registered an ETW provider (or similar), unregister it
if (m_regHandle != 0) {
EventUnregister(m_regHandle);
m_regHandle = 0;
}
// 2) Destroy each CComPtr<IAntimalwareUacProvider> in the array
// (matches the eh_vector_destructor_iterator)
for (int i = 0; i < 16; ++i) {
m_providers[i].~CComPtr<IAntimalwareUacProvider>();
}
// 3) If we ever initialized the critical section, delete it
if (m_csInitialized != 0) {
m_csInitialized = 0;
DeleteCriticalSection(&m_cs);
}
// Note: base‑class (CComObjectRootEx) cleanup is automatic
}
HRESULT CAmsiAntimalware::Scan(IAmsiStream* stream,
AMSI_RESULT* result,
IAntimalwareProvider** winner)
{
if (!result) return E_POINTER;
*result = AMSI_RESULT_CLEAN;
if (winner) *winner = nullptr;
trace_enter(stream); // WPP
AMSI_RESULT best = AMSI_RESULT_CLEAN;
int bestIdx = -1;
HRESULT hrLast = S_OK;
for (unsigned i = 0; i < m_providerCount; ++i)
{
AMSI_RESULT rProv = AMSI_RESULT_CLEAN;
ULONGLONG t0 = now();
hrLast = m_providers[i]->Scan(stream, &rProv);
ULONGLONG dt = now() - t0;
sample_latency_telemetry(i, dt, hrLast, rProv);
if (SUCCEEDED(hrLast))
{
if (rProv < best) { best = rProv; bestIdx = i; }
trace_provider_ok(i, rProv == best);
}
else
trace_provider_failed(i, hrLast);
}
if (bestIdx >= 0) // at least one success
{
*result = best;
if (winner) { *winner = m_providers[bestIdx]; (*winner)->AddRef(); }
}
else // nobody succeeded
*result = AMSI_RESULT_NOT_DETECTED;
emit_final_etw(stream, hrLast, *result);
trace_exit(stream, hrLast, *result);
return hrLast;
}
// Pseudo‑implementation of CAmsiAntimalware::CloseSession
void CAmsiAntimalware::CloseSession(ULONGLONG session)
{
// If we have any registered providers, notify each one to close the session
for (size_t i = 0; i < m_providerCount; ++i) {
// Grab the raw interface pointer from our CComPtr array
IAntimalwareProvider* pProvider = m_providers[i];
if (pProvider) {
// Call the provider’s CloseSession (vtable slot at offset 0x20)
pProvider->CloseSession(session);
}
}
}
HRESULT CAmsiAntimalware::FinalConstruct()
{
// 1) If we already have an ETW registration, unregister it (preserving last‑error)
if (m_regHandle != 0) {
DWORD dwErr = GetLastError();
EventUnregister(m_regHandle);
SetLastError(dwErr);
}
m_regHandle = 0;
// 2) Register our AMSI ETW provider
// - &AMSI is the GUID for our ETW provider
// - EtwConfigurationCallback will be invoked on config changes
// - 'this' is context passed to the callback
// - regHandle receives the registration handle
ULONG status = EventRegister(&AMSI,
EtwConfigurationCallback,
this,
&m_regHandle);
if (status != ERROR_SUCCESS) {
// Convert the Win32 error into an NTSTATUS via WIL helper
return wil::details::in1diag3::Return_NtStatus(/*return‑addr*/,
0x270,
this,
status);
}
// 3) Check whether our ETW provider is currently enabled
// TRACE_LEVEL_INFORMATION (4) and keyword 1 are what AMSI uses
isEventProvEnabled = (EventProviderEnabled(m_regHandle, /*level*/4, /*keyword*/1) != FALSE);
// 4) Load configured COM‑based AmsiProviders from the registry
// - regHandle: ETW registration handle
// - hKeyRoot: root hive where AMSI settings live (e.g. HKEY_LOCAL_MACHINE)
// - amsiProviders: output array of CComPtr<IAntimalwareProvider>
// - clsids: array of GUID slots we zeroed in ctor
// - in_stack…: reserved/context info (pass through)
// - &providerCount: number of providers actually created
HRESULT hr = AmsiComCreateProviders<IAntimalwareProvider>(
m_regHandle,
HKEY_LOCAL_MACHINE, // or whichever root hive you use
m_providers,
m_clsids,
/*reserved*/ nullptr,
&m_providerCount);
// 5) Special case: if no registry entries were found but ETW is still enabled,
// treat that as success (AMSI can still work via ETW)
if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) && m_isEventProvEnabled) {
hr = S_OK;
}
return hr;
}
void WINAPI CAmsiAntimalware::EtwConfigurationCallback(
const GUID* /*sourceId*/,
ULONG /*isEnabled*/,
UCHAR /*level*/,
ULONGLONG /*matchAnyKeyword*/,
ULONGLONG /*matchAllKeyword*/,
const EVENT_FILTER_DESCRIPTOR* filterData,
PVOID context
)
{
// Recover our 'this' pointer from the callback context
CAmsiAntimalware* self = static_cast<CAmsiAntimalware*>(context);
// If no filter descriptor was provided, optionally emit a WPP trace
if (filterData == nullptr) {
// If WPP tracing is initialized and the Information level is enabled...
if (WPP_GLOBAL_Control != nullptr && (WPP_GLOBAL_Control->Flags & 0x4) != 0) {
// Log a “configuration-cleared” event (opcode 0x23)
WPP_SF_(WPP_GLOBAL_Control->u,
0x23,
&WPP_775dec0a01b93551f8626bd70cc9859c_Traceguids);
}
}
// Otherwise, if the filter descriptor carries our special config type...
else if (filterData->Type == static_cast<ULONG>(-0x80000000)) {
// The FilterData->Ptr points to a block whose offsets 0x18 and 0x1C
// hold the new ETW level and keyword mask, respectively.
auto dataBlock = reinterpret_cast<uint8_t*>(filterData->Ptr);
// Update our instance fields at offsets 0x1D4 and 0x1D8
// (these correspond to the same fields set in FinalConstruct)
self->m_minEtwContentSize = *reinterpret_cast<ULONG*>(dataBlock + 0x18);
self->m_maxEtwContentSize = *reinterpret_cast<ULONG*>(dataBlock + 0x1C);
}
// If we got a descriptor of any other type, optionally log a warning
else {
if (WPP_GLOBAL_Control != nullptr && (WPP_GLOBAL_Control->Flags & 0x4) != 0) {
// Log a “configuration-unknown-type” event (opcode 0x24)
WPP_SF_(WPP_GLOBAL_Control->u,
0x24,
&WPP_775dec0a01b93551f8626bd70cc9859c_Traceguids);
}
}
}
HRESULT CAmsiAntimalware::GenerateEtwEvent(
IAmsiStream* stream,
ScanStatus scanStatus,
AMSI_RESULT result
)
{
if (!isEventProvEnabled) {
// ETW tracing is disabled—nothing to do.
return S_OK;
}
HRESULT hr;
ULONG fetched = 0;
// 1) Read the content size (ATTRIBUTE_CONTENT_SIZE == 2)
ULONGLONG contentSize = 0;
hr = stream->GetAttribute(
AMSI_ATTRIBUTE_CONTENT_SIZE,
sizeof(contentSize),
&contentSize,
&fetched);
if (FAILED(hr)) {
return wil::Return_Hr(HRESULT_FROM_WIN32(ERROR_AMSI_ATTRIBUTE),
L"Failed to read content size",
hr);
}
// 2) Read the content address (ATTRIBUTE_CONTENT_ADDRESS == 3)
BYTE* contentAddr = nullptr;
hr = stream->GetAttribute(
AMSI_ATTRIBUTE_CONTENT_ADDRESS,
sizeof(contentAddr),
&contentAddr,
&fetched);
if (FAILED(hr)) {
return wil::Return_Hr(HRESULT_FROM_WIN32(ERROR_AMSI_ATTRIBUTE),
L"Failed to read content address",
hr);
}
// 3) Read the AMSI session ID (ATTRIBUTE_SESSION == 4)
ULONGLONG sessionId = 0;
hr = stream->GetAttribute(
AMSI_ATTRIBUTE_SESSION,
sizeof(sessionId),
&sessionId,
&fetched);
if (FAILED(hr)) {
return wil::Return_Hr(HRESULT_FROM_WIN32(ERROR_AMSI_ATTRIBUTE),
L"Failed to read session ID",
hr);
}
// 4) Read the application name (ATTRIBUTE_APP_NAME == 0)
// First query length, then allocate CoTaskMem and fetch the string.
ULONG appNameLen = 0;
hr = stream->GetAttribute(
AMSI_ATTRIBUTE_APP_NAME,
/*cbSize*/ 1,
/*pBuffer*/ nullptr,
&appNameLen);
if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)) {
// Allocate space for the wide‑string (appNameLen bytes)
PWSTR appName = wil::make_cotaskmem_string(nullptr, appNameLen);
if (!appName) {
return E_OUTOFMEMORY;
}
hr = stream->GetAttribute(
AMSI_ATTRIBUTE_APP_NAME,
appNameLen,
appName,
&appNameLen);
if (FAILED(hr)) {
CoTaskMemFree(appName);
return wil::Return_Hr(HRESULT_FROM_WIN32(ERROR_AMSI_ATTRIBUTE),
L"Failed to read application name",
hr);
}
// 5) Similarly, read the content name (ATTRIBUTE_CONTENT_NAME == 1)
ULONG contentNameLen = 0;
hr = stream->GetAttribute(
AMSI_ATTRIBUTE_CONTENT_NAME,
/*cbSize*/ 1,
/*pBuffer*/ nullptr,
&contentNameLen);
if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)) {
PWSTR contentName = wil::make_cotaskmem_string(nullptr, contentNameLen);
if (!contentName) {
CoTaskMemFree(appName);
return E_OUTOFMEMORY;
}
hr = stream->GetAttribute(
AMSI_ATTRIBUTE_CONTENT_NAME,
contentNameLen,
contentName,
&contentNameLen);
if (FAILED(hr)) {
CoTaskMemFree(appName);
CoTaskMemFree(contentName);
return wil::Return_Hr(HRESULT_FROM_WIN32(ERROR_AMSI_ATTRIBUTE),
L"Failed to read content name",
hr);
}
// 6) Only emit the event if contentSize meets our thresholds
if (contentSize >= minEtwContentSize) {
// Truncate to maxEtwContentSize if necessary
SIZE_T truncatedSize = static_cast<SIZE_T>(std::min<ULONGLONG>(
contentSize, maxEtwContentSize));
// Allocate a buffer and copy the first truncatedSize bytes
BYTE* buffer = static_cast<BYTE*>(CoTaskMemAlloc(truncatedSize));
if (!buffer) {
CoTaskMemFree(appName);
CoTaskMemFree(contentName);
return E_OUTOFMEMORY;
}
memcpy_s(buffer, truncatedSize, contentAddr, truncatedSize);
// 7) Compute a SHA‑256 hash of the buffer (optional)
BYTE hashValue[32] = {};
ComputeSha256Hash(buffer, truncatedSize, hashValue);
// 8) Prepare ETW data descriptors
EVENT_DATA_DESCRIPTOR desc[10] = {};
EventDataDescCreate(&desc[0], &sessionId, sizeof(sessionId));
EventDataDescCreate(&desc[1], &scanStatus, sizeof(scanStatus));
EventDataDescCreate(&desc[2], &result, sizeof(result));
EventDataDescCreate(&desc[3], appName, appNameLen);
EventDataDescCreate(&desc[4], contentName, contentNameLen);
EventDataDescCreate(&desc[5], &minEtwContentSize, sizeof(minEtwContentSize));
EventDataDescCreate(&desc[6], &maxEtwContentSize, sizeof(maxEtwContentSize));
EventDataDescCreate(&desc[7], &contentSize, sizeof(contentSize));
EventDataDescCreate(&desc[8], &truncatedSize, sizeof(truncatedSize));
EventDataDescCreate(&desc[9], buffer, truncatedSize);
// (If you want to include the hash, add more descriptors here)
// 9) Fire the ETW event
ULONG etwHr = EventWrite(m_regHandle, &AMSI_SCANBUFFER, _countof(desc), desc);
// ignore etwHr failures for this pseudocode
// 10) Cleanup the buffer
CoTaskMemFree(buffer);
}
// 11) Cleanup string allocations
CoTaskMemFree(contentName);
}
CoTaskMemFree(appName);
}
return S_OK;
}
IAmsiStream
[
object,
uuid(3e47f2e5‑81d4‑4d3b‑897f‑545096770373),
pointer_default(unique)
]
interface IAmsiStream : IUnknown
{
HRESULT GetAttribute( // ask for metadata
[in] AMSI_ATTRIBUTE attribute,
[in] ULONG dataSize, // size of caller’s buffer
[out] unsigned char *data, // receives the value
[out] ULONG *retData); // bytes returned
HRESULT Read( // pull payload bytes
[in] ULONGLONG position, // offset from start
[in] ULONG size, // bytes requested
[out] unsigned char *buffer, // caller‑allocated
[out] ULONG *readSize);// bytes actually copied
};
static const GUID IID_IAmsiStream =
{ 0x3e47f2e5, 0x81d4, 0x4d3b, { 0x89, 0x7f, 0x54, 0x50, 0x96, 0x77, 0x03, 0x73 } };
class AmsiStream : public IAmsiStream {
private:
long refCount_;
PVOID buffer_; // pointer to payload data
ULONGLONG length_; // length of buffer
PVOID name_; // arbitrary name identifier
LPCWSTR contentName_; // content name (e.g., file path)
HAMSISESSION amsiSession_; // AMSI session handle
public:
// Constructor
AmsiStream(PVOID buffer,
ULONGLONG length,
PVOID name,
LPCWSTR contentName,
HAMSISESSION amsiSession)
: refCount_(1),
buffer_(buffer),
length_(length),
name_(name),
contentName_(contentName),
amsiSession_(amsiSession)
{}
// IUnknown methods
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override {
if (riid == IID_IUnknown || riid == IID_IAmsiStream) {
*ppvObject = static_cast<IAmsiStream*>(this);
AddRef();
return S_OK;
}
*ppvObject = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return InterlockedIncrement(&refCount_);
}
ULONG STDMETHODCALLTYPE Release() override {
ULONG count = InterlockedDecrement(&refCount_);
if (count == 0) {
delete this;
}
return count;
}
// IAmsiStream methods
HRESULT STDMETHODCALLTYPE GetAttribute(
AMSI_ATTRIBUTE attribute,
ULONG dataSize,
unsigned char *data,
ULONG *retData) override
{
if (!data || !retData) {
return E_POINTER;
}
switch (attribute) {
case AMSI_ATTRIBUTE_APP_NAME:
{
size_t nameLen = (wcslen(contentName_) + 1) * sizeof(WCHAR);
*retData = (ULONG)min((size_t)dataSize, nameLen);
memcpy(data, contentName_, *retData);
}
break;
case AMSI_ATTRIBUTE_CONTENT_NAME:
{
// Example: return contentName_
size_t nameLen = (wcslen(contentName_) + 1) * sizeof(WCHAR);
*retData = (ULONG)min((size_t)dataSize, nameLen);
memcpy(data, contentName_, *retData);
}
break;
case AMSI_ATTRIBUTE_CONTENT_SIZE:
{
// Return length as ULONGLONG
if (dataSize < sizeof(ULONGLONG)) {
return E_INVALIDARG;
}
*(ULONGLONG*)data = length_;
*retData = sizeof(ULONGLONG);
}
break;
default:
return E_NOTIMPL;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE Read(
ULONGLONG position,
ULONG size,
unsigned char *buffer,
ULONG *readSize) override
{
if (!buffer || !readSize) {
return E_POINTER;
}
if (position >= length_) {
*readSize = 0;
return S_OK;
}
// Calculate available bytes
ULONGLONG available = length_ - position;
ULONG toRead = (ULONG)min<ULONGLONG>(size, available);
memcpy(buffer, (BYTE*)buffer_ + position, toRead);
*readSize = toRead;
return S_OK;
}
};
IAntimalwareProvider
static const IID IID_IAntimalwareProvider =
{ 0xb2cabfe3, 0xfe04, 0x42b1,
{ 0xa5, 0xdf, 0x08, 0xd4, 0x83, 0xd4, 0xd1, 0x25 } };
[
object,
uuid(B2CABFE3‑FE04‑42B1‑A5DF‑08D483D4D125),
pointer_default(unique)
]
interface IAntimalwareProvider : IUnknown
{
// v‑table slot 3 (offset +0x18)
HRESULT Scan(
[in] IAmsiStream *stream,
[out] AMSI_RESULT *verdict);
// v‑table slot 4 (offset +0x20)
void CloseSession(
[in] ULONGLONG session); // same handle AmsiOpenSession returns
// Vendors are free to add more methods after slot 4,
// but amsi.dll never calls them.
};
Keep functions related to bypass
The call stack for AMSI provider initialization
The call stack for initialization:
AmsiInitialize
└─► DllGetClassObject
└─► CComCreator<class_ATL::CComAggObject_>::CreateInstance
└─► CAmsiAntimalware::FinalConstruct
└─► AmsiComCreateProviders
└─► AmsiComSecureLoadInProcServer
└─► DllGetClassObject -- 3rd party provider com dll function
CheckTrustLevel
└─► CheckTrustLevelImpl
└─► NtGetCachedSigningLevel
NtSetCachedSigningLevel
NtCompareSigningLevels
From the pdf file mentioned in the beginning of this article, besides other methods to bypass, the new ones focus on the point when providers are being loaded. The function “DllGetClassObject” of each 3rd party provider com dlls create the requested provider classes. “DllGetClassObject” fails can lead to the result of create providers failure. No providers then no detection.
There’s another point can do the same, which is “CheckTrustLevel” function under the same function “AmsiComSecureLoadInProcServer”. This function loads NT functions from ntdll.dll for trust level checking. However, if we patch functions from ntdll.dll to fail the provider creation, it might not succeed because EDRs may monitor ntdll.dll memory to detect memory patch.
AmsiComCreateProviders<IAntimalwareProvider>
void AmsiComCreateProviders<IAntimalwareProvider>(HKEY hRoot,
IAntimalwareProvider* providers[16],
GUID clsids[16],
UINT *pCount)
{
*pCount = 0;
CGuidEnum e;
HRESULT hr = e.StartEnum(hRoot, L"Software\\Microsoft\\AMSI\\Providers");
if (FAILED(hr)) { TRACE_ERROR(hr); return; }
const bool sample = (rand() % 100) == 0;
while (*pCount < 16)
{
GUID clsid;
hr = e.NextGuid(&clsid);
if (hr == ERROR_NO_MORE_ITEMS) break;
if (FAILED(hr)) { TRACE_WARN(hr); continue; }
IAntimalwareProvider* prov = nullptr;
ULONGLONG t0 = UtilGetCurrentTime();
hr = AmsiComSecureLoadInProcServer(&clsid,
&IID_IAntimalwareProvider,
&prov);
ULONGLONG dt = UtilGetCurrentTime() - t0;
if (SUCCEEDED(hr))
{
providers[*pCount] = prov; // refcount kept
clsids[*pCount] = clsid;
++*pCount;
TRACE_INFO_LOAD_OK(clsid, dt, sample);
}
else
{
TRACE_WARN_LOAD_FAIL(clsid, hr);
if (prov) prov->Release();
}
}
if (*pCount == 0)
TRACE_ERROR_NO_PROVIDER();
}
AmsiComSecureLoadInProcServer
void AmsiComSecureLoadInProcServer(
const GUID& clsid,
const IID& iid,
IAntimalwareProvider** provider)
{
// 1. Initialize output
*provider = nullptr;
// 2. Attempt to find a cached module under shared lock
AcquireSRWLockShared(&g_ModCacheLock);
HMODULE hModule = FindModuleInCache(clsid);
ReleaseSRWLockShared(&g_ModCacheLock);
if (hModule) {
// Cached: directly instantiate via DllGetClassObject
auto dllGetClassObject =
(PFN_DllGetClassObject)GetProcAddress(hModule, "DllGetClassObject");
if (!dllGetClassObject) return; // handle error
IClassFactory* factory = nullptr;
HRESULT hr = dllGetClassObject(clsid, IID_IClassFactory,
reinterpret_cast<void**>(&factory));
if (FAILED(hr)) return; // handle error
hr = factory->CreateInstance(nullptr, iid,
reinterpret_cast<void**>(provider));
factory->Release();
return;
}
// 3. Convert CLSID to string for registry lookup
LPOLESTR clsidStr = nullptr;
if (FAILED(StringFromCLSID(clsid, &clsidStr))) {
return; // handle error
}
// 4. Build registry key path: HKLM\Software\Classes\CLSID\<clsidStr>\InprocServer32
std::wstring regPath = L"Software\\Classes\\CLSID\\" +
std::wstring(clsidStr) + L"\\InprocServer32";
CoTaskMemFree(clsidStr);
// 5. Read DLL path from registry
WCHAR rawPath[MAX_PATH] = {0};
DWORD size = sizeof(rawPath);
if (ERROR_SUCCESS != RegGetValueW(
HKEY_LOCAL_MACHINE,
regPath.c_str(),
nullptr,
RRF_RT_REG_SZ,
nullptr,
rawPath,
&size))
{
return; // handle error
}
// 6. Expand environment variables and trim quotes/spaces
std::wstring dllPath = ExpandEnvironmentStringsW(rawPath);
TrimQuotesAndSpaces(dllPath);
// 7. Verify trust of the DLL
if (FAILED(CheckTrustLevel(dllPath.c_str(), clsid))) {
return; // handle error
}
// 8. Load library and cache under exclusive lock
hModule = LoadLibraryExW(dllPath.c_str(), nullptr, 0);
if (!hModule) {
return; // handle error
}
AcquireSRWLockExclusive(&g_ModCacheLock);
InsertModuleInCache(clsid, hModule);
ReleaseSRWLockExclusive(&g_ModCacheLock);
// 9. Instantiate provider via class factory
auto dllGetClassObject2 =
(PFN_DllGetClassObject)GetProcAddress(hModule, "DllGetClassObject");
if (!dllGetClassObject2) return; // handle error
IClassFactory* factory2 = nullptr;
HRESULT hr2 = dllGetClassObject2(clsid, IID_IClassFactory,
reinterpret_cast<void**>(&factory2));
if (FAILED(hr2)) return; // handle error
factory2->CreateInstance(nullptr, iid,
reinterpret_cast<void**>(provider));
factory2->Release();
}
CheckTrustLevel
HRESULT CheckTrustLevel(LPWSTR dllPath, const GUID *clsid)
{
// -----------------------------------------------------------------
// 1. Read AMSI\FeatureBits from HKLM
// -----------------------------------------------------------------
DWORD featureBits = 0;
DWORD cbData = sizeof(featureBits);
DWORD type = 0;
int policy = 1; // default if anything goes wrong
LSTATUS sts = RegGetValueW(
HKEY_LOCAL_MACHINE,
L"Software\\Microsoft\\AMSI",
L"FeatureBits",
RRF_RT_REG_DWORD | RRF_SUBKEY_WOW6432KEY, // 0x10000010
&type,
&featureBits,
&cbData);
if (sts == ERROR_SUCCESS)
{
// Accept only 1, 2 or 4. Value 3 or anything outside that range ⇒ revert to 1
if ((featureBits < 1 || featureBits > 4) || featureBits == 3)
{
// WPP_TRACE: out‑of‑range registry value
policy = 1;
}
else
{
policy = static_cast<int>(featureBits);
}
}
else
{
// WPP_TRACE: registry read failed (sts holds the Win32 error)
policy = 1;
}
// WPP_TRACE: selected policy (1,2,4)
// -----------------------------------------------------------------
// 2. Derive the flag for the implementation call
// -----------------------------------------------------------------
BYTE flag = 4; // normal
if (policy == 4) flag = 7; // “highest” trust level becomes 7
BYTE implResult = 0; // will be written by the callee
// -----------------------------------------------------------------
// 3. Invoke the real checker
// -----------------------------------------------------------------
HRESULT hr = CheckTrustLevelImpl(dllPath, flag, &implResult);
// WPP_TRACE: dllPath, flag, implResult
// -----------------------------------------------------------------
// 4. Occasionally emit telemetry
// -----------------------------------------------------------------
if ((rand() % 1000) == 0 &&
UtilGetCurrentTime() > DAT_180018050 &&
(_DAT_180018060 & 0x400000000000ull) != 0 &&
(DAT_180018068 & 0x400000000000ull) == DAT_180018068)
{
// Package up a telemetry payload (sizes, pointers etc. omitted here)
// _tlgWriteTransfer_EventWriteTransfer(...);
}
// -----------------------------------------------------------------
// 5. Done
// -----------------------------------------------------------------
return hr;
}
CheckTrustLevelImpl
HRESULT CheckTrustLevelImpl(LPCWSTR dllPath, DWORD flag, DWORD *trustLevelOut)
{
// 1. Lazy‑initialise the function‑pointer table
if (!g_CodeIntegrityAPIs.initialized)
{
AcquireSRWLockExclusive(&g_CILock);
if (!g_CodeIntegrityAPIs.initialized)
{
if (!InitCodeIntegrity(&g_CodeIntegrityAPIs)) // loads ntdll & resolves Nt* APIs
{
// WPP trace: InitCodeIntegrity failed
ReleaseSRWLockExclusive(&g_CILock);
return 0x8008136F; // ‑0x7FF8EC61
}
}
ReleaseSRWLockExclusive(&g_CILock);
}
// 2. Open the target file read‑only
HANDLE hFile = CreateFileW(dllPath,
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (hFile == INVALID_HANDLE_VALUE)
return HRESULT_FROM_WIN32(GetLastError()); // plus WPP logging
// 3. Try to read its cached signing level
SIGNING_LEVEL level = 0;
NTSTATUS status = g_CodeIntegrityAPIs.NtGetCachedSigningLevel(
hFile, nullptr, &level, nullptr, nullptr, nullptr);
// 4. If there is no cache entry, ask the kernel to compute one
if (!NT_SUCCESS(status))
{
status = g_CodeIntegrityAPIs.NtSetCachedSigningLevel(
SE_IMAGE_TYPE, // 4
flag, // 4 or 7 from caller
&hFile, // array of 1 handle
1,
hFile);
if (!NT_SUCCESS(status))
{
CloseHandle(hFile);
return HRESULT_FROM_NT(status) | 0x10000000; // mark as CI error
}
// Retry the “Get”
status = g_CodeIntegrityAPIs.NtGetCachedSigningLevel(
hFile, nullptr, &level, nullptr, nullptr, nullptr);
if (!NT_SUCCESS(status))
{
CloseHandle(hFile);
return HRESULT_FROM_NT(status) | 0x10000000;
}
}
// 5. Success: give the caller the level byte
*trustLevelOut = level;
// 6. Compare the obtained level against the policy flag
status = g_CodeIntegrityAPIs.NtCompareSigningLevels(level, (SIGNING_LEVEL)flag);
HRESULT hr = NT_SUCCESS(status) ? S_OK
: (HRESULT_FROM_NT(status) | 0x10000000);
CloseHandle(hFile);
return hr;
}