Building virus-scanning functionality with file system filter drivers

As our reliance on computers has grown, so have the networks that connect computers. We typically connect computers via a LAN, and either directly or indirectly via the Internet. Although this connectivity facilitates sharing programs and documents, it heightens the risk of infecting files with annoying or destructive viruses. Consequently, you rarely find a Windows NT system that doesn't run a virus-scanning product to check files for the presence of viruses and prevent them from entering the system.

This month, I'll explore the internals of on-access virus scanners for NT. First, I'll briefly describe how on-access virus scanners work. Next, because on-access virus scanners work with file system drivers to check files for viruses, I'll introduce how file system drivers (FAT, NTFS, etc.) interact with NT through the I/O Manager. I'll conclude by describing where on-access virus scanners fit into NT.

Virus Scanning Basics
Virus scanners check files in one of two ways. An on-demand virus scanner examines every file on a disk (or within subdirectories that you specify) and searches for viruses it knows about or for signatures common to certain types of viruses. You can usually trigger on-demand virus scanners manually, or automatically at regular intervals (e.g., during system boot). The drawback of on-demand virus detection is that files you download or copy onto the system can infect the computer before the virus scanner checks for viruses.

On-access virus scanning is proactive. On-access virus scanners stop virus activation because the scanners check files at the time you open or execute them. Thus, if you download an infected Microsoft Word document to your hard disk, before Word can open the file, the virus scanner makes sure the file is clean. When the virus scanner detects a virus in a file, the scanner either removes the virus or returns an error code to the application opening the file to prevent the open operation from proceeding.

Virus-scanning products for NT perform either or both types of virus scanning. Implementing on-demand virus scanning is relatively straightforward: The scanner opens the files and looks for signs of viruses. A Win32 program that uses standard APIs can easily provide this functionality.

Implementing an on-access virus scanner is much trickier. You cannot use the Win32 API to direct NT to check files whenever other programs (including NT) open or execute the files. Furthermore, scanning must be transparent to the applications running on the system. For example, Word must be able to open files in its usual manner. The only way to provide this functionality is to write a special type of device driver known as a file system filter driver.

The I/O Manager and File Systems
File system filter drivers hook themselves on top of file systems so that they can intercept requests headed toward the file systems. The drivers review each request and reject it, pass it to the file system, or change it on the way to the file system. The drivers can also examine the results of requests on the way back from the file systems. On-access virus scanners ensure that files are free of viruses when you open them, so the file system filter drivers process only open requests.

To better understand this design, you need to understand how file systems are integrated with NT and how a file system services I/O requests (e.g., from a Win32 program). The following description generally holds true for all types of file system requests (e.g., create, read, write), but I'll concentrate on what happens when a typical Win32 program uses the ReadFile API to read from a file.

