Process Synchronization


Concurrent programming requires the use of process synchronization services the kernel exposes to userland applications. Both UNIX and Windows provide these services; however, they differ greatly in their implementation and semantics. The following sections present both the UNIX and Windows synchronization APIs and their fundamental synchronization primitives.

System V Process Synchronization

Chapter 10, "UNIX II: Processes," introduced the System V IPC mechanisms available in most UNIX OSs, which includes three objects that are visible in the kernel namespace and can be used by unrelated processes to interact with each other: semaphores, message queues, and shared memory segments. This discussion focuses on semaphores, as they are most relevant in discussions of synchronization.

Note

Shared memory segments have some relevance in synchronization, as processes sharing a memory segment must ensure that mutually exclusive access is achieved correctly so that the shared memory segment isn't accessed when it's in an inconsistent state. However, the issue of synchronization isn't the shared memory itself, but the mechanisms put in place to access that object (as is the case for any other shared resource). Therefore, shared memory isn't discussed further in this section.


Semaphores

A semaphore is a locking device that uses a counter to limit the number of instances that can be acquired. This counter is decremented every time the semaphore is acquired and incremented every time a semaphore is released. When the count is zero, any attempts to acquire the semaphore cause the caller to block.

Semaphores are represented by IDs in the System V IPC API. System V also allows semaphores to be manipulated in sets, which are arrays of semaphores that programmers create to group related semaphores into one unit. The functions for manipulating semaphores and semaphore sets are described in the following paragraphs.

The semget() function creates a new semaphore set or obtains an existing semaphore set:

int semget(key_t key, int nsems, int semflg)


A new semaphore set is created if the value of key is IPC_PRIVATE or if the IPC_CREAT flag is set in semflg. An existing semaphore set is accessed by supplying the corresponding key for the first parameter; an error is returned if the key does not match an existing semaphore. If both the IPC_CREAT and IPC_EXCL flags are set and a semaphore with the same key already exists, an error is returned instead of a new semaphore being created.

The nsems parameter indicates how many semaphores should exist in the specified set; if a single semaphore is used, a value of 1 is supplied. The semflg parameter is used to indicate what access permissions the semaphore set should have, as well as the following arguments:

  • IPC_CREAT Create a new set if one doesn't exist already.

  • IPC_EXCL Create a new semaphore set, or return an error if one already exists.

  • IPC_NOWAIT Return with an error if the request is required to wait for the resource.

The low nine bits of semflg provide a standard UNIX permission mask for owner, group, and world. The read permission allows semaphore access, write provides alter permission, and execute is not used.

The semop() function performs an operation on selected semaphores in the semaphore set referenced by semid:

int semop(int semid, struct sembuf *sops, unsigned nsops)


The sops array contains a series of sembuf structures that describe operations to be performed on specific semaphores in the set. This function is used primarily to wait on or signal a semaphore, depending on the value of sem_op in each structure. The value of sem_op has the following effects:

  • If the sem_op parameter is greater than 0, it is added to the internal integer in the semaphore structure, which is effectively the same as issuing multiple signals on the semaphore.

  • If the sem_op value is equal to 0, the process waits (is put to sleep) until the semaphore value becomes 0.

  • If the sem_op value is less than 0, that value is added to the internal integer in the semaphore structure. Because sem_op is negative, the operation is really a subtraction. This operation is like issuing multiple waits on the semaphore and may put the process to sleep.

The semctl() function is used to perform a control operation on the semaphore referenced by semid:

int semctl(int semid, int semnum, int cmd, ...)


The cmd value can be one of the following:

  • IPC_STAT Copy the semaphore structure stored in the kernel to a user space buffer. It requires read privileges to the semaphore.

  • IPC_SET Update the UID, GID, or mode of the semaphore set. It requires the caller to be a super-user or the creator of the set.

  • IPC_RMID Remove the semaphore set. It requires super-user privileges or for the caller to be the creator of the set.

  • SETALL Set the integer value in all semaphores in the set to be a specific value.

  • SETVAL Set a specific semaphore in the semaphore set to be a specific value.

