|
|
51CTO旗下网站
|
|
移动端

Sharing Kernel Objects Across Process Boundaries

《Windows核心编程(第5版•英文版)》第三章介绍了内核对象的相关知识。本文主要描述在不同的程序之间如何分享使用内核对象,分析了原因,详细介绍了使用内核控制块继承性特征,改变控制块标记,定义对象以及如何复制控制块的步骤。

作者:Jeffrey Richter, Christophe Nasarre来源:Microsoft Press|2008-08-29 02:38

Sharing Kernel Objects Across Process Boundaries
Frequently, threads running in different processes need to share kernel objects. Here are some of the reasons why:

File-mapping objects allow you to share blocks of data between two processes running on a single machine.

Mailslots and named pipes allow applications to send blocks of data between processes running on different machines connected to the network.

Mutexes, semaphores, and events allow threads in different processes to synchronize their continued execution, as in the case of an application that needs to notify another application when it has completed some task.

Because kernel object handles are process-relative, performing these tasks is difficult. However, Microsoft had several good reasons for designing the handles to be process-relative. The most important reason was robustness. If kernel object handles were systemwide values, one process could easily obtain the handle to an object that another process was using and wreak havoc on that process. Another reason for process-relative handles is security. Kernel objects are protected with security, and a process must request permission to manipulate an object before attempting to manipulate it. The creator of the object can prevent an unauthorized user from touching the object simply by denying access to it.

In the following section, we'll look at the three different mechanisms that allow processes to share kernel objects: using object handle inheritance, naming objects, and duplicating object handles.

Using Object Handle Inheritance
Object handle inheritance can be used only when processes have a parent-child relationship. In this scenario, one or more kernel object handles are available to the parent process, and the parent decides to spawn a child process, giving the child access to the parent's kernel objects. For this type of inheritance to work, the parent process must perform several steps.

First, when the parent process creates a kernel object, the parent must indicate to the system that it wants the object's handle to be inheritable. Sometimes I hear people use the term object inheritance. However, there is no such thing as object inheritance; Windows supports object handle inheritance. In other words, it is the handles that are inheritable, not the objects themselves.

To create an inheritable handle, the parent process must allocate and initialize a SECURITY_ATTRIBUTES structure and pass the structure's address to the specific Create function. The following code creates a mutex object and returns an inheritable handle to it:

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;   // Make the returned handle inheritable.
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL); 

This code initializes a SECURITY_ATTRIBUTES structure indicating that the object should be created using default security and that the returned handle should be inheritable.

Now we come to the flags that are stored in a process' handle table entry. Each handle table entry has a flag bit indicating whether the handle is inheritable. If you pass NULL as the PSECURITY_ATTRIBUTES parameter when you create a kernel object, the handle returned is not inheritable and this bit is zero. Setting the bInheritHandle member to TRUE causes this flag bit to be set to 1.

Imagine a process' handle table that looks like the one shown in Table 3-2.

Table 3-2: Process' Handle Table Containing Two Valid Entries 

Index

Pointer to Kernel Object Memory Block

Access Mask (DWORD of Flag Bits)

Flags

1

0xF0000000

0x????????

0x00000000

2

0x00000000

(N/A)

(N/A)

3

0xF0000010

0x????????

0x00000001

Table 3-2 indicates that this process has access to two kernel objects (handles 1 and 3). Handle 1 is not inheritable, and handle 3 is inheritable.

The next step to perform when using object handle inheritance is for the parent process to spawn the child process. This is done using the CreateProcess function:

BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD dwCreationFlags,
PVOID pvEnvironment,
PCTSTR pszCurrentDirectory,
LPSTARTUPINFO pStartupInfo,
PPROCESS_INFORMATION pProcessInformation);

We'll examine this function in detail in the next chapter, but for now I want to draw your attention to the bInheritHandles parameter. Usually, when you spawn a process, you pass FALSE for this parameter. This value tells the system that you do not want the child process to inherit the inheritable handles that are in the parent process' handle table.

If you pass TRUE for this parameter, however, the child inherits the parent's inheritable handle values. When you pass TRUE, the operating system creates the new child process but does not allow the child process to begin executing its code right away. Of course, the system creates a new, empty process handle table for the child process—just as it would for any new process. But because you passed TRUE to CreateProcess' bInheritHandles parameter, the system does one more thing: it walks the parent process' handle table, and for each entry it finds that contains a valid inheritable handle, the system copies the entry exactly into the child process' handle table. The entry is copied to the exact same position in the child process' handle table as in the parent's handle table. This fact is important because it means that the handle value that identifies a kernel object is identical in both the parent and child processes.

In addition to copying the handle table entry, the system increments the usage count of the kernel object because two processes are now using the object. For the kernel object to be destroyed, both the parent process and the child process must either call CloseHandle on the object or terminate. The child does not have to terminate first—but neither does the parent. In fact, the parent process can close its handle to the object immediately after the CreateProcess function returns without affecting the child's ability to manipulate the object.

Table 3-3 shows the child process' handle table immediately before the process is allowed to begin execution. You can see that entries 1 and 2 are not initialized and are therefore invalid handles for the child process to use. However, index 3 does identify a kernel object. In fact, it identifies the kernel object at address 0xF0000010—the same object as in the parent process' handle table.

Index

Pointer to Kernel Object Memory Block

