2008年9月19日星期五

how to make a corect ssdt hook

Plague in (security) software drivers
by Jakub Břečka & David Matoušek, members of Matousec – Transparent security Research team,
special thanks to Ondřej Vlček of ALWIL Software for corrections
Published: 2007/09/18
Last update: 2008/04/14 - BSODhook version 2.0.0 released, the information about it moved to a separate web page
During our security analyses of personal firewalls and other security-related software that uses SSDT hooking, we found out that many vendors simply do not implement the hooks in a proper way. This allows local Denial of Service by unprivileged users or even privilege escalations exploits to be created. 100% of tested personal firewalls that implement SSDT hooks do or did suffer from this vulnerability! This article reviews the results of our testing and describes how a proper SSDT hook handler should be implemented. We also introduce BSODhook – a handy tool for every developer that deals with SSDT hooks and a possible cure for the plague in today's Windows drivers world.
Contents:
Introduction
The bug
Structures with pointers
Other issues and exploitability
About BSODhook utility
Research results
Conclusion

--------------------------------------------------------------------------------

Introduction
Hooking kernel functions by modifying the System Service Descriptor Table (SSDT) is a very popular method of implementation of additional security features and is used frequently by personal firewalls and other security and low-level software. Although undocumented and despised by Microsoft, this technique can be implemented in a correct and stable way. However, many software vendors do not follow the rules and recommendations for kernel-mode code writing and many drivers that implement SSDT hooking do not properly validate the parameters of the hooking functions.
Microsoft's Common Driver Reliability Issues document describes the correct parameter checking and contains many important notes to problems related to writing Windows drivers. Many vendors of today's software do not bother to read such documents and their implementations are thus vulnerable, and making the stable and trustworthy Windows kernel unreliable.
Parameters to SSDT function handlers are passed directly from user-mode and therefore must be checked before they are used. This article shows some incorrect implementations of SSDT hooking functions and describes how a proper validity check should be performed on various parameter types. To understand the following text, you will need some knowledge of Windows NT architecture. We do not cover a proper implementation of SSDT hooking techniques here. We focus on parameter validation problems. Some related and interesting information can be also found in Microsoft's Memory Management: What Every Driver Writer Needs to Know document.

--------------------------------------------------------------------------------

The bug
For a demonstration of the bug, see the following code sample:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
if (ClientId->UniqueProcess==ProtectedProcess)
return STATUS_ACCESS_DENIED;

...
}
Example 1: No parameter validation at all.
This code shows a hook handler for a Windows Native API function NtOpenProcess and it implements a simple security check that will deny all requests to open a protected process with a specified process ID. For the purpose of this article, it is not important how such a hook is set, which is also a task that is pretty complicated if it is supposed to be performed correctly, or what is done after the security check, which is also non-trivial.
Unfortunately, the implementation in Example 1 is incorrect, because this hook handler function receives the parameters exactly as they were sent from user-mode, and it must therefore check all parameters' validity before using them. If a similar hook was implemented in user-mode (e.g. to intercept kernel32.OpenProcess or ntdll.NtOpenProcess calls), the need for a parameter validation would not be so necessary, because an invalid memory read or write in user-mode cannot corrupt the system's stability nor can it be exploited to escalate privileges. Such a lack of parameter checking will only corrupt or crash the application that invoked the invalid API call.
In the kernel however, the situation is different. All parameters coming from user-mode must be checked for validity before they are used. This is especially related to pointers to various structures. To access structure contents, the pointer must be dereferenced, which is a risky operation that can result in one of the following:
A pointer to a valid memory is dereferenced – success.
The pointer is invalid and points into user-mode memory – a page fault will occur and its handler will raise a SEH exception of ACCESS_VIOLATION, which, if not handled, will crash the system.
The pointer is invalid and points into kernel-mode memory – this will not raise an exception, but instead a bugcheck (BSOD) will be invoked immediately.
SEH exceptions can be caught by enclosing the unsafe operations in try/except blocks. However, using try/except only is not enough, see the following code sample:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
try
{
ProcessId=ClientId->UniqueProcess;
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}

if (ProcessId==ProtectedProcess)
return STATUS_ACCESS_DENIED;

...
}
Example 2: Invalid parameter validation, only user-mode pointers are validated properly.
In this function, the parameter ClientId is properly validated only for user-mode pointers. If ClientId points into an invalid kernel memory, the dereference will cause a BSOD to occur. A correct approach is to use the ProbeForRead function:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
if (ClientId!=NULL)
{
try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ClientId,sizeof(CLIENT_ID),1);
ProcessId=ClientId->UniqueProcess;
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}

if (ProcessId==ProtectedProcess)
{
return STATUS_ACCESS_DENIED;
}
}

