
TLDR
- A Message to the uninitiated reader, this post is vey COM heavy, if you dont know what COM is, go read Inside COM by Dale Rogerson and come back!
- Attacking AMSI by targeting its COM architecture. This includes corrupting vftable and IID/GUIDS.
- Targeting the RPC comm channel between Defender and AMSI
AMSI The Component
How to identify a component?
The components that implement COM interfaces need to be registered as Computer\HKEY_CLASSES_ROOT\CLSID\{<GUID>} in the Windows Registry. This is a convenient way to locate the dll/exe components, so that apis like CoCreateinstance() can go and fetch components by using GUID values from library, and instantiate the interface.
In this section we are going to discuss about COM aspects of AMSI, so just to begin our investigation into its internals, lets check the registry to confirm that AMSI is indeed a component. Manually checking for the entry in the CLSID key will be a tedious (and senseless) job, I have a loosely written Python code that does the job!
As shown in the image below, we can see the CLSID and file location of component. This means the AMSI is implemented using COM.

Here I am sharing code for reg.py
import winreg
import itertools
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-m", "--mod",nargs="+", type=str)
args = parser.parse_args()
ar = args.mod
aReg = winreg.ConnectRegistry(None, winreg.HKEY_CLASSES_ROOT)
aKey = winreg.OpenKey(aReg, r'CLSID' )
for i in itertools.count():
try:
aValue_name = winreg.EnumKey(aKey, i)
oKey = winreg.OpenKey(aKey, aValue_name )
infokey = winreg.QueryInfoKey(oKey)
for x in range(infokey[0]):
subkey = winreg.EnumKey(oKey, 0)
kn = "CLSID" +"\\" +aValue_name+"\\"+subkey
sKey = winreg.OpenKey(aReg, kn)
if subkey == "InprocServer32":
sValue = winreg.QueryValueEx(sKey, None)
#print(sValue[0])
if ar[0] in sValue[0]:
print(f"Module : {sValue[0]}")
print(f"CLSID : {aValue_name}")
except Exception as e:
if "WinError 259" in str(e):
break
continue
The image below shows the exports of amsi.dll. The core AMSI functionalities are accessible to the client via functions with name that starts with Amsi*. The highlighted functions are for COM operations.

The only thing which is available to public in plain text is ofcourse the header file – amsi.h. This is enough to get us going in terms of understanding the code structure and identifying interfaces and IID/GUID values. The class CAntimalware implements functions exported by amsi.dll. This class has a unique identifier fdb00e52-a214-4aa1-8fba-4357bb0072ec, we will see this id being mentioned very often in the following sections. This id is same as the id we saw in the reg.py output above. This class is not a component, it simply acts as wrapper for invoking AMSI COM methods via instantiated components, we will see this later.

No lets check all the AMSI interfaces, among various interfaces, one shown below is the most important one – IAntimalware2. This interface inherits IAntimalware. Pay attention to the interface id given to this interface, we will discuss about this interface in detail in the following sections.

Interface declaration of IAntimalware is shown below, there lies the interesting method – Scan.

