Table of Contents
Previous Section Next Section

12.6. Memory Disinfection

This chapter would not be complete without some words about the deactivation possibilities of different virus types. A memory scanner should work closely with an on-access virus scanner and should always know the same set of viruses that are known by the file scanner components of the antivirus product. The on-access virus scanner can detect most known viruses, even if the virus code is active in some processes. But it cannot stop the virus from infecting new objects because the active virus can infect the disinfected object again. Typically antivirus software cannot detect a virus in applications before the virus code is written to them; however, a new copy of the known virus code cannot be executed as a process because the on-access scanner will be active.

A particular virus can probably become active on a machine in the following situations:

  • The virus scanner has not been installed on the computer, but the virus code has already executed.

  • The virus is new, and the scanner needs an update installed to detect it.

12.6.1. Terminating a Particular Process That Contains Virus Code

Probably the easiest way to deactivate the virus in memory is to kill the particular task in which the virus code is detected by the memory scanner. This can be done easily by using TerminateProcess() API and the appropriate rights (PROCESS_TERMINATE access is needed). Terminating a task is a risky procedure, however, and should be used with great care. Because active virus code is most likely attached to a user application, important user data could be lost if the infected process were simply killed. Any application could keep several database files open, which most likely could not be kept consistent if the process were killed. Consequently, TerminateProcess() should be used in situations in which the virus code is active as a separate process, such as the WNT/RemEx or W32/Parvo viruses.

Some viruses, such as W32/Semisoft variants, try to avoid termination by executing two different virus processes. Whenever one virus process is terminated, the active copy of the virus will restart the terminated one, protecting itself very efficiently. This is why memory scanning should assume an on-access virus scanner in the background that will not allow the new virus task to be executed again.

12.6.2. Detecting and Terminating Virus Threads

If a virus creates its own threads in a process, the memory scanner should be able to eliminate the threads belonging to the virus itself and terminate those threads in the process. The previously mentioned W32/Niko virus (Listing 12.10) creates two threads for itself. One thread is used for the trigger routine and will terminate by itself. The infection thread will be active as long as the process (with at least one thread of its own) is running. A thread handle is needed with the necessary THREAD_TERMINATE access to terminate a particular thread of a process.

OpenThread() is not available in the subsystem DLLs on most NT-based systems. The function is undocumented and available only from the NTDLL.DLL as NtOpenThread(). Listing 12.11 is my own, "hand-made" declaration.