...
}
Example 3: Correct parameter validation for single-pointer structures like CLIENT_ID.
The call to ProbeForRead must be enclosed in the try/catch block as well as the access to the user supplied memory, because if the probing fails, an exception will be raised. You should also notice that ClientId is an OPTIONAL parameter. This means that it might not be present in a correct call. If it was NULL and we did not check this, the assignment to ProcessId would fail and we will return STATUS_INVALID_PARAMETER, which would disqualify some of correct NtOpenProcess calls.
See the following pseudo-code implementation of ProbeForRead:
VOID ProbeForRead(IN CONST VOID *Address,IN SIZE_T Length,IN ULONG Alignment)
{
// check for zero Length
if (Length==0)
return;

// check the alignment of Address
if (!CheckAlignment(Address,Alignment))
RaiseException(DATATYPE_MISALIGNMENT);

// check for an integer overflow
if (IsSumOverflow((ULONG)Address,Length))
RaiseException(ACCESS_VIOLATION);

// check if Address points into user-mode memory
if (((ULONG)Address+Length)>MmUserProbeAddress)
RaiseException(ACCESS_VIOLATION);

return;
}
Example 4: Internals of ProbeForRead in pseudo-code.
ProbeForRead checks whether the given pointer points into user-mode memory by comparing it to MmUserProbeAddress, which is the lowest invalid address for user-mode buffers. MmUserProbeAddress is typically set to 0x7FFF0000, or to 0xBFFF0000 on systems with 3GB user-space memory, activated by the /3GB switch in boot.ini.
It might seem odd, that the function does not perform any memory access to the desired memory area. But in fact, it is perfectly valid to access any user-mode memory, if the access is performed inside a try/except block. So, ProbeForRead only checks if the structure lies in the user-mode memory. This also means, that a function with a ProbeForRead call cannot be called from kernel-mode with a kernel memory pointer, because only user-mode addresses will pass the probe. This is why we use KeGetPreviousMode check, to allow kernel calls to pass without checking. You can find some information about it in Argument Validation in Windows NT part of Microsoft Windows NT: Design Goals article.
Note that you cannot use ProbeForRead or any similar function to perform a validity check on a kernel memory pointer. There is no easy mechanism that checks if an address in the kernel is valid or not. All kernel-mode pointers are trusted and there is no need to validate them – that is also the reason, why an invalid kernel memory access results in a BSOD and not a SEH exception. Simply, invalid kernel memory access is, unlike a user-mode memory access, a fatal error and should never occur in properly written drivers.

--------------------------------------------------------------------------------

Structures with pointers
All we discussed so far were single-pointer structures. However, some Native API functions require complex structures with additional pointers in them as parameters. See the following example of a hook handler for NtQueryValueKey:
NTSTATUS HookNtQueryValueKey(IN HANDLE KeyHandle,IN PUNICODE_STRING ValueName,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,OUT PULONG ResultLength)
{
if (_wcsicmp(ValueName->Buffer,L"ProtectedValue")==0)
return STATUS_ACCESS_DENIED;

...
}
Example 5: No validation for a PUNICODE_STRING parameter.
One of NtQueryValueKey function's parameters is a pointer to a UNICODE_STRING structure, which consist of three values: Length, MaximumLength and another pointer to an array of WCHARs. The code sample in Example 5 implements no parameter checking, so we know already that this is wrong.
If a function with a UNICODE_STRING is to be hooked properly, the hook handler must check the validity of the pointer to the Unicode string, before accessing the member values of this structure. Then the pointer to the array of wide characters must be validated. See the following example, which shows a correctly performed validity check:
NTSTATUS HookNtQueryValueKey(IN HANDLE KeyHandle,IN PUNICODE_STRING ValueName,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,OUT PULONG ResultLength)
{
UNICODE_STRING name;
WCHAR *buffer=NULL;

try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ValueName,sizeof(UNICODE_STRING),1);

RtlCopyMemory(&name,ValueName,sizeof(name));

if (name.Length==wcslen(L"ProtectedValue")*sizeof(WCHAR))
{
if (KeGetPreviousMode()!=KernelMode)
{
ProbeForRead(name.Buffer,name.Length,1);
buffer=(WCHAR*)ExAllocatePoolWithTag(NonPagedPool,name.Length,TAG);
if (!buffer) return STATUS_INSUFFICIENT_RESOURCES;
RtlCopyMemory(buffer,name.Buffer,name.Length);
} else buffer=name.Buffer;

if (_wcsnicmp(buffer,L"ProtectedValue",name.Length/sizeof(WCHAR))==0)
return STATUS_ACCESS_DENIED;
}
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}

