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.
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()
NTSYSAPI NTSTATUS NTAPI NtOpenThread ( OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId OPTIONAL );
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:
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
PID: 0x03b0 (INETINFO.EXE) Threads: TID CTXSWITCH LOADADDR WIN32STR STATE 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.