Using Windows Antimalware Scan Interface in .NET

 
 
  • Gérald Barré

Windows ships with Windows Defender as its built-in antivirus, but you can replace it with any third-party antivirus. These antivirus solutions are effective at detecting malicious files on disk. However, it is common for applications to download content directly into memory and use it without ever writing it to disk. This can happen in PowerShell, in .NET (via dynamically loaded assemblies), in a Node.js application, and more. Traditional antivirus tools were not able to analyze such in-memory content or detect malicious scripts. That is what the Antimalware Scan Interface (AMSI) solves: it gives applications a standard way to ask the antivirus to scan a script or data stream on demand. AMSI is not tied to Windows Defender. Any antivirus vendor can implement the AMSI interface, and any application can take advantage of it. Here is the architecture of AMSI:

AMSI architecture (source)

AMSI exposes two main methods: one for scanning a string, and another for scanning a binary blob. It also supports sessions to correlate multiple scan requests. Before calling a scan function, you must create a context and pass its handle with each request. Since this involves a native handle, you should use a SafeHandle as described in my previous post. Here is the P/Invoke code for the AMSI functions:

C#
internal static class Amsi
{
    // Restrict loading of `amsi.dll` from system32 folder to avoid loading
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport("Amsi.dll", EntryPoint = "AmsiInitialize", CallingConvention = CallingConvention.StdCall)]
    internal static extern int AmsiInitialize([MarshalAs(UnmanagedType.LPWStr)]string appName, out AmsiContextSafeHandle amsiContext);

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport("Amsi.dll", EntryPoint = "AmsiUninitialize", CallingConvention = CallingConvention.StdCall)]
    internal static extern void AmsiUninitialize(IntPtr amsiContext);

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport("Amsi.dll", EntryPoint = "AmsiOpenSession", CallingConvention = CallingConvention.StdCall)]
    internal static extern int AmsiOpenSession(AmsiContextSafeHandle amsiContext, out AmsiSessionSafeHandle session);

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport("Amsi.dll", EntryPoint = "AmsiCloseSession", CallingConvention = CallingConvention.StdCall)]
    internal static extern void AmsiCloseSession(AmsiContextSafeHandle amsiContext, IntPtr session);

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport("Amsi.dll", EntryPoint = "AmsiScanString", CallingConvention = CallingConvention.StdCall)]
    internal static extern int AmsiScanString(AmsiContextSafeHandle amsiContext, [In, MarshalAs(UnmanagedType.LPWStr)]string payload, [In, MarshalAs(UnmanagedType.LPWStr)]string contentName, AmsiSessionSafeHandle session, out AmsiResult result);

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport("Amsi.dll", EntryPoint = "AmsiScanBuffer", CallingConvention = CallingConvention.StdCall)]
    internal static extern int AmsiScanBuffer(AmsiContextSafeHandle amsiContext, byte[] buffer, uint length, string contentName, AmsiSessionSafeHandle session, out AmsiResult result);

    internal static bool AmsiResultIsMalware(AmsiResult result) => result >= AmsiResult.AMSI_RESULT_DETECTED;
}

internal enum AmsiResult
{
    AMSI_RESULT_CLEAN = 0,
    AMSI_RESULT_NOT_DETECTED = 1,
    AMSI_RESULT_BLOCKED_BY_ADMIN_START = 16384,
    AMSI_RESULT_BLOCKED_BY_ADMIN_END = 20479,
    AMSI_RESULT_DETECTED = 32768,
}

internal class AmsiContextSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    public AmsiContextSafeHandle()
        : base(ownsHandle: true)
    {
    }

    protected override bool ReleaseHandle()
    {
        Amsi.AmsiUninitialize(handle);
        return true;
    }
}

internal class AmsiSessionSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    internal AmsiContextSafeHandle Context { get; set; }

    public AmsiSessionSafeHandle()
        : base(ownsHandle: true)
    {
    }

    public override bool IsInvalid => Context.IsInvalid || base.IsInvalid;

    protected override bool ReleaseHandle()
    {
        Amsi.AmsiCloseSession(Context, handle);
        return true;
    }
}

You can then create a nicer wrapper:

C#
public sealed class AmsiContext : IDisposable
{
    private readonly AmsiContextSafeHandle _context;

    private AmsiContext(AmsiContextSafeHandle context)
    {
        _context = context;
    }

    public static AmsiContext Create(string applicationName)
    {
        int result = Amsi.AmsiInitialize(applicationName, out var context);
        if (result != 0)
            throw new Win32Exception(result);

        return new AmsiContext(context);
    }

    public AmsiSession CreateSession()
    {
        var result = Amsi.AmsiOpenSession(_context, out var session);
        session.Context = _context;
        if (result != 0)
            throw new Win32Exception(result);

        return new AmsiSession(_context, session);
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

public sealed class AmsiSession : IDisposable
{
    private readonly AmsiContextSafeHandle _context;
    private readonly AmsiSessionSafeHandle _session;

    internal AmsiSession(AmsiContextSafeHandle context, AmsiSessionSafeHandle session)
    {
        _context = context;
        _session = session;
    }

    public bool IsMalware(string payload, string contentName)
    {
        var returnValue = Amsi.AmsiScanString(_context, payload, contentName, _session, out var result);
        if (returnValue != 0)
            throw new Win32Exception(returnValue);

        return Amsi.AmsiResultIsMalware(result);
    }

    public bool IsMalware(byte[] payload, string contentName)
    {
        var returnValue = Amsi.AmsiScanBuffer(_context, payload, (uint)payload.Length, contentName, _session, out var result);
        if (returnValue != 0)
            throw new Win32Exception(returnValue);

        return Amsi.AmsiResultIsMalware(result);
    }

    public void Dispose()
    {
        _session.Dispose();
    }
}

Finally, you can use the code to test a script file:

C#
using (var application = AmsiContext.Create("MyApplication"))
using (var session = application.CreateSession())
{
    // https://en.wikipedia.org/wiki/EICAR_test_file
    Assert.IsTrue(session.IsMalware(@"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*", "EICAR"));
    Assert.IsFalse(session.IsMalware("0000", "EICAR"));
}

If you prefer, you can add the NuGet package Meziantou.Framework.Win32.Amsi (NuGet, GitHub) to your project and use the same API directly.

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?