Listing 12.11. "Handmade" API Definition for NtOpenThread()
NtOpenThread (
    OUT PHANDLE ThreadHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes,

To eliminate the virus threads from the clean application threads, the memory scanner should check the Win32StartAddress of each thread. Win32StartAddress is available in the performance data, but it is easier to get by using another, undocumented API. This API, called NtQueryInformationThread(), has five parameters:

  • The first is a thread handle with THREAD_QUERY_INFORMATION access.

  • The second parameter is the QueryWin32StartAddress class value, which is 9.

  • The third parameter is the address of the return value.

  • The fourth parameter is the size in bytes (four) of the information to be returned.

  • The last parameter is BytesWritten, a PULONG optional value that can be NULL.

NtQueryInformationThread() will return the correct start address of a particular thread, as shown by the tlist.exe application (available in the Windows NT resource kit). (Listing 12.9 is an output of tlist.exe used on a process in which Win32/Niko virus is active.) In the example, the starts of the two virus threads are 0x0040f021 and 0x0040f01c, respectively. Both of these addresses point into the active virus image, each to a jump instruction (0xe9) that will in turn give control to the entry points of the virus thread functions.

By checking the Win32StartAddress of a thread, the memory scanner can determine whether or not a thread belongs to a virus because the start address of the thread will point into the active virus image in memory. In the case of Niko, the virus code is executed as the main thread of the host application, so the Win32StartAddress (0x0040f000) of the main thread (entry point) should not be terminated because that same thread is used by the host program. The final step is to terminate the thread with the TerminateThread() API and THREAD_TERMINATE access.

Essentially, the preceding procedure can be used safely to detect and kill CodeRed threads in the process address space of Microsoft IIS.

Listing 12.12 is a partial log of the threads inside the INETINFO.EXE process (Microsoft's IIS) after infection by both CodeRed I and CodeRed II on the same system. Any thread is identified as an active one and detected based on the signature of the virus code found at a thread start address. This ensures avoidance of potential ghost positives. (Ghost positives could result because unsuccessful worm attacks could still place worm code on the application heap in inactive form.) Attempts to freeze the detected CodeRed threads were successful in stopping the worm from spreading further and in gaining sufficient CPU time for patch installation processing.

Note the high context switch number for worm-related threads, even after only a few seconds of infection. CodeRed II infections were fresh and have a lower context switch number. Note that most CodeRed II threads have almost identical context switch values.

Listing 12.12. Two W32/CodeRed Variants and Some of Their Threads


3ac          63   77e878c1    01002ec0    Wait:Executive
260         458   77e92c50    77dc95c5    Wait:Userrequest
410         927   77e92c50    78002432    Wait:Userrequest
414         921   77e92c50    78002432    Wait:Userrequest
418         131   77e92c50    00000000    Wait:Lpcreceive
41c         459   77e92c50    77dc95c5    Wait:Userrequest
494           2   77e92c50    6a176539    Wait:Userrequest
498           8   77e92c50    6d703017    Wait:Userrequest
49c           7   77e92c50    69de3ce1    Wait:Userrequest
4a0           1   77e92c50    69e0d719    Wait:Eventpairlow
4a4           1   77e92c50    69e0d719    Wait:Eventpairlow
4bc        178     77e92c50    6783b085    Wait:Userrequest
348      10507     77e92c50    730c752b    Wait:Userrequest
598      10509     77e92c50    010ce918    CodeRed I Thread
59c      10509     77e92c50    0230fe7c    CodeRed I Thread
5a0      10510     77e92c50    0234fe7c    CodeRed I Thread
5a4      10509     77e92c50    0238fe7c    CodeRed I Thread
* Hundreds of threads not shows to make list shorter 
708      10509     77e92c50    039cfe7c    CodeRed I Thread
70c      10509     77e92c50    03a0fe7c    CodeRed I Thread
710      10510     77e92c50    03a4fe7c    CodeRed I Thread
714      10509     77e92c50    03a8fe7c    CodeRed I Thread
718      10509     77e92c50    03acfe7c    CodeRed I Thread
71c      10509     77e92c50    03b0fe7c    CodeRed I Thread
720      10509     77e92c50    03b4fe7c    CodeRed I Thread
724          2     77e92c50    03b8fe7c    CodeRed I Thread
26c         65     77e92c50    00000000    Wait:Lpcreceive
518          1     77e92c50    6d70175a    Wait:Eventpairlow
320          7     77e92c50    6d70175a    Wait:Eventpairlow
568        839     77e92c50    004202a1    CodeRed II Thread
58c        810     77e92c50    004202a1    CodeRed II Thread
390        810     77e92c50    004202a1    CodeRed II Thread
4d8        810     77e92c50    004202a1    CodeRed II Thread
800        814     77e92c50    004202a1    CodeRed II Thread
804       7868     77e92c50    74fd68fd    Wait:Eventpairlow
808        813     77e92c50    004202a1    CodeRed II Thread
80c        812     77e92c50    004202a1    CodeRed II Thread
810        812     77e92c50    004202a1    CodeRed II Thread
* Hundreds of threads not shows to make list shorter 
b3c        812     77e92c50    004202a1    CodeRed II Thread
b40        812     77e92c50    004202a1    CodeRed II Thread
b44        814     77e92c50    004202a1    CodeRed II Thread
b48        812     77e92c50    004202a1    CodeRed II Thread
b4c        812     77e92c50    004202a1    CodeRed II Thread
b50        812     77e92c50    004202a1    CodeRed II Thread
b54        812     77e92c50    004202a1    CodeRed II Thread

In some tricky cases, the threads cannot be killed immediately. An increasingly common trick is to inject a thread into a standard Windows process to prevent the killing of another worm process. If the protection thread is terminated, then, the worm process immediately reinjects the thread. In this case, the thread needs to be frozen first and the process of the worm terminated before the frozen thread can be killed. But of course there are even bigger complications than this, for which there are no simple solutions.

12.6.3. Patching the Virus Code in the Active Pages

The most difficult case of deactivation is when the virus is active as part of a loaded EXE or DLL image or the virus allocates pages for itself on a per-process basis and hooks some imports of the host application to itself. In these situations, the active virus code must be patched in memory so that the virus is deactivated. This procedure must be very carefully developed because an incorrect patch of the virus code in memory could cause a new variant to be created accidentally by the memory disinfection itself.

When the virus hooks APIs to itself by patching the host application's import address table (IAT), the IAT should be fixed in each of the infected processes. This will remove the virus code from the API chain. This operation must be done very quickly. Perhaps the safest way is to suspend each thread of an infected process at the time of this fix. When the IAT is fixed, threads can be resumed. WriteProcessMemory() can be used to write into the necessary pages in this situation. The disinfection should be done from instance to instance of the virus. The protection flags of each page that need modification must first be checked. If the page has PAGE_READONLY access, the protection flag should be changed to PAGE_READWRITE. The VirtualProtectEx() function can be used with PROCESS_VM_OPERATION access in such cases.

A much more difficult case is when a particular subsystem DLL is infected by the virus, as in the case of W32/Heretic. Some other worms patch the socket communication library (WSOCK32.DLL), as done by the W32/Ska.A virus11.

In the case of the W32/Heretic virus, KERNEL32.DLL is infected so that the export addresses of two APIs are patched in the file itself (not in memory only). When a particular process gets the address of such an API with the GetProcAddress() function, it will get a pointer to the virus code. Because some applications determine the addresses of certain APIs during initialization, they will "remember" such addresses as long as they are running. This is why the export address table of KERNEL32.DLL should not be fixed during memory disinfection; in some situations, the virus could be activated again regardless of this particular fix. Instead of fixing the export table, the disinfector should patch the active virus code in memory very carefully. This can be done by modifying the virus code at the entry point of its hook routines, so the control will be given to the exit of the hook functions where the virus calls the original API entry point. That way, the virus can no longer replicate. Of course, this procedure is virus-specific and needs exact identification of the virus code.

12.6.4. How to Disinfect Loaded DLLs and Running Applications

A loaded subsystem DLL is shared in memory and cannot be written to. The image can be disinfected in memory but not in the file itself because the disinfector cannot open the file for writes. The easiest solution to this particular problem is to build a list of such applications and ask the user to reboot. For instance, the disinfection can be done by a native disinfector even from user mode. A list of native Windows NT applications is executed even before any subsystem is loaded. Some of the standard Windows NT applications, such as AUTOCHK.EXE, are native applications.

An alternative solution is to build a scanner and disinfection system on top of Windows PE (Microsoft Windows Preinstallation Environment), which allows easy access to NTFS disks with clean memory. In fact, Windows PE allows many features that other systems cannot; however, WinPE needs a special license.

Yet another alternative is Bart Lagerweij's BartPE (also known as PE builder)12.

    Table of Contents
    Previous Section Next Section