Using Windows Antimalware Scan Interface in .NET

  • Gérald Barré

Windows comes with an antivirus, Windows Defender. You can replace it with any other third party antivirus. These antiviruses are good for detecting malicious files on the disk. However, it's common for an application to download a file in memory and use it directly. It can happen in PowerShell, in .NET (dynamically loaded assembly), in a Node.js application, etc. Antiviruses were not able to analyze those files and detect malicious scripts. That's what Antimalware Scan Interface (AMSI) is for, provide a way for an application to ask the antivirus to analyze a script/stream when needed. AMSI is not tied to Windows Defender. Any antivirus provider can implement the AMSI interface, so it can be used by any application that uses AMSI. Here's the architecture of AMSI (source)

AMSI architecture

AMSI provides mainly 2 methods: one for scanning a string, and another for scanning a binary blob. There is also a notion of session to correlate multiple scan requests. Before calling the scan function, you need to create a new context and use its handle for each scanning request. As there is a native handle, you should use a SafeHandle as mentioned in my previous post. Here's the P/Invoke code for the AMSI functions:

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:

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:

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 a reference to the NuGet package Meziantou.Framework.Win32.Amsi (NuGet, GitHub) in your project and directly use the code above.

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

Follow me:
Enjoy this blog?Buy Me A Coffee