Single Byte Hooks II
#include <ntddk.h>
extern PULONG *KeServiceDescriptorTable;
typedef NTSTATUS (NTAPI *NT_TERMINATE_PROCESS)(HANDLE ProcessHandle,NTSTATUS ExitStatus);
NTSTATUS NTAPI NtTerminateProcess(HANDLE ProcessHandle,NTSTATUS ExitStatus);
typedef NTSTATUS (NTAPI *NT_CREATE_FILE)(PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PLARGE_INTEGER AllocationSize, OPTIONAL IN ULONG FileAttributes, IN ULONG ShareAccess, IN ULONG CreateDisposition, IN ULONG CreateOptions, IN PVOID EaBuffer, OPTIONAL IN ULONG EaLength);
NTSTATUS NTAPI NtCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PLARGE_INTEGER AllocationSize, OPTIONAL IN ULONG FileAttributes, IN ULONG ShareAccess, IN ULONG CreateDisposition, IN ULONG CreateOptions, IN PVOID EaBuffer, OPTIONAL IN ULONG EaLength);
typedef NTSTATUS (NTAPI *NT_OPEN_FILE)( PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, ULONG ShareAccess, ULONG OpenOptions);
NTSTATUS NTAPI NtOpenFile( PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, ULONG ShareAccess, ULONG OpenOptions);
typedef NTSTATUS (NTAPI *NT_CREATE_PROCESS)( OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OPTIONAL IN HANDLE ParentProcess, IN BOOLEAN InheritObjectTable, IN HANDLE SectionHandle, OPTIONAL IN HANDLE DebugPort, OPTIONAL IN HANDLE ExceptionPort OPTIONAL);
#define MAKELONG(a, b) ((unsigned long) (((unsigned short) (a)) | ((unsigned long) ((unsigned short) (b))) << 16))
NTSTATUS NTAPI NtTerminateProcessHook(PHANDLE ProcessHandle,NTSTATUS ExitStatus);
NTSTATUS NTAPI NtCreateFileHook(PHANDLE FileHandle,ACCESS_MASK DesiredAccess,POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,PLARGE_INTEGER AllocationSize,ULONG FileAttributes,ULONG ShareAccess,
ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength);
NT_TERMINATE_PROCESS pNtTerminateProcess;
NT_CREATE_FILE pNtCreateFile;
PVOID pSavedIntHandler;
__declspec(naked) PVOID ByteHookIntHandler()
{
__asm
{
pop eax
add esp,8
sti
cmp eax,[pNtCreateFile]
jnz NotNtCreateFile
jmp [NtCreateFileHook]
NotNtCreateFile:
cmp eax,[pNtTerminateProcess]
jnz NotNtTerminateProcess
jmp [NtTerminateProcessHook]
NotNtTerminateProcess:
jmp [pSavedIntHandler]
}
}
#pragma pack(1)
struct IDTENTRY
{
unsigned short LowOffset , selector;
unsigned char unused_lo, segment_type:4,system_segment_flag:1,DPL:2,P:1;
unsigned short HiOffset;
} ;
struct IDTINFO
{
unsigned short IDTLimit;
unsigned short LowIDTbase;
unsigned short HiIDTbase;
};
#pragma pack()
NTSTATUS NTAPI NtTerminateProcessHook(PHANDLE ProcessHandle,NTSTATUS ExitStatus)
{
DbgPrint("NtTerminateProcessHook: %p killing handle %p\n", PsGetCurrentProcess(), ProcessHandle );
return pNtTerminateProcess( ProcessHandle,ExitStatus);
}
NTSTATUS NTAPI NtCreateFileHook(PHANDLE FileHandle, ACCESS_MASK DesiredAccess,POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,PLARGE_INTEGER AllocationSize,LONG FileAttributes,ULONG ShareAccess,
ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer,ULONG EaLength)
{
DbgPrint( "NtCreateFileHook: %wZ\n", ObjectAttributes ? ObjectAttributes->ObjectName : NULL );
return pNtCreateFile(FileHandle,DesiredAccess,ObjectAttributes,IoStatusBlock,AllocationSize,FileAttributes,ShareAccess,
CreateDisposition,CreateOptions,EaBuffer,EaLength );
}
void RtlCopyBytesProtected(PVOID DestBuffer,PVOID SrcBuffer,ULONG Length)
{
PMDL pMdl;
PUCHAR pBuffer;
pMdl = IoAllocateMdl( DestBuffer, Length, FALSE, FALSE, NULL );
MmBuildMdlForNonPagedPool( pMdl );
pBuffer = MmMapLockedPages( pMdl, KernelMode );
RtlCopyBytes( pBuffer, SrcBuffer, Length );
MmUnmapLockedPages( pBuffer, pMdl );
}
VOID DriverUnload(PDRIVER_OBJECT pDrvObj)
{
struct IDTINFO idt_info;
struct IDTENTRY *idt_entries;
PVOID pAddr = *(NT_CREATE_PROCESS*)(*KeServiceDescriptorTable+0x2f);
DbgPrint("Unload2");
*((PUCHAR)pNtTerminateProcess-2) = 0x8b;
*((PUCHAR)pNtCreateFile-2) = 0x8b;
RtlCopyBytesProtected( (PUCHAR)pAddr-5, "\x90\x90\x90\x90\x90", 5 );
__asm sidt idt_info
idt_entries = (struct IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);
__asm cli
idt_entries[255].LowOffset = (unsigned short) pSavedIntHandler;
idt_entries[255].HiOffset = (unsigned short)((unsigned long)pSavedIntHandler >> 16);
__asm sti
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObj,PUNICODE_STRING pRegistry)
{
struct IDTINFO idt_info;
struct IDTENTRY *idt_entries,*i;
ULONG addr;
PVOID pAddr = *(NT_CREATE_PROCESS*)(*KeServiceDescriptorTable+0x2f );
ULONG_PTR offset;
pDrvObj->DriverUnload = DriverUnload;
pNtTerminateProcess = *(NT_TERMINATE_PROCESS*)(*KeServiceDescriptorTable + 0x101);
pNtCreateFile = *(NT_CREATE_FILE*)(*KeServiceDescriptorTable + 0x25 );
DbgPrint("DriverEntry2 pAddr=%x pNtTerminateProcess=%x pNtCreateFile=%x",pAddr,pNtTerminateProcess,pNtCreateFile);
offset = (ULONG_PTR)ByteHookIntHandler - (ULONG_PTR)pAddr;
RtlCopyBytesProtected( (PUCHAR)pAddr-5,"\xe9",1);
RtlCopyBytesProtected((PUCHAR)pAddr-4,&offset,4);
__asm sidt idt_info
idt_entries = (struct IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);
i = idt_entries + 0xff;
pSavedIntHandler = MAKELONG(i->LowOffset, i->HiOffset);
__asm cli
i->LowOffset = (unsigned short)((PUCHAR)pAddr-5) ;
i->HiOffset = (unsigned short)((unsigned long)((PUCHAR)pAddr-5) >> 16);
__asm sti
(PUCHAR)pNtTerminateProcess = (PUCHAR)pNtTerminateProcess + 2;
(PUCHAR)pNtCreateFile = (PUCHAR)pNtCreateFile + 2;
*((PUCHAR)pNtTerminateProcess-2)= 0xCD;
*((PUCHAR)pNtCreateFile-2) = 0xCD;
return STATUS_SUCCESS;
}
The method NtCreateProcess has its entry at offset 2f in the SSDT array. Its begins at location 805ad314 in memory. Using the u command in WinDbg gives us the confirmation we need.
lkd> u nt!NtCreateProcess
nt!NtCreateProcess:
805ad314 8bff mov edi,edi
805ad316 55 push ebp
805ad317 8bec mov ebp,esp
Thus pAddr gives us the address of NtCreateProcess
and pNtTerminateProcess the address of NtTerminateProcess which is 80582c2b and
pNtCreateFile the address of NtCreateFile which is
80570d48. Check it out yourselves. Thus we have the
addresses of three functions in kernel memory.
Fortunately for us the 5 bytes before the code of
NtCreateProcess starts has 5 NOP’s in them. This means that we can safely
override these bytes as no one is using them. WinDbg confirms this.
lkd> db 805ad314-5
805ad30f 90
90 90 90 90 8b ff 55-8b ec 33
Thus we have found an area of memory that has some
empty space and we can now use this to our advantage. In part 1 we showed how
we calculated the offset of our function that we wanted to be called which in
our case is ByteHookIntHandler. We use the same variable offset to tell us the
difference between the start of ByteHookHandler and the starting address of
NtCreateProcess. We write the jmp instruction on these 5 bytes using the same
Rtl functions that are optional.
The key method is set_entry_idt which is passed the
interrupt to be hooked 0xff and the starting point of this newly written jump
instruction pAddr-5.
The method set_entry_idt is written in assembler but
could have been written in C. We simply want to change the address that gets
called on interrupt 2f. This is the only part of the code we rewrote in C. We
first found the address of the IDT which is a series of structures and then
placed the address pAddr-5 at the
location 255 from the start. We save the original value and when we unload the
driver we place the original value back.
We then increase the two pointers to the method
NtTerminateProcess and NtCreateFile by 2 and place 0xCd as the first byte. The
opcode cd means call interrupt which becomes call interrupt FF as the second
byte of every function is FF. Thus interrupt FF will get called each time we
call these two functions. This in turn will call the jmp instruction at pAddr-5
which will call our hook method ByteHookIntHandler.
Now lets understand the handler method which calls
one of the two functions. The calling convention is naked which means the
compiler will generate no prologue or epilogue. This method is also called with
no parameters whatsoever. When the user calls either of the two functions, int
ff gets called which in turn calls the function ByteHookIntHandler. On the
stack is the address of the function that is being called. This value we place
in the eax register.
We then add 8 to esp because we have a 8 byte ret
value on the stack. We compare this value in eax with the value of
pNtCreateFile, if it ss equal we call the function NtCreateFileHook which in
turn calls the original plus 2. If not we check whether eax is equal to
pNtTerminateProcess, if true we jmp to the method NtTerminateProcessHook. If
none is true we finally call the original handler.
This is how we use a one byte hook to beat the SSDT table
check and yet get our code to be called. We change the first byte of our
functions to CD and then get the same code to be called each time the interrupt
occurs. We then figure which of the two functions were called and then jump to
them.