Understanding Anonymous Pipes — Part 1 — The Handles
Interprocess communication is fascinating in its own right. It is a mechanism that allows different processes, each running in its own dedicated address space, to talk to each other. The Windows operating system supports a variety of IPC mechanisms. In this article, we will look at an IPC mechanism called anonymous pipes.
What’s an anonymous pipe? Here’s an example that uses an anonymous pipe to communicate between two processes dir
and findstr
:
C:\>dir | findstr "Wind"
Volume in drive C is Windows
01-Apr-21 10:00 AM <DIR> Windows
What is happening here? The dir
command lists the contents of a directory. This output of the dir
command is then sent to an anonymous pipe (|
). The findstr
process reads from this pipe, searches the content for the string “Wind” and prints it to the console output.
How is all this implemented under the hood? Let’s take a look. Before going further, just wanted to clarify that in this article, when I refer to “pipe” it means anonymous pipes.
Creating an Anonymous Pipe
The easiest way to understand pipes is to just create one and see what happens in the system. How can we create a pipe? The Win32 API provides the CreatePipe function exactly for this purpose.
Below is a barebones code segment showing a call to the CreatePipe function:
HANDLE hReadPipe, hWritePipe;//The CreatePipe API will create an anonymous pipe.
bRetVal = CreatePipe(
&hReadPipe, //handle to read from pipe
&hWritePipe, //handle to write from pipe
nullptr, //security attributes
0 //suggested buffer size
);printf("The pipe handles are:\n");
printf("Read Handle = %p\n", hReadPipe);
printf("Write Handle = %p\n", hWritePipe);
I am going to wrap this code inside a simple C program (Pipes.exe) and run it.
Viewing Handle Information
From the output of Pipes.exe, we see that the anonymous pipe has been created. We have two handles, to read and write to the pipe with IDs C8 and CC respectively. The handle IDs are in hex. We can also view the handle information using Process Hacker.
The output of the “Handles” tab in Process Hacker (when run as Administrator) will look similar to the above. We’ll restrict our interest to pipe handles which are the last two entries in the output.
- “Type” — A handle is basically a pointer to an object in the kernel. We can see that the pipe handles are pointing to kernel objects of the type FILE.
- “Name” — This value stores the name of the kernel object. Anonymous Pipes do not have names. However, when Process Hacker is run as administrator it displays the pipe name as “Unnamed file: \FileSystem\Npfs”. If you run Process Hacker as a normal user then the Name column will be empty for our pipe handles.
- “Handle” — This column displays the ID of the handles created through the CreatePipe API call. The handle IDs are 0xc8 (200) and 0xcc (204).
More Handle Info!
To see additional information about a pipe handle we can double-click on the corresponding handle entry in Process Hacker’s handle table. This will open up a “Handle Properties” window showing some information about the object to which this handle was created.
From the “Handle Properties” we can see that:
- The pipe handles provide access to “File” objects. These objects are located in the Kernel memory (ref: ?, ?) and their address is shown in the “Object address” field.
- The permissions that the handle has to the object are given in the “Granted access” field. The value of this field is also commonly called the handle’s Access Mask.
- There’s also the “Security” tab, Quota changes, and References information, but they are not of particular interest for this discussion.
So, at this point, this is what we know about the Pipe handles:
+------+------------+----------+---------------------+-------------+
| ID | Purpose | Obj Type | Object Address | Access Mask |
+------+------------+----------+---------------------+-------------+
| 0xC8 | Pipe Read | File | 0xffffab02c2294080 | 0x120189 |
| 0xCC | Pipe Write | File | 0xffffab02cd698830 | 0x120196 |
+------+------------+----------+---------------------+-------------+
The Access Mask
Let’s now understand what the Access Mask means. Basically, the mask is a hex number identifying what operations the handle can perform on an object. Microsoft has some documentation (here or here) on how the mask stores such information. If you want to skip the details and just decode the value, you can use zodiacon’s AccessMask.exe tool. The tool expects two positional inputs — the mask value and the object type. In our case we’ll have to run the following commands:
AccessMask.exe 0x120189 file //For the read pipe handle
AccessMask.exe 0x120196 file //For the write pipe handle
The decoded Access mask is shown below:
Looking at the access mask we see that:
- The pipe read handle has permissions to read data and the pipe write handle has permissions to write data.
- Both handles can read/write attribute information (i.e. object metadata).
- The pipe handles can also work with the object’s EA or Extended Attributes.
Side Note: Also found a simple Python script that will decode the input access mask, but works fully only with the File type of objects.
What do we know now?
- We can create Anonymous Pipes using the CreatePipe function.
- When a pipe is successfully created, CreatePipe returns two handles to read and write to the pipe respectively.
- Handles contain three important pieces of information:
1. The Type of kernel object referenced by the handle. In the case of pipes, the kernel object type is “File”.
2. The Address of the kernel object
3. The handle’s Access Mask. - Process Hacker can be used to view information about these handles.
- The AccessMask.exe tool can be used to view all the permissions associated with an Access Mask.
What’s next?
Next, we can jump into kernel land and look at the objects that the pipe handles are pointing to. You can find the article here.