Incorporate user-installed Antimalware to scan user input in your .NET Framework 4.8, .NET 6 and C++ application.
Table of Contents
The example code is hosted at Github.
Introduction
For those of you who came in and half-expecting a long and complicated article, things couldn't be simpler through Antimalware Scan Interface (AMSI) available on Windows 10 since 2015 to allow the anti-malware scanning on user-supplied content. AMSI is agnostic of the antimalware vendor. The scanning is done through the anti-malware installed on the user's computer. Anti-malware vendor and version are not provided by AMSI even though this information is vital for contacting the anti-malware vendor to report false positive detection. Another missing information is the name of malware detected and the type of vulnerability to aid the further investigation. AMSI is actively used in Microsoft products such as MS Office.
This article is divided into two main sections: the raw API section and the wrapper class section. Another way to call AMSI is through its COM API but the complexity of COM interop on .NET is best avoided since the raw API provides the same functionality as the COM API. Only raw Win32 AMSI APIs are covered here.
Raw Win32 Functions
AMSI Context
Note: The function definitions listed in this section have been converted through P/Invoke. For instance, the HRESULT
return type has been changed to uint
and the handle types for the context and session are changed to IntPtr
. To use AMSI, a context must be first created with AmsiInitialize()
. Supply your application name in appName
parameter. The function returns a HRESULT
where S_OK
(0) means success. An AMSI context may fail to initialize when Microsoft Defender is disabled and no other anti-malware product is present on the user's computer.
uint AmsiInitialize(String appName, out IntPtr amsiContext);
HRESULT AmsiInitialize(LPCWSTR appName, HAMSICONTEXT *amsiContext);
After use, the context must be uninitialized with a call to AmsiUninitialize()
.
void AmsiUninitialize(IntPtr amsiContext);
void AmsiUninitialize(HAMSICONTEXT amsiContext);
AMSI Session
To do scanning, if you do not want to group the scannings under a session or when you only have one content to scan, a session is not required. To open a session, call AmsiOpenSession()
with the context created from AmsiInitialize().
uint AmsiOpenSession(IntPtr amsiContext, out IntPtr amsiSession);
HRESULT AmsiOpenSession(HAMSICONTEXT amsiContext, HAMSISESSION *amsiSession);
After the scanning is done, close the session with a call to AmsiCloseSession()
with the same context supplied to AmsiOpenSession()
.
void AmsiCloseSession(IntPtr amsiContext, IntPtr amsiSession);
void AmsiCloseSession(HAMSICONTEXT amsiContext, HAMSISESSION amsiSession);
AMSI Scan Functions
There are two types of scanning: string and binary buffer. Use AmsiScanString()
to scan text and the AmsiScanBuffer()
to scan buffer. contentName
is either the filename or the URL where this content is from. amsiSession
can be IntPtr.Zero
for not providing a session. result
is the output of these functions. IsMalware()
must be called on result
to check if it indicates malware. Other parameters are self-explanatory. The function returns a HRESULT
where S_OK
(0) means success. AmsiScanString()
is called on the script written in Powershell, JavaScript, VBScript or Office VBA macros.
uint AmsiScanString(IntPtr amsiContext, String text, String contentName,
IntPtr amsiSession, out uint result);
HRESULT AmsiScanString(HAMSICONTEXT amsiContext, LPCWSTR string,
LPCWSTR contentName, HAMSISESSION amsiSession,
AMSI_RESULT *result);
AmsiScanBuffer()
is for scanning a binary buffer that could contain a malicious executable program. An example scenario is your software implements a plugin system where any third party can supply their own plugins to extend your software, it is advisable to call AmsiScanBuffer()
to scan the DLLs prior to loading and running them.
uint AmsiScanBuffer(IntPtr amsiContext, IntPtr buffer, uint length, String contentName,
IntPtr amsiSession, out uint result);
HRESULT AmsiScanBuffer(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length,
LPCWSTR contentName, HAMSISESSION amsiSession,
AMSI_RESULT *result);
AmsiNotifyOperation()
is to notify the anti-malware in the case that malware is found. No session is required to call this function. It is best not to assume scanning is done in AmsiNotifyOperation
because this function may not be implemented by third-party anti-malware vendors, so do not rely on the result
.
uint AmsiNotifyOperation(IntPtr amsiContext, IntPtr buffer,
uint length, String contentName, out uint result);
HRESULT AmsiNotifyOperation(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length,
LPCWSTR contentName, AMSI_RESULT *result);
IsMalware
is defined as below:
bool IsMalware(uint result)
{
return (result >= 32768);
}
bool IsMalware(AMSI_RESULT result)
{
return (result >= AMSI_RESULT_DETECTED);
}
Raw Win32 Functions Example
Below is an example of using the raw Win32 AMSI functions.
IntPtr amsiContext = IntPtr.Zero;
uint hr = AmsiMethods.AmsiInitialize("ScanContentCSharp", out amsiContext);
if (hr != 0)
{
Console.WriteLine("AmsiInitialize failed!");
return;
}
IntPtr amsiSession = IntPtr.Zero;
hr = AmsiMethods.AmsiOpenSession(amsiContext, out amsiSession);
if (hr != 0)
{
Console.WriteLine("AmsiOpenSession failed!");
AmsiMethods.AmsiUninitialize(amsiContext);
return;
}
uint result = 0;
hr = AmsiMethods.AmsiScanString
(amsiContext, "Hello World!", "Testing.txt", amsiSession, out result);
if (hr != 0)
{
Console.WriteLine("AmsiScanString failed!");
}
else
{
if (AmsiMethods.IsMalware(result))
Console.WriteLine("Malware detected");
else
Console.WriteLine("No malware detected");
}
AmsiMethods.AmsiCloseSession(amsiContext, amsiSession);
AmsiMethods.AmsiUninitialize(amsiContext);
HAMSICONTEXT amsiContext;
HRESULT hr = AmsiInitialize(L"ScanContentCpp", &amsiContext);
if (FAILED(hr))
{
printf("AmsiInitialize failed!");
return 1;
}
HAMSISESSION amsiSession;
hr = AmsiOpenSession(amsiContext, &amsiSession);
if (FAILED(hr))
{
printf("AmsiOpenSession failed!");
AmsiUninitialize(amsiContext);
return 1;
}
AMSI_RESULT result;
hr = AmsiScanString(amsiContext, L"Hello World!", L"Testing.txt", amsiSession, &result);
if (FAILED(hr))
printf("AmsiScanString failed!");
else
{
if (result >= AMSI_RESULT_DETECTED)
printf("Malware detected");
else
printf("No malware detected");
}
AmsiCloseSession(amsiContext, amsiSession);
AmsiUninitialize(amsiContext);
Wrapper Class: AmsiHelper
A wrapper called AmsiHelper
is written to simplify the AMSI usage. A context and session are created in the constructor and destroyed in the finalizer. Its public
methods are listed below:
AmsiHelper(string appName);
~AmsiHelper(string appName);
bool IsValidAmsi();
bool ScanString(string text, string contentName, out bool isMalware);
bool ScanBuffer(IntPtr buffer, uint length,
string contentName, out bool isMalware);
bool NotifyOperation(IntPtr buffer, uint length, string contentName,
out bool isMalware);
AmsiHelper(); ~AmsiHelper();
bool IsValidAmsi() const;
bool ScanString(LPCWSTR text, LPCWSTR contentName, bool* isMalware); bool ScanBuffer(PVOID buffer, ULONG length,
LPCWSTR contentName, bool* isMalware); bool NotifyOperation(PVOID buffer, ULONG length, LPCWSTR contentName,
bool* isMalware);
Wrapper Class Example
Below is an example of using the AmsiHelper
.
using (AmsiHelper amsi = new AmsiHelper("ScanContentCSharp"))
{
if (!amsi.IsValidAmsi())
Console.WriteLine("AmsiOpenSession failed!");
bool isMalware = false;
if (!amsi.ScanString("Hello World!", "Testing.txt", out isMalware))
Console.WriteLine("AmsiScanString failed!");
else
{
if (isMalware)
Console.WriteLine("Malware detected");
else
Console.WriteLine("No malware detected");
}
}
AmsiHelper amsi(L"ScanContentCpp");
if (!amsi.IsValidAmsi())
printf("AmsiOpenSession failed!");
bool isMalware = false;
if (!amsi.ScanString(L"Hello World!", L"Testing.txt", &isMalware))
printf("AmsiScanString failed!");
else
{
if (isMalware)
printf("Malware detected");
else
printf("No malware detected");
}
Though the AmsiHelper
simplifies the usage and resource management, the raw APIs that allow the flexibility of not using the session. .NET Framework, .NET 6 and C++ sample codes are made available for download.
History
- 8th June, 2022: Added C++ source code examples to the article via C++ tab
- 3rd May, 2022: First release
Other Articles in the Bring Your... Series