---------------------------------------------------------------------
Notes from "Windows NT System-Call Hooking" (Dr. Dobb's Journal, '97)
---------------------------------------------------------------------
Intro : kernel hooks (from "Rootkits : Subverting the Windows kernel")
- kernel mem : high virtual address memory region
- x86 : kernel mem resides in region of mem 0x8000000 and above
-- if use /3GB boot config, kernel mem starts at 0xC0000000
- processes can't access kernel mem
-- exception : when a process has debug privileges or
when a call gate has been installed
- rootkit : can access krenel memory IF implements a device driver
-- then rootkit operating in ring 0
Core components of NT : ntoskrnl (ntoskrnl.exe), win32k (win32k.sys)
- NTOSKRNL and WIN32K : are part of the kernel
- win32k : added to kernel in NT 4.0
-- implements graphical system services
-- much of win32 API's graphics engine moved to kernel to boost perf
-- that fxnality was previously implemented in user mode by gdi32.dll
and user32.dll
- NTDLL.DLL : also exists at user level, sits on top of NTOSKRNL
- win32 (API) : consists of kernel32.dll, gdi32.dll, user32.dll
- win32 (API) : sits on top of NTDLL.DLL, exists at user level
-- win32 subsystem
-- when an app makes a call to a win32 (api) function, usually the
called DLL will in the end call upon native NT services provided by
NTOSKRNL or WIN32K
- invoke NTOSKRNL via calling to NTDLL.DLL
- for example CreateFile(...) is a function exported by kernel32.dll
- but this function calls several kernel functions depending upon
the values of the flags passed to CreateFile(...)
- call to kernel to see whether desired file already exists
- call to kernel to create or open the file
- call to kernel to get the new/opened file's attributes
User-level subsystems : win32 (by which we mean the win32 api), POSIX, OS/2
NTDLL.DLL : exports NTOSKRNL to user-level subsystems
- populates EAX register with system call number of kernel function
- then executes system call trap (int 2Eh for x86 NT)
- provides a very thin wrapper on kernel services
- for example the ntdll.dll function ZwCreateFile(...) is invoked by CreateFile(...)
-- ZwCreateFile(...) disassembled
mov eax, 17h // system call # is 0x17 for NT (0x20 for 2k, 0x25 for XP)
lea edx, [esp+4] // make edx point to function params (user-mode stack)
// lea : loads edx with address of user args
int 2Eh // execute sys call trap for NT, 2k on x86
// (SYSENTER is used for XP,2003 on x86)
ret 2Ch
- ZwCreateFile(...) has alias NtCreateFile(...) - either can be used
-- this is true for all Zw* calls :
- for user-mode programs, either of these two can be used interchangeably
-- each refers to the same entry point in ntdll.dll
- for kernel-mode programs, these are linked against ntoskrnl.exe,
not against ntdll.dll; so the different forms (NtXxx and ZwXxx)
refer to different entry points
ZwXxx : contains copy of the code from ntdll.dll
- reenters the kernel, uses the system service dispatcher
- system service dispatcher sets previous mode to be kernel mode
- then when the actual code of the system service is exec'd,
much of the preamble is skipped
- b/c preamble code includes privilege checks etc which will
succeed regardless of ACLs on any object since previous mode
== kernel mode
NtXxx : results depend on the previous mode
- kernel mode code has little control over what previous mode is here
- if service has any params that are pointers and these pointers
point to automatic or static vars in the kernel mode program
then if previous mode is user mode then call will fail
- some fraction of native API calls are defined in <ntddk.h>
- and in general user code can access ntdll functions directly (though
the official documentation on these is sparse)
- Trapping to kernel-mode ( x86 ):
- there is no mode of x86 cpu called "kernel-mode"
- contrast : Motorola 68000 which has a flag in a status regiter that tells
the cpu if it is currently exec'ing in user-mode or supervisor-mode
- for x86 : the privilege level of the code segment that is currently
executing determinies the privilege level of the executing program
- the kernel finds the addy of the service (function) to handle the call
by looking at the executing thread's Thread Environment Block (TEB)
- this contains a thread's registers, priority level, a pointer to its
process, and a pointer to the thread's Service Table List
- The Service Table List contains a pointer to a table that contains the
addresses of all kernel services
-- so Service Table List is a data structure, contains :
- a pointer to the NTOSKRNL call table
- the # of NTOSKRNL syscalls
- a pointer to the NTOSKRNL arg table
- a pointer to the win32k call table
- the # of win32k syscalls
- a pointer to the win32k arg table
- the appropriate address is determined via returning the entry in that
table at the location specified by eax * 4 (each entry is 4 bytes long)
-- kernel's system call trap handler does this (& makes sure sys call #
is a valid one -- i.e. is within the range covered by this table)
- there is a parallel table which contains the size of each function's
arguments in bytes
- win32 (api) "system calls" have "system call numbers" that start at
0x1000 whereas kernel system call numbers start at 0
- kernel's system call trap handler then gets the address of the service
it must call
- then kernel's system call trap handler reads how many bytes are
required by this function's arguments -- this will be what that handler
pushes onto its stack from the caller's stack as it calls the service
- Some weirdness clarification : each thread could potentially point to a
unique Service Table List ... however all such lists actually point to
global (shared) service and argument-length tables
==> so if can change an entry in either the ntoskrnl or win32k
service tables (to make such point to a hook routine) : will cause
all threads to use these new <altered> addresses & thus our routines
++++++++++++++++++++++++++++
Actually hooking the calls :
++++++++++++++++++++++++++++
Unlike for the Win '9x kernel, NT doesn't provide a service-hooking function
So must write NT-version-dependent code to achieve this functionality
NT versions vary in two relevant parameters :
(1) the offset in the TEB where the Service Table List pointer lives
(2) the system call numbers that identify services
UPDATED knowledge : re (1)
- KeServiceDescriptorTable is a symbol exported by ntoskrnl.exe
- so can load ntoskrnl.exe into memory
- then search its export table for that symbol to get the table's offset address (RVA)
- to get the physical addy from the offset addy, get the kernel's base address
- then from that eventually get the System Service Dispatch Table (SSDT)
which is equivalent to what we call the NTOSKRNL Service Table above
and the System Service Parameter Table (SSPT) is equivalent to what we
call the NTOSKRNL Argument Table above (contains lengths of args to fxns).
+ anyway, code can be written to accomplish this regardless of the NT version
- which was not the case for original implementation which required
using a fixed offset to manually index into the TEB
- where that offset varied (and still does) by version
- as does the location of the KeServiceDescriptorTable
===================================
More in-depth & current description
===================================
"Windows NT Native API" : set of system services provided by the kernel to
both the user and the kernel
- the address for each function which is part of this native API can be
found in the SSDT; the length of the args for each such function can be
found in the SSPT
-- both tables are indexed by system call # times a constant, which for
the SSDT is 4 and for the SSPT is 1
- KeServiceDescriptorTable : exported by ntoskrnl.exe
-- contains a pointer to each of: SSDT, SSPT
-- is equivalent to the "Service Table List" from above
- to call a specific function, the system service dispatcher (previously
referred to as the "kernel's system call trap handler") -- which is
called KiSystemService -- takes value in EAX and multiplies it by four
to get the index into the SSDT and takes the value in EAX and uses that
to index into the SSPT
- KiSystemService *acts* when int 2eh or SYSENTER is executed
- an application can call KiSystemService directly
- once you get your code loaded as a (kernel) device driver (described elsewhere),
your code can change the SSDT to point to a function it provides
instead of into NTOSKRNL.exe or WIN32K.SYS
- when non-kernel code calls into the kernel, the request is processed by
KiSystemService which uses the altered SSDT
+++++++++++++++++++++++++++++++
Obtaining a pointer to the SSDT
+++++++++++++++++++++++++++++++
typedef struct ServiceDescriptorTable {
SDE ServiceDescriptor[4];
} SDT;
typedef struct ServiceDescriptorEntry {
PDWORD KiServiceTable;
PDWORD CounterBaseTable;
DWORD ServiceLimit;
PBYTE ArgumentTable;
} SDE;
ServiceDescriptor[0].KiServiceTable : contains pointer to SSDT of system
services implemented by ntoskrnl.exe
Now if the syscall # for NtWriteFile is 0xed (as it is for win 2k; it's
0xc8 for NT and 0x0112 for XP and 0x011c for win 2003 server), then the
DWORD value at KiServiceTable[0xed] is a function pointer to NtWriteFile
---------------------------------------------------
Determining the physical memory address of the SSDT
---------------------------------------------------
Recall that the KeServiceDescriptorTable has a KiServiceTable member,
which contains the address of the SSDT.
So first we'll look for the KeServiceDescriptorTable
But its address in mem varies across versions of the OS
But the KeServiceDescriptorTable is a symbol exported by ntoskrnl.exe
So strategy is to load ntoskrnl.exe into memory then search for that
symbol in ntoskrnl.exe's export table : this will give us the offset
address (RVA) of that symbol.
So to convert that to a physical memory address, we must first know the
kernel's (ntoskrnl.exe's) base address "in protected-mode virtual memory"
- call ZwQuerySystemInformation with SystemModuleInformation as 1st param
PhysMemAddyKeSvcDescrTbl = KernelVirtualBaseAddress + Offset (from above)
- 0x8000000
-- where that 0x80000000 comes from above
[x86 : kernel mem resides in region of mem 0x8000000 and above]
So then we map the physical memory page containing the
KeServiceDescriptorTable into the userland process's virtual memory
[see below]
- then we get the address of the SSDT via
KeServiceDescriptorTable[0].KiServiceTable
- then we have to convert that address to a physical memory address
PhysMemAddySSDT = VirtualMemAddyServiceTable - 0x80000000
==============================
Modifying SSDT from user space [Chew Keong Tan, SIG^2]
==============================
- write directly to kernel memory using \device\physicalmemory
- assumes program running with administrator privilege
(1) use NtOpenSection (exported by ntdll.dll) with access flags
SECTION_MAP_READ | SECTION_MAP_WRITE to get a handle to \device\physicalmemory
- will usually fail since administrator doesn't have
SECTION_MAP_WRITE privileges on \device\physicalmemory
(2) use NtOpenSection with access flags READ_CONTROL | WRITE_DAC to get
a handle to \device\physicalmemory
- allows a new DACL to be added to the \device\physicalmemory
object
(3) add a DACL to \device\physicalmemory granting SECTION_MAP_WRITE
access to the administrator account
(4) repeat step (1)
- so now user-space prog should have a handle to \device\physicalmemory
- to write to physical memory, must map the physical memory page into
its virtual address space
-- use NtMapViewOfSection
ntStatus = NtMapViewOfSection(
hPhysMem, // handle to \device\physicalmemory
(HANDLE)-1, //
virtualAddr, // OUT : virt mem where phys mem mapped to
0, //
*length, //
&viewBase, // IN/OUT : phys mem addy to map in
length, // IN/OUT : size of mapped phys mem
ViewShare, //
0, //
PAGE_READWRITE // map for read/write access
);
- after mapping the physical memory pages into its virtual memory space,
the user-space program can read and write to those pages like any other
allocated memory
--------------------
Overwriting the SSDT
--------------------
Recall that the SSDT lives in kernel memory
- this is why our hooker must be a kernel device driver
- then we're running in kernel mode and thus can modify the SSDT, theoretically
However, the SSDT may be read-only which decreases the effect of our
kernel-mode existence on ability to write to the SSDT; in this case we
have to do some funny stuff in order to be able to write to the SSDT
[NB: if attempt to write to read-only mem, get blue screen of death]
(1) modify CR0 register to bypass memory protections (pgs. 66-7,
"Rootkits: Subverting...")
- control register zero (cr0)
-- contains bits which control how the processor behaves
-- modify cr0 to disable memory-access protection in the kernel
- has write-protect bit : controls whether processor will allow writes
to memory pages marked as read-only
-- set to zero disables memory protection (hey, is this what we do
when we create a REG_DWORD value in
HKLM\System\CurrentControlSet\Control\SessionManager\Memory Management
called "EnforceWriteProtection" which has value 0?
- assembly code to achieve this
(2) using an MDL
- you can describe a region of memory with a Memory Descriptor List (MDL)
- MDL : contains start addy, owning process, # of bytes, and flags for
that memory region
// <ntddk.h>, <wdm.h>
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process; // owning process
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
- so we'll want to change the flags in order to be able to write to the SSDT
***********************************************************************************
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} SSDT_Entry;
#pragma pack()
__declspec( dllimport ) SSDT_Entry KeServiceDescriptorTable;
PMDL g_pmdlSystemCall;
PVOID *MappedSystemCallTable;
// obtain value for KeServiceDescriptorTable.ServiceTableBase and
// obtain value for KeServiceDescriptorTable.NumberOfServices
// save old sys call locations
// map the SSDT into userland
g_pmdlSystemCall = MmCreateMdl( NULL,
KeServiceDescriptorTable.ServiceTableBase,
KeServiceDescriptorTable.NumberOfServices );
// 1st arg : IN memory descriptor list
// 2nd arg : IN base (PVOID) --> think this becomes PVOID StartVa
// 3rd arg : IN length (SIZE_T) --> think this becomes CSHORT Size
// See also : IoAllocateMdl(...)
if ( !g_pmdlSystemCall )
return STATUS_UNSUCCESSFUL;
// updates g_pmdlSystemCall MDL;
// given the starting virtual address (ServiceTableBase) and its size,
// figure out the corresponding physical pages' address, ByteCount, etc.
MmBuildMdlForNonPagedPool( g_pmdlSystemCall );
// change permissions on the MDL : will allow you to write to this mem region
g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA;
// locks desired pages (the MDL pages)
// maps the physical pages described by g_pmdlSystemCall
// using access mode == KernelMode
// returns : starting address of the mapped pages
MappedSystemCallTable = MmMapLockedPages( g_pmdlSystemCall, KernelMode );
// now change addresses in KeServiceDescriptorTable to what you want...
// MappedSystemCallTable : same address as original SSDT but now writable.
***********************************************************************************
Taking a step back : what did we just do?
(1) imposed a description onto a region of memory
- where that region contains our SSDT
(2) change a property of that description so that we could write to
that region
----------------
Actually hooking
----------------
Both of the following two #defines work because all Zw* functions
exported by NTOSKRNL.exe start with :
mov eax, ULONG // where ULONG is the index # of the syscall in th SSDT
--> so look at the second byte of any Zw* function in order to get its
syscall # (clever! see Hoglund/Butler)
// takes addy of a Zw* function and returns the index # of its Nt* in the SSDT
#define SYSTEMSERVICE( _func ) KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR) _func + 1) ]
// takes addy of a Zw* function and returns its index in the SSDT
#define SYSCALL_INDEX( _Function ) *(PULONG)((PUCHAR) _Function + 1)
// take the address of a Zw* function, get its index, exchange the addy
// at that index in the SSDT with the addy of _Hook
// write the original Zw* function address to _Orig (used for restoration later)
#define HOOK_SYSCALL( _Function, _Hook, _Orig ) _Orig = (PVOID) InterlockedExchange( (PLONG) &MappedSystemCallTable[ SYSCALL_INDEX( _Function )], (LONG) _Hook )
//
// not sure why this takes _Hook as an argument?
//
#define UNHOOK_SYSCALL( _Func, _Hook, _Orig ) InterlockedExchange( (PLONG) &MappedSystemCallTable[ SYSCALL_INDEX( _Func ) ], (LONG) _Orig )
============
References :
============
(1) Windows NT System Call Hooking
http://www.ddj.com/documents/s=945/ddj9701e/
(2) Windows System Call Table (NT/2k/XP/2003)
http://www.metasploit.com/users/opcode/syscalls.html
(3) Windows NT/2000 Native API Reference (Gary Nebbett)
(4) How Do Windows NT System Calls REALLY Work? (John Gulbrandsen)
http://www.codeguru.com/Cpp/W-P/system/devicedriverdevelopment/article.php/c8035/
(5) Defeating Kernel Native-API hookers via SSDT Restoration
http://www.security.org.sg/code/SIG2_DefeatingNativeAPIHookers.pdf
(6) Rootkits: Subverting the Windows Kernel (Hoglund/Butler)