A number of other operations can be performed, but they aren't relevant to this discussion. Interested readers can refer to the semctl() man page.

Windows Process Synchronization

The Win32 API provides objects that can synchronize a number of threads in a single process, as well as objects that can be used for synchronizing processes on a system. There are four interprocess synchronization objects: mutexes (Mutex or Mutant), events (Event), semaphores (Semaphore), and waitable timers (WaitableTimer). Each object has a signaled state in which it can be acquired and an unsignaled state in which an attempt to acquire it will force the caller to wait on a corresponding release. Sychronization objects can be created as named or unnamed objects and, as with all securable objects, are referenced with the HANDLE data type.

Note

Windows uses a single namespace for all mutexes, events, semaphores, waitable timers, jobs, and file-mappings. So no instances of these six object types can share the same name. For example, an attempt to create a mutex named MySync fails if a semaphore named MySync already exists.


Wait Functions

All windows synchronization objects are acquired (waited on) by the same set of functions. These functions put the calling process to sleep until the waited-on object is signaled. Some objects may also be modified by a call to a wait function. For example, with a mutex, the caller gains ownership of the object after successful completion of a wait function. Because the wait functions are common to all synchronization objects, it's best to discuss them before the objects themselves.

The WaitForSingleObject() function waits on a synchronization object specified by hHandle for a maximum period of time specified by dwMilliseconds:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds)


The following function works the same way as WaitForSingleObject(), except it has an additional parameter, bAlertable:

DWORD WaitForSingleObjectEx(HANDLE hHandle, DWORD dwMilliseconds,                               BOOL bAlertable)


This parameter indicates that the process is alertable (that is, an I/O completion routine or asynchronous procedure call (APC) can be run after successful return from this function). This parameter is irrelevant for the purposes of this discussion.

Note

APCs are a common Windows idiom in I/O and IPC routines. At the most basic level, they are callback routines that can be scheduled to run at the earliest convenient time for the process. The earliest convenient time is when the process is alertable (waiting on an object) and is running userland-level code (i.e., it isn't in the middle of performing a system call). For more information on APCs, see Microsoft Windows Internals 4th Edition by Mark Russinovich and David Solomon (Microsoft Press, 2004).


The following function is similar to the WaitForSingleObject() function, except it waits on multiple objects that are specified as an array of handles (lpHandles) with nCount elements:

DWORD WaitForMultipleObjects(DWORD nCount, const HANDLE *lpHandles,                                BOOL bWaitAll,                                DWORD dwMilliseconds)


If bWaitAll is set to TRUE, this function waits for all objects specified in the lpHandles array to be signaled; otherwise, it waits for just one of the objects to be signaled before returning. Like WaitForSingleObject(), the dwMilliseconds parameter defines the maximum amount of time the function should wait before returning.

The following function works the same way as WaitForMultipleObjects(), except it has an additional parameter, bAlertable:

DWORD WaitForMultipleObjectsEx(DWORD nCount, const HANDLE *lpHandles,                                  BOOL bWaitAll,                                  DWORD dwMilliseconds,                                  BOOL bAlertable)


As with WaitForSingleObjectEx(), this parameter indicates that an I/O completion routine or APC can be run after successful return from this function.

Mutex Objects

Windows provides an implementation of the standard mutex synchronization primitive. When a thread locks a mutex, other threads that attempt to lock the mutex are put to sleep until it is released. After it has been released, one of the waiting threads will be awakened and acquire the mutex. There are three API functions specifically for creating and managing mutexes.

The CreateMutex() function is used to create a new mutex:

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,                     BOOL bInitialOwner, LPCSTR lpName)


The lpMutexAttributes parameter describes security attributes for the mutex being created. Setting the bInitialOwner parameter to TRUE creates the mutex in a locked state and grants the caller initial ownership. The final parameter, lpName, passes the object's name or NULL for an unnamed mutex. If a mutex with the same name already exists, that existing mutex is returned to the caller instead of a new one. When an existing mutex is opened the bInitialOwner parameter is ignored.

