mirror of https://github.com/fdiskyou/Zines.git
822 lines
34 KiB
Plaintext
822 lines
34 KiB
Plaintext
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.
|