Freigeben über


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:

  1. A static place where the pipe or collection of
    pipes would be stored
  2. A method for knowing when a handle has been
    created
  3. 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.