How to Create and Use Anonymous Pipes in .NET
How to Create and Use Anonymous Pipes in .NET
.NET offers easy support for using named pipes, but what about anonymous pipes? Can those be done in .NET? The answer is yes, but it’s tricky to do and only useful in certain situations. You would need several things in order to accomplish this:
- A static place where the pipe or collection of
pipes would be stored - A method for knowing when a handle has been
created - A method for getting the handles in another
process
Assuming we’re not talking about the scenario where Process A always creates Process B where CreateProcess handles the work, the easiest way to implement them would be with a C++/CLI dll. Here’s the steps to make an example implementation in Visual Studio 2013 with comments about what’s happening where in the code:
Create a CLR Class Library project named PipeMaker.
Add a new module definition file to it.
Fill it out with the following contents:
LIBRARY PipeMaker
EXPORTS
AddHandles @1
Overwrite the contents of PipeMaker.h with the following:
// PipeMaker.h - this is not production quality code
#pragma once
#include <windows.h>
#include <vcclr.h>
//Structure to hold handles
typedef struct _handleHolder
{
int ProcessId;
void* ReadHandle;
void* WriteHandle;
}HandleHolder,*PHandleHolder;
using namespace System;
using namespace System::Collections::Concurrent;
using namespace System::Collections::Generic;
using namespace System::Diagnostics;
using namespace System::IO;
using namespace System::Runtime::InteropServices;
namespace PipeMaker {
//Delegate for when handles arrive
public delegate void HandlesArrived(int, IntPtr, IntPtr);
//Delegate for when pipes arrive
public delegate void PipesArrived(int pid, FileStream^ readStream, FileStream^ writeStream);
//Static class that stores handles
public ref class PipeStore
{
public:
//Read/Write handle tuple stored as a per process id collection
static ConcurrentDictionary<int, List<Tuple<IntPtr, IntPtr>^>^>^ HandlePairsByProcess;
//Adds read/write handles for a process
static void AddHandles(int pid, IntPtr read, IntPtr write)
{
if (HandlePairsByProcess == nullptr)
{
HandlePairsByProcess = gcnew ConcurrentDictionary<int, List<Tuple<IntPtr, IntPtr>^>^>();
}
Tuple<IntPtr, IntPtr>^ t = gcnew Tuple<IntPtr, IntPtr>(read, write);
if (HandlePairsByProcess->ContainsKey(pid))
{
HandlePairsByProcess[pid]->Add(t);
FireEvent(pid, read, write);
}
else
{
List<Tuple<IntPtr, IntPtr>^>^ l = gcnew List<Tuple<IntPtr, IntPtr>^>();
l->Add(t);
if (HandlePairsByProcess->TryAdd(pid, l))
{
FireEvent(pid, read, write);
}
else if (HandlePairsByProcess->ContainsKey(pid))
{
HandlePairsByProcess[pid]->Add(t);
FireEvent(pid, read, write);
}
}
}
//Handles arrived event
static event HandlesArrived^ HandleHandler;
//Pipes arrived event
static event PipesArrived^ PipeHandler;
private:
//Fire the events
static void FireEvent(int pid, IntPtr read, IntPtr write)
{
HandleHandler(pid, read, write);
//Wrap the handles in FileStream objects
System::IO::FileStream^ fsRead = gcnew FileStream(gcnew Microsoft::Win32::SafeHandles::SafeFileHandle(read, true),FileAccess::Read);
System::IO::FileStream^ fsWrite = gcnew FileStream(gcnew Microsoft::Win32::SafeHandles::SafeFileHandle(write, true), FileAccess::Write);
PipeHandler(pid, fsRead, fsWrite);
}
};
//Creates anonymous pipes for cross process communication
//Works if one process isn't the parent of the other too
public ref class PipeMaker
{
public:
//Target process id
PipeMaker(int targetPid)
{
InitializeForProcess(targetPid);
}
//Target process
PipeMaker(Process^ targetProcess)
{
if (targetProcess != nullptr) InitializeForProcess(targetProcess->Id);
}
//Destructor
~PipeMaker()
{
if (hProcess != IntPtr::Zero)
{
CloseHandle((HANDLE)(void*)hProcess);
}
if (hTarget != IntPtr::Zero)
{
CloseHandle((HANDLE)(void*)hTarget);
}
}
//Returns null if successful
Exception^ MakeNewPipes()
{
SIZE_T structSize;
HandleHolder holder;
HandleHolder localHolder;
void* remAddress;
SIZE_T bytesWritten;
__int3264 localProc;
HMODULE localModule;
__int3264 remoteModule;
__int3264 remoteAddress;
HANDLE remoteThread;
//Verify the remote module exists
localHolder.ProcessId = GetProcessId((HANDLE)(void*)hTarget);
remoteModule = 0;
Process^ p = Process::GetProcessById(localHolder.ProcessId);
for each(ProcessModule^ m in p->Modules)
{
if (m->ModuleName->EndsWith(L"PipeMaker.dll", StringComparison::OrdinalIgnoreCase))
{
remoteModule = (__int3264)m->BaseAddress.ToPointer();
break;
}
}
if (remoteModule == 0) return gcnew Exception("Could not locate remote module");
//Create the pipe pairs
if (!CreatePipe(&holder.ReadHandle, &holder.WriteHandle, NULL, NULL)) return Marshal::GetExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
if (!CreatePipe(&localHolder.ReadHandle, &localHolder.WriteHandle, NULL, NULL)) return Marshal::GetExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
//Fill out the remaining local and remote structures
holder.ProcessId = GetCurrentProcessId();
structSize = sizeof(holder);
void* tmpWrite = holder.WriteHandle;
//The local read will be the remote write and vice versa
if (!DuplicateHandle((HANDLE)hProcess, holder.ReadHandle, (HANDLE)(void*)hTarget, &holder.ReadHandle, 0, FALSE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) return Marshal::GetExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
if (!DuplicateHandle((HANDLE)hProcess, localHolder.WriteHandle, (HANDLE)(void*)hTarget, &holder.WriteHandle, 0, FALSE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) return Marshal::GetExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
localHolder.WriteHandle = tmpWrite;
//Allocate space remotely
remAddress = VirtualAllocEx((HANDLE)(void*)hTarget, NULL, structSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remAddress == NULL) return Marshal::GetExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
//Fill in remote structure
if (!WriteProcessMemory((HANDLE)hTarget, remAddress, &holder, structSize, &bytesWritten)) return Marshal::GetExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
//Call remote method
localModule = GetModuleHandle(L"PipeMaker.dll"); //hardcoding it to this name...
localProc = (__int3264)GetProcAddress(localModule, "AddHandles");
remoteAddress = localProc - (__int3264)localModule + remoteModule; //offset added to remote base
//There are better ways to do this with more planning in an actual application as CreateRemoteThread is subject to caveats as mentioned in its documentation
remoteThread = CreateRemoteThread((HANDLE)hTarget, NULL, NULL, (PTHREAD_START_ROUTINE)remoteAddress, remAddress, NULL, NULL);
if (remoteThread == NULL) return Marshal::GetExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
CloseHandle(remoteThread);
//Add it to the local collection
PipeStore::AddHandles(localHolder.ProcessId, (IntPtr)localHolder.ReadHandle, (IntPtr)localHolder.WriteHandle);
return nullptr;
//Note the leaks that occur with the way this is currently written as several things aren't closed when they are no longer in use in other parts of the program and in the situations where we return from this function with an exception without cleaning up what we've already made
}
private:
IntPtr hProcess;
IntPtr hTarget;
//Opens the process tokens
void InitializeForProcess(int targetPid)
{
hProcess = (IntPtr)OpenProcess(PROCESS_ALL_ACCESS, false, GetCurrentProcessId());
hTarget = (IntPtr)OpenProcess(PROCESS_ALL_ACCESS, false, targetPid);
}
};
}
//Export the function that will actually do the remote work
EXTERN_C
{
DWORD __declspec(dllexport) __stdcall AddHandles(PHandleHolder handles)
{
if (handles == NULL)
{
return ERROR_BAD_ARGUMENTS;
}
PipeMaker::PipeStore::AddHandles(handles->ProcessId, (IntPtr)handles->ReadHandle, (IntPtr)handles->WriteHandle);
}
}
Create a C# Console Application.
Reference the PipeMaker project.
Replace the contents of Program.cs with the following:
using PipeMaker;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
namespace AnonymousPipes
{
class Program
{
static ManualResetEventSlim eventLock = new ManualResetEventSlim(false);
static void Main(string[] args)
{
PipeStore.HandleHandler += HandlesArrived;
PipeStore.PipeHandler += PipesArrived;
if(args.Length == 0)
{
Process p = Process.Start(System.Reflection.Assembly.GetExecutingAssembly( ).Location, "taking up space");
//Poll to wait until the needed module is loaded; there are much better ways to do this in code that isn't an example for another concept
bool found = false;
while(!found)
{
p.Refresh( );
foreach(ProcessModule pm in p.Modules)
{
if(pm.ModuleName.EndsWith("PipeMaker.dll", StringComparison.OrdinalIgnoreCase))
{
found = true;
break;
}
}
if (!found) Thread.Sleep(50);
}
using (PipeMaker.PipeMaker pm = new PipeMaker.PipeMaker(p))
{
Exception e = pm.MakeNewPipes( );
if(e == null)
{
eventLock.Wait( );
string s;
do
{
Console.WriteLine("Enter text to send:");
s = Console.ReadLine( );
byte[] buffer = Encoding.ASCII.GetBytes(s);
List<byte> allbytes = new List<byte>( );
allbytes.AddRange(BitConverter.GetBytes(buffer.Length));
allbytes.AddRange(buffer);
buffer = allbytes.ToArray( );
w.Write(buffer, 0, buffer.Length);
w.Flush( );
}
while (!string.IsNullOrEmpty(s));
}
else
{
Console.WriteLine("Unable to create pipes: {0}", e.ToString());
}
}
}
Console.ReadLine( );
}
static FileStream r;
static FileStream w;
static void HandlesArrived(int pid, IntPtr read, IntPtr write)
{
}
static void PipesArrived(int pid, FileStream read, FileStream write)
{
eventLock.Set( );
Console.WriteLine("Received pipe connection for Process Id: {0}", pid);
r = read;
w = write;
ThreadPool.QueueUserWorkItem(ContinueRead);
}
static void ContinueRead(object o)
{
try
{
while (true)
{
byte[] sizeBuffer = new byte[4];
int read = r.Read(sizeBuffer, 0, 4);
if (read < 4) throw new Exception("Didn't read the size accurately");
read = BitConverter.ToInt32(sizeBuffer,0);
byte[] buffer2 = new byte[read];
read = 0;
while (read < buffer2.Length)
{
read += r.Read(buffer2, read, buffer2.Length - read);
}
Console.WriteLine("Message arrived: {0}", Encoding.ASCII.GetString(buffer2));
}
}
catch(Exception ex)
{
Console.WriteLine("Unexpected error reading message: {0}", ex.ToString( ));
}
}
}
}
Set the startup project to be your C# console application.
Debug and you should get something like this:
Download the example project here: https://1drv.ms/1ERuUMA.
Follow us on Twitter, https://www.twitter.com/WindowsSDK.