Table of Contents
Previous Section Next Section

12.4. Memory Scanning in User Mode

The first question of memory scanning is how to access a particular process' data in memory. As discussed previously, process A cannot interfere with process B. How can a user-mode scanner read the contents of all the other processes? The answer is an API called ReadProcessMemory(). This API is usually used by debuggers to control the execution of the traced application by the debugger. The ReadProcessMemory() API needs a handle to a process, which can be gotten by the OpenProcess() API and the PROCESS_VM_READ access right. OpenProcess() needs the ID of a process. From where do we get a process ID?

The answer was not obvious for quite some time because the actual DLL (PSAPI.DLL) in which documented process enumeration APIs have been placed is not part of the standard Windows NT environment. (It was introduced later in Windows 2000.) The lack of PSAPI.DLL and the missing documentation suggested to me how NT actually does this itself. Because Task Manager and several other applications can display all the running processes and their IDs, it was obvious that it is possible to do so without PSAPI.DLL. In fact, it turns out that most APIs in PSAPI.DLL are just wrappers around native service APIs placed in NTDLL.DLL, such as NtQuerySystemInformation().

The native API set is not documented by Microsoft and is mostly used by subsystems. Most applications do not link to NTDLL.DLL directly for this reason. In fact, Microsoft suggests using the documented interfaces. However, Task Manager (TASKMGR.EXE) is linked to NTDLL.DLL directly, even if the information could be obtained by using performance data. Oh, well!

Task Manager uses the NtQuerySystemInformation() native API to get a list of all running processes and their process IDs. A user-mode application can link itself to NTDLL.DLL or simply use GetProcAddress() to get the address of the API to call it.

When the process ID of a particular process is available, ReadProcessMemory() can be used to read the actual address space of that particular application. To do so, a memory scanner should know the exact location of the used pages of an application. Fortunately, the VirtualQueryEx() function provides information about the range of pages within the virtual address space of a specified process. It needs an open handle to a process and returns the attributes and the sizes of regions.

It also needs PROCESS_QUERY_INFORMATION access for this operation. Free and reserved pages can easily be eliminated with this function, and those should not be accessed, but the rest must be checked. This can be done by using the ReadProcessMemory() API on those pages.

12.4.1. The Secrets of NtQuerySystemInformation()

NtQuerySystemInformation() (NtQSI) is not documented by Microsoft, and it is not necessary to use it because a user-mode application can link itself to PSAPI.DLL, which in turn will call NtQSI. As we will see later on, however, this function can be useful in a kernel-mode implementation of a memory scanner, so it is worth talking about it a bit.

NtQSI has four 32-bit (DWORD or ULONG) parameters.

The first parameter could be named SystemInformationClass. This parameter specifies the type of information to be returned by the function. (It has several possible values; 5 specifies the running process list query.)

The second parameter is the address of the returned buffer, which should be allocated by the caller; let's call this SystemInformationBuffer.

The third parameter is the allocated size in bytes. The fourth parameter is an optional value, PULONG BytesWritten, which is the number of bytes returned in the caller.

NtQSI() returns an NTSTATUS value. When the returned value is not STATUS_SUCCESS (0), it is usually STATUS_INFO_LENGTH_MISMATCH, which means that the allocated buffer length does not match the length required for the specified information class. Therefore, NtQSI() must be called with bigger and bigger buffers in a loop until the information can be placed into the allocated buffer completely by the Windows NT kernel.

On correct return, the necessary information is placed in the buffer in the form of a linked list. The first DWORD value specifies the relative pointer of the next process block information from the start of the buffer. The DWORD value at offset 0x44 of each block is the process ID (of course this position is dependent on the platform and is different on IA64). With this ID, several additional APIs can be called, which is why it is the most important.

After all of this, here is the "hand-made" definition for NtQuerySystemInformation():

    IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
    IN OUT PVOID SystemInformationBuffer,
    IN ULONG SystemInformationLength,

Other important information, such as the loaded images (EXE and DLLs) and their base addresses, can be examined by other native API calls by using this process ID, such as RtlQueryProcessDebugInformation(), which uses allocated buffers created by RtlCreateQueryDebugBuffer() and deallocated by RtlDestroyQueryDebugBuffer() APIs. Of course, these are all undocumented native APIs.