The following function opens an existing mutex object:

HANDLE OpenMutex(DWORD dwDesiredAccess,                   BOOL bInheritHandle, LPCSTR lpName)


The dwDesiredAccess parameter describes what access rights the caller is requesting. The bInheritHandle parameter describes whether this handle should be inherited across a CreateProcess() call, and the lpName parameter is the name of the mutex to open.

The ReleaseMutex() function signals the mutex so that other threads waiting on it can claim ownership of it (lock it):

BOOL ReleaseMutex(HANDLE hMutex)


A thread using this function must own the mutex and have the MUTEX_MODIFY_STATE access right to perform this operation. The current owner of a mutex can repeatedly acquire it without ever blocking. However, the mutex is not released until the number of calls to Release mutex equals the number of times the mutex was acquired by the current owner. In the discussion on "IPC Object Scoreboards" later in this chapter, you see exactly how this can be an issue.

Event Objects

An event object is used to inform another thread or process that an event has occurred. Like a mutex, an event object is always in a signaled or nonsignaled state. When it's in a nonsignaled state, any thread that waits on the event is put to sleep until it becomes signaled. An event differs from a mutex in that it can be used to broadcast an event to a series of threads simultaneously. In this case, a thread doesn't have exclusive ownership of the event object.

Event objects can be further categorized into two subtypes: manual-reset events and auto-reset events. A manual-reset event is one in which the object stays in a signaled state until a thread manually sets it to a nonsignaled state. An auto-reset event is one that's automatically set to a nonsignaled state after a waiting thread is woken up. Creating and manipulating an event requires using the functions described in the following paragraphs.

The CreateEvent() function is used to create a new event object with the security attributes described by the lpEventAttributes parameter:

HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,                     BOOL bManualReset, BOOL bInitialState,                     LPCSTR lpName)


The bManualReset parameter indicates whether the object is manual-reset or auto-reset; a value of TRUE creates a manual-reset object and a value of FALSE creates an auto-reset object. The bInitialState parameter indicates the initial state of the event; a value of TRUE sets the object to a signaled state and a value of FALSE sets it to a nonsignaled state. Finally, lpName indicates the name of the event object being created or NULL for an unnamed event. Like mutexes, passing the name of an existing event object causes it to be opened instead.

The OpenEvent() function works in the same way OpenMutex() does, except it opens a previously created event rather than a mutex:

HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL  bInheritHandle, LPCSTR lpName)


The SetEvent() function sets an event to a signaled state. The caller must have EVENT_MODIFY_STATE access rights to use this function:

BOOL SetEvent(HANDLE hEvent)


The ResetEvent() function sets an event to a nonsignaled state:

BOOL ResetEvent(HANDLE hEvent)


This function is used only for manual-reset events because they require threads to reset the event to a nonsignaled state. This function also requires that the caller has EVENT_MODIFY_STATE access rights for the event.

Semaphore Objects

As in other operating systems, semaphores are used to allow a limited number of threads access to some shared object. A semaphore maintains a count initialized to the maximum number of acquiring threads. This count is decremented each time a wait function is called on the object. When the count becomes zero, the object is no longer signaled, so additional threads using a wait function on the object are blocked. The functions for dealing with semaphores are described in the following paragraphs.

The CreateSemaphore() function creates a new semaphore or opens an existing semaphore if one with the same name already exists:

HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpAttributes,                        LONG lInitialCount, LONG lMaximumCount,                        LPCSTR lpName)


The lInitialCount parameter indicates the initial value of the semaphore counter. This value must be between 0 and lMaximumCount (inclusive). If the value is 0, the semaphore is in a nonsignaled state; otherwise, it's in a signaled state when initialized. The lMaximumCount parameter specifies the maximum number of threads that can simultaneously wait on this object without blocking.

The OpenSemaphore() function opens an existing semaphore and works in the same way that OpenMutex() and OpenEvent() do:

HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritable,                        LPCSTR lpName)


