Interprocess Communication


UNIX systems provide several mechanisms for processes to communicate with each other to share information or synchronize their activities. These mechanisms are typically used for transactions across multiple processes, sharing data elements, and coordinating resource sharing. Naturally, the power that IPC primitives afford also presents a potential for vulnerability in applications that use these mechanisms haphazardly.

Pipes

Pipes are a simple mechanism for IPC in UNIX. A pipe is a unidirectional pair of file descriptors; one descriptor is used for writing information, and the other is used for reading information. A process can write data to the write side of the pipe, and another process can read that data from the read side of the pipe. The pipe descriptors are created at the same time by the pipe() system call, so they are useful for setting up IPC in advance, typically by handing one side of the pipe to a child process via a fork().

Not surprisingly, pipes are the underlying mechanism shell programs use when you link programs by using pipe characters. Say you run a command like this:

echo hi | more


The shell creates a pipe and gives the write end to a child process that uses it as its standard output descriptor (which is file descriptor 1, if you recall). The read end is handed to a different child process that uses it as its standard input. Then one process runs echo hi and the other process runs the more program, and communication takes place across that pipe.

You've already looked at a library function based on the use of pipes, popen(). It creates a pipe and hands one end of it to the child process running the requested program. In this way, it can read from the standard output of the subprogram or write to the standard output of the subprogram.

One interesting feature of a pipe is that writing to a pipe with a closed read end causes your program to receive a SIGPIPE, which has a default behavior of terminating the process. If the process deals with the SIGPIPE, the write call returns a failure code of EPIPE to the program.

Named Pipes

Named pipes (also called "FIFOs" because of their first-in, first-out nature) are pipes that exist on the file system and can be opened just like normal files. Software can use named pipes to set up IPC with a process it isn't related to. Pipes are typically created with mkfifo() or mknod() and then opened with open(). Like regular files, named pipes have associated file permissions specified in the creation call, and they are modified by the umask. Therefore, an application that creates a FIFO needs to ensure that it applies appropriate permissions to the new object. In this context, "appropriate" means using a restrictive set of permissions that only allows specific applications access to the pipe.

