Like I said in my previous post, I already coded something cool in my framework, and here it is: .NET code to call the Windows antivirus API. The best use case scenario I can think of is when retrieving and then redistributing file content from an untrusted data source, especially a web upload form. In cases like this, content might slip through the realtime protection of most antivirus products, and an API like the one I created is the only solution.
The API
I learned of the API’s existence when I saw Firefox scan my downloads. Luckily it’s open source, and here is the source code for download scanning. Then it was easy to find official documentation on MSDN, too. This API is called IAttachmentExecute. Apparently it’s not an antivirus API per se, it’s rather something that once was intended to be called by e-mail clients when they save an attachment. But luckily most if not all antivirus products subscribe to this interface so in the absence of a real antivirus API, this is the next best thing to use.
COM interop
If I were into C/C++, I’d skip this part, but in .NET, there were a few challenges, even though COM interop is supposed to be as easy as a pie and I’ve used it before. I’m not a COM expert, so my choice of words might be inaccurate in the following paragraphs, but I’ll tell you my findings anyway.
First of all, this API doesn’t come with a type library (it contains a compiled interface description), and Visual Studio offers you to add a reference to COM interfaces with type libraries only. So much for being easy. There is an ShObjIdl.idl file in the Windows SDK, I converted it to a type library with midl.exe, but tlbimp.exe couldn’t import it. Then I stripped down the idl file to only contain what I needed, now tlbimp.exe worked, but I still couldn’t add a reference to the tlb directly because of an unspecified error, even though the add reference dialog contains a *.tlb filter. I didn’t want to include the manually tlbimp‘d dll in my project, so I coded the interface myself, here’s how it looks like:
[Guid("73DB1241-1E85-4581-8E4F-A81E1D0F8C57"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IAttachmentExecute { void SetClientTitle(string pszTitle); // and so on } |
But you can’t instantiate it, there’s no CLSID entry in the registry with that GUID, and here’s the next piece of COM wisdom: there are COM service classes, which you can instantiate, and COM interfaces, which you can query on COM instances, and you can only make calls to the classes through interfaces. A service class has its own name and GUID, and some other properties, but let’s skip that for now. Apparently, the class that provides the above interface is called AttachmentServices. See, it’s a service, not an interface. In native code, you would call CoCreateInstance, and now we know why it has a CLSID and also an interface identifier parameter. In managed code, however, the most straightforward way is this:
var type = Type.GetTypeFromCLSID(new Guid("4125DD96-E03A-4103-8F70-E0597D803B9C")); var svc = (IAttachmentExecute)Activator.CreateInstance(type); |
The first line gets some kind of proxy type that can instantiate the object based on it’s CLSID, the second line instantiates it and the cast is translated to querying the interface. However, if you take a look at COM interop assemblies in Reflector, you can’t find code like this. There’s a lot of magic going on in those assemblies, but the magic we need is the ComImport attribute, which allows us to instantiate COM classes like managed classes. The result:
[ComImport, Guid("4125DD96-E03A-4103-8F70-E0597D803B9C")] public class AttachmentServices { } |
This class doesn’t need any members, you can just create it, cast it to IAttachmentServices and it works. As far as I can tell, it doesn’t make a difference if I apply ComImport on the interface, too, but then I decided to do it anyway to make the code cleaner.
COM apartment support
And the last challenge: it doesn’t always work, in certain contexts the interface cast throws an InvalidCastException (0×80004002, E_NOINTERFACE, I’ve encountered this before). The problem lies within COM apartments. For the purposes of this post, we don’t need to understand them, let’s assume that they’re just an obstacle. Some COM services can be called from STA threads, some from MTA threads, and some from both (as I mentioned above, it’s another property of a COM service).
This particular interface works on STA threads only. I haven’t found generic solutions to deal with this problem in .NET COM clients, and the problem doesn’t exist in .NET COM services because they always support both apartments, so I came up with my own solution. I created a COMInvoke function which can execute code in an arbitrary apartment state, and it creates a new thread if the current one is not suitable. You can find the code in COMUtils.cs.
My code
I decided to clearly separate low level Windows imports and higher level functions built on top of them. The former went to ShObjIdl.cs in the Internal namespace: the name suggests that it’s use is not preferred, but it’s still public in case anyone needs it (I’ve been frustrated a lot in the past by non-public Microsoft code, and I’m sure I’m not the only one). The latter can be found in VirusUtils.cs.
A lot of effort and research has gone into VirusUtils. The problem is that the behavior of IAttachmentExecute is not clearly defined. Some antivirus products indicate a virus with an exception, some others don’t. Some products delete the file immediately, some others just deny access to it, and the behavior also varies with different settings in the same product. I ended up with a function which calls the COM API and also checks if the file is deleted or access to it is denied after the scanning. So this function should reliably tell if a file contains a virus or not.
I have access to Windows Defender, Microsoft Security Essentials and ESET NOD32 Antivirus. I tested them rigorously using the awesome EICAR test file and documented the behaviors in the source code. My function worked with all of them, with multiple settings, too, so I’m confident in publishing it and I suggest it for general use by anyone. Also, if you can give me detailed test results with other antivirus products and would like your name and work to appear in the source code, don’t hesitate to contact me!