The ReleaseSemaphore() function increments the semaphore count by the amount specified in lReleaseCount:

BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount,                       LPLONG lpPreviousCount)


This function fails if lReleaseCount causes the semaphore to exceed its internal maximum count. The lpPreviousCount stores the previous count held by the semaphore before this function call. Usually, a call to this function leaves the semaphore in a signaled state because the resulting count is greater than zero.

Waitable Timer Objects

A waitable timer, or timer, is used to schedule threads for work at a later time by becoming signaled after a time interval has elapsed. There are two types of waitable timers: manual-reset and synchronization timers. A manual-reset timer remains signaled until it's manually reset to a nonsignaled state. A synchronization timer stays signaled until a thread completes a wait function on it. In addition, any waitable timer can be a periodic timera timer that's automatically reactivated each time the specified interval expires. The functions for dealing with waitable timers are described in the following paragraphs.

The CreateWaitableTimer() function works the same way other Create*() functions do:

HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpAttributes,                            BOOL bManualReset, LPCSTR lpName)


The bManualReset parameter specifies whether the timer should be a manual-reset timer or synchronization timer. A value of TRUE indicates it's a manual-reset timer, and a value of FALSE indicates it's a synchronization timer.

The OpenWaitableTimer() function is used to open an existing named waitable timer object. It works the same way other Open*() functions do:

HANDLE OpenWaitableTimer(DWORD dwDesiredAccess, BOOL bInheritable, LPCSTR lpName)


The SetWaitableTimer() function is responsible for initializing a waitable timer with a time interval:

BOOL SetWaitableTimer(HANDLE hTimer, const LARGE_INTEGER *pDueTime,                       LONG lPeriod,                       PTIMERAPCROUTINE pfnCompletionRoutine,                       LPVOID lpArgToCompletionRoutine,                       BOOL fResume)


The pDueTime parameter specifies the interval for the timer to be signaled after, and the lPeriod parameter specifies whether this timer should be reactivated after the time interval has elapsed. A value larger than 0 indicates it should, and a value of 0 indicates that it should signal only once. The next two parameters are a pointer to an optional completion routine that's called after the timer is signaled and an argument for that completion routine. The routine is queued as a user-mode APC. Finally, the fResume parameter indicates that the system should recover out of suspend mode if it's in suspend when the timer is activated.

The following function deactivates an active timer:

BOOL CancelWaitableTimer(HANDLE hTimer)


The caller must have TIMER_MODIFY_STATE access to the object for this function to succeed.

Vulnerabilities with Interprocess Synchronization

Now that you're familiar synchronization primitives, you can begin to explore what types of vulnerabilities could occur from incorrect or unsafe use of these primitives.

Lack of Use

Obviously, there's a problem when synchronization objects are required but not used. In particular, if two processes are attempting to access a shared resource, a race condition could occur. Take a look at a simple example:

char *users[NUSERS]; int curr_idx = 0; DWORD phoneConferenceThread(SOCKET s) {     char *name;     name = readString(s);     if(name == NULL)         return 0;     if(curr_idx >= NUSERS)         return 0;     users[curr_idx] = name;     curr_idx++;     .. more stuff .. }


Say a daemon accepted connections on a listening socket, and each new connection caused a thread to be spawned, running the code shown in the example. Clearly, there is a problem with modifying the users and curr_idx variables without using synchronization objects. You can see that the function is not reentrant due to its handling of global variables; so calling this function in multiple concurrent threads will eventually exhibit unexpected behavior due to not accessing the global variables atomically. A failure to use synchronization primitives in this instance could result in an overflow of the users array, or cause a name to unexpectedly overwritten in the users array.

When you're auditing code that operates on an improperly locked shared resource, it's important to determine the implications of multiple threads accessing that resource. In reality, it's quite uncommon for developers to disregard concurrency issues and not use any form of synchronization objects. However, developers can make mistakes and forget to use synchronization primitives in unexpected or infrequently traversed code paths. The "Threading Vulnerabilities" section later in this chapter presents an example of this issue in the Linux kernel.

