Making an antivirus engine : the guidelines


 

Making an antivirus engine : the guidelines

 

Introduction

When roaming around the techies forums, I often see some people (and many not very experienced) asking for “How do I make an antivirus”, sometimes with not very adapted languages (bat, PHP, …) and having a wrong idea of what an antivirus is, and how it should be built.

I’ve also seen many “Antivirus softwares” made by kiddies, with very few still-at-school people and about 4 hours per day of coding on several weeks. I’m not telling kiddies are not skilled, but I’m telling building an antivirus engine needs either lot of skilled people with full time job plus lot of time to release a decent software or lot of money to pay them :) (in case they are not volunteer).

So, I’ll cover here the guidelines for a basic antivirus coding, for Windows and in C/C++. One can found here the pointers to design an antivirus engine, or simply learn how most of them are built.

 

Protection

For a good protection, an Antivirus must have at least one driver, to be able to run code in kernel and overall have access to kernel APIs. Starting with Vista, Microsoft understood that the Antivirus industry needed keys to enter the kernel and activate filters in strategic places, such as file system, registry and network. Don’t be stunned if building an antivirus for pre-Vista systems can be a real pain, because it was not designed for this.

  •  However, on Pre-Vista systems, Antivirus companies used to use rootkit-like features to guard the doors (even if it was not recommended at all by Microsoft) and be able to protect your system. They used what we call “Hooks” (API detours for filtering purpose).
  • On Vista+, Microsoft provided APIs to insert our low level driver between userland calls and kernel APIs. That way, it’s easy to register an antivirus product into the kernel. More, that kind registration based system allows us to dispatch our system security into layers, where several products with different aims can cohabit. This was not the case for hooks, as the implementation was totally product dependant.

 

NOTE: I will not cover the workarounds with hooks for pre-Vista systems , because it’s easy to find on the internet, and because it would need a whole chapter to explain how to hook, where to hook and so… But you have to know it’s the same idea than the kernel APIs, except that you have to implement yourself what Microsoft provided on Vista+ systems.

 

To learn about coding drivers, you can check that useful links:

http://msdn.microsoft.com/en-us/library/windows/hardware/gg490655.aspx

http://www.codeproject.com/Articles/9504/Driver-Development-Part-1-Introduction-to-Drivers

 

To learn about hooks, you can check that basic example:

http://www.unknowncheats.me/forum/c-and-c/59147-writing-drivers-perform-kernel-level-ssdt-hooking.html

 

Process

The first thing to protect the user from, is the launching of malicious processes. This is the basic thing. Antivirus should register a PsSetCreateProcessNotifyRoutineEx callback. By doing this, on each process creation, and before the main thread starts to run (and cause malicious things) the antivirus callback is notified and receives all the necessary information.

It receives the process name, the file object, the PID, and so. As the process is pending, the driver can tell its service to analyse the process’s memory for anything malicious. It it founds something, the driver will simply set CreationStatus to FALSE and return.

 

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  _In_  PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  _In_  BOOLEAN Remove
);
VOID CreateProcessNotifyEx(
  _Inout_   PEPROCESS Process,
  _In_      HANDLE ProcessId,
  _In_opt_  PPS_CREATE_NOTIFY_INFO CreateInfo
);
typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG  Flags;
    struct {
      ULONG FileOpenNameAvailable  :1;
      ULONG Reserved  :31;
    };
  };
  HANDLE              ParentProcessId;
  CLIENT_ID           CreatingThreadId;
  struct _FILE_OBJECT  *FileObject;
  PCUNICODE_STRING    ImageFileName;
  PCUNICODE_STRING    CommandLine;
  NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

 

Threads

In the same idea than for processes, threads can be a way for malicious things to cause damages. For example, one can inject some code into a legit process, and start a remote thread on that code inside the process’s context (easy to follow? :) ). That way, a legit process can do malicious things.

We can filter new threads with the  PsSetCreateThreadNotifyRoutine callback. Each time a thread is created, the antivirus is notified with the TID and the PID. Thus, it’s able to look into the thread’s start address code, analyse it and either stop the thread or resume it.

 