Access Mask (DWORD of Flag Bits)

Flags

1

0x00000000

(N/A)

(N/A)

2

0x00000000

(N/A)

(N/A)

3

0xF0000010

0x????????

0x00000001

As you will see in Chapter 13, "Windows Memory Architecture," the content of kernel objects is stored in the kernel address space that is shared by all processes running on the system. For 32-bit systems, this is in memory between the following memory addresses: 0x80000000 and 0xFFFFFFFF. For 64-bit systems, this is in memory between the following memory addresses: 0x00000400'00000000 and 0xFFFFFFF'FFFFFFFF. The access mask is identical to the mask in the parent, and the flags are also identical. This means that if the child process were to spawn its own child process (a grandchild process of the parent) with the same bInheritHandles parameter of CreateProcess set to TRUE, this grandchild process would also inherit this kernel object handle with the same handle value, same access, and same flags, and the usage count on the object would again be incremented.

Be aware that object handle inheritance applies only at the time the child process is spawned. If the parent process were to create any new kernel objects with inheritable handles, an already-running child process would not inherit these new handles.

Object handle inheritance has one very strange characteristic: when you use it, the child has no idea that it has inherited any handles. Kernel object handle inheritance is useful only when the child process documents the fact that it expects to be given access to a kernel object when spawned from another process. Usually, the parent and child applications are written by the same company; however, a different company can write the child application if that company documents what the child application expects.

By far, the most common way for a child process to determine the handle value of the kernel object that it's expecting is to have the handle value passed as a command-line argument to the child process. The child process' initialization code parses the command line (usually by calling _stscanf_s) and extracts the handle value. Once the child has the handle value, it has the same access to the object as its parent. Note that the only reason handle inheritance works is because the handle value of the shared kernel object is identical in both the parent process and the child process. This is why the parent process is able to pass the handle value as a command-line argument.

Of course, you can use other forms of interprocess communication to transfer an inherited kernel object handle value from the parent process into the child process. One technique is for the parent to wait for the child to complete initialization (using the WaitForInputIdle function discussed in Chapter 9, "Thread Synchronization with Kernel Objects"); then the parent can send or post a message to a window created by a thread in the child process.

Another technique is for the parent process to add an environment variable to its environment block. The variable's name would be something that the child process knows to look for, and the variable's value would be the handle value of the kernel object to be inherited. Then when the parent spawns the child process, the child process inherits the parent's environment variables and can easily call GetEnvironmentVariable to obtain the inherited object's handle value. This approach is excellent if the child process is going to spawn another child process, because the environment variables can be inherited again. The special case of a child process inheriting its parent console is detailed in the Microsoft Knowledge Base at http://support.microsoft.com/kb/190351.

Changing a Handle's Flags
Occasionally, you might encounter a situation in which a parent process creates a kernel object retrieving an inheritable handle and then spawns two child processes. The parent process wants only one child to inherit the kernel object handle. In other words, you might at times want to control which child processes inherit kernel object handles. To alter the inheritance flag of a kernel object handle, you can call the SetHandleInformation function:

BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);

As you can see, this function takes three parameters. The first, hObject, identifies a valid handle. The second parameter, dwMask, tells the function which flag or flags you want to change. Currently, two flags are associated with each handle:

#define HANDLE_FLAG_INHERIT            0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002

You can perform a bitwise OR on both of these flags together if you want to change each object's flags simultaneously. SetHandleInformation's third parameter, dwFlags, indicates what you want to set the flags to. For example, to turn on the inheritance flag for a kernel object handle, do the following:

SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
To turn off this flag, do this:
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);

The HANDLE_FLAG_PROTECT_FROM_CLOSE flag tells the system that this handle should not be allowed to be closed:

SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE,
HANDLE_FLAG_PROTECT_FROM_CLOSE);
CloseHandle(hObj);   // Exception is raised

When running under a debugger, if a thread attempts to close a protected handle, CloseHandle raises an exception. Outside the control of a debugger, CloseHandle simply returns FALSE. You rarely want to protect a handle from being closed. However, this flag might be useful if you had a process that spawned a child that in turn spawned a grandchild process. The parent process might be expecting the grandchild to inherit the object handle given to the immediate child. It is possible, however, that the immediate child might close the handle before spawning the grandchild. If this were to happen, the parent might not be able to communicate with the grandchild because the grandchild did not inherit the kernel object. By marking the handle as "protected from close," the grandchild has a better chance to inherit a handle to a valid and live object.

This approach has one flaw, however. The immediate child process might call the following code to turn off the HANDLE_FLAG_PROTECT_FROM_CLOSE flag and then close the handle:

SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
CloseHandle(hObj);

The parent process is gambling that the child process will not execute this code. Of course, the parent is also gambling that the child process will spawn the grandchild, so this bet is not that risky.

For the sake of completeness, I'll also mention the GetHandleInformation function:

BOOL GetHandleInformation(
HANDLE hObject,
PDWORD pdwFlags);

This function returns the current flag settings for the specified handle in the DWORD pointed to by pdwFlags. To see if a handle is inheritable, do the following:

DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));

Naming Objects
The second method available for sharing kernel objects across process boundaries is to name the objects. Many—though not all—kernel objects can be named. For example, all of the following functions create named kernel objects:

HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);
HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTES psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName);
HANDLE CreateWaitableTimer(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName);
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
HANDLE CreateJobObject(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName);

All these functions have a common last parameter, pszName. When you pass NULL for this parameter, you are indicating to the system that you want to create an unnamed (anonymous) kernel object. When you create an unnamed object, you can share the object across processes by using either inheritance (as discussed in the previous section) or DuplicateHandle (discussed in the next section). To share an object by name, you must give the object a name.

If you don't pass NULL for the pszName parameter, you should pass the address of a zero-terminated string name. This name can be up to MAX_PATH characters long (defined as 260). Unfortunately, Microsoft offers no guidance for assigning names to kernel objects. For example, if you attempt to create an object called "JeffObj," there's no guarantee that an object called "JeffObj" doesn't already exist. To make matters worse, all these objects share a single namespace even though they don't share the same type. Because of this, the following call to CreateSemaphore always returns NULL—because a mutex already exists with the same name:

HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("JeffObj"));
HANDLE hSem = CreateSemaphore(NULL, 1, 1, TEXT("JeffObj"));
DWORD dwErrorCode = GetLastError();

If you examine the value of dwErrorCode after executing the preceding code, you'll see a return code of 6 (ERROR_INVALID_HANDLE). This error code is not very descriptive, but what can you do?

Now that you know how to name an object, let's see how to share objects this way. Let's say that Process A starts up and calls the following function:

HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));

This function call creates a new mutex kernel object and assigns it the name "JeffMutex". Notice that in Process A's handle, hMutexProcessA is not an inheritable handle—and it doesn't have to be when you're only naming objects.

Some time later, some process spawns Process B. Process B does not have to be a child of Process A; it might be spawned from Windows Explorer or any other application. The fact that Process B need not be a child of Process A is an advantage of using named objects instead of inheritance. When Process B starts executing, it executes the following code:

HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));

When Process B's call to CreateMutex is made, the system first checks to find out whether a kernel object with the name "JeffMutex" already exists. Because an object with this name does exist, the kernel then checks the object type. Because we are attempting to create a mutex and the object with the name "JeffMutex" is also a mutex, the system then makes a security check to see whether the caller has full access to the object. If it does, the system locates an empty entry in Process B's handle table and initializes the entry to point to the existing kernel object. If the object types don't match or if the caller is denied access, CreateMutex fails (returns NULL).

 Note  Kernel object creation functions (such as CreateSemaphore) always return handles with a full access right. If you want to restrict the available access rights for a handle, you can take advantage of the extended versions of kernel object creation functions (with an Ex postfix) that accept an additional DWORD dwDesiredAccess parameter. For example, you can allow or disallow ReleaseSemaphore to be called on a semaphore handle by using or not SEMAPHORE_MODIFY_STATE in the call to CreateSemaphoreEx. Read the Windows SDK documentation for the details of the specific rights corresponding to each kind of kernel object at http://msdn2.microsoft.com/en-us/library/ms686670.aspx.

When Process B's call to CreateMutex is successful, a mutex is not actually created. Instead, Process B is simply assigned a process-relative handle value that identifies the existing mutex object in the kernel. Of course, because a new entry in Process B's handle table references this object, the mutex object's usage count is incremented. The object will not be destroyed until both Process A and Process B have closed their handles to the object. Notice that the handle values in the two processes are most likely going to be different values. This is OK. Process A will use its handle value, and Process B will use its own handle value to manipulate the one mutex kernel object.

 Note  When you have kernel objects sharing names, be aware of one extremely important detail. When Process B calls CreateMutex, it passes security attribute information and a second parameter to the function. These parameters are ignored if an object with the specified name already exists! An application can determine whether it did, in fact, create a new kernel object rather than simply open an existing object by calling GetLastError immediately after the call to the Create* function:

HANDLE hMutex = CreateMutex(&sa, FALSE, TEXT("JeffObj"));
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// Opened a handle to an existing object.
// sa.lpSecurityDescriptor and the second parameter
// (FALSE) are ignored.
} else {
// Created a brand new object.
// sa.lpSecurityDescriptor and the second parameter
// (FALSE) are used to construct the object.
}

An alternative method exists for sharing objects by name. Instead of calling a Create* function, a process can call one of the Open* functions shown here:

HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
HANDLE OpenWaitableTimer(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
HANDLE OpenFileMapping(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
HANDLE OpenJobObject(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

Notice that all these functions have the same prototype. The last parameter, pszName, indicates the name of a kernel object. You cannot pass NULL for this parameter; you must pass the address of a zero-terminated string. These functions search the single namespace of kernel objects attempting to find a match. If no kernel object with the specified name exists, the functions return NULL and GetLastError returns 2 (ERROR_FILE_NOT_FOUND). However, if a kernel object with the specified name does exist, but it has a different type, the functions return NULL and GetLastError returns 6 (ERROR_INVALID_HANDLE). And if it is the same type of object, the system then checks to see whether the requested access (via the dwDesiredAccess parameter) is allowed. If it is, the calling process' handle table is updated and the object's usage count is incremented. The returned handle will be inheritable if you pass TRUE for the bInheritHandle parameter.

The main difference between calling a Create* function versus calling an Open* function is that if the object doesn't already exist, the Create* function will create it, whereas the Open* function will simply fail.

As I mentioned earlier, Microsoft offers no real guidelines on how to create unique object names. In other words, it would be a problem if a user attempted to run two programs from different companies and each program attempted to create an object called "MyObject." For uniqueness, I recommend that you create a GUID and use the string representation of the GUID for your object names. You will see another way to ensure name uniqueness in "Private Namespaces" on page 53.

Named objects are commonly used to prevent multiple instances of an application from running. To do this, simply call a Create* function in your _tmain or _tWinMain function to create a named object. (It doesn't matter what type of object you create.) When the Create* function returns, call GetLastError. If GetLastError returns ERROR_ALREADY_EXISTS, another instance of your application is running and the new instance can exit. Here's some code that illustrates this:

int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine,
int nCmdShow) {
HANDLE h = CreateMutex(NULL, FALSE,
TEXT("{FA531CC1-0497-11d3-A180-00105A276C3E}"));
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// There is already an instance of this application running.
// Close the object and immediately return.
CloseHandle(h);
return(0);
}
   // This is the first instance of this application running.
...
// Before exiting, close the object.
CloseHandle(h);
return(0);
}

Terminal Services Namespaces
Note that Terminal Services changes the preceding scenario a little bit. A machine running Terminal Services has multiple namespaces for kernel objects. There is one global namespace, which is used by kernel objects that are meant to be accessible by all client sessions. This namespace is mostly used by services. In addition, each client session has its own namespace. This arrangement keeps two or more sessions that are running the same application from trampling over each other— one session cannot access another session's objects even though the objects share the same name.

These scenarios are not just related to server machines because Remote Desktop and Fast User Switching features are also implemented by taking advantage of Terminal Services sessions.

 Note  Before any user logs in, the services are starting in the first session, which is nonin-terative. In Windows Vista, unlike previous version of Windows, as soon as a user logs in, the applications are started in a new session—different from Session 0, which is dedicated to services. That way, these core components of the system, which are usually running with high privileges, are more isolated from any malware started by an unfortunate user.

For service developers, necessarily running in a session different from their client application affects the naming convention for shared kernel objects. It is now mandatory to create objects to be shared with user applications in the global namespace. This is the same kind of issue that you face when you need to write a service that is supposed to communicate with applications that might run when different users are logged in to different sessions through Fast User Switching—the service can't assume that it is running in the same session as the user application. For more details on Session 0 isolation and the impact it has on service developers, you should read "Impact of Session 0 Isolation on Services and Drivers in Windows Vista," which is located at http://www.microsoft.com/whdc/system/vista/services.mspx.

If you have to know in which Terminal Services session your process is running, the ProcessIdToSessionId function (exported by kernel32.dll and declared in WinBase.h) is what you need, as shown in the following example:

DWORD processID = GetCurrentProcessId();
DWORD sessionID;
if (ProcessIdToSessionId(processID, &sessionID)) {
tprintf(
TEXT("Process '%u' runs in Terminal Services session '%u'"),
processID, sessionID);
} else {
// ProcessIdToSessionId might fail if you don't have enough rights
// to access the process for which you pass the ID as parameter.
// Notice that it is not the case here because we're using our own process ID.
tprintf(
TEXT("Unable to get Terminal Services session ID for process '%u'"),
processID);
}

A service's named kernel objects always go in the global namespace. By default, in Terminal Services, an application's named kernel object goes in the session's namespace. However, it is possible to force the named object to go into the global namespace by prefixing the name with "Global\", as in the following example:

HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Global\\MyName"));

You can also explicitly state that you want a kernel object to go in the current session's namespace by prefixing the name with "Local\", as in the following example:

HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Local\\MyName"));


Microsoft considers Global and Local to be reserved keywords that you should not use in object names except to force a particular namespace. Microsoft also considers Session to be a reserved keyword. So, for example, you could use Session\\. However, it is not possible to create an object with a name in another session with the Session prefix—the function call fails, and GetLastError returns ERROR_ACCESS_DENIED.

 Note  All these reserved keywords are case sensitive.

Private Namespaces
When you create a kernel object, you can protect the access to it by passing a pointer to a SECURITY_ATTRIBUTES structure. However, prior to the release of Windows Vista, it was not possible to protect the name of a shared object against hijacking. Any process, even with the lowest privileges, is able to create an object with a given name. If you take the previous example where an application is using a named mutex to detect whether or not it is already started, you could very easily write another application that creates a kernel object with the same name. If it gets started before the singleton application, this application becomes a "none-gleton" because it will start and then always immediately exit, thinking that another instance of itself is already running. This is the base mechanism behind a couple of attacks known as Denial of Service (DoS) attacks. Notice that unnamed kernel objects are not subject to DoS attacks, and it is quite common for an application to use unnamed objects, even though they can't be shared between processes.

If you want to ensure that the kernel object names created by your own applications never conflict with any other application's names or are the subject of hijack attacks, you can define a custom prefix and use it as a private namespace as you do with Global and Local. The server process responsible for creating the kernel object defines a boundary descriptor that protects the namespace name itself.

The Singleton application 03-Singleton.exe (with the corresponding Singleton.cpp source code listed a bit later in the chapter) shows how to use private namespaces to implement the same singleton pattern presented earlier but in a more secure way. When you start the program, the window shown in Figure 3-5 appears.

Figure 3-5: First instance of Singleton running
If you start the same program with the first one still running, the window shown in Figure 3-6 explains that the previous instance has been detected.

Figure 3-6: Second instance of Singleton when started while the first one is still running

The CheckInstances function in the following source code shows how to create a boundary, associate a security identifier (or SID) corresponding to the Local Administrators group with it, and create or open the private namespace whose name is used as a prefix by the mutex kernel object. The boundary descriptor gets a name, but more importantly, it gets a SID of a privileged user group that is associated with it. That way, Windows ensures that only the applications running under the context of a user that is a part of this privileged group is able to create the same namespace in the same boundary and thereby access the kernel objects created within this boundary that are prefixed with the private namespace name.

If a low-privileged malware application creates the same boundary descriptor because the name and the SID have been stolen, for example, when it tries to create or open the private namespace protected with a high-privileged account, the corresponding calls fail, with GetLastError returning ERROR_ACCESS_DENIED. If the malware application has enough privileges to create or open the namespace, worrying about this is not important because the malware application has enough control that it can cause much more damage than simply hijacking a kernel object name.

Singleton.cpp 

/******************************************************************************
Module:  Singleton.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
//
#include "stdafx.h"
#include "resource.h"
#include "..\CommonFiles\CmnHdr.h"     /* See Appendix A,. */
#include
#include           // for SID management
#include
#include
 
///////////////////////////////////////////////////////////////////////////////

// Main dialog
HWND     g_hDlg;
// Mutex, boundary and namespace used to detect previous running instance
HANDLE   g_hSingleton = NULL;
HANDLE   g_hBoundary = NULL;
HANDLE   g_hNamespace = NULL;
// Keep track whether or not the namespace was created or open for clean-up
BOOL     g_bNamespaceOpened = FALSE;
// Names of boundary and private namespace
PCTSTR   g_szBoundary = TEXT("3-Boundary");
PCTSTR   g_szNamespace = TEXT("3-Namespace");
#define DETAILS_CTRL GetDlgItem(g_hDlg, IDC_EDIT_DETAILS)

///////////////////////////////////////////////////////////////////////////////

// Adds a string to the "Details" edit control
void AddText(PCTSTR pszFormat, ...) {
   va_list argList;
va_start(argList, pszFormat);
   TCHAR sz[20 * 1024];
   Edit_GetText(DETAILS_CTRL, sz, _countof(sz));
_vstprintf_s(
_tcschr(sz, TEXT('\0')), _countof(sz) - _tcslen(sz),
pszFormat, argList);
Edit_SetText(DETAILS_CTRL, sz);
va_end(argList);
}

///////////////////////////////////////////////////////////////////////////////

void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
   switch (id) {
case IDOK:
case IDCANCEL:
// User has clicked on the Exit button
// or dismissed the dialog with ESCAPE
EndDialog(hwnd, id);
break;
}
}

///////////////////////////////////////////////////////////////////////////////
void CheckInstances() {
   // Create the boundary descriptor
g_hBoundary = CreateBoundaryDescriptor(g_szBoundary, 0);
   // Create a SID corresponding to the Local Administrator group
BYTE localAdminSID[SECURITY_MAX_SID_SIZE];
PSID pLocalAdminSID = &localAdminSID;
DWORD cbSID = sizeof(localAdminSID);
if (!CreateWellKnownSid(
WinBuiltinAdministratorsSid, NULL, pLocalAdminSID, &cbSID)) {
AddText(TEXT("AddSIDToBoundaryDescriptor failed: %u\r\n"),
GetLastError());
return;
}
   // Associate the Local Admin SID to the boundary descriptor
// --> only applications running under an administrator user
//     will be able to access the kernel objects in the same namespace
if (!AddSIDToBoundaryDescriptor(&g_hBoundary, pLocalAdminSID)) {
AddText(TEXT("AddSIDToBoundaryDescriptor failed: %u\r\n"),
GetLastError());
return;
}
   // Create the namespace for Local Administrators only
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = FALSE;
if (!ConvertStringSecurityDescriptorToSecurityDescriptor(
TEXT("D:(A;;GA;;;BA)"),
SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL)) {
AddText(TEXT("Security Descriptor creation failed: %u\r\n"), GetLastError());
return;
}
   g_hNamespace =
CreatePrivateNamespace(&sa, g_hBoundary, g_szNamespace);
   // Don't forget to release memory for the security descriptor
LocalFree(sa.lpSecurityDescriptor);

// Check the private namespace creation result
DWORD dwLastError = GetLastError();
if (g_hNamespace == NULL) {
// Nothing to do if access is denied
// --> this code must run under a Local Administrator account
if (dwLastError == ERROR_ACCESS_DENIED) {
AddText(TEXT("Access denied when creating the namespace.\r\n"));
AddText(TEXT("   You must be running as Administrator.\r\n\r\n"));
return;
        } else {
if (dwLastError == ERROR_ALREADY_EXISTS) {
// If another instance has already created the namespace,
// we need to open it instead.
AddText(TEXT("CreatePrivateNamespace failed: %u\r\n"), dwLastError);
g_hNamespace = OpenPrivateNamespace(g_hBoundary, g_szNamespace);
if (g_hNamespace == NULL) {
AddText(TEXT(" and OpenPrivateNamespace failed: %u\r\n"),
dwLastError);
return;
} else {
g_bNamespaceOpened = TRUE;
AddText(TEXT(" but OpenPrivateNamespace succeeded\r\n\r\n"));
}
} else {
AddText(TEXT("Unexpected error occurred: %u\r\n\r\n"),
dwLastError);
return;
}
}
}
    // Try to create the mutex object with a name
// based on the private namespace
TCHAR szMutexName[64];
StringCchPrintf(szMutexName, _countof(szMutexName), TEXT("%s\\%s"),
g_szNamespace, TEXT("Singleton"));
    g_hSingleton = CreateMutex(NULL, FALSE, szMutexName);
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// There is already an instance of this Singleton object
AddText(TEXT("Another instance of Singleton is running:\r\n"));
AddText(TEXT("--> Impossible to access application features.\r\n"));
} else {
// First time the Singleton object is created
AddText(TEXT("First instance of Singleton:\r\n"));
AddText(TEXT("--> Access application features now.\r\n"));
}
}

///////////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {
   chSETDLGICONS(hwnd, IDI_SINGLETON);
   // Keep track of the main dialog window handle
g_hDlg = hwnd;
   // Check whether another instance is already running
CheckInstances();
   return(TRUE);
}
///////////////////////////////////////////////////////////////////////////////
INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
   switch (uMsg) {
chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand);
chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
}
   return(FALSE);
}

///////////////////////////////////////////////////////////////////////////////

int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR    lpCmdLine,
int       nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
   // Show main window
DialogBox(hInstance, MAKEINTRESOURCE(IDD_SINGLETON), NULL, Dlg_Proc);
   // Don't forget to clean up and release kernel resources
if (g_hSingleton != NULL) {
CloseHandle(g_hSingleton);
}
   if (g_hNamespace != NULL) {
if (g_bNamespaceOpened) { // Open namespace
ClosePrivateNamespace(g_hNamespace, 0);
} else { // Created namespace
ClosePrivateNamespace(g_hNamespace, PRIVATE_NAMESPACE_FLAG_DESTROY);
}
}
   if (g_hBoundary != NULL) {
DeleteBoundaryDescriptor(g_hBoundary);
}
   return(0);
}
//////////////////////////////// End of File //////////////////////////////////
 

Let's examine the different steps of the CheckInstances function. First, the creation of a boundary descriptor requires a string identifier to name the scope where the private namespace will be defined. This name is passed as the first parameter of the following function:

HANDLE CreateBoundaryDescriptor(
PCTSTR pszName,
DWORD dwFlags);

Current versions of Windows do not use the second parameter, and therefore you should pass 0 for it. Note that the function signature implies that the return value is a kernel object handle; however, it is not. The return value is a pointer to a user-mode structure containing the definition of the boundary. For this reason, you should never pass the returned handle value to CloseHandle; you should pass it to DeleteBoundaryDescriptor instead.

The next step is to associate the SID of a privileged group of users that the client applications are supposed to run under with the boundary descriptor by calling the following function:

BOOL AddSIDToBoundaryDescriptor(
HANDLE* phBoundaryDescriptor,
PSID pRequiredSid);

In the Singleton example, the SID of the Local Administrator group is created by calling AllocateAndInitializeSid with SECURITY_BUILTIN_DOMAIN_RID and DOMAIN_ALIAS_RID_ADMINS as parameters that describe the group. The list of all well-known groups is defined in the WinNT.h header file.

This boundary descriptor handle is passed as the second parameter when you call the following function to create the private namespace:

HANDLE CreatePrivateNamespace(
PSECURITY_ATTRIBUTES psa,
PVOID pvBoundaryDescriptor,
PCTSTR pszAliasPrefix);

The SECURITY_ATTRIBUTES that you pass as the first parameter to this function is used by Windows to allow or not allow an application calling OpenPrivateNamespace to access the namespace and open or create objects within that namespace. You have exactly the same options as within a file system directory. This is the level of filter that you provide for opening the namespace. The SID you added to the boundary descriptor is used to define who is able to enter the boundary and create the namespace. In the Singleton example, the SECURITY_ATTRIBUTE is constructed by calling the ConvertStringSecurityDescriptorToSecurityDescriptor function that takes a string with a complicated syntax as the first parameter. The security descriptor string syntax is documented at http://msdn2.microsoft.com/en-us/library/aa374928.aspx and http://msdn2.microsoft.com/en-us/library/aa379602.aspx.

The type of pvBoundaryDescriptor is PVOID, even though CreateBoundaryDescriptor returns a HANDLE—even at Microsoft it is seen as a pseudohandle. The string prefix you want to use to create your kernel objects is given as the third parameter. If you try to create a private namespace that already exists, CreatePrivateNamespace returns NULL and GetLastError returns ERROR_ALREADY_EXISTS. In this case, you need to open the existing private namespace using the following function:

HANDLE OpenPrivateNamespace(
PVOID pvBoundaryDescriptor,
PCTSTR pszAliasPrefix);

Note that the HANDLEs returned by CreatePrivateNamespace and OpenPrivateNamespace are not kernel object handles; you close any of these pseudohandles by calling ClosePrivate-Namespace:

BOOLEAN ClosePrivateNamespace(
HANDLE hNamespace,
DWORD dwFlags);