Incorrect Use of Synchronization Objects

Misusing synchronization objects can also cause problems. These types of errors generally occur because developers don't fully understand the API or fail to check when certain exceptional conditions occur, such as not checking for return values. To determine when this error has been made, you need to cross-check synchronization API calls with how they appear in the program, and then determine whether they correspond with the developer's intentions. The following code shows an example of incorrect use of a synchronization function. First, there's a function to initialize a program containing multiple threads. One thread reads requests from a network and adds jobs to a global queue, and a series of threads read jobs from the queue and process them.

HANDLE queueEvent, jobThreads[NUMTHREADS+1]; struct element *queue; HANDLE queueMutex; SOCKET fd; DWORD initJobThreads(void) {     int i;     queueEvent = CreateEvent(NULL, TRUE, FALSE, NULL);     if(queueEvent == NULL)         return -1;     queueMutex = CreateMutex(NULL, FALSE, NULL);     for(i = 0; i < NUMTHREADS; i++)     {         jobThreads[i] = CreateThread(NULL, 0, processJob,                             NULL, 0, NULL);         if(jobThreads[i] == NULL)         {             .. error handle ..         }     }     jobThreads[i] = CreateThread(NULL, 0, processNetwork,                         NULL, 0, NULL);     if(jobThreads[i] == NULL)     {         .. error handle ..     }     return 0; }


After the initJobThreads() function is done, the processJob() and processNetwork() functions are responsible for doing the actual work. They use mutex objects to ensure mutually exclusive access to the queue resource and an event to wake up threads when the queue contains elements that need to be dequeued and processed.

Their implementations are shown in the following code:

DWORD processJob(LPVOID arg) {     struct element *elem;     for(;;)     {         WaitForSingleObject(queueMutex, INFINITE);         if(queue == NULL)             WaitForSingleObject(queueEvent, INFINITE);         elem = queue;         queue = queue->next;         ReleaseMutex(queueMutex);         .. process element ..     }     return 0; } DWORD processNetwork(LPVOID arg) {     struct element *elem, *tmp;     struct request *req;     for(;;)     {         req = readRequest(fd);         if(req == NULL) // bad request             continue;         elem = request_to_job_element(req);         HeapFree(req);         if(elem == NULL)             continue;         WaitForSingleObject(queueMutex, INFINITE);         if(queue == NULL)         {             queue = elem;             SetEvent(queueEvent);         }         else         {             for(tmp = queue; tmp->next; tmp = tmp->next)                 ;             tmp->next = elem;         }         ReleaseMutex(queueMutex);     }     return 0; }


Do you see the problem with this code? Look at the way the event object is initialized:

queueEvent = CreateEvent(NULL, TRUE, FALSE, NULL);


Setting the second parameter to TRUE indicates the object is a manual-reset event. However, by reading the code, you can tell that the developer intended to use an automatic-reset event, because after the first time the event is signaled, the manual-reset event remains in that state forever, even when the queue is empty. The incorrect use of CreateEvent() in this example leads to a NULL pointer dereference in processJob(), as a successful return from WaitForSingleObject() indicates that the queue is not empty. Astute readers might notice an additional flaw: This code is vulnerable to deadlock. If the queue is empty when processJob() runs, the running thread calls WaitForSingleObject(), which puts the caller to sleep until the processNetwork() function signals the event object. However, the processJob() routine waiting on the event is holding the queueMutex lock. As a result, processNetwork() can never enter, thus resulting in deadlock.

As you can see, errors resulting from incorrect use of synchronization objects are quite easy to make, especially when a multitude of objects are used. Creating a program without deadlocking and race conditions can be tricky; often the logic just isn't obvious, as shown in the previous example. In "IPC Object Scoreboards" later in this chapter, you learn a technique that utilizes scoreboards to track IPC object use. These scoreboards can help you determine how each object is used and whether there's a possibility it's being misused.

Squatting with Named Synchronization Objects

Chapter 11 introduced Windows namespace squatting, which occurs when a rogue application creates a named object before the real application can. This type of attack is a serious consideration for named synchronization objects. Imagine, for example, a program with the following code during its initialization:

int checkForAnotherInstance(void) {     HANDLE hMutex;     hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, "MyProgram");     if(hMutex == NULL)         return 1;     CloseHandle(hMutex);     return 0; }


The checkForAnotherInstance() function is called in the early stages of a program invocation. If it returns 1, the process exits because another instance of the program is already running.

Note

Synchronization objects are often used to prevent multiple instances of a program from running on a single host.


Say you run another process that creates a mutex named MyProgram and holds the lock indefinitely. In this case, the checkForAnotherInstance() function always returns 1, so any attempt to start this application fails. If this mutex is created in the global namespace, it prevents other users in a Terminal Services or XP environment from starting the application as well.

In addition to creating objects for the purpose of preventing an application from running correctly, a rogue application might be able to take possession of an object that another application created legitimately. For example, consider a scenario in which a process creates a global object and a number of other processes later manipulate this object. Processes attempting to manipulate the object do so by waiting on a mutex, as shown in this example:

int modifyObject(void) {     HANDLE hMutex;     DWORD status;     hMutex = OpenMutex(MUTEX_MODIFY_STATE, FALSE, "MyMutex");     if(hMutex == NULL)         return -1;     status = WaitForSingleObject(hMutex, INFINITY);     if(status == WAIT_TIMEOUT)         return -1;     .. modify some global object ..     ReleaseMutex(hMutex); }


What's the problem with this code? What if a rogue application also opens MyMutex and holds onto it indefinitely? The other waiting processes are left sleeping indefinitely, thus unable to complete their tasks.

You can also cause denial-of-service conditions in UNIX programs that bail out when an attempt to initialize a semaphore set fails or when the value of IPC_PRIVATE is not passed as the key parameter to semget(). For example, look at the following code:

int initialize_ipc(void) {     int semid;     semid = semget(ftok("/home/user/file", 'A'), 10,                     IPC_EXCL|IPC_CREAT | 0644);     if(semid < 0)         return -1;     return semid; }


This code creates a semaphore set with ten semaphores. Because IPC_CREAT and IPC_EXCL are defined, semget() returns an error if a semaphore with the same key already exists. If you create a set beforehand, the initialize_ipc() function returns an error and the program never starts.

Note

Notice the use of the ftok() function. Ostensibly, it's used to generate keys for use with IPC, but this function doesn't guarantee key uniqueness. In fact, a brief examination of the source code in glibc shows that if you supply the same arguments, you generate the same key value, or you could determine the key value it generates easily.


If the IPC_EXCL flag isn't supplied, you can still cause semget() to fail by initializing a semaphore set with restrictive permissions. You could also initialize a semaphore set with the same key but fewer semaphores in it, which also causes semget() to return an error.

Other Squatting Issues

So far, the squatting issues discussed usually result in a denial of service by not allowing a process access to an object. Squatting can also occur by taking advantage of a nuance of how the CreateEvent(), CreateMutex(), CreateSemaphore(), and CreateWaitableTimer() functions work. When called with a non-NULL name parameter, these functions check to see whether the specified name already exists. If it does, the existing object is returned to the caller instead of creating a new object. The only way to tell that an existing object is returned rather than a new one is for the developer to call GetLastError(), check whether the error is ERROR_ALREADY_EXISTS, and then handle that case specifically. Failure to do so can result in some interesting situations. If an existing object is returned, several parameters to the Create*() functions are ignored. For example, the CreateMutex() function takes three parameters: the security attributes structure describing access rights to the object, a Boolean value indicating whether the caller initially holds the lock, and the name of the object. If the named mutex already exists, the first two parameters are ignored! To quote from the MSDN's CreateMutex() function description:

If lpName matches the name of an existing named mutex, this function requests the MUTEX_ALL_ACCESS access right. In this case, the bInitialOwner parameter is ignored because it has already been set by the creating process. If the lpMutexAttributes parameter is not NULL, it determines whether the handle can be inherited, but its security-descriptor member is ignored.

Interesting. So if the ERROR_ALREADY_EXISTS value isn't checked for using GetLastError(), it's possible for an attacker to create a mutex with the same name before the real application does. This can undermine the security attributes that would otherwise be placed on the object because they are ignored when the application calls the CreateMutex() function. Furthermore, consider any code that calls CreateMutex() with the bInitialOwner parameter passed as TRUE. The caller might manipulate a shared object under the assumption that it holds the mutex lock, when in fact it doesn't, thus resulting in a race condition. Here is an example.

int modifyObject(HANDLE hObject) {     HANDLE hMutex;     hMutex = CreateMutex(NULL, TRUE, "MyMutex");     if(hMutex == NULL)         return -1;     .. modify object pointed to by hObject ..     ReleaseMutex(hMutex); }


The bInitialOwner parameter passed to CreateMutex() is set to TRUE to indicate that this process should have initial ownership of the lock. However, there's no call to GetLastError() to check for ERROR_ALREADY_EXISTS; therefore, it's possible that the returned mutex is a preexisting object. In this case, the bInitialOwner value is ignored, so this process would not in fact hold the lock for hMutex, and any access of hObject is subject to race conditions.

The other synchronization object creation functions have similar issues. The security attributes parameterand potentially other parametersare ignored if the named object already exists. For example, the lInitialCount and lMaximumCount parameters for CreateSemaphore() are ignored if an existing object is returned because those parameters are initialized by the original creator of the object. Ignoring these parameters might make it possible to create a semaphore with a different maximum count than the application expects, which might cause it to work incorrectly. In fact, if an arbitrarily large maximum count is set, the semaphore provides no mutual exclusion at all, again resulting in a race condition. Similarly, with an event object, the bManualReset and bInitialState parameters are ignored if a previously created object is returned. Therefore, a program initializing an event object as an auto-reset object could instead receive a manual-reset object, which stays signaled so that multiple processes receive the event instead of just one, when the process is expecting it to be delivered to only a single process or thread.

Another thing to keep in mind with squatting issues is that if you create the object, you're free to change it whenever you like and in whatever way you choose. If you create an event or waitable timer object that's subsequently returned to a privileged application through the use of CreateEvent() or CreateWaitableTimer(), you can arbitrarily signal those objects whenever you like. For instance, the owner of an event can generate a signal by calling the SetEvent() function at any time. This call could be dangerous when a process is expecting that the receipt of an event signal is acknowledgement that some object transaction has taken place, when in fact it hasn't.

Semaphore sets in UNIX (and other System V IPC objects) are vulnerable to similar squatting issues, but only to a limited extent because of the way the API works. A process creating a semaphore should use the IPC_CREAT and IPC_EXCL flags or the IPC_PRIVATE value for a key. Doing so guarantees that a new semaphore has been created. If the process supplies a key value and neglects to use the IPC_EXCL flag, it might mistakenly get access to an existing semaphore set. Here's an example of a vulnerable call:

int semid; semid = semget(ftok("/home/user/file", 'A'), 10,                IPC_CREAT | 0644);


This call to semget() takes an existing semaphore set if one exists with the same key and creates a new one only if one does not exist. If the semaphore set does already exist, it must have at least as many semaphore objects in the set as the second argument indicates. If it doesn't, an error is returned. There are still some interesting possibilities related to what you can do to the semaphore set at the same time another process is using it because you're the owner of the semaphore.

Note

If permissions are relaxed enough, such as everyone having full modify privileges to the semaphore created by a privileged process, the same attacks described in the following sections are also possible.


Semaphore sets are not like file descriptors. When a semaphore set is open, it's not persistently linked to the application. Instead, a semaphore ID is returned to the caller, and every subsequent use of the semaphore set involves looking up that ID in the global namespace. Therefore, if you have sufficient access to the semaphore set (as you do if you're the creator), you can do anything you want to it between accesses by the privileged process using the malicious semaphore set. For example, it would be possible to delete the set or re-create it after semget() returns in the privileged process with a smaller number of semaphore objects. You could also manually reset all semaphore integers in the set to arbitrary values, thus causing race conditions in the privileged process. Therefore, when auditing applications that make use of semaphores, the flags used in semget() are quite important.