12.4.2. Common Processes and Special System Rights

On a typical Windows NTbased system, several processes are already running, even if the user has not logged in. The most important of these processes are the System Idle Process, the System Process, SMSS.EXE, CSRSS.EXE, WINLOGON.EXE, and SERVICES.EXE. A Windows NT scanner should scan all of these address spaces and also any other running processes executed by the user.

The trick is that some of these processes cannot be opened by OpenProcess() to get a handle for the other APIs with the necessary access. In Microsoft Press documentation7 (such as the Advanced Windows NT Third Edition), it is stated that some of the processes are secure processes and therefore cannot be opened for QUERY_INFORMATION or VM_WRITE operations. (These processes include WINLOGON.EXE, CLIPSRV.EXE, and EVENTLOG.EXE.) Such processes first need an additional system security privilege to be adjusted. (This information is missing from the Microsoft documentation.)

In particular, the SeDebugPrivilege privilege value must be adjusted to the SE_PRIVILEGE_ENABLED attribute. The SeDebugPrivilege is available to administrators and equivalent users or to anyone who has been granted this privilege by an administrator. However, even under an administrator account, the default attribute of this privilege is not enabled, so OpenProcess() will fail to open secured processes. To enable this privilege, the OpenProcessToken() function must specify TOKEN_ADJUST_PRIVILEGES, and then LookupPrivilegeValue() can be used to check whether the user has the privilege at all. If the user has the rights to do it, this privilege can be set to SE_PRIVILEGE_ENABLED by the AdjustTokenPrivileges() API.

It makes very good sense to protect some standard applications this way. For instance, Windows NT simply crashes if WINLOGON.EXE stops working. A modification caused by any user-mode application inside a random location of WINLOGON.EXE's address space could cause the system to crash! Of course, this would not be great. In any case, WINLOGON.EXE can even be written in memory when this privilege is enabled, but the privilege would have to be granted to all users first to scan such applications in memory. If WINLOGON.EXE were infected, the infected process could not be detected. Giving debug privileges to all users would definitely not make the system more secure. This is why a memory scanner is much better developed as a kernel-mode driver, where PROCESS_ALL_ACCESS is easily gained because drivers are running with the highest rights on a Windows NT machine.

12.4.3. Viruses in the Win32 Subsystem

This section introduces the different ways that viruses can become active as part of a particular process. Most 32-bit user-mode applications run in the Win32 subsystem, which is the most important subsystem of Windows NT. It is created and used by default and unlike the other subsystems, cannot be disabled. This is the subsystem in which Win32 viruses can be active.

The Win32 subsystem consists of the following major components: CSRSS.EXE (the environment subsystem process); the kernel-mode device driver WIN32K.SYS; and subsystem DLLs (such as USER32.DLL, ADVAPI32.DLL, GDI32.DLL, and KERNEL32.DLL), which translate documented Win32 API functions into the appropriate undocumented kernel-mode system service calls to NTOSKRNL.EXE and WIN32K.SYS. There is one other very important part of the Win32 subsystem: NTDLL.DLL, primarily for subsystem DLLs. NTDLL.DLL is used in the other subsystems of Windows NT also and by native applications that do not run in a subsystem. (Listing 12.2 shows some of the system processesthe loaded DLLs with their base addresses and sizes.)

Listing 12.2. Some System Executables with Their DLLs
PID: 0x0014

BaseAddress  Size        Name
0x023a0000   0x0000c000  \SystemRoot\System32\smss.exe
0x77f60000   0x0005c000  C:\WINNT\System32\ntdll.dll

PID: 0x001c

BaseAddress  Size        Name
0x5ffe0000   0x00005000  \??\C:\WINNT\system32\csrss.exe
0x77f60000   0x0005c000  C:\WINNT\System32\ntdll.dll
0x77e70000   0x00051000  C:\WINNT\system32\USER32.dll
0x77f00000   0x0005e000  C:\WINNT\system32\KERNEL32.dll
0x5f810000   0x00007000  C:\WINNT\system32\rpcltc1.dll

PID: 0x0022

BaseAddress  Size        Name
0x02880000   0x00030000  \??\C:\WINNT\SYSTEM32\winlogon.exe
0x77f60000   0x0005c000  C:\WINNT\System32\ntdll.dll
0x78000000   0x00048000  C:\WINNT\system32\MSVCRT.dll
0x77f00000   0x0005e000  C:\WINNT\system32\KERNEL32.dll
0x77dc0000   0x0003e000  C:\WINNT\system32\ADVAPI32.dll
0x77850000   0x0003a000  C:\WINNT\SYSTEM32\NETUI1.dll

12.4.4. Win32 Viruses That Allocate Private Pages

Some Win32 viruses allocate private pages for themselves with the PAGE_EXECUTE_READWRITE attribute. When the infected application is loaded, the virus code is activated from the executed application code. The virus then allocates new pages for its own use and moves its code there. Write access to those pages is important for the virus because it stores data in itself that must change, and read-only pages cannot be written to.

For instance, W32/Cabanas.3014.A8 allocates a 12,232-byte block that is represented as three pages (3*4,096=12,888 bytes) from the address space of the infected process (see Listing 12.3). Because Cabanas uses the MEM_TOP_DOWN flag when it allocates memory with the VirtualAlloc() function, the actual three pages will be available at the very end of the user address space, usually somewhere around the 0x7FFA0000 address.

Listing 12.3. W32/Cabanas at the Very End of the User Address Space
PID: 0x0051

BaseAddress  Size        Name
0x01b40000   0x00010000  C:\WINNT\system32\notepad.exe
0x77f60000   0x0005b000  C:\WINNT\System32\ntdll.dll
0x77d80000   0x00032000  C:\WINNT\system32\comdlg32.dll
0x77f00000   0x0005c000  C:\WINNT\system32\KERNEL32.dll
0x77e70000   0x00053000  C:\WINNT\system32\USER32.dll
0x77ed0000   0x0002b000  C:\WINNT\system32\GDI32.dll
0x77dc0000   0x0003e000  C:\WINNT\system32\ADVAPI32.dll
0x77e20000   0x0004f000  C:\WINNT\system32\RPCRT4.dll
0x77c40000   0x0013b000  C:\WINNT\system32\SHELL32.dll
0x77bf0000   0x0004f000  C:\WINNT\system32\COMCTL32.dll
0x779f0000   0x00046000  C:\WINNT\system32\MSVCRT.dll


Cabanas hooks some of the KERNEL32.DLL APIs to itself by patching the import table entries of the host program to its own routines. Whenever the host application calls any of the hooked APIs, the virus has the chance to replicate on the fly to another application or to call its directory stealth routines.

The W32/Parvo.138579 virus allocates 132,605 bytes from the address space of the infected process (more exactly: 33 pages, 135,168 bytes) because it needs a lot of memory for its polymorphic engine and for its communication modules.

W32/Parvo does not use the MEM_TOP_DOWN flag, so its allocated pages will be reserved from the first free gap of the user address space that is large enough (at address 0x002F0000 in the infected NOTEPAD.EXE in this particular example, as shown in Listing 12.4).

Listing 12.4. W32/Parvo Inside NOTEPAD's Address Space
PID: 0x004d

BaseAddress  Size         Name


0x01760000   0x00011000  C:\WINNT35\system32\NOTEPAD.EXE
0x77f80000   0x0004e000  C:\WINNT35\System32\ntdll.dll
0x77df0000   0x0002b000  C:\WINNT35\system32\comdlg32.dll
0x77f20000   0x00054000  C:\WINNT35\system32\KERNEL32.dll
0x77ea0000   0x00038000  C:\WINNT35\system32\USER32.dll
0x77ee0000   0x00033000  C:\WINNT35\system32\GDI32.dll

The virus code will be active with the name of the original infected and executed application. Only one copy of the virus is active at a time. The original host will be executed as the child process of the infected application under a random name, as shown in Listing 12.5.

Because the host program will be executed almost immediately, the virus can silently infect other applications from its own process and propagate itself to other locations with its communication module, based on the use of WSOCK32.DLL APIs.

Listing 12.5. W32/Parvo Runs Original NOTEPAD.EXE as JRWK.EXE
PID: 0x003c
BaseAddress  Size        Name
0x01760000   0x00011000  C:\WINNT35\SYSTEM32\JWRK.EXE
0x77f80000   0x0004e000  C:\WINNT35\System32\ntdll.dll
0x77df0000   0x0002b000  C:\WINNT35\system32\comdlg32.dll
0x77f20000   0x00054000  C:\WINNT35\system32\KERNEL32.dll
0x77ea0000   0x00038000  C:\WINNT35\system32\USER32.dll
0x77ee0000   0x00033000  C:\WINNT35\system32\GDI32.dll