Pipes have an interesting behavior in how they're opened by a process that might prove useful in an attack. If a process opens a pipe for reading, the pipe is blocked until another process opens the same pipe for writing. So open() doesn't return until a peer process has joined the game. Similarly, opening a pipe for writing causes a program to block until another process opens the pipe for reading. Opening a pipe in read/write mode (O_RDWR) is undefined behavior, but it usually results in the pipe being opened as a reader without blocking occurring. You can open pipes in nonblocking mode if you want to avoid the blocking behavior. Programs expecting regular files could instead be passed a named pipe that causes the blocking behavior. Although this isn't a security problem in-itself, it could slow down the program when attempting to perform some other TOCTOU-based attack. In addition to open() blocking, attackers can cause the read pipe to block whenever they choose if they are the only writer attached to the other end of the pipe, thus providing additional control over process execution. In fact, Michael Zalewski (a researcher that we have noted previously in this chapter) demonstrated this attack when exploiting a race condition in the GNU C Compiler (GCC). It's more of an exploitation technique but is worth mentioning because race conditions that might have seemed infeasible become more readily exploitable (the technique is detailed at http://seclists.org/bugtraq/1998/Feb/0077.html).

There are also quirks in writing to named pipes. If you try to write to a named pipe with no attached reader, you the get same result as with a normal pipe: a SIGPIPE signal and the EPIPE error from the write system call.

Another potential problem when dealing with pipes is nonsecure use of mkfifo() and mknod(). Unlike open(), these two functions don't return a file descriptor upon successful creation of a resource; instead, they return a value of 0 indicating success. Therefore, a program that creates a named pipe must subsequently call open() on the created pipe to use it. This situation creates the potential for a race condition; if the pipe is deleted and a new file is created in its place between the time mkfifo() is used and open() is called, the program might inadvertently open something it didn't intend to. Here's an example of vulnerable code:

int open_pipe(char *pipename) {     int rc;     rc = mkfifo(pipename, S_IRWXU);     if(rc == -1)         return 1;     return open(pipename, O_WRONLY); }


In this case, if the process can be interrupted between mkfifo() and open(), it might be possible to delete the created file and create a symlink to a system file or perform a similar attack.

From a code-auditing standpoint, the existence of named pipes introduces three potential issues in UNIX-based applications:

  • Named pipes created with insufficient privileges might result in unauthorized clients performing some sort of data exchange, potentially leading to compromise via unauthorized (or forged) data messages.

  • Applications that are intended to deal with regular files might unwittingly find themselves interacting with named pipes. This allows attackers to cause applications to stall in unlikely situations or cause error conditions in unexpected places. When auditing an application that deals with files, if it fails to determine the file type, consider the implications of triggering errors during file accesses and blocking the application at those junctures.

  • The use of mknod() and mkfifo() might introduce a race condition between the time the pipe is created and the time it's opened.

System V IPC

System V IPC mechanisms are primitives that allow unrelated processes to communicate with each other or achieve some level of synchronization. Three IPC mechanisms in System V IPC are message queues, semaphores, and shared memory.

Message queues are a simple stateless messaging system that allows processes to send each other unspecified data. The kernel keeps messages until the message queue is destroyed or a process receives the messages. Unlike file system access, message queue permissions are checked for each operation instead of just when the process is opened. The functions for using message queues are msget(), msgctl(), msgrcv(), and msgsend().

Semaphores are a synchronization mechanism that processes can use to control the sequence of activities that occur between them. The semaphore primitives provide the capability to manipulate semaphore sets, which are a series of semaphores that can be operated on independently. The functions for manipulating semaphores are semget(), semop(), and semctl().

Finally, shared memory segments are a mechanism whereby a memory segment can be mapped to more than one process simultaneously. By reading or writing to the memory in this segment, processed can exchange information or maintain state and variables among a number of processes. Shared memory segments can be created and manipulated with shmget(), shmctl(), shmat(), and shmdt().

The System V IPC mechanisms have their own namespace in kernel memory that isn't tied to the file system, and they implement their own simple permissions model. In reality, these mechanisms are rarely used in applications; however, you should know about them in case you encounter code that does use them. The most important issue is permissions associated with an IPC entity. IPC implements its own simple permissions model. Each IPC object has its own mode field that describes the requirements for accessing it. This field is nine bits: three bits describing the owner's privileges, three bits describing the group privileges (of the group the owner belongs to), and three bits describing the permissions for everybody else. The bits represent whether the object can be read from or written to for the appropriate group (with one extra bit that's reserved).

These permissions are a simplified version of how file system permissions work (except IPC mechanisms don't have the execute permission). Obviously, programs that set these permissions inappropriately are vulnerable to attacks in which arbitrary processes interfere with a communication channel being used by a more privileged process. The consequences can range from simple denial-of-service attacks to memory corruption vulnerabilities to logic errors resulting in privilege escalation. Recently, a denial-of-service vulnerability was found in Apache Web server related to shared memory access for users who could run data with privileges of the Apache user (that is, could write scripts for the Web server to run). In an article at www.securityfocus.com/archive/1/294026, Zen-parse noted that running scripts in this context allowed users to access the HTTPd scoreboard, which was stored in a shared memory segment. He describes several attacks that resulted in Apache spawning endless numbers of processes or being able to send signals to arbitrary processes as root.

Another issue when dealing with shared memory segments is that when a process forks, both the child and parent receive a copy of the mapped shared memory segment. This means if one of the processes is compromised to a level that user-malleable code can be run, each process can access shared memory segments with the permissions it was mapped in with. If an exec() occurs, the shared memory segment is detached.

Finally, the use of shared resources might introduce the possibility of race conditions, particularly in shared memory segments. Because the data segment can be mapped into multiple processes simultaneously, any of those processes that can write to the segment might be able to cause race conditions by modifying data after another process has read it but before the data has been acted on. Of course, there are also complications if multiple writers are acting at the same time. Synchronization issues are covered in more depth in Chapter 13, "Synchronization and State."

UNIX Domain Sockets

UNIX domain sockets are similar to pipes, in that they allow processes on a local system to communicate with each other. Like pipes, UNIX domain sockets can be named or anonymous. Anonymous domain sockets are created by using the socketpair() function. It works similarly to the pipe() function; it creates a pair of unnamed endpoints that a process can use to communicate information. Anonymous domain sockets are typically used when a process intends to fork and needs a communication channel between a parent and a child.

Named domain sockets provide a general-purpose mechanism for exchanging data in a stream-based or record-based fashion. They use the socket API functions to create and manage a connection over a domain socket. In essence, the code to implement connection management and data exchange over named pipes is almost identical to networked applications, although the security implications of using local domain sockets are quite different. Named sockets are implemented by using special socket device files, created automatically when a server calls bind(). The location of the filename is specified in the socket address structure passed to the bind() function. A socket device file is created with permissions (777 & ~umask). Therefore, if a setuid program creates a socket, setting the umask to 0 before starting the program creates the socket file with full read, write, and execute privileges for everyone, meaning any user on the system could connect to the socket and write arbitrary data to the process that bound the socket. An example of a dangerous socket creation is shown:

int create_sock(char *path) {     struct sockaddr_un sun;     int s;     bzero(&sun, sizeof(sun));     sun.sun_family = AF_UNIX;     strncpy(sun.sun_path, path, sizeof(sun.sun_path)-1;     s = socket(AF_UNIX, SOCK_STREAM, 0);     if(s < 0)         return s;     if(bind(s, (struct sockaddr *)&sun, sizeof(sun)) < 0)         return -1;     return s; }


Assuming this code is running in a privileged context, it could be dangerous because it doesn't explicitly set the umask before creating the socket. Therefore, the calling user might be able to clear the umask and write to a socket that's not intended to receive connections from arbitrary clients. It's easy to overlook file permissions in this situation because they aren't addressed in the socket functions (as opposed to pipe functions such as mkfifo(), which have a mode argument for creating a new pipe).

Of course, if users can specify any part of the pathname generated to store the socket or if any writeable directories are used in the path, race attacks could be performed to intercept traffic between a client and server. Specifically, consider the following code:

int create_sock(void) {     struct sockaddr_un sun;     char *path = "/data/fifo/sock1";     int s;     bzero(&sun, sizeof(sun));     sun.sun_family = AF_UNIX;     strncpy(sun.sun_path, path, sizeof(sun.sun_path)-1);     s = socket(AF_UNIX, SOCK_STREAM, 0);     if(s < 0)         return s;     if(bind(s, (struct sockaddr *)&sun, sizeof(sun)) < 0)         return -1;     return s; }


This slightly modified example shows that a socket is created in /data/fifo. If the /data directory is writeable, you could let the server create the socket, and then unlink the /fifo directory or symlink it to some other location where another socket named sock1 has been created. Any client connecting to this socket would then be connecting to the wrong program unwittingly. This might have security implications if sensitive data is being transmitted or if resources are being passed across the socket, such as file descriptors.

Auditing code that uses UNIX domain sockets requires paying attention to the manner in which the socket is created. Because socket files need to be opened explicitly with the socket API, socket files can't be passed as arguments to setuid programs in an attempt to manipulate the speed at which a process is running, as described for named pipes. There is one exceptionwhen the socket has already been opened and a new process inherits the descriptor via a call to execve().

Note

Also, bear in mind that when a server closes a socket, the socket file isn't deleted from the file system; it needs to be deleted with the unlink() function. Failure to do so by the server might result in it being unable to bind again when it needs to be restarted (if a static pathname is used) or a directory being continually filled up with unused socket files. This isn't so much a security issue but can result in the application not being able to bind sockets when it needs to.





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