Note

In case you're wondering what happens when IPC_EXCL is set and IPC_CREAT isn't, this is invalid and doesn't cause a new semaphore set to be created. The semget() function just returns an error.


Synchronization Object Scoreboards

As you have seen, it is relatively easy to misuse synchronization APIs, and inadvertently render a program vulnerable to a denial-of-service or race condition. When you're auditing for these vulnerabilities, it's best to keep a record of likely problems resulting from improper use of these IPC synchronization mechanisms, so that you can refer back to it at later stages of the code audit. The audit logs described in previous chapters don't address many of the details associated with concurrency vulnerabilities. Instead, you can use synchronization object scoreboards, which are a small logs providing the security-relevant details of a synchronization object: where it was instantiated, how it was instantiated, where it's used, and where it's released. Table 13-1 shows an example of this scoreboard.

Table 13-1. Synchronization Object Scoreboard

Object name

MyMutex

Object type

mutex

Use

Used for controlling access to the shared resource hObject (declared in main.c line 50). This object can have only one thread accessing it at a time (whether it's a reader or a writer).

Instantiated

open_mutex(), util.c, line 139

Instantiation parameters

OpenMutex(NULL, TRUE, "MyMutex")

Object permissions

Default

Used by

writer_task(), writer.c, line 139

reader_task(), reader.c, line 158

Protects

A linked list, queue, declared in main.c, line 76

Notes

This mutex uses a static name, and the code doesn't check GetLastError() when OpenMutex() returns. A squatting attack is possible.

Possible race condition in reader.c line 140, where one of the code paths fails to lock the mutex before operating on hObject.


As you can see, this scoreboard technique provides a concise summary of the object's use and purpose. You can note any observations about the way the object is instantiated or used and possibly follow up later. Not only does this scoreboard aid you as a quick reference when encountering new code that deals with the synchronization object, but later changes to the codebase can be checked against your summary to ensure that the object is used correctly.

Lock Matching

Another effective tool for auditing synchronization objects is lock matching. Lock matching is simply the process of checking synchronization objects to ensure that for every lock on an object, there's no path where a corresponding unlock can't occur. Obviously, this technique is applicable only to a subset of objectsthose that require signaling after they have been waited on. So this technique would be applicable primarily to semaphores and mutexes. If a path is found where a wait doesn't have a complementary signal on the same object, deadlock could occur.

Note

If a thread exits in Windows while owning an object, the system normally allows another waiting thread to take ownership of the object. However, if the thread does not exit cleanlynormally a result of a TerminateThread() callthe objects are not properly released and deadlock can occur.


A simple example helps demonstrate lock matching in action:

struct element *queue; HANDLE hMutex; int fd; int networkThread(void) {     struct element *elem;     for(;;)     {         elem = read_request(fd);         WaitForSingleObject(hMutex, INFINITY);         add_to_queue(queue, elem);         ReleaseMutex(hMutex);     }     return 0; } int processThread(void) {     struct element *elem;     for(;;)     {         WaitForSingleObject(hMutex);         elem = remove_from_queue(queue);         if(elem == NULL) // nothing in queue             continue;         ReleaseMutex(hMutex);         process_element(elem);     }     return 0; }


The processThread() function contains a path where hMutex isn't signaled after it's waited on. If elem is NULL when processThread() runs, it jumps back to the top of the for loop, failing to call ReleaseMutex(). The next call to WaitForSingleObject() doesn't cause this process deadlock, however, because the calling thread owns the mutex. Instead, it prevents the number of release calls from ever being equal to the number of wait calls. This means no other process or thread can ever acquire this mutex because the calling thread never releases it.

Be aware when performing lock matching checks to ensure that nonobvious paths don't exist where an object might never be released. For example, can a signal interrupt a thread that holds a lock and then reenter the program at some other point?




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