NTSTATUS PsSetCreateThreadNotifyRoutine(
  _In_  PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
VOID
(*PCREATE_THREAD_NOTIFY_ROUTINE) (
    IN HANDLE  ProcessId,
    IN HANDLE  ThreadId,
    IN BOOLEAN  Create
    );

 

Images

The third dynamic threat is about images that can be loaded into memory. An image is a PE file, either a EXE, a DLL or SYS file. To be notified of loaded images, simply register PsSetLoadImageNotifyRoutine. That callback allows us to be notified when the image is loaded into virtual memory, even it’s never executed. We can then detect when a process attempts to load a DLL, to load a driver, or to fire a new process.

The callback gets information about the full image path (useful for static analysis), and the more important in my opinion, the Image base address (for in-memory analysis). If the image is malicious the antivirus can use little tricks to avoid the execution, like parsing the in-memory image and go to the entrypoint, then call the assembly opcode “ret” to nullify it.

 

NTSTATUS PsSetLoadImageNotifyRoutine(
  _In_  PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);
VOID
  (*PLOAD_IMAGE_NOTIFY_ROUTINE)(
    __in_opt PUNICODE_STRING  FullImageName,
    __in HANDLE  ProcessId,
    __in PIMAGE_INFO  ImageInfo
    );
typedef struct  _IMAGE_INFO {
    union {
        ULONG  Properties;
        struct {
            ULONG ImageAddressingMode  : 8; //code addressing mode
            ULONG SystemModeImage      : 1; //system mode image
            ULONG ImageMappedToAllPids : 1; //mapped in all processes
            ULONG Reserved             : 22;
        };
    };
    PVOID  ImageBase;
    ULONG  ImageSelector;
    ULONG  ImageSize;
    ULONG  ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;

 

File system

Once every dynamic thing is secured, an antivirus should be able to notify user for malicious things on-the-fly, not only when they are about to start. An antivirus should be able to scan files when user opens a folder, an archive, or when it’s downloaded on the disk. More, an antivirus should be able to protect himself, by forbidding any program to delete its files.

The way to do all of this, is to install a driver into the file system, and more specifically a minifilter of a legacy filter (old way). Here we will talk about minifilter.

A minifilter is a specific kind of driver, able to register callbacks on every Read/Write operation made on the file system (IRP major functions). An IRP (Interrupt Request Paquet) is an object used to describe a Read/Write operation on the disk, which is transmitted along with the driver stack. The minifilter will simply be inserted into that stack, and receive that IRP to decide what to do with it (allow/deny operation).

For a little example of minifilter, please check that useful link or that one. The microsoft guidelines are here.

You’ll find also 2 examples of the WDK documentation here and here.

A basic minifilter callback look like this. There are 2 kinds of callback, Pre operation and Post operation, which are able to filter before of after the query. Here’s a preOperation pseudo code:

 

FLT_PREOP_CALLBACK_STATUS    PreOperationCallback    (__inout    PFLT_CALLBACK_DATA    Data,    
    __in    PCFLT_RELATED_OBJECTS    FltObjects,    
    __deref_out_opt    PVOID    *CompletionContext)
{
    ...

    if    (    all_good    )
    {
        return    FLT_PREOP_SUCCESS_NO_CALLBACK;
    }
    else
    {
        //    Access    denied                
        Data->IoStatus.Information    =    0;
        Data->IoStatus.Status    =    STATUS_ACCESS_DENIED;
        return    FLT_PREOP_COMPLETE    ;
    }    
}

 

Registry

The registry is one of the most critical place to guard. There are many many ways for a malware to keep persistent hand on the system by registering a single (or few) keys/values into the registry. The most known places are Run keys, and Services. This is also the place where the antivirus can be defeated (along with the file system), by simply removing its driver/service keys, so that it will no longer restart at system boot.

This is not a real necessity for an antivirus to guard restart places, most of them don’t. But they must guard their install registry keys, to avoid being defeated easily by malwares. This can be done by registering CmRegisterCallback.

The callback gives enough information to get the full key name, the kind of access (Create, Rename, Delete, … ) and the caller PID. That way it’s easy to grant access or not to the call, by setting the Status field of Post Operation callback.

 

NTSTATUS CmRegisterCallbackEx(
_In_        PEX_CALLBACK_FUNCTION Function,
_In_        PCUNICODE_STRING Altitude,
_In_        PVOID Driver,
_In_opt_    PVOID Context,
_Out_       PLARGE_INTEGER Cookie,
_Reserved_  PVOID Reserved
);
EX_CALLBACK_FUNCTION RegistryCallback;

NTSTATUS RegistryCallback(
  _In_      PVOID CallbackContext,
  _In_opt_  PVOID Argument1,
  _In_opt_  PVOID Argument2
)
{ ... }

Argument1 = typedef enum _REG_NOTIFY_CLASS {
  RegNtDeleteKey,
  RegNtPreDeleteKey = RegNtDeleteKey,
...

Argument2 = typedef struct _REG_POST_OPERATION_INFORMATION {
  PVOID    Object;
  NTSTATUS Status;
  PVOID    PreInformation;
  NTSTATUS ReturnStatus;
  PVOID    CallContext;
  PVOID    ObjectContext;
  PVOID    Reserved;
} REG_POST_OPERATION_INFORMATION, *PREG_POST_OPERATION_INFORMATION;

 

Network (Firewall)

To guard the doors of the whole internet traffic which can be huge on certain systems (servers, huge bandwidth users) without being slowed down by the context switching that takes place in userland, it’s totally not recommended to install a firewall that have no underlying driver, except for some web browser filters that can be enough for http traffic, but that will not protect against malware communication in/out.

In order to have a correct implementation of firewall, one should code a NDIS, TDI or another method for low level IP filtering driver. NDIS/TDI is a bit tricky to do, and would require lot of knowledge (more than other filters in my opinion).

Anyway, here’s some pointers to start coding such a driver, the microsoft guidelines, and old codeproject tutorial (but still good to read), an example of NDIS firewall, and an example of TDI firewall. Here’s also a good writing about NDIS firewall bypass trick, and a little explanation about the network driver stack,

 

Userland protection

The userland protection is not a necessity, but can be an additional module against Trojan Bankers, and more specifically against process spies. They are generally injected into every process, for several reasons.

First, they are able (on demand) to kill the process if it has been identified as malware (this should not happen, because AVs are supposed to stop it before it starts). It’s always easier to stop a process when you are into its context.

Second they are able to guard critical process, like web browsers, against hooking malwares able to detour and filter API calls in order to gather passwords, banking information, and redirect internet flow to malware servers. They only watch for IAT modification, for splicing, and can also set hooks themselves to avoid LoadLibray of a malware DLL, and thus forbid certain methods of code injection.

An easy way to inject a protector DLL into all processes is to use the AppInitDll registry key to register the protector DLL. It will load the DLL into every process started on the system, as soon as they link the User32.dll image (most of them do).

 

Analysis Engine

The analysis engine is one of the most important part, it’s responsible for analysing file/memory samples coming from the drivers. If must be fast (even with a huge database), and should be able to handle most of the file types (Self-extracted executables, Archives – RAR, ZIP, Packed files – UPX, … ) and thus should have many modules to do this:

 

  • Signature engine: The database of an antivirus contains millions of signatures, and the engine should be able to fast search for them into a sample. Thus, a very powerful algorithm should be part of it. Some examples : AhoCorasick, RabinKarp, string matching algorithms.
  • Sandbox : That module is not necessary, but would be a plus to be able to run samples into a limited memory, with no effect on the system. That could help to unpack samples packed with unknown packers, and help the heuristic engine (see after) to detect API calls that could be considered as suspicious or malicious. Some good sandbox here.
  • Heuristic engine : As said above, the heuristic engine does not search for signatures, but rather look for suspicious behaviour (ie. sample that opens a connexion on the website hxxp://malware_besite.com). That can be done by static analysis, or through a sandbox.

 

Signature syntax

The signature syntax is the “dictionary” of the language that the signature engine understands. It’s a way to formalize what is the pattern to find, how to search it and where to search it into the sample. The syntax has to be simple enough for the researchers to understand, powerful enough to handle every use case, and easy to parse for better engine performances.

 

VirusTotal has developed a good syntax and engine (Yara project), which is open source. That should be a good pointer to make your own syntax, or simply use it. Here’s also a good post blog on how create signatures for antivirus.

 

Example of signature:

rule silent_banker : banker
{
    meta:                                        
        description = “This is just an example”
        thread_level = 3
        in_the_wild = true

    strings:
        $a = {6A 40 68 00 30 00 00 6A 14 8D 91}  
        $b = {8D 4D B0 2B C1 83 C0 27 99 6A 4E 59 F7 F9}
        $c = “UVODFRYSIHLNWPEJXQZAKCBGMT”

    condition:
        $a or $b or $c
}

 

Self-protection

The self protection is very important for an antivirus, to avoid being defeated by a malware and continue to protect the user. This is why an antivirus should be able to guard its own installation and keep persistence at reboot.

 

There are several place to protect: Files, Registry keys, Processes/Threads, Memory.

  • File protection is implemented into the minifilter, with particular rules on the files of the antivirus (No access in deletion, renaming, moving, writing).
  • Registry protection is made into the registry filter, with access denied for registry keys of the driver and the service.
  • The drivers threads are protected, because it’s quite impossible to unload kernel module without crashing the system
  • To be able to protect the service, which is a userland process, 2 solutions:

 

  1. The easiest would be to add rules for failures in the service manager, and set every failure rule to “restart service”. That way, when the service is not stopped by service manager, it restarts. Of course, the service should not be able to accept commands until the system is not restarting (or stopping).

491838-428-487

 

  1. The second method, which is more generic, would be to set callbacks on process handles with ObRegisterCallbacks.

By setting the ObjectType to PsProcessType and the Operation to OB_OPERATION_HANDLE_CREATE, you receive a Pre and Post operation callback, and you are able to return ACCESS_DENIED into ReturnStatus if the process handle queried has GrantedAccess which have process terminate rights (or process write rights, or anything that can lead to a crash/kill),  and of course if the process is one the antivirus needs to guard (its service for example).

Of course, one also needs to guard Duplicate handle and the PsThreadType to avoid any termination method that requires to grab a handle on the process or a thread. Here’s a little example of usage of that callback.

 

NTSTATUS ObRegisterCallbacks(
  _In_   POB_CALLBACK_REGISTRATION CallBackRegistration,
  _Out_  PVOID *RegistrationHandle
);
typedef struct _OB_CALLBACK_REGISTRATION {
  USHORT                    Version;
  USHORT                    OperationRegistrationCount;
  UNICODE_STRING            Altitude;
  PVOID                     RegistrationContext;
  OB_OPERATION_REGISTRATION *OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
typedef struct _OB_OPERATION_REGISTRATION {
  POBJECT_TYPE                *ObjectType;
  OB_OPERATION                Operations;
  POB_PRE_OPERATION_CALLBACK  PreOperation;
  POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;
VOID ObjectPostCallback(
  _In_  PVOID RegistrationContext,
  _In_  POB_POST_OPERATION_INFORMATION OperationInformation
);
typedef struct _OB_POST_OPERATION_INFORMATION {
  OB_OPERATION                  Operation;
  union {
    ULONG  Flags;
    struct {
      ULONG KernelHandle  :1;
      ULONG Reserved  :31;
    };
  };
  PVOID                         Object;
  POBJECT_TYPE                  ObjectType;
  PVOID                         CallContext;
  NTSTATUS                      ReturnStatus;
  POB_POST_OPERATION_PARAMETERS Parameters;
} OB_POST_OPERATION_INFORMATION, *POB_POST_OPERATION_INFORMATION;
typedef union _OB_POST_OPERATION_PARAMETERS {
  OB_POST_CREATE_HANDLE_INFORMATION    CreateHandleInformation;
  OB_POST_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation;
} OB_POST_OPERATION_PARAMETERS, *POB_POST_OPERATION_PARAMETERS;
typedef struct _OB_POST_CREATE_HANDLE_INFORMATION {
  ACCESS_MASK GrantedAccess;
} OB_POST_CREATE_HANDLE_INFORMATION, *POB_POST_CREATE_HANDLE_INFORMATION;

 

GUI (Graphical User Interface)

This is the visible part of the iceberg. In my opinion, one (maybe THE) most important part if you want to sell your product. Users love what is beautiful, easy to use, intuitive. Even if it’s not 100% efficient. The GUI must be sexy.

The GUI is only an empty shell, it does only graphical treatments, and sends/receive commands to the core (the service). It also displays progress bars, what is being analysed, provides configuration, and so…

Here’s the Avast UI. Sexy, right? :)

 

Capture

Source: Avast!

 

Architecture

The global architecture could be something that look like this:

  1. GUI : No administrative rights, WEAK
  2. Guard DLL(s) : web browser protection, MEDIUM
  3. Service : Admin rights. Serves as a gateway to kernel code and take decisions along with some database, STRONG
  4. Driver(s) : Kernel filters, STRONG

 

The GUI doesn’t need any administrative right, it only takes user actions and transmits them to the service. It also displays product status. Nothing more, this is not its aim. If the GUI is killed, this is not a problem as the service should be able to restart it.

The guard DLLs (if any), are massively injected into all processes, and should be able to look for IAT hooks and/or malicious threads. They should be quite hard to unload or defeat. They are not critical but important.

The service is the core of the product. It should be unkillable, or at least should be able to self-restart on kill. The service is responsible for communication between all modules of the product, it sends commands to drivers, takes commands from user, and queries the database for sample analysis. This is the brain.

The kernel drivers are also critical. They are the tentacles that gather information on everything that happen on the system, and transmit them to the service for decision. They are also able to deny access to guarded places, based on service’s decision.

 

Conclusion

Making a strong, reliable and stable antivirus engine is a complicated task, that needs experimented people with a very strong knowledge in windows kernel programming, windows application programming, GUI design, software architecture, malware analysis,

Building stable drivers is also a complicated task, because a little grain of sand can crash the whole system. It needs testing, testing, and lot of testing.

When your engine is done and working, you’ll just need to hire researchers to analyse malwares and add signatures to your database :) What are you waiting for? Go on! :D

 

More links

- http://www.symantec.com/connect/articles/building-anti-virus-engine

 

Leave a Reply