...
}
Example 6: Correct double-pointer structure (UNICODE_STRING) validation.
Note that we have to make a copy of ValueName before we can use its contents. If we did not make a copy and used its contents directly, another thread could change its contents after we checked its Length or after we probed its Buffer.
There are some other structures with additional pointers that appear as parameters in some Native API functions. All these pointers must be checked before they are dereferenced. See the following example, which checks the validity of a POBJECT_ATTRIBUTES parameter, which contains a pointer to a UNICODE_STRING structure. Therefore, a triple pointer check must be performed:
NTSTATUS HookNtCreateFile(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)
{
UNICODE_STRING name;
WCHAR *buffer=NULL;

try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ObjectAttributes,sizeof(OBJECT_ATTRIBUTES),1);

PUNICODE_STRING ObjectName=ObjectAttributes->ObjectName;
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ObjectName,sizeof(UNICODE_STRING),1);

RtlCopyMemory(&name,ObjectName,sizeof(name));

if (name.Length==wcslen(L"ProtectedValue")*sizeof(WCHAR))
{
if (KeGetPreviousMode()!=KernelMode)
{
ProbeForRead(name.Buffer,name.Length,1);
buffer=(WCHAR*)ExAllocatePoolWithTag(NonPagedPool,name.Length,TAG);
if (!buffer) return STATUS_INSUFFICIENT_RESOURCES;
RtlCopyMemory(buffer,name.Buffer,name.Length);
} else buffer=name.Buffer;

if (_wcsnicmp(buffer,L"ProtectedValue",name.Length/sizeof(WCHAR))==0)
return STATUS_ACCESS_DENIED;
}
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}

...
}
Example 7: Correct triple-pointer structure (OBJECT_ATTRIBUTES) validation.

--------------------------------------------------------------------------------

Other issues and exploitability
Even more necessary is to perform validation checks on output parameters. If a hook handler writes to the memory through a user-mode supplied pointer (which again might be wrapped in a structure), it must check that the whole write memory area is valid for writing, for example using ProbeForWrite. It will similarly produce an exception, if some part of the memory is not writable.
Note that there also is a possibility of invalidating a memory during the processing of the hook handler. Therefore, even if a user-mode memory address is validated, a driver still cannot consider it safe to read/write. The memory can be invalidated at any time, so every consequent read or write attempt must be enclosed in a try/except block.
If a driver communicates with user-mode using Windows I/O or any other mechanism, all user-mode addresses must be validated in exactly the same way (ProbeForRead and/or ProbeForWrite) as stated before. If the application logic treats such address as a structure with additional pointers, all of these must be checked too. Especially, this validation must be performed on user buffers supplied through METHOD_NEITHER I/O.
Generally, there is no common pattern for exploiting these bugs. An invalid memory read will only produce a BSOD. However, some special cases of missing ProbeForWrite validation can certainly be exploited and may lead to a privilege escalation or even a local root exploit. For example, a missing parameter validation on an OUT PHANDLE argument may, in some cases, be exploited to bypass system's security checks or modify kernel objects. The outcoming value of a newly opened handle can be predicted and if we set this parameter to point somewhere in the kernel, for example inside the kernel structures or a carefully selected address inside kernel code, we can alter the code flow and bypass access checks.
Since Windows XP, a memory write protection is keeping any driver from altering the kernel code, which effectively blocks these kinds of exploits. However, we can still overwrite any part of the kernel stack or kernel objects (for example modify current EPROCESS structure to gain privileges). In general, in case of incorrect OUT parameter validation implementation, we may have arbitrary kernel mode write possibility, which is usually enough to take over the whole machine.

--------------------------------------------------------------------------------

About BSODhook utility
We have developed a tool codenamed BSODhook that helps finding improper validation bugs in drivers that implement (not only) SSDT hooks. BSODhook (aka Kernel hooks probing tool) calls native functions with both valid and invalid parameters to produce a system crash (bugcheck). However, this tool comes with a kernel driver, which intercepts certain system functions to catch these bugchecks. Instead of crashing the system, an invalid memory access or other faulty behaviour that invoke the bugcheck will be caught, the calling thread will be terminated and the application will report that the tested API function is improperly validated. Moreover, BSODhook writes out the exact parameters of the function call that caused the crash. This allows vendors to find bugs in their drivers very quickly and efficiently.
In the second version, we have added support for SSDT GDI functions. We have also created a separate BSODhook web page and moved the information about this tool there.
You can download BSODhook right now and start probing. If you find some bugs in a software you use, which is not listed below, or if your Windows 2000 or XP kernel is not supported, please contact us. If your kernel is not supported, be sure to include a full information about your kernel version. If you are a vendor of a software that implements SSDT hooks and our tool helped you to improve it, we will be very glad if you tell us about it. Everyone is allowed to use BSODhook freely as is. There is no warranty or support for this product, but we will be glad to receive the feedback. There are also number of ways how to improve BSODhook itself, if you are an experienced Windows coder interested in doing so, feel free to contact us too.

没有评论: