Objects


An object is the fundamental unit of abstraction for Windows system resources. In the most generic sense, an object is simply a mechanism the kernel uses to manage virtual and physical resources. In some sense, an object is similar to a class in Java or C++; it's defined by a specific type (such as a file), and then instances of that object are created (such as the file C:\boot.ini) and manipulated.

The Windows Kernel Object Manager (KOM) is the component responsible for kernel-level creation, manipulation, and maintenance of objects. All object types the KOM maintains are known as system objects or securable objects; the following list shows the most common groups of securable objects:

  • Directory service objects

  • File-mapping objects

  • Interprocess synchronization objects (Event, Mutex, Semaphore, and

    WaitableTimer objects)

  • Job objects

  • Named and anonymous pipes

  • Network shares

  • NTFS files and directories

  • Printers

  • Processes and threads

  • Registry keys (but not registry values)

  • Services

  • Window-management objects (but not windows)

Note

You can see a complete list of object types with the WinObj utility, available at www.sysinternals.com. If you're interested in learning more about the Windows architecture and KOM, check out Windows Internals 4th Edition by Mark E. Russinovich and David A. Solomon (Microsoft Press, 2005).


Most securable objects are instantiated or connected to with a user-mode function of the form Create*() or Open*(). These functions generally return an object handle (the HANDLE data type) if the requested object is opened successfully. From the application's point of view, a handle is an opaque identifier for an open object not unlike file descriptors in UNIX. When an object is no longer needed, it can usually be closed by using the CloseHandle() function. One major advantage of this consistent object interface is that it allows unified access control mechanisms to be applied to all objects, regardless of their type or function.

Note

Although most objects are closed with CloseHandle(), a few require a specialized close routine, notably the RegCloseKey() function for closing registry key objects.


Other programmatic constructs maintain the object metaphor, although they aren't true system objects. They are occasionally referred to as "nonsecurable" or "pseudo-objects," but these terms are just a generalization. Pseudo-objects include registry values and GUI windows, for example; the related securable objects are registry keys and window stations. For the purposes of this discussion, the most important distinction is that pseudo-objects don't accept a SECURITY_ATTRIBUTES structure as part of their creation, so they can't have Windows access control mechanisms applied to them.

Object Namespaces

Before you learn about access rights associated with objects, you need to understand the object namespace. In Windows, objects can be named or unnamed. Unnamed objects are anonymous and can be shared between processes only by duplicating an object handle or through object handle inheritance (discussed in "Handle Inheritance" later in this chapter). Conversely, named objects are given names when they are created. These names are used to identify objects by clients who want to access them.

Named objects are stored in a hierarchical fashion so that applications can refer to them later. This hierarchy is referred to as an object namespace. Object namespaces are managed by the KOM. Historically, there has been only a single global namespace in Windows. However, the addition of Terminal Services adds a local namespace for every active terminal session. (Terminal Services are discussed in Chapter 12.) For now, assume the term "object namespace" refers to the global object namespace.

An object namespace is similar to a typical file system; it's organized into directories that can contain both subdirectories and objects. It can also contain links to other objects or directories in the object namespace. These links are actually objects of the type SymbolicLink.

You can view the object namespace with WinObj, a tool written by Mark Russinovich (available from www.sysinternals.com). Figure 11-1 shows the WinObj interface. On the left are several base directories containing objects and possibly subdirectories of their own. From a security-auditing perspective, you need to be aware that named objects created by anyone on the system are generally visible (although not necessarily accessible) to applications that query the namespace.

Figure 11-1. The WinObj main window


Note

Readers more accustomed to UNIX systems might be curious about the security implications of the SymbolicLink object. Because it can point to arbitrary locations in the object namespace, it might seem as though the potential exists for symlink attacks, not unlike those that can occur at the file system level. However, creating SymbolicLink objects requires administrative privileges on the system, which makes an attack a nonissue.


Namespace Collisions

Because multiple applications (or multiple instances of the same application) often need to refer to objects, they are given a name by the creator and stored in the object namespace. This presents the opportunity for attackers to create objects of the same name before a legitimate application does. An object can then be manipulated to force the legitimate application to not function correctly or even steal credentials from a more privileged process. This type of attack is commonly referred to as a namespace collision attack, or name squatting.

To understand how these attacks work, you need to be familiar with the Windows object creation API. Generally, each object type has a function to create an object instance and another function to connect to an existing instance. For example, the Mutex object uses the CreateMutex() and OpenMutex() functions. However, many of the Create*() functions actually support both operations; they can create a new object or open an existing one. This support can lead to vulnerabilities when an application attempts to create a new object but unwittingly opens an existing object created by a malicious user. Most Create*() functions take a pointer to a SECURITY_ATTRIBUTES structure, which includes the security descriptor for the object being created. If the Create*() function opens an existing object, it already has a security descriptor, so the security attributes being passed to the Create*() function are silently ignored. As a result, the application uses an object with entirely different access restrictions than intended.

Most functions that support both creating and opening objects provide some way for the application to ensure that it creates a unique object or to detect that it has opened a preexisting object. Generally, this restriction is enforced through object creation flags and by checking return codes from the Create*() function. However, it might also require checking return values or using the GetLastError() function. As a code auditor, you need to understand the semantics of these functions so that you know when objects aren't instantiated safely. To emphasize this point, namespace collisions are revisited in a number of examples as you progress through this chapter and Chapter 12.

Vista Object Namespaces

Microsoft Windows Vista adds private object namespaces to help address name-squatting issues. A private object namespace allows an application to create its own restricted namespace via the CreatePrivateNamespace() and OpenPrivateNamespace() functions. Objects are then created and opened within the namespace by prepending the namespace name and a backslash (\). For example, the object name NS0\MyMutex refers to the MyMutex object in the NS0 namespace.

The namespace is also a securable object, which raises the question: Is it possible to squat on namespace names in the same way that other objects' names can be squatted on? The answer will become clearer when the final implementation is done and Vista is released. Based on initial implementations and documentation, it appears that attacks of this nature are mostly mitigated because of the use of a new type of (pseudo) object, known as a boundary descriptor. A boundary descriptor object describes SIDs and session IDs that an application must belong to in order to open a private namespace. The namespace is identified by both its name and boundary descriptor; different namespaces can have identical names if they have differing boundary descriptors.

A boundary descriptor is created with the CreateBoundaryDescriptor() function. Any call to OpenPrivateNamespace() must include a boundary descriptor matching the associated call to CreatePrivateNamespace(). Presently, AddSIDToBoundaryDescriptor() is the only documented function for adding restrictions to a boundary descriptor; this function adds a supplied SID to an existing boundary descriptor. The preliminary documentation for namespaces, however, states that boundary descriptors will include other information, such as session identifiers. The documentation also states that any process can open a namespace regardless of the boundary descriptor, if the namespace doesn't supply a SECURITY_ATTRIBUTES structure with adequate access control. This statement gives the impression that the security of private namespaces will depend heavily on the namespace security descriptor and when the boundary descriptor is made visible to client processes.

One final point: Private namespaces are intended only to address name-squatting issues. They won't provide any protection against direct access to an existing object with weak access control.

Object Handles

As mentioned, most securable objects are accessed by using the HANDLE data type. More accurately, the kernel references all securable objects by using handles; however, the corresponding user space data type might not directly expose the HANDLE data type in the object reference. An object can be referenced by name when it's created or opened, but any operations on the object are always performed by using the handle.

The kernel maintains a list of all open handles categorized by the owning process. This list is enumerated with the native API function NtQuerySystemInformation() using the SystemHandleInformation class. In this manner, even an unnamed object could be accessed by another process. An object's discretionary access control list (DACL) is the only thing that prevents the object from being manipulated by another user context. DACLs and the dangers of NULL DACLs are discussed in "Security Descriptors" later in this chapter. However, note that any object not properly secured by access control can be manipulated, regardless of whether it's named.

INVALID_HANDLE_VALUE Versus NULL

You need to pay close attention to any function call that returns a handle in Windows because Windows API calls are inconsistent as to whether an error results in a NULL or an INVALID_HANDLE_VALUE (-1). For example, CreateFile() returns INVALID_HANDLE_VALUE if it encounters an error; however, OpenProcess() returns a NULL handle on an error. To make things even more confusing, developers can't necessarily test for both values because of functions such as GetCurrentProcess(), which returns a pseudo-handle value of -1 (equivalent to INVALID_HANDLE_VALUE). Fortunately, the pseudo-handle issue isn't likely to affect a security vulnerability, but it does show how a developer can get confused when dealing with Windows handles. Take a look at an example of this issue:

HANDLE lockUserSession(TCHAR *szUserPath) {     HANDLE hLock;     hLock = CreateFile(szUserPath, GENERIC_ALL, 0,         NULL, CREATE_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, 0);     return hLock; } BOOL isUserLoggedIn(TCHAR *szUserPath) {     HANDLE hLock;     hLock = CreateFile(szUserPath, GENERIC_ALL, 0,         NULL, CREATE_NEW, FLAG_DELETE_ON_CLOSE, 0);     if (hLock == NULL)         return TRUE;     CloseHandle(hLock);     return FALSE; }


At first glance, this code might seem like a logical set of functions for locking a user's state. The first function simply creates a lock file with the share mode set to zero; so any other attempts to access this file fail. The second function can then be used to test for the file's existence; it should return TRUE if present or FALSE if not. It provides a simple way of maintaining some state between processes on remote systems by using a file share.

The problem with this implementation is that it checks to see whether the returned handle is NULL, not INVALID_HANDLE_VALUE. Therefore, the function actually behaves the opposite of how it was intended. Although this type of issue is normally a functionality bug, it can be a security issue in untested and rarely traversed code paths. Unfortunately, there's no particular method to determine which value to expect without consulting the Windows documentation. This issue is an artifact from the evolution of Windows. You simply have to refer to the MSDN and make sure the correct failure condition is tested for a handle returned from a particular function.

Handle Inheritance

People familiar with UNIX often aren't accustomed to how Windows handles process relationships. One of the biggest differences from UNIX is that Windows provides no special default privileges or shared object access to a child process. However, Windows does provide an explicit mechanism for passing open object instances to children, called handle inheritance.

When a new process is created, the parent process can explicitly allow the child to inherit marked handles from the current process. This is done by passing a true value to the bInheritable parameter in a CreateProcess() call, which causes any handle marked as inheritable to be duplicated into the new process's handle table. The handles are marked as inheritable by setting a true value in the bInheritable member of the SECURITY_ATTRIBUTES structure supplied to most object creation functions. Alternately, the handle can be marked inheritable by calling DuplicateHandle() and passing a true value for the bInheritable argument.

Typically, handle inheritance isn't a security issue because a parent process usually runs in the same context as the child. However, vulnerabilities can occur when handle inheritance is used carelessly with children spawned under another context. Handle inheritance can allow a child process to obtain a handle to an object that it shouldn't otherwise have access to. This error occurs because handle rights are assigned when the object is opened, so the OS views the handle in the context of the process that opened it, not the process that inherited it.

For an example of where handle inheritance might be an issue, say a service listens on a named pipe interface and launches a command shell when a client connects. To prevent privilege escalation, the service impersonates the client user so that the shell runs with the appropriate permissions. (Impersonation is discussed in Chapter 12.) The following code demonstrates a function that might implement this capability. Some error checking was omitted for the sake of brevity. In particular, the CreateProcess() call was encapsulated inside CreateRedirectedShell(), but you can assume it passes true for the bInheritable argument. You can also assume the function creating this thread generated the handle by using ConnectNamedPipe() and has read client data, allowing impersonation to succeed.

int tclient(HANDLE io) {     int hr = 0;     HANDLE hStdin, hStdout, hStderr,         hProc = GetCurrentProcess();     if(!ImpersonateNamedPipeClient(io))         return GetLastError();     DuplicateHandle(hProc, io, hProc, &hStdin, GENERIC_READ,                     TRUE, 0);     DuplicateHandle(hProc, io, hProc, &hStdout, GENERIC_WRITE,                     TRUE, 0);     DuplicateHandle(hProc, io, hProc, &hStderr, GENERIC_WRITE,                     TRUE, 0);     CloseHandle(io);     hProc = CreateRedirectedShell(hStdin, hStdout, hStderr);     CloseHandle(hStdin);     CloseHandle(hStdout);     CloseHandle(hStderr);     hr = RevertToSelf();     if (hProc != NULL) WaitForSingleObject(hProc);     return hr; }


This code contains a subtle vulnerability that might cause the standard IO handles to leak into more than one process. Consider what would happen if two different users connected simultaneously and caused one of the threads to block inside the CreateRedirectedShell() function. Say that thread 1 blocks, and thread 2 continues to run. Thread 2 then spawns shell 2 and inherits its redirected IO handles. However, shell 2 also inherits the redirected handles from thread 1, which is currently blocked inside CreateRedirectedShell(). This occurs because the handles for shell 1 are marked as inheritable when shell 2 is spawned, so they are added to the process handle table for shell 2. Attackers could exploit this vulnerability by connecting at the same time as a more privileged user. This simultaneous connection would cause them to inherit the standard IO handles for the higher privileged process in addition to their own. This access allows attackers to simply issue commands directly to the higher privileged shell.

This vulnerability might seem a bit contrived, but variations of it have been identified in deployed applications. In this example, the solution is to wrap the shell creation in a critical section and ensure that inheritable handles aren't used elsewhere in the application. In a more general sense, you should always scrutinize any use of handle inheritance and be especially careful when it involves different security contexts. This requires you to identify any process creation that can occur over the inheritable handle's lifespan. Therefore, it's generally a good idea for developers to keep the lifespan of these handles as short as possible.

Handle inheritance vulnerabilities are actually rare because the use cases that lead to them are uncommon. The first step in finding them is to determine whether the application runs any processes in a separate security context and allows the child process to inherit handles. This step is easy; first you need to look for impersonation functions or other functions that allow altering the security context. Then you just need to look for the bInheritable parameter in calls to the CreateProcess() family of functions or in the SHELLEXECUTEINFO structure passed to ShellExecuteEx().

If you identify any children that can inherit handles, you need to identify inheritable handles by looking at all object creation calls and any calls to DuplicateHandle(). A well-written application should never create an inheritable handle at object instantiation, however; instead, it should duplicate an inheritable handle immediately before the process is created and free it immediately afterward. However, many applications aren't written this well, so you might have a difficult time finding all possible inheritable handles, especially if the developers had a habit of marking all handles as inheritable.

After you have identified all the inheritable handles, you need to trace their use and determine whether their lifespan overlaps any child process creations you identified earlier. This part can be difficult because the handle might be marked inheritable in entirely unrelated code, or it might be inherited only in a race condition, as in the previous example. Fortunately, you can leverage some techniques discussed in Chapter 13, "Synchronization and State."

Live analysis is also helpful, and Process Explorer (from www.SysInternals.com) is a useful tool for this purpose. This tool gives you detailed information on any process, including a list of open handles. It can also be used to search the process handle table for any named handles. Unfortunately, Process Explorer doesn't identify whether a handle is marked inheritable, but it's still useful in tracking down and validating the handles available to a process.




The Art of Software Security Assessment. Identifying and Preventing Software Vulnerabilities
The Art of Software Security Assessment: Identifying and Preventing Software Vulnerabilities
ISBN: 0321444426
EAN: 2147483647
Year: 2004
Pages: 194

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net