Windows NT System-Call Hooking (Dr. Dobb's Journal)

来源:互联网 发布:高中数学算法程序框图 编辑:程序博客网 时间:2024/06/06 02:39
---------------------------------------------------------------------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 0Core 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 attributesUser-level subsystems : win32 (by which we mean the win32 api), POSIX, OS/2NTDLL.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 pointsZwXxx : 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 functionSo must write NT-version-dependent code to achieve this functionalityNT 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 servicesUPDATED 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 KeServiceDescriptorTableBut its address in mem varies across versions of the OSBut the KeServiceDescriptorTable is a symbol exported by ntoskrnl.exeSo strategy is to load ntoskrnl.exe into memory then search for thatsymbol in ntoskrnl.exe's export table : this will give us the offsetaddress (RVA) of that symbol. So to convert that to a physical memory address, we must first know thekernel's (ntoskrnl.exe's) base address "in protected-mode virtual memory" - call ZwQuerySystemInformation with SystemModuleInformation as 1st paramPhysMemAddyKeSvcDescrTbl = 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 theKeServiceDescriptorTable 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 addressPhysMemAddySSDT = 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 to0, // *length, // &viewBase, // IN/OUT : phys mem addy to map inlength, // IN/OUT : size of mapped phys memViewShare, // 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, theoreticallyHowever, the SSDT may be read-only which decreases the effect of ourkernel-mode existence on ability to write to the SSDT; in this case wehave 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)
原创粉丝点击