GREPEXEC: Grepping Executive Objects from Pool Memory bugcheck chris@bugcheck.org 1) Foreword Abstract: As rootkits continue to evolve and become more advanced, methods that can be used to detect hidden objects must also evolve. For example, relying on system provided APIs to enumerate maintained lists is no longer enough to provide effective cross-view detection. To that point, scanning virtual memory for object signatures has been shown to provide useful, but limited, results. The following paper outlines the theory and practice behind scanning memory for hidden objects. This method relies upon the ability to safely reference the Windows system virtual address space and also depends upon the building and locating effective memory signatures. Using this method as a base, suggestions are made as to what actions might be performed once objects are detected. The paper also provides a simple example of how object-independent signatures can be built and used to detect several different kernel objects on all versions of Windows NT+. Due to time constraints, the source code associated with this paper will be made publicly available in the near future. Thanks: Thanks to skape, Peter, and the rest of the uninformed hooligans; you guys and gals rock! Disclaimer: The author is not responsible for how the papers contents are used or interpreted. Some information may be inaccurate or incorrect. If the reader feels any information is incorrect or has not been properly credited please contact the author so corrections can be made. All content refers to the Windows XP Service Pack 2 platform unless otherwise noted. 2) Introduction As rootkits become increasingly popular and more sophisticated than ever before, detection methods must also evolve. While rootkit technologies have evolved beyond API hooking methods, detectors have also evolved beyond the hook detection ages. At first rootkits such as FU were detected using various methods which exploited its weak and proof-of-concept design by applications such as Blacklight. These specific weaknesses were addressed in FUTo. However, some still remain excluding the topic of this paper. RAIDE, a rootkit detection tool, uses a memory signature scanning method in order to find EPROCESS blocks hidden by FUTo. This specific implementation works, however, it too has its weaknesses. This paper attempts to outline the general concepts of implementing a successful rootkit detection method using memory signatures. The following chapters will discuss how to safely enumerate system memory, what to look for when building a memory signature, what to do once a memory signature has been found, and potential methods of breaking memory signatures. Finally, an accompanying tool will be used to concretely illustrate the subject of this paper. After reading the following paper, the reader should have an understanding of the concepts and issues related to kernel object detection using memory signatures. The author believes this to be an acceptable method of rootkit detection. However, as with most things in the security realm, no one technique is the ultimate solution and this technique should only be considered complimentary to other known detection methods. 3) Scanning Memory Enumerating arbitrary system memory is nowhere near a science since its state can change at anytime while you are attempting to access it. While this is true, the memory that surrounds kernel executive objects should be fairly consistent. With proper care, memory accesses should be safe and the chance of false positives and negatives should be fairly minimal. The following sections will outline a safe method to enumerate the contents of both the system's PagedPool and NonPagedPool. 3.1) Retrieving Pool Ranges For the purpose of enumerating pool memory it is unnecessary to enumerate the entire system address space. The system maintains a few global variables such as nt!MmPagedPoolStart, nt!MmPagedPoolEnd and related NonPagedPool variables that can be used in order to speed up a search and reduce the possibility of unnecessary false positives. Although these global variables are not exported, there are a couple ways in that they can be obtained. The most reliable method on modern systems (Windows XP Service Pack 2 and up) is through the use of the KPCR->KdVersionBlock pointer located at fs:[0x34]. This points to a KDDEBUGGER_DATA64 structure which is defined in the Debugging Tools For Windows SDK header file wdbgexts.h. This structure is commonly used by malicious software in order to gain access to non-exported global variables to manipulate the system. A second method to obtain PagedPool values is to reference the per-session nt!_MM_SESSION_SPACE found at EPROCESS->Session. This contains information about the session owning the process, including its ranges and many other PagedPool related values shown here. kd> dt nt!_MM_SESSION_SPACE +0x01c NonPagedPoolBytes : Uint4B +0x020 PagedPoolBytes : Uint4B +0x024 NonPagedPoolAllocations : Uint4B +0x028 PagedPoolAllocations : Uint4B +0x044 PagedPoolMutex : _FAST_MUTEX +0x064 PagedPoolStart : Ptr32 Void +0x068 PagedPoolEnd : Ptr32 Void +0x06c PagedPoolBasePde : Ptr32 _MMPTE +0x070 PagedPoolInfo : _MM_PAGED_POOL_INFO +0x244 PagedPool : _POOL_DESCRIPTOR While enumerating the entire system address space is not preferable, it can still be used in situations where pool information cannot be obtained. The start of the system address space can be assumed to be any address above nt!MmHighestUserAddress. However, it would appear that an even safer assumption would be the address following the LARGE_PAGE where ntoskrnl.exe and hal.dll are mapped. This can be obtained by using any address exported by hal.dll and rounding up to the nearest large page. 3.2) Locking Memory When accessing arbitrary memory locations, it is important that pages be locked in memory prior to accessing them. This is done to ensure that accessing the page can be done safely and will not cause an exception due to a race condition, such as if it were to be de-allocated between a check and a reference. The system provides a routine to lock pages named nt!MmProbeAndLockPages. This routine can be used to lock either pagable or non-paged memory. Since physical pages maintain a reference count in the nt!MmPfnDatabase there is no worry of an outside source unlocking the pages and having them page out to disk or become invalid. In order to use MmProbeAndLockPages, a caller must first build an MDL structure using something such as nt!IoAllocateMdl or nt!MmInitializeMdl. The MDL creation routines are passed a virtual address and length describing the block of virtual memory to be referenced. On a successful call to nt!MmProbeAndLockPages, the virtual address range described by the MDL structure is safe to access. Once the block is no longer needed to be accessed, the pages must be unlocked using nt!MmUnlockPages. A trick can be used to further reduce the number of pages locked when enumerating the NonPagedPool. As documented, MmProbeAndLockPages can be called at DISPATCH_LEVEL with the limitation of it only being allowed to lock resident memory pages and failing otherwise, which is a desirable side-effect in this case. 4) Detecting Executive Objects In general, all of the executive components of the NT kernel rely on the object manager in order to manage the objects they allocate. All objects allocated by the object manager have a common header named OBJECT_HEADER and additional optional headers such as OBJECT_HEADER_NAME_INFO, process quota information, and handle trace information. Let's take a look to see what is common to all executive objects and how we can use the pool block header information to identify an allocated executive object. Lastly, some object specific information will be discussed in terms of generating a useful memory signature for an object. 4.1) Generic Object Information Since the OBJECT_HEADER is common to all objects, let's look at it in detail. A static field here refers to all objects of specific type, not all executive objects in the system. kd> dt _OBJECT_HEADER +0x000 PointerCount : Int4B +0x004 HandleCount : Int4B +0x004 NextToFree : Ptr32 Void +0x008 Type : Ptr32 _OBJECT_TYPE +0x00c NameInfoOffset : UChar +0x00d HandleInfoOffset : UChar +0x00e QuotaInfoOffset : UChar +0x00f Flags : UChar +0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION +0x010 QuotaBlockCharged : Ptr32 Void +0x014 SecurityDescriptor : Ptr32 Void +0x018 Body : _QUAD -------------------+------------+------------------------------------- PointerCount | Variable | of references HandleCount | Variable | of open handles NextToFree | NotValid | Used when freed Type | Static | Pointer to OBJECTTYPE NameInfoOffset | Static | 0 or offset to related header HandleInfoOffset | Static | 0 or offset to related header QuotaInfoOffset | Static | 0 or offset to related header Flags | NotCertain | Not certain ObjectCreateInfo | Variable | Pointer to OBJECTCREATEINFORMATION QuotaBlockCharged | NotCertain | Not certain SecurityDescriptor | Variable | Pointer to SECURITYDESCRIPTOR Body | NotValid | Union with the actual object -------------------+------------+------------------------------------- From this it is assumed that the most reliable and unique signature is the Type field of the OBJECT_HEADER which could be used in order to identify objects of a specific type such as EPROCESS, ETHREAD, DRIVER_OBJECT, and DEVICE_OBJECT objects. 4.2) Validating Pool Block Information Kernel pool management appears to be slightly different from usermode heap management. However, if one assumes that the only concern is dealing with pool memory allocations which are less then PAGE_SIZE, it is fairly similar. Each call to ExAllocatePoolWithTag() returns a pre-buffer header as follows: kd> dt _POOL_HEADER +0x000 PreviousSize : Pos 0, 9 Bits +0x000 PoolIndex : Pos 9, 7 Bits +0x002 BlockSize : Pos 0, 9 Bits +0x002 PoolType : Pos 9, 7 Bits +0x000 Ulong1 : Uint4B +0x004 ProcessBilled : Ptr32 _EPROCESS +0x004 PoolTag : Uint4B +0x004 AllocatorBackTraceIndex : Uint2B +0x006 PoolTagHash : Uint2B For the purposes of locating objects, the following is a breakdown of what could be useful. Again, static refers to fields common between similar executive objects and not all allocated POOL_HEADER structures. ------------------------+------------+---------------------------------- PreviousSize | Variable | Offset to previous pool block PoolIndex | NotCertain | Not certain BlockSize | Static | Size of pool block PoolType | Static | POOL_TYPE Ulong1 | Union | Padding, not valid ProcessBilled | Variable | Allocator EPROCESS when no Tag specified PoolTag | Static | Pool Tag (ULONG) AllocatorBackTraceIndex | NotCertain | Not certain PoolTagHash | NotCertain | Not certain ------------------------+------------+---------------------------------- The POOL_HEADER contains several fields that appear to be common to similar objects which could be used to further verify the likelihood of locating an object of a specific type such as BlockSize, PoolType, and PoolTag. In addition to the mentioned static fields, two other fields, PreviousSize and BlockSize, can be used to validate that the currently assumed POOL_HEADER appears to be a valid, allocated pool block and is in one of the pool managers maintained link lists. PreviousSize and BlockSize are multiples of the minimum pool alignment which is 8 bytes on a 32bit system and 16 bytes on a 64bit system. These two elements supply byte offsets to the neighboring pool blocks. If PreviousSize equals 0, the current POOL_HEADER should be the first pool block in the pool's contiguous allocations. If it is not, it should be the same as the previous POOL_HEADERs BlockSize. The BlockSize should never equal 0 and should always be the same as the proceeding POOL_HEADERs PreviousSize. The following code validates a POOL_HEADER of an allocated pool block. // // Assumes BlockOffset < PAGE_SIZE // ASSERTS Flink == Flink->Blink && Blink == Blink->Flink // BOOLEAN ValidatePoolBlock ( IN PPOOL_HEADER pPoolHdr, IN VALIDATE_ADDR pValidator ) { BOOLEAN bReturn = FALSE; PPOOL_HEADER pPrev; PPOOL_HEADER pNext; pPrev = (PPOOL_HEADER)((PUCHAR)pPoolHdr - (pPoolHdr->PreviousSize * sizeof(POOL_HEADER))); pNext = (PPOOL_HEADER)((PUCHAR)pPoolHdr + (pPoolHdr->BlockSize * sizeof(POOL_HEADER))); if (( ( pPoolHdr == pNext ) ||( pValidator( pNext + sizeof(POOL_HEADER) - 1 ) && pPoolHdr->BlockSize == pNext->PreviousSize ) ) && ( ( pPoolHdr != pPrev ) ||( pValidator( pPrev ) && pPoolHdr->PreviousSize == pPrev->BlockSize ) )) { bReturn = TRUE; } return bReturn; } 4.3) Object Specific Signatures So far a few useful signatures have been shown which apply to all executive objects and could be used to identify them in memory. For some cases these may be enough to be effective. However, in other cases, it may be necessary to examine information within the object's body itself in order to identify them. It should be noted that some objects of interest may be clearly defined and documented while others may not be. Furthermore, executive object definitions may vary between OS versions. The following subsections briefly outline obvious memory signatures for a few objects which generally are of interest when identifying rootkit-like behavior. A few examples of object-specific signatures will also be discussed, some of which have been used in previous work. 4.3.1) Process Objects Here are just a few of the most basic EPROCESS fields which can form a simple signature using rather predictable constant values which hold true for all EPROCESS structures in the same system. -----------------------------+------------------------------------------ Pcb.Header.Type | Dispatch header type number Pcb.Header.Size | Size of dispatcher object Pcb.Affinity | CPU affinity bit mask, typically CPU in system Pcb.BasePriority | Typically the default of 8 Pcb.ThreadQuantum | Workstations is typically 18 ExitTime | 0 for running processes UniqueProcessId | 0 if bitwise AND with 0xFFFF0002 SectionBaseAddress | Typically 0x00400000 for non-system executables InheritedFromUniqueProcessId | Same as UniqueProcessId, typically a valid running pid Session | Unique on a per-session basis ImageFileName | Printable ASCII, typically ending in '.exe' Peb | 0x7FF00000 if bitwise AND with 0xFFF00FFF SubSystemVersion | XP Service Pack 2 is 0x400 -----------------------------+------------------------------------------ Note that there are several other DISPATCH_HEADERs embedded within locks, events, timers, etc in the structure which also have a predicable Header.Type and Header.Size. 4.3.2) Thread Objects Here are just a few of the most basic ETHREAD fields which can form a simple signature using rather predictable constant values which hold true for all ETHREAD structures in the same system. ------------------+------------------------------------------------------ Tcb.Header.Type | Dispatch header type number Tcb.Header.Size | Size of dispatcher object Teb | 0x7FF00000 if bitwise AND with 0xFFF00FFF BasePriority | Typically the default of 8 ServiceTable | nt!KeServiceDescriptorTable(Shadow) used by RAIDE Affinity | CPU affinity bit mask, typically CPU in system PreviousMode | 0 or 1, which is KernelMode or UserMode Cid.UniqueProcess | 0 if bitwise AND with 0xFFFF0002 Cid.UniqueThread | 0 if bitwise AND with 0xFFFF0002 ------------------+------------------------------------------------------ Note that there are several other DISPATCH_HEADERs embedded within locks, events, timers, etc in the structure which also have a predicable Header.Type and Header.Size. 4.3.3) Driver Objects A tool written previously named MODGREPPER by Joanna Rutkowska of invisiblethings.org used a signature based approach to detect hidden DRIVER_OBJECTs. This signature was later 'broken' by valerino described in a rootkit.com article titled "Please don't greap me!". Listed here are a few fields which a signature could be built upon to detect DRIVER_OBJECTs. --------------+----------------------------------------------------------- Type | I/O Subsystem structure type ID, should be 4 Size | Size of the structure, should be 0x168 DeviceObject | Pointer to a valid first created device object(can be NULL) DriverSection | Pointer to a nt!_LDR_DATA_TABLE_ENTRY structure DriverName | A UNICODE_STRING structure containing the driver name --------------+----------------------------------------------------------- The following fields of the DRIVER_OBJECT can be validated by assuring they fall within the range of a loaded driver image such that: DriverStart < FIELD < DriverStart + DriverSize. --------------------+---------------------------------------------------- DriverInit | Address of DriverEntry() function DriverUnload | Address of DriverUnload() function, can be NULL MajorFunction[0x1c] | Dispatch handlers for IRPMJXXX, can default to ntoskrnl.exe --------------------+---------------------------------------------------- 4.3.4) Device Objects For the DEVICE_OBJECT structure there are few static signatures which are usable. Here are the only obvious ones. -------------+---------------------------------------------------------- Type | I/O Subsystem structure type ID, should be 3 Size | Size of the structure, should be 0xb8 DriverObject | Pointer to a valid driver object -------------+---------------------------------------------------------- Note that the DriverObject field must be valid in order for the device to function. 4.3.5) Miscellaneous So far the memory signatures discussed have been fairly straightforward and for the most part are simply a binary comparison with a specific value. Later in this paper, a technique called N-depth pointer validation will be discussed as a method of developing a more effective signature in situations where pointer based memory signatures are attempted to be evaded. Another way of considering an object field as a signature is to validate it in terms of its characteristics instead of by its value. A common example of this would be to validate an object field LIST_ENTRY. Validating a LIST_ENTRY structure can be done as follows: Entry == Entry->Flink->Blink == Entry->Blink->Flink. A pointer to any object or memory allocation can also be checked using the function shown previously, named ValidatePoolBlock. Even a UNICODE_STRING.Buffer can be validated this way provided the allocation is less than PAGE_SIZE. 5) Found An Object, Now What? The question of what to do after potentially identifying an executive object through a signature depends on what the underlying goal is. For the purpose of a the sample utility included with this paper, the goal may be to simply display some information about the objects as it finds them. In the context of a rootkit detector, however, there may be many more steps that need to be taken. For example, consider a detector looking for EPROCESS blocks which have been unlinked from the process linked list or a driver module hidden from the system service API. In order to determine this, some cross-view comparisons of the raw objects detected and the output from an API call or a list enumeration is needed. Detectors must also take into consideration the race condition of an object being created or destroyed in between the memory enumeration and the acquisition of the "known to the system" data. Additionally, it may be desired that some additional sanity checks be performed on these objects in addition to the signature. Do the object fields x,y,z contain valid pointers? Is field c equal to b? Does this object appear to be valid however has signs of tampering in order to hide it? Does the number of detected objects match up with a global count value such as the one maintained in an OBJECT_TYPE structure? The following sections will briefly mention some random thoughts of what to do with a suspected object of the four types previously mentioned in this paper in Chapter 4. 5.1) Process Objects Here is a brief list of things to check when scanning for EPROCESS objects. 1. Compare against a high level API such as kernel32!CreateToolhelp32Snapshot. 2. Compare against a system call such as nt!NtQuerySystemInformation. 3. Compare against the EPROCESS->ActiveProcessLinks list. 4. Does the process have a valid list of threads? 5. Can PsLookupProcessByProcessId open its 6. UniqueProcessId? 7. Is ImageFileName a valid string? zeroed? garbage? 5.2) Thread Objects Here is a brief list of things to check when scanning for ETHREAD objects. 1. Compare against a high level API such as kernel32!CreateToolhelp32Snapshot. 2. Compare against a system call such as nt!NtQuerySystemInformation. 3. Does the process have a valid owning process? 4. Can PsLookupThreadByThreadId open its 5. Cid.UniqueThread? 6. What does Win32StartAddress point to? Is it a valid module address? 7. What is its ServiceTable value? 8. If it is in a wait state, for how long? 9. Where is its stack? What does its stack trace look like? 5.3) Driver Objects Here is a brief list of things to check when scanning for DRIVER_OBJECT objects. 1. Compare against services found in the service control manager database. 2. Compare against a system call such as nt!NtQuerySystemInformation. 3. Is the object in the global system namespace? 4. Does the driver own any valid device objects? 5. Does the drive base address point to a valid MZ header? 6. Do the object's function pointer fields look correct? 7. Does DriverSection point to a valid nt!LDRDATATABLEENTRY? 8. Does DriverName or the 9. LDR_DATA_TABLE_ENTRY have valid strings? zeroed? garbage? 5.4) Device Objects Here is a brief list of things to check when scanning for DEVICE_OBJECT objects. 1. Is the owning driver object valid? 2. Is the device named and is it mapped into the global namespace? 3. Does it appear to be in a valid device stack? 4. Are its Type and Size fields correct? 6) Breaking Signatures Memory signatures can be an effective method of identifying allocated objects and can serve as a low level baseline in order to detect objects hidden by several different methods. Although the memory signature detection method may be effective, it doesn't come without its own set of problems. Many signatures can be evaded using several different techniques and non-evadable signatures for objects, if any exist, have yet to be explored. The following sections discuss issues and counter measures related to defeating memory signatures. 6.1) Pointer Based Signatures Using a memory signature which is a valid pointer to some common object or static data is a very appealing signature to use for detection due to its reliability, however is also an easy signature to bypass. The following demonstrates the most simplistic method of bypassing the OBJECT_HEADER->Type signature this paper uses as a generic object memory signature. This is possible because the OBJECT_TYPE is just an allocated structure of fairly stable data. Many pointer based signatures with similar static characteristics are open to the same attack. NTSTATUS KillObjectTypeSignature ( IN PVOID Object ) { NTSTATUS ntStatus = STATUS_SUCESS; PVOID pDummyObject; POBJECT_HEADER pHdr; pHdr = OBJECT_TO_OBJECT_HEADER( Object ); pDummyObject = ExAllocatePool( sizeof(OBJECT_TYPE) ); RtlCopyMemory( pDummyObject, pHdr->Type, sizeof(OBJECT_TYPE) ); pHdr->Type = pDummyObject; return STATUS_SUCCESS; } 6.2) N-Depth Pointer Validation As demonstrated in the previous section, pointer based signatures are effective. However, in some cases, they may be trivial to bypass. The following code demonstrates an example which does what this paper refers to as N-depth pointer validation in an attempt to create a more complex, and potentially more difficult to bypass, signature using pointers. The following example is also evadable using the same principal of relocation shown above. The algorithm assumes a given address is an executive object and attempts validation by performing the following steps: 1. Calculates an assumed OBJECT_HEADER 2. Assumes pObjectHeader->Type is an OBJECT_TYPE 3. Calculates an assumed OBJECT_HEADER for the OBJECT_TYPE 4. Assumes pObjectHeader->Type is nt!ObpTypeObjectType 5. Validates pTypeObject->TypeInfo.DeleteProcedure == nt!ObpDeleteObjectType BOOLEAN ValidateNDepthPtrSignature ( IN PVOID Address, IN VALIDATE_ADDR pValidate ) { PVOID pObject; POBJECT_TYPE pTypeObject; POBJECT_HEADER pHdr; pHdr = OBJECT_TO_OBJECT_HEADER( Address ); if( ! pValidate(pHdr) || ! pValidate(&pHdr->Type) ) return FALSE; // Assume this is the OBJECT_TYPE for this assumed object pTypeObject = pHdr->Type; // OBJECT_TYPE's have headers too pHdr = OBJECT_TO_OBJECT_HEADER( pTypeObject ); if( ! pValidate(pHdr) || ! pValidate(&pHdr->Type) ) return FALSE; // OBJECT_TYPE's have an OBJECT_TYPE of nt!ObpTypeObjectType pTypeObject = pHdr->Type; if( ! pValidate(&pTypeObject->TypeInfo.DeleteProcedure) ) return FALSE; // \ObjectTypes\Type has a DeleteProcedure of nt!ObpDeleteObjectType if( pTypeObject->TypeInfo.DeleteProcedure != nt!ObpDeleteObjectType ) return FALSE; return TRUE; } 6.3) Miscellaneous An obvious method of preventing detection from memory scanning would be to use what is commonly referred to as the Shadow Walker memory subversion technique. If virtual memory is unable to be read then of course a memory scan will skip over this area of memory. In the context of pool memory, however, this may not be an easy attack since it may create a situation where the pool appears corrupted which could lead to crashes or system bugchecks. Of course, attacking a function like nt!MmProbeAndLockPages or IoAllocateMdl globally or specifically in the import address table of the detector itself would work. For memory signatures based on constant or predicable values it may be feasible to either zero out or change these fields and not disturb system operation. For example take the author's enhancements to the FUTo rootkit where it is seen that the EPROCESS->UniqueProcessId can be safely cleared to 0 or previously mentioned rootkit.com article titled "Please don't greap me!" which clears DRIVER_OBJECT->DriverName and its associated buffer in order to defeat MODGREPPER. For the case of some pointer signatures a simple binary comparison may not be enough to validate it. Take the above example and using nt!ObpDeleteObjectType. This could be defeated by overwriting pTypeObject->TypeInfo.DeleteProcedure to point to a simple jump trampoline which is allocated elsewhere which simple jumps back to nt!ObpDeleteObjectType. 7) GrepExec: The Tool Included with this paper is a proof-of-concept tool complete with source which demonstrates scanning the pool for signatures to detect executable objects. Objects detected are DRIVER_OBJECT, DEVICE_OBJECT, EPROCESS, and ETHREAD. The tool does nothing to determine if an object has been attempted to be hidden in any way. Instead, it simply displays found objects to standard output. At this time the author has no plans to continue work with this specific tool, however, there are plans to integrate the memory scanning technique into another project. The source code for the tool can be easily modified to detect other signatures and/or other objects. 7.1) The Signature For demonstration purposes the signature used is simple. All objects are allocated in NonPagedPool so only non-paged memory is enumerated for the search. The signature is detected as follows: 1. Enumeration is performed by assuming the start of a pool block. 2. The signature offset is added to this pointer. 3. The assumed signature is compared with the OBJECT_HEADER->Type for the object type being searched for. 4. The assumed POOL_HEADER->PoolType is compared to the objects known pool type. 5. The assumed POOL_HEADER is validated using the function from section , ValidatePoolBlock. The following is the function which sets up the parameters in order to perform the pool enumeration and validation of a block by a single PVOID signature. On a match, a callback is made using the pointer to the start of the matching block. As an alternative to the PVOID signature, the poolgrep.c code can easily be modified to accept either a structure to several signatures and offsets or a validation function pointer in order to perform a more complex signature validation. NTSTATUS ScanPoolForExecutiveObjectByType ( IN PVOID Object, IN FOUND_BLOCK_CB Callback, IN PVOID CallbackContext ) { NTSTATUS ntStatus = STATUS_SUCCESS; POBJECT_HEADER pObjHdr; PPOOL_HEADER pPoolHdr; ULONG_PTR blockSigOffset; ULONG_PTR blockSignature; pObjHdr = OBJECT_TO_OBJECT_HEADER( Object ); pPoolHdr = OBJHDR_TO_POOL_HEADER( pObjHdr ); blockSigOffset = (ULONG_PTR)&pObjHdr->Type - (ULONG_PTR)pObjHdr + OBJHDR_TO_POOL_BLOCK_OFFSET(pObjHdr); blockSignature = (ULONG_PTR)pObjHdr->Type; (VOID)ScanPoolForBlockBySignature( pPoolHdr->PoolType - 1, 0, // pPoolHdr->PoolTag OPTIONAL, blockSigOffset, blockSignature, Callback, CallbackContext ); return ntStatus; } 7.2) Usage GrepExec usage is pretty straightforward. Here is the output of the help command. ********************************************************** GREPEXEC 0.1 * Grepping executive objects from the pool * Author: bugcheck Built on: May 30 2006 ********************************************************** Usage: grepexec.exe [options] --help, -h Displays this information --install, -i Manually install driver --uninstall, -u Manually uninstall driver --status, -s Display installation status --process, -p GREP process objects --thread, -t GREP thread objects --driver, -d GREP driver objects --device, -e GREP device objects 7.3) Sample Output The standard output is also straight forward. Here is a sample of each supported command. C:\grepexec>grepexec.exe -p EPROCESS=81736C88 CID=0354 NAME: svchost.exe EPROCESS=8174E238 CID=0634 NAME: explorer.exe EPROCESS=81792020 CID=027c NAME: winlogon.exe ... C:\grepexec>grepexec.exe -t EPROCESS=817993C0 ETHREAD=815D4A58 CID=0778.077c wscntfy.exe EPROCESS=8174AA88 ETHREAD=815D6860 CID=0408.0678 svchost.exe EPROCESS=819CA830 ETHREAD=815F3B30 CID=0004.0368 System EPROCESS=81792020 ETHREAD=81600398 CID=027c.0460 winlogon.exe ... C:\grepexec>grepexec.exe -d DRIVER=81722DA0 BASE=F9B5C000 \FileSystem\NetBIOS DRIVER=819A4B50 BASE=F983D000 \Driver\Ftdisk DRIVER=81725DA0 BASE=00000000 \Driver\Win32k DRIVER=81771880 BASE=F9EB4000 \Driver\Beep ... C:\grepexec>grepexec.exe -e DEVICE=81733860 \Driver\IpNat NAME: IPNAT DEVICE=81738958 \Driver\Tcpip NAME: Udp DEVICE=817394B8 \Driver\Tcpip NAME: RawIp DEVICE=81637CE0 \FileSystem\Srv NAME: LanmanServer ... 8) Conclusion From reading this paper the reader should have a good understanding of the concepts and issues related to scanning memory for signatures in order to detect objects in the system pool. The reader should be able to enumerate system memory safely, construct their own customized memory signatures, locate signatures in memory, and implement their own reporting mechanism. It is obvious that object detection using memory scanning is no exact science. However, it does provide a method which, for the most part, interacts with the system as little as possible. The author believes that the outlined technique can be successfully implemented to obtain acceptable results in detecting objects hidden by rootkits. Bibliography Blackhat.com. RAIDE: Rootkit Analysis Identification Elimination. http://www.blackhat.com/presentations/bh-europe-06/bh-eu-06-Silberman-Butler.pdf; Accessed May. 30, 2006. F-Secure. Blacklight. http://www.f-secure.com/blacklight/; Accessed May. 30, 2006. Invisiblethings.org. MODGREPPER. http://www.invisiblethings.org/tools.html; Accessed May. 30, 2006. Phrack.org. Shadow Walker. http://www.phrack.org/phrack/63/p63-0x08_Raising_The_Bar_For_Windows_Rootkit_Detection.txt; Accessed May. 30, 2006. Rootkit.com. FU. http://rootkit.com/project.php?id=12; Accessed May. 30, 2006. Rootkit.com. Please don't greap me!. http://rootkit.com/newsread.php?newsid=316; Accessed May. 30, 2006. Uninformed.org. futo. http://uninformed.org/?v=3&a=7&t=sumry; Accessed May. 30, 2006. Windows Hardware Developer Central. Debugging Tools for Windows. http://www.microsoft.com/whdc/devtools/debugging/default.mspx; Accessed May. 30, 2006.