If you created the namespace and you don't want it to be visible after you close it, you should pass PRIVATE_NAMESPACE_FLAG_DESTROY as the second parameter and pass 0 otherwise. The boundary is closed either when the process ends or if you call the DeleteBoundaryDescriptor function with the boundary pseudohandle as the only parameter. The namespace must not be closed while the kernel object is used. If you close the namespace while an object exists inside it, it becomes possible to create another kernel object with the same name in the same re-created namespace in the same boundary, thus enabling DoS attacks once more.

To sum up, a private namespace is just a directory where you create kernel objects. Like other directories, a private namespace has a security descriptor associated with it that is set when you call CreatePrivateNamespace. However, unlike file system directories, this namespace does not have a parent or a name—the boundary descriptor is used as a name to refer to it. This is the reason why the kernel objects created with a prefix based on a private namespace appear in Process Explorer from Sysinternals with a "…\" prefix instead of the expected "namespace name\". The "…\" prefix hides information, thereby granting more protection against potential hackers. The name that you give to a private namespace is an alias visible only within the process. Other processes (and even the same process) can open the very same private namespace and give it a different alias.

To create regular directories, an access check is performed against the parent to determine whether or not a subdirectory can be created. To create namespaces, a boundary test is performed—the token of the current thread must include all the SIDs that are part of the boundary.

Duplicating Object Handles
The last technique for sharing kernel objects across process boundaries requires the use of the DuplicateHandle function:

BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);

Simply stated, this function takes an entry in one process' handle table and makes a copy of the entry into another process' handle table. DuplicateHandle takes several parameters but is actually quite straightforward. The most general usage of the DuplicateHandle function could involve three different processes that are running in the system.

When you call DuplicateHandle, the first and third parameters—hSourceProcessHandle and hTargetProcessHandle—are kernel object handles. The handles themselves must be relative to the process that is calling the DuplicateHandle function. In addition, these two parameters must identify process kernel objects; the function fails if you pass handles to any other type of kernel object. We'll discuss process kernel objects in more detail in Chapter 4; for now, all you need to know is that a process kernel object is created whenever a new process is invoked in the system.

The second parameter, hSourceHandle, is a handle to any type of kernel object. However, the handle value is not relative to the process that calls DuplicateHandle. Instead, this handle must be relative to the process identified by the hSourceProcessHandle handle. The fourth parameter, phTargetHandle, is the address of a HANDLE variable that receives as its value the HANDLE of the entry in the handle table of the process identified by hTargetProcessHandle, where the source's handle information gets copied.

DuplicateHandle's last three parameters allow you to indicate the value of the access mask and the inheritance flag that should be used in the target's entry for this kernel object handle. The dwOptions parameter can be 0 (zero) or any combination of the following two flags: DUPLICATE_SAME_ACCESS and DUPLICATE_CLOSE_SOURCE.

Specifying DUPLICATE_SAME_ACCESS tells DuplicateHandle that you want the target's handle to have the same access mask as the source process' handle. Using this flag causes Duplicate-Handle to ignore its dwDesiredAccess parameter.

Specifying DUPLICATE_CLOSE_SOURCE has the effect of closing the handle in the source process. This flag makes it easy for one process to hand a kernel object over to another process. When this flag is used, the usage count of the kernel object is not affected.

I'll use an example to show you how DuplicateHandle works. For this demonstration, Process S is the source process that currently has access to some kernel object and Process T is the target process that will gain access to this kernel object. Process C is the catalyst process that will execute the call to DuplicateHandle. In this example, I use hard-coded numbers for handle values only to demonstrate how the function operates. In real applications, you would have the various handle values in variables and pass the variables as arguments to the function.

Process C's handle table (Table 3-4) contains two handle values, 1 and 2. Handle value 1 identifies Process S's process kernel object, and handle value 2 identifies Process T's process kernel object.

Table 3-4: Process C's Handle Table 

Index

Pointer to Kernel Object Memory Block

Access Mask (DWORD of Flag Bits)

Flags

1