Figure 1 shows the main system components involved in servicing the read request. NT implements the ReadFile API in Kernel32.dll, a standard NT component that's part of the Win32 subsystem. Kernel32 is the library in which NT implements all file, process, and memory-management Win32 APIs. Some APIs, such as CreateProcess, require that the library send messages to the Win32 subsystem process to service the APIs. However, most APIs (including ReadFile) do not. Kernel32 takes the parameters passed in the ReadFile call and constructs a call to NT's kernel-mode API. In the kernel-mode API (also known as NT's native API), all the functions begin with "Nt." The kernel-mode function for reading a file is NtReadFile.

When Kernel32 issues a native NT API call, the processor switches into kernel mode, and the native API call enters the NT kernel. All native API calls enter through the same doorway: The kernel funnels requests to the kernel-mode function that handles them. The kernel-mode NtReadFile function constructs an I/O request packet (IRP) and initializes it with all the information to describe the request (e.g., which file to read from, the starting offset and length of the read, and the buffer that will receive the data on successful completion). NtReadFile calls the I/O Manager to send the IRP to the file system of the drive where the file resides. For example, if the C drive is an NTFS drive, reading C:\mark1 causes the I/O Manager to call the NTFS driver.

File systems resolve some requests without ever touching the disk. In the read example, if the data resides in the file system cache, NTFS completes the IRP immediately. If the cache does not contain the data, NTFS must create one or more new IRPs that instruct the hardware device driver managing the C drive's hard disk to fetch the data from the disk. When the driver completes these IRPs, NTFS can complete the IRP that NtReadFile sent. When the target file system completes NtReadFile's IRP, the call to ReadFile ends and control returns to the Win32 program; the program can now look at the file data.

Well, that's the general idea, anyway. I've described a synchronous system, in which control does not return to the originator of an IRP (NtReadFile or NTFS in the read example) until the IRP finishes. NT can service requests synchronously, but it more often services requests asynchronously. When a file system cannot process a request immediately because a disk driver must fetch some data, the file system returns a pending status for the request. Through an event object associated with the IRP, the I/O Manager reports the IRP's completion to the initiator of the IRP. When the caller wants to wait for the IRP to finish, the caller waits for the event object's signal. The I/O Manager creates an event object in an un-signaled state and switches it to a signaled state when the IRP finishes.

NtReadFile (from the read example) is by default an asynchronous API. Nt ReadFile returns control to Kernel32 whether or not the request has finished; Kernel32 must wait for the call's event object signal. This arrangement preserves the design behavior of Win32 ReadFile, which is by default a synchronous API.

File System Filter Drivers
The I/O Manager provides a facility whereby a device driver can attach itself to another driver, letting the attaching driver filter the requests directed to the other driver. File system filter drivers are very popular and versatile; you can build in all types of functionality with them. For example, a file system filter driver can encrypt data on the way into a file system and decrypt it on the way out. You can design a file system filter driver to encrypt specific files or an entire disk, providing high levels of security. A file system filter driver can function as a license manager, which ensures that users obtain a token from a controller before they can open (and run) certain applications. Of course, file system filter drivers are also ideal for implementing on-access virus scanners.

Figure 2 shows how a virus-scanning file system filter driver might intercept and process a request to open a file. NT uses CreateFile IRPs to create new files or open existing files. Therefore, virus scanners intercept CreateFile requests to ensure that any file being opened is free of viruses. Most NT virus scanners let you specify that only certain types of files (e.g., with .dll, .exe, or .doc extensions) be scanned. The file system filter driver first checks to see whether a file is on the list of file types to scan. If the file does not need to be scanned, the file system filter driver gets out of the way of the request, passing the IRP to the file system and ignoring the IRP's return status.

If the file needs to be scanned, the file system filter driver lets the CreateFile IRP continue to the file system and waits for the IRP to finish. If the IRP's result status shows that an error occurred during opening of the file, the virus scanner simply passes the status back to the I/O Manager. This arrangement prevents the scanner from doing extra work when the file being opened does not exist, or the user does not have permission to access the file in the way the user requested. If the file system determines that the file was opened successfully, the file system filter driver sends ReadFile IRPs to the file system to obtain the contents of the entire file. The scanner waits for the IRPs to complete before it continues its proprietary scanning step.

The virus scanner's library of viruses and virus signatures guides the scanning step. A virus-scanning file system filter driver will usually obtain this library from a Win32 program that runs as the system boots, or by reading the library from a file when the virus scanner initializes. The value of an on-access virus scanner is the comprehensiveness of its library; as long as the scanner works transparently, users don't care how cleverly the virus scanner's programmers have implemented the file system filter driver.

If the scanner detects a virus in the file, the file system filter driver closes the file and returns an error to the I/O Manager. The default error code on many virus-scanning packages is access denied, which is the error code returned when you open a file for exclusive access and another program already has it open, or when you try to open a file and its security settings prevent you from doing so. Most NT virus scanners also have options to log detections to a file, copy infected files to a specified location, or delete infected files.

Once a virus scanner identifies a virus, it can often eliminate the virus from the file. If you've enabled virus removal, the virus scanner's file system filter driver removes the virus and writes a clean version of the file to the file system before the file open operation continues. This process is transparent to the program opening the file because the file system filter driver holds the CreateFile IRP until it finishes the scanning and cleaning steps. An application cannot distinguish introduced delays from delays that the application encounters when other programs read and write files.

The organization I've presented in Figure 2 isn't the only strategy that on-access virus scanners can implement. Some solutions perform part of their processing in a Win32 program with a tight communications link to the file system filter driver. This off-loaded processing can include anything from scanning or cleaning the file to moving the file to the dedicated directory and notifying the user via a dialog box.

That's a Wrap
Remember that when you evaluate a virus scanner for your systems, the key factor is how many viruses the scanner's library contains, rather than how much of the program the developers implement in a device driver. Of equal importance is whether the vendor provides online access to virus-library updates. With these points in mind and with your understanding of how on-access virus scanners integrate with NT, you're well-equipped to select a virus scanner that meets your needs.