12.4.5. Native Windows NT Service Viruses

A new class of Windows NT viruses activate by dropping executable images loaded as a native Windows NT service, as done by WNT/RemEx3 (commonly known as the RemoteExplorer). The RemEx virus runs as a user-mode service called ie403r.sys, as shown in Listing 12.6. The virus sleeps for a while and then wakes up and tries periodically to infect other applications.

Listing 12.6. WNT/RemEx Running as ie403r.sys Service
PID: 0x0036

BaseAddress  Size        Name
0x00400000   0x0002b000  C:\WINNT\system32\drivers\ie403r.sys
0x77f60000   0x0005b000  C:\WINNT\System32\ntdll.dll
0x77f00000   0x0005c000  C:\WINNT\system32\KERNEL32.dll
0x77e70000   0x00053000  C:\WINNT\system32\USER32.dll
0x77ed0000   0x0002b000  C:\WINNT\system32\GDI32.dll
0x77dc0000   0x0003e000  C:\WINNT\system32\ADVAPI32.dll
0x77e20000   0x0004f000  C:\WINNT\system32\RPCRT4.dll
0x77720000   0x00011000  C:\WINNT\system32\MPR.dll
0x77e10000   0x00007000  C:\WINNT\system32\rpcltc1.dll

12.4.6. Win32 Viruses That Use a Hidden Window Procedure

A few viruses such as { W32,W97M} /Beast.41472.A10 install a hidden window procedure for their own use and use a timer. Timers were available back in 16-bit Windows versions, and they were sometimes used to simulate multithreaded functionality. As explained in Chapter 3, "Malicious Code Environments," this virus runs as a complete process and uses OLE APIs to inject embedded macros and executable code (the binary virus code itself) into Office 97 documents. Because the virus can infect Office 97 documents from its active process, a macro virus-specific scanner and disinfector has a hard time removing it from documents if it cannot detect and terminate the virus in memory first.

12.4.7. Win32 Viruses That Are Part of the Executed Image Itself

W32/Heretic.1986.A was the first virus to infect KERNEL32.DLL correctly under Windows NT. KERNEL32.DLL is used by most applications; most of the crucial Win32 APIs are exported from it. When KERNEL32.DLL is infected, most executed applications will be attached to it because they need to call APIs from it.

Heretic patches the export address table of KERNEL32.DLL so that the CreateProcessA() and CreateProcessW() functions will point to the last section of the DLL where the virus code is placed, as shown in Listing 12.7.

Listing 12.7. W32/Heretic.1986.A Modifies the Export Address of CreateProcess APIs
image base  77F00000

00015385    59  CreateNamedPipeA
000153FA    60  CreateNamedPipeW
00017DB6    61  CreatePipe
0005E451    62  CreateProcessA -> (77F5E451)
0005E442    63  CreateProcessW -> (77F5E442)
00004F9A    64  CreateRemoteThread
0001C893    65  CreateSemaphoreA

When these functions are called by the host program, the virus has the chance to infect other applications on the fly. The virus enlarges the last section (.reloc) of KERNEL32.DLL and puts its code there, modifying the characteristics of that section to both MEM_EXECUTE and MEM_WRITE types. Listing 12.8 shows the virus code in memory at the end of an infected KERNEL32.DLL.

Listing 12.8. W32/Heretic.1986.A at the End of Infected KERNEL32.DLL in Memory

