Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Bring Antimalware Scanning into your Application

4.88/5 (12 votes)
7 Jun 2022CPOL4 min read 14.6K   468  
Incorporate Antimalware scanning inside your application
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.

C#
uint AmsiInitialize(String appName, out IntPtr amsiContext);
C++
HRESULT AmsiInitialize(LPCWSTR appName, HAMSICONTEXT *amsiContext);

After use, the context must be uninitialized with a call to AmsiUninitialize().

C#
void AmsiUninitialize(IntPtr amsiContext);
C++
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().

C#
uint AmsiOpenSession(IntPtr amsiContext, out IntPtr amsiSession);
C++
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().

C#
void AmsiCloseSession(IntPtr amsiContext, IntPtr amsiSession);
C++
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.

C#
uint AmsiScanString(IntPtr amsiContext, String text, String contentName,
            IntPtr amsiSession, out uint result);
C++
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.

C#
uint AmsiScanBuffer(IntPtr amsiContext, IntPtr buffer, uint length, String contentName,
            IntPtr amsiSession, out uint result);
C++
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.

C#
uint AmsiNotifyOperation(IntPtr amsiContext, IntPtr buffer, 
                         uint length, String contentName, out uint result);
C++
HRESULT AmsiNotifyOperation(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length,
                            LPCWSTR contentName, AMSI_RESULT  *result);

IsMalware is defined as below:

C#
bool IsMalware(uint result)
{
    return (result >= 32768);
}
C++
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.

C#
// Example of using the raw 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);
C++
// Example of using the raw AMSI functions
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:

C#
AmsiHelper(string appName);  // Constructor
~AmsiHelper(string appName); // Finalizer

bool IsValidAmsi();          // Check if managed to get a ASMI context and session

bool ScanString(string text, string contentName, out bool isMalware); // Scan text
bool ScanBuffer(IntPtr buffer, uint length, 
                string contentName, out bool isMalware);              // Scan buffer
bool NotifyOperation(IntPtr buffer, uint length, string contentName, 
                     out bool isMalware); // Notify anti-malware of this buffer
C++
AmsiHelper();  // Constructor
~AmsiHelper(); // Destructor

bool IsValidAmsi() const; // Check if managed to get a ASMI context and session

bool ScanString(LPCWSTR text, LPCWSTR contentName, bool* isMalware); // Scan text
bool ScanBuffer(PVOID buffer, ULONG length, 
                LPCWSTR contentName, bool* isMalware); // Scan buffer
bool NotifyOperation(PVOID buffer, ULONG length, LPCWSTR contentName,
                     bool* isMalware); // Notify anti-malware of this buffer

Wrapper Class Example

Below is an example of using the AmsiHelper.

C#
// Example of using the AMSI wrapper class: 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");
    }
}
C++
// Example of using the AMSI wrapper class: AmsiHelper
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)