Targeting the vfTable
The methods implemented by a component are kept in the memory in a special table called virtual table or virtual function table. Each entry in a vftable will be a pointer to a method implementation. As an attacker this is an interesting target, what if the pointer entry in the table gets overwritten with some attacker provided code?
Writing AMSI scanner
before we take a deep dive, lets write a very basic AMSI client to perform our testing. The client code is shown below. In our test, we are feeding Seatbelt.exe to the AMSI.
#include <iostream>
#include <windows.h>
#include <amsi.h>
/*
HRESULT AmsiInitialize(
[in] LPCWSTR appName,
[out] HAMSICONTEXT *amsiContext
);
HRESULT AmsiScanBuffer(
[in] HAMSICONTEXT amsiContext,
[in] PVOID buffer,
[in] ULONG length,
[in] LPCWSTR contentName,
[in, optional] HAMSISESSION amsiSession,
[out] AMSI_RESULT *result
);
*/
typedef HRESULT (WINAPI* AmsiInitializeT)(LPCWSTR, HAMSICONTEXT*);
typedef HRESULT(WINAPI* AmsiScanBufferT)(HAMSICONTEXT, PVOID, ULONG, LPCWSTR, HAMSISESSION, AMSI_RESULT*);
//<Seatbelt, Version = 1.0.0.0, Culture = neutral, PublickeyToken = null>
const DWORD assembly_size = 610304;
const BYTE assembly[] = { 0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* truncated*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
void main()
{
HAMSICONTEXT AmsiCtx;
AMSI_RESULT amsiResult;
HMODULE hAmsi = LoadLibrary(L"amsi");
AmsiInitializeT AmsiInitialize = (AmsiInitializeT)GetProcAddress(hAmsi, "AmsiInitialize");
AmsiScanBufferT _AmsiScanBuffer = (AmsiScanBufferT)GetProcAddress(hAmsi, "AmsiScanBuffer");
HRESULT hr = AmsiInitialize(L"Test", &AmsiCtx);
if(FAILED(hr))
{
std::cout << "Failed";
}
hr = _AmsiScanBuffer(AmsiCtx, (PVOID)assembly, assembly_size, L"MAL_BUFF", nullptr, &amsiResult);
if (FAILED(hr))
{
std::cout << "Failed";
}
}
AMSI Initialization & HAMSICONTEXT
AMSI initialization is an important step for the antimalware scanning interface as it will help in creating the instance of component CAmsiAntimalware. The scanning capability is implemented by this component. We will go into greater depth in the following sections. The function prototype of AmsiInitialize is shown below. The AMSI client must call this api before making any other calls like AmsiScanBuffer. This is due to the fact that AMSI functions expect HAMSICONTEXT to be provided by the client.

Now going back to our small AMSI client, break the execution following the call made to AmsiInitialize(), and check the pointer the variable AmsiCtx. Following a successful AmsiInitialize() call, we will receive a pointer to memory blob of type HAMSICONTEXT. Interestingly this type is not documented. As shown in the image below, amsi context is located at 0x0200F0B73580.

Its mentioned in the msdn document for AmsiInitialize api that HAMSICONTEXT, as the name suggests, that it is simply a handle. We will get to know that this is not the case. It is not just a simple handle, but an instance of CAmsiAntimalware component. Why this information is so important? Lets find out! 🙂

As discussed before, contents of the HAMSICONTEXT is shown below. Since we dont know about the structure [or size] of the HAMSICONTEXT, it is little bit difficult to make sense of the contents. But we will get an idea when we start calling few amsi functions. Lets play with amsi.dll!AmsiScanBuffer() and find out the suspense behind this mysterious HAMSICONTEXT type!

AmsiScanBuffer
In our amsi scanner client, Seatbelt assembly is being passed to AMSI via AmsiScanBuffer() api. The amsi context created by AmsiInitialize() is passed to the function as the first argument. IDA decompiled code of AmsiScanbBuffer is shown below.

The CAmsiBufferStream is the component that implements IAmsiStream interface. The call to CAmsiBufferStream ::CAmsiBufferStream() instantiates the corresponding object with data pased to AmsiScanBuffer(). The tail call looks very interesting as we can see some memory referenced using amsiContext, this is the same context of type HAMSICONTEXT created by AmsiInitialize(). The IDA disassembly of the tail call is shown below. Now code is more clear, mov rcx,[rbx + 0x10] retrieves a pointer from amsiContext at offset 0x10 and stores it in rcx. This pointer is dereferenced again to retrieve another pointer, this pointer is dereferenced again using an offset of 0x18 and stores it in rax. Finally the tail call is invoked by calling rax.

To see the pointer dereferencing using amsiContext and tail call in action, we need to run the code as shown below. The below code is same as the afforementioned IDA disassembly.

The vftable
When we step over mov rcx, [rbc + 0x10], the value stored that gets stored in the rcx is shwon below. With the help of symbols we can see amsiContext + 0x10 points to vfTable of CAmsiAntiMalware.

Lets revisit the memory layout of AmsiCtx as shown below, based on our analysis, CAmsiAntiMalware instance is located at offset 0x10 (16). This means we can use HAMSICONTEXT “handle” created by AmsiInitialize() can be used to access vfTable of CAmsiAntiMalware .

We can see it clearly with the help of symbols, the address 0x200F0CA1720 points to CAmsiAntiMalware::vfTable.

Now going back to AmsiScanBuffer tail call, the code move rax, [rax + 0x18] store the address of CAmsiAntiMalware::Scan() in rax.

The CAmsiAntiMalware:;vfTable is shown below.

When stepping into tail call we can see a jmp rax, the rax points to CAmsiAntiMalware::Scan(). So basically amsi.dll!AmsiScanBuffer invokes COM method CAmsiAntiMalware::Scan().

The data stored in the rax before the tail call is shown below.

The vfTable is stored in the read only section.

Now we have seen the vftable and we know the location of CAmsiAntiMalware::Scan() and pointer entry in the table, why not just make the pointer entry in the table point to some ret gadget present in amsi.dll? 🙂 Calling VirtualProtect on amsi.dll to grant write access permission IS bit RISKY.
Targeting AMSI Initialization
Corrupting CLSID
The AmsiInitialize() code encapsulates object creation and various checks, CLSID which is used to identify the AMSI component is stored in the object map (ATL). Below code retrieves CLSID_AntiMalware that has a value of fdb00e52-a214-4aa1-8fba-4357bb0072ec. As highlighted in the image below, code validates the CLID starting with the first four bytes – FD B0 0E 52.

Lets take a close look at the ATL object map __pobjmap_CAmsiAntimalware in the memory. The __pobjmap_CAmsiAntimalware map hold the pointer to map entry.

The map entry is shown below, this is another pointer to the actual data located at 0x7FFD16ED07A0.

The CLSID is stored at 0x7FFD16ED07A0.

The map entry pointer is located in the .data section which has both read and write protection enabled. This means we dont have to call VirtualProtect on the memory page to make it writable before we corrupt the pointer.

The easy way to find this map entry pointer is to search for 0x7FFD16ED07A0 in the reversed byte order in the .data section. The memory address returned by the search will be the map entry pointer. Now we can simply overwrite this pointer with address that points to 4 bytes past the start address of clsid.
This will fail the initialization and AmsiInitialize() will return error code shown below.

Corrupting IID
An important step in the initialization is the creation of IAntimalware2 interface object as shown below. The guid of the IAntimalware2 is passed to r8 and invokes ATL::CComClassFactory::CreateInstance().

Tampering the GUID pointer will result in the following error. Unfortunately, there is no pointer in the writable section to tamper this guid, you will have to rely on VirtualProtect.

Defender RPC Call
This not directly related to amsi, but curiosity poked me to go and find out how the communication would take place between the AV and AMSI. The AMSI will load additional dlls like mpoav.dll and mpclient.dll which are both defender related files and call various internal apis as shown in the image below. The interesting thing about below stack trace is that we can see a function inside mpclient.dll (defender client) making RPC call to defender service.

The question that comes to my mind is that what would happen if we mess with RPC?
This has been already answered by this post Andrea Bocchetti, he calls this technique – Ghosting AMSI.
Leave a Reply