77f5e000 84 69 01 00 00 89 47 28 66 81 38 4d 5a 0f 85 52  .i....G(f.8MZ.àR
77f5e410 3f 01 75 06 3c 22 75 f6 eb 08 3c 20 74 04 0a c0 ?.u.<""u÷d.< t..+
77f5e420 75 ec c6 46 ff 00 8d 85 0c 15 40 00 89 47 08 e8 u81F..@..G.F
77f5e430 31 fb ff ff 57 ff 95 92 17 40 00 ff 95 92 17 40 1v W ...@. ...@
77f5e440 00 c3 68 34 84 f1 77 9c 60 e8 0a ff ff ff 61 9d .+h4ä±W£ `F.   a¥
77f5e450 c3 68 51 7f f1 77 9c 60 e8 56 ff ff ff 61 9d c3 +hQ±W.`FV   a.+
77f5e460 5b 48 65 72 65 74 69 63 5d 20 62 79 20 4d 65 6d [Heretic] by Mem
77f5e470 6f 72 79 20 4c 61 70 73 65 00 46 6f 72 20 6d 79 ory Lapse.For my

Another class of Win32 viruses stay active as part of an infected executable image, as done by the W32/Niko.5178 virus. (See Listing 12.9 for an illustration). The W32/Niko virus is activated from an infected portable executable (PE) application. The virus adds itself to the last section of the PE application and modifies the characteristics of the last section to MEM_WRITE. This allows the virus code to be modified in memory. The virus does not allocate memory for its full code but only for small data blocks whenever they are needed.

Listing 12.9. W32/Niko.5178 Virus in an Infected ASD.EXE Application in Page 0x0040F000

0040f000 e9 21 00 00 00 b8 97 01 41 00 c3 b8 c1 03 41 00 T!...+ù.A.++-.A.
0040f010 c3 e9 ba 48 ff ff b8 06 00 00 00 c3 e9 bf 10 00 +T1H  +....+T+..
0040f020 00 e9 d5 0e 00 00 e8 eb ff ff ff 50 e8 d4 ff ff .T+...Fd  PF+
00410190 d0 e9 5d ff ff ff 00 72 00 4e 49 43 4f 5f 56 49 -T] .r.NICO_VI
004101a0 52 5f 4f 46 46 00 4b 45 52 4e 45 4c 33 32 00 47 R_OFF.KERNEL32.G
004101b0 65 74 45 6e 76 69 72 6f 6e 6d 65 6e 74 56 61 72 etEnvironmentVar
004101c0 69 61 62 6c 65 41 00 4e 49 43 4f 5f 56 49 52 5f iableA.NICO_VIR_
004101d0 43 48 49 4c 44 5f 4f 46 46 00 7b 00 00 00 43 72 CHILD_OFF.{...Cr
004101e0 65 61 74 65 54 68 72 65 61 64 00 47 6c 6f 62 61 eateThread.Globa
004101f0 6c 41 6c 6c 6f 63 00 6c 73 74 72 63 70 79 00 47 lAlloc.lstrcpy.G
00410200 6c 6f 62 61 6c 46 72 65 65 00 6c 73 74 72 63 6d lobalFree.lstrcm
00410210 70 69 00 5c 2a 2e 2a 00 6c 73 74 72 63 61 74 00 pi.\*.*.lstrcat.
00410220 46 69 6e 64 46 69 72 73 74 46 69 6c 65 41 00 2e FindFirstFileA..

Niko is one of the first computer viruses to be multithreaded. The virus creates two threads for its own use, as shown in Listing 12.10. One is the trigger thread, which is supposed to display a message on a particular day; the other is the infection thread. The host program is executed after the virus creates the threads.

As long as the host program is running, the virus's infection thread will also be active. If the host application (main thread) terminates, all threads of the process will be killed by Windows NT, so the virus will be no longer active. The virus can replicate to other files only from those applications that are running and used for a longer time. In such a situation, the infection thread will infect other applications from the background.

Listing 12.10. W32/Niko.5178 Virus Creates Two Threads (68 and 123 in This Example)
117 asd.exe           Dtsactivation automatique (ASD)
CWD:     C:\LOOK\
VirtualSize:    20152 KB   PeakVirtualSize:     20192 KB
WorkingSetSize:  1604 KB   PeakWorkingSetSize:  1612 KB
NumberOfThreads: 3
122 Win32StartAddr:0x0040f000 LastErr:0x00000002     State:Waiting
68 Win32StartAddr:0x0040f021 LastErr:0x00000002 State:Waiting
123 Win32StartAddr:0x0040f01c LastErr:0x00000000 State:Waiting shp  0x00400000  ASD.EXE
   4.0.1381.130 shp  0x77f60000  ntdll.dll
   4.0.1381.133 shp  0x77e70000  USER32.dll
   4.0.1381.133 shp  0x77f00000  KERNEL32.dll

    Table of Contents
    Previous Section Next Section