0xF0000000 (Process S's kernel object)

0x????????

0x00000000

2

0xF0000010 (Process T's kernel object)

0x????????

0x00000000

Table 3-5 is Process S's handle table, which contains a single entry with a handle value of 2. This handle can identify any type of kernel object—it doesn't have to be a process kernel object.

Index

Pointer to Kernel Object Memory Block

Access Mask (DWORD of Flag Bits)

Flags

1

0x00000000

(N/A)

(N/A)

2

0xF0000020 (any kernel object)

0x????????

0x00000000

Table 3-6 shows what Process T's handle table contains before Process C calls the DuplicateHandle function. As you can see, Process T's handle table contains only a single entry with a handle value of 2; handle entry 1 is currently unused.

Index

Pointer to Kernel Object Memory Block

Access Mask (DWORD of Flag Bits)

Flags

1

0x00000000

(N/A)

(N/A)

2

0xF0000030 (any kernel object)

0x????????

0x00000000

If Process C now calls DuplicateHandle using the following code, only Process T's handle table has changed, as shown in Table 3-7:

  DuplicateHandle(1, 2, 2, &hObj, 0, TRUE, DUPLICATE_SAME_ACCESS);
Table 3-7: Process T's Handle Table After Calling DuplicateHandle 

Index

Pointer to Kernel Object Memory Block

Access Mask (DWORD of Flag Bits)

Flags

1

0xF0000020

0x????????

0x00000001

2

0xF0000030 (any kernel object)

0x????????

0x00000000

The second entry in Process S's handle table has been copied to the first entry in Process T's handle table. DuplicateHandle has also filled in Process C's hObj variable with a value of 1, which is the index in process T's handle table in which the new entry was placed.

Because the DUPLICATE_SAME_ACCESS flag was passed to DuplicateHandle, the access mask for this handle in Process T's table is identical to the access mask in Process S's table entry. Also, passing the DUPLICATE_SAME_ACCESS flag causes DuplicateHandle to ignore its dwDesiredAccess parameter. Finally, notice that the inheritance bit flag has been turned on because TRUE was passed for DuplicateHandle's bInheritHandle parameter.

As with inheritance, one of the odd things about the DuplicateHandle function is that the target process is not given any notification that a new kernel object is now accessible to it. So Process C must somehow notify Process T that it now has access to a kernel object, and it must use some form of interprocess communication to pass the handle value in hObj to Process T. Obviously, using a command-line argument or changing Process T's environment variables is out of the question because the process is already up and running. A window message or some other interprocess communication (IPC) mechanism must be used.

What I have just explained is the most general usage of DuplicateHandle. As you can see, it is a very flexible function. However, it is rarely used when three different processes are involved (partly because it is unlikely that Process C would know the handle value of an object in use by Process S). Usually, DuplicateHandle is called when only two processes are involved. Imagine a situation in which one process has access to an object that another process wants access to, or a case in which one process wants to give access to a kernel object to another process. For example, let's say that Process S has access to a kernel object and wants to give Process T access to this object. To do this, you call DuplicateHandle as follows:

 // All of the following code is executed by Process S.
// Create a mutex object accessible by Process S.
HANDLE hObjInProcessS = CreateMutex(NULL, FALSE, NULL);
// Get a handle to Process T's kernel object.
HANDLE hProcessT = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
dwProcessIdT);
HANDLE hObjInProcessT;   // An uninitialized handle relative to Process T.
// Give Process T access to our mutex object.
DuplicateHandle(GetCurrentProcess(), hObjInProcessS, hProcessT,
&hObjInProcessT, 0, FALSE, DUPLICATE_SAME_ACCESS);
// Use some IPC mechanism to get the handle value of hObjInProcessS into Process T.
...
// We no longer need to communicate with Process T.
CloseHandle(hProcessT);
...
// When Process S no longer needs to use the mutex, it should close it.
CloseHandle(hObjInProcessS);

The call to GetCurrentProcess returns a pseudohandle that always identifies the calling process—Process S in this example. Once DuplicateHandle returns, hObjInProcessT is a handle relative to Process T that identifies the same object that the handle for hObjInProcessS does when referenced by code in Process S. Process S should never execute the following code:

// Process S should never attempt to close the duplicated handle.
CloseHandle(hObjInProcessT);

If Process S executed this code, the call might or might not fail. But this is not the problem. The call would succeed if Process S happened to have access to a kernel object with the same handle value as hObjInProcessT. This call would have the unexpected effect of closing some random kernel object so that the next time Process S tried to access it, it would certainly cause the application to behave undesirably (to put it nicely).

Here is another way to use DuplicateHandle: Suppose that a process has read and write access to a file-mapping object. At some point, a function is called that is supposed to access the file-mapping object by only reading it. To make our application more robust, we can use Duplicate-Handle to create a new handle for the existing object and ensure that this new handle has read-only access on it. We would then pass this read-only handle to the function; this way, the code in the function would never be able to accidentally write to the file-mapping object. The following code illustrates this example:

int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE,
LPTSTR szCmdLine, int nCmdShow) {
   // Create a file-mapping object; the handle has read/write access.
HANDLE hFileMapRW = CreateFileMapping(INVALID_HANDLE_VALUE,
NULL, PAGE_READWRITE, 0, 10240, NULL);
   // Create another handle to the file-mapping object;
// the handle has read-only access.
HANDLE hFileMapRO;
DuplicateHandle(GetCurrentProcess(), hFileMapRW, GetCurrentProcess(),
&hFileMapRO, FILE_MAP_READ, FALSE, 0);
   // Call the function that should only read from the file mapping.
ReadFromTheFileMapping(hFileMapRO);
   // Close the read-only file-mapping object.
CloseHandle(hFileMapRO);
   // We can still read/write the file-mapping object using hFileMapRW.
...
// When the main code doesn't access the file mapping anymore,
// close it.
CloseHandle(hFileMapRW);
【责任编辑:阚书 TEL:(010)68476606】

回书目   上一节   下一节
点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

我的运维日志系统构建之路

我的运维日志系统构建之路

数据驱动运维
共18章 | 我叫于小炳

191人订阅学习

CentOS文件服务的最佳实战

CentOS文件服务的最佳实战

涨薪跳槽必备技能
共15章 | 追风蚂蚁

88人订阅学习

小白网工宝典

小白网工宝典

一次搞定思科华为
共15章 | 思科小牛

437人订阅学习

读 书 +更多

SQL实用简明教程(第2版)

SQL(结构化查询语言)是数据库系统的通用语言,利用它可以用几乎同样的语句在不同的数据库系统上执行同样的操作,在数据库系统